dav_server/
caldav.rs

1//! CalDAV (Calendaring Extensions to WebDAV) support
2//!
3//! This module provides CalDAV functionality on top of the base WebDAV implementation.
4//! CalDAV is defined in RFC 4791 and provides standardized access to calendar data
5//! using the iCalendar format.
6
7#[cfg(feature = "caldav")]
8use icalendar::Calendar;
9use xmltree::Element;
10
11// CalDAV XML namespaces
12pub const NS_CALDAV_URI: &str = "urn:ietf:params:xml:ns:caldav";
13pub const NS_CALENDARSERVER_URI: &str = "http://calendarserver.org/ns/";
14
15// CalDAV property names
16pub const CALDAV_PROPERTIES: &[&str] = &[
17    "C:calendar-description",
18    "C:calendar-timezone",
19    "C:supported-calendar-component-set",
20    "C:supported-calendar-data",
21    "C:max-resource-size",
22    "C:min-date-time",
23    "C:max-date-time",
24    "C:max-instances",
25    "C:max-attendees-per-instance",
26    "C:calendar-home-set",
27    "C:calendar-user-address-set",
28    "C:schedule-inbox-URL",
29    "C:schedule-outbox-URL",
30];
31
32/// CalDAV resource types
33#[derive(Debug, Clone, PartialEq)]
34pub enum CalDavResourceType {
35    Calendar,
36    ScheduleInbox,
37    ScheduleOutbox,
38    CalendarObject,
39    Regular,
40}
41
42/// CalDAV component types supported in a calendar collection
43#[derive(Debug, Clone, PartialEq)]
44pub enum CalendarComponentType {
45    VEvent,
46    VTodo,
47    VJournal,
48    VFreeBusy,
49    VTimezone,
50    VAlarm,
51}
52
53impl CalendarComponentType {
54    pub fn as_str(&self) -> &'static str {
55        match self {
56            CalendarComponentType::VEvent => "VEVENT",
57            CalendarComponentType::VTodo => "VTODO",
58            CalendarComponentType::VJournal => "VJOURNAL",
59            CalendarComponentType::VFreeBusy => "VFREEBUSY",
60            CalendarComponentType::VTimezone => "VTIMEZONE",
61            CalendarComponentType::VAlarm => "VALARM",
62        }
63    }
64}
65
66/// CalDAV calendar collection properties
67#[derive(Debug, Clone)]
68pub struct CalendarProperties {
69    pub description: Option<String>,
70    pub timezone: Option<String>,
71    pub supported_components: Vec<CalendarComponentType>,
72    pub max_resource_size: Option<u64>,
73    pub color: Option<String>,
74    pub display_name: Option<String>,
75}
76
77impl Default for CalendarProperties {
78    fn default() -> Self {
79        Self {
80            description: None,
81            timezone: None,
82            supported_components: vec![
83                CalendarComponentType::VEvent,
84                CalendarComponentType::VTodo,
85                CalendarComponentType::VJournal,
86                CalendarComponentType::VFreeBusy,
87            ],
88            max_resource_size: Some(1024 * 1024), // 1MB default
89            color: None,
90            display_name: None,
91        }
92    }
93}
94
95/// Calendar query filters for REPORT requests
96#[derive(Debug, Clone)]
97pub struct CalendarQuery {
98    pub comp_filter: Option<ComponentFilter>,
99    pub time_range: Option<TimeRange>,
100    pub properties: Vec<String>,
101}
102
103#[derive(Debug, Clone)]
104pub struct ComponentFilter {
105    pub name: String,
106    pub is_not_defined: bool,
107    pub time_range: Option<TimeRange>,
108    pub prop_filters: Vec<PropertyFilter>,
109    pub comp_filters: Vec<ComponentFilter>,
110}
111
112#[derive(Debug, Clone)]
113pub struct PropertyFilter {
114    pub name: String,
115    pub is_not_defined: bool,
116    pub text_match: Option<TextMatch>,
117    pub time_range: Option<TimeRange>,
118    pub param_filters: Vec<ParameterFilter>,
119}
120
121#[derive(Debug, Clone)]
122pub struct ParameterFilter {
123    pub name: String,
124    pub is_not_defined: bool,
125    pub text_match: Option<TextMatch>,
126}
127
128#[derive(Debug, Clone)]
129pub struct TextMatch {
130    pub text: String,
131    pub collation: Option<String>,
132    pub negate_condition: bool,
133}
134
135#[derive(Debug, Clone)]
136pub struct TimeRange {
137    pub start: Option<String>, // ISO 8601 format
138    pub end: Option<String>,   // ISO 8601 format
139}
140
141/// CalDAV REPORT request types
142#[derive(Debug, Clone)]
143pub enum CalDavReportType {
144    CalendarQuery(CalendarQuery),
145    CalendarMultiget { hrefs: Vec<String> },
146    FreeBusyQuery { time_range: TimeRange },
147}
148
149/// Helper functions for CalDAV XML generation
150pub fn create_supported_calendar_component_set(components: &[CalendarComponentType]) -> Element {
151    let mut elem = Element::new("supported-calendar-component-set");
152    elem.namespace = Some(NS_CALDAV_URI.to_string());
153
154    for comp in components {
155        let mut comp_elem = Element::new("comp");
156        comp_elem.namespace = Some(NS_CALDAV_URI.to_string());
157        comp_elem
158            .attributes
159            .insert("name".to_string(), comp.as_str().to_string());
160        elem.children.push(xmltree::XMLNode::Element(comp_elem));
161    }
162
163    elem
164}
165
166pub fn create_supported_calendar_data() -> Element {
167    let mut elem = Element::new("supported-calendar-data");
168    elem.namespace = Some(NS_CALDAV_URI.to_string());
169
170    let mut calendar_data = Element::new("calendar-data");
171    calendar_data.namespace = Some(NS_CALDAV_URI.to_string());
172    calendar_data
173        .attributes
174        .insert("content-type".to_string(), "text/calendar".to_string());
175    calendar_data
176        .attributes
177        .insert("version".to_string(), "2.0".to_string());
178
179    elem.children.push(xmltree::XMLNode::Element(calendar_data));
180    elem
181}
182
183pub fn create_calendar_home_set(path: &str) -> Element {
184    let mut elem = Element::new("calendar-home-set");
185    elem.namespace = Some(NS_CALDAV_URI.to_string());
186
187    let mut href = Element::new("href");
188    href.namespace = Some("DAV:".to_string());
189    href.children.push(xmltree::XMLNode::Text(path.to_string()));
190
191    elem.children.push(xmltree::XMLNode::Element(href));
192    elem
193}
194
195/// Check if a resource is a calendar collection based on resource type
196pub fn is_calendar_collection(resource_type: &[Element]) -> bool {
197    resource_type
198        .iter()
199        .any(|elem| elem.name == "calendar" && elem.namespace.as_deref() == Some(NS_CALDAV_URI))
200}
201
202/// Check if content appears to be iCalendar data
203pub fn is_calendar_data(content: &[u8]) -> bool {
204    content.starts_with(b"BEGIN:VCALENDAR")
205        && (content.ends_with(b"END:VCALENDAR") || content.ends_with(b"END:VCALENDAR\n"))
206}
207
208#[cfg(feature = "caldav")]
209/// Validate iCalendar data using the icalendar crate
210pub fn validate_calendar_data(content: &str) -> Result<Calendar, String> {
211    content
212        .parse::<Calendar>()
213        .map_err(|e| format!("Invalid iCalendar data: {}", e))
214}
215
216#[cfg(not(feature = "caldav"))]
217/// Stub implementation when caldav feature is disabled
218pub fn validate_calendar_data(_content: &str) -> Result<(), String> {
219    Err("CalDAV feature not enabled".to_string())
220}
221
222/// Extract the UID from calendar data
223pub fn extract_calendar_uid(content: &str) -> Option<String> {
224    for line in content.lines() {
225        let line = line.trim();
226        if let Some(uid) = line.strip_prefix("UID:") {
227            return Some(uid.to_string());
228        }
229    }
230    None
231}
232
233/// Generate a simple calendar collection resource type XML
234pub fn calendar_resource_type() -> Vec<Element> {
235    let mut collection = Element::new("collection");
236    collection.namespace = Some("DAV:".to_string());
237
238    let mut calendar = Element::new("calendar");
239    calendar.namespace = Some(NS_CALDAV_URI.to_string());
240
241    vec![collection, calendar]
242}
243
244/// Generate schedule inbox resource type XML
245pub fn schedule_inbox_resource_type() -> Vec<Element> {
246    let mut collection = Element::new("collection");
247    collection.namespace = Some("DAV:".to_string());
248
249    let mut schedule_inbox = Element::new("schedule-inbox");
250    schedule_inbox.namespace = Some(NS_CALDAV_URI.to_string());
251
252    vec![collection, schedule_inbox]
253}
254
255/// Generate schedule outbox resource type XML
256pub fn schedule_outbox_resource_type() -> Vec<Element> {
257    let mut collection = Element::new("collection");
258    collection.namespace = Some("DAV:".to_string());
259
260    let mut schedule_outbox = Element::new("schedule-outbox");
261    schedule_outbox.namespace = Some(NS_CALDAV_URI.to_string());
262
263    vec![collection, schedule_outbox]
264}