Skip to main content

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
11use crate::davpath::DavPath;
12
13// Re-export shared filter types
14pub use crate::dav_filters::{ParameterFilter, TextMatch};
15
16// CalDAV XML namespaces
17pub const NS_CALDAV_URI: &str = "urn:ietf:params:xml:ns:caldav";
18pub const NS_CALENDARSERVER_URI: &str = "http://calendarserver.org/ns/";
19
20// CalDAV property names
21pub const CALDAV_PROPERTIES: &[&str] = &[
22    "C:calendar-description",
23    "C:calendar-timezone",
24    "C:supported-calendar-component-set",
25    "C:supported-calendar-data",
26    "C:max-resource-size",
27    "C:min-date-time",
28    "C:max-date-time",
29    "C:max-instances",
30    "C:max-attendees-per-instance",
31    "C:calendar-home-set",
32    "C:calendar-user-address-set",
33    "C:schedule-inbox-URL",
34    "C:schedule-outbox-URL",
35];
36
37/// The default caldav directory, which is being used for the preprovided filesystems. Path is without trailing slash
38pub const DEFAULT_CALDAV_NAME: &str = "calendars";
39pub const DEFAULT_CALDAV_DIRECTORY: &str = "/calendars";
40pub const DEFAULT_CALDAV_DIRECTORY_ENDSLASH: &str = "/calendars/";
41
42/// CalDAV resource types
43#[derive(Debug, Clone, PartialEq)]
44pub enum CalDavResourceType {
45    Calendar,
46    ScheduleInbox,
47    ScheduleOutbox,
48    CalendarObject,
49    Regular,
50}
51
52/// CalDAV component types supported in a calendar collection
53#[derive(Debug, Clone, PartialEq)]
54pub enum CalendarComponentType {
55    VEvent,
56    VTodo,
57    VJournal,
58    VFreeBusy,
59    VTimezone,
60    VAlarm,
61}
62
63impl CalendarComponentType {
64    pub fn as_str(&self) -> &'static str {
65        match self {
66            CalendarComponentType::VEvent => "VEVENT",
67            CalendarComponentType::VTodo => "VTODO",
68            CalendarComponentType::VJournal => "VJOURNAL",
69            CalendarComponentType::VFreeBusy => "VFREEBUSY",
70            CalendarComponentType::VTimezone => "VTIMEZONE",
71            CalendarComponentType::VAlarm => "VALARM",
72        }
73    }
74}
75
76/// CalDAV calendar collection properties
77#[derive(Debug, Clone)]
78pub struct CalendarProperties {
79    pub description: Option<String>,
80    pub timezone: Option<String>,
81    pub supported_components: Vec<CalendarComponentType>,
82    pub max_resource_size: Option<u64>,
83    pub color: Option<String>,
84    pub display_name: Option<String>,
85}
86
87impl Default for CalendarProperties {
88    fn default() -> Self {
89        Self {
90            description: None,
91            timezone: None,
92            supported_components: vec![
93                CalendarComponentType::VEvent,
94                CalendarComponentType::VTodo,
95                CalendarComponentType::VJournal,
96                CalendarComponentType::VFreeBusy,
97            ],
98            max_resource_size: Some(1024 * 1024), // 1MB default
99            color: None,
100            display_name: None,
101        }
102    }
103}
104
105/// Calendar query filters for REPORT requests
106#[derive(Debug, Clone)]
107pub struct CalendarQuery {
108    pub comp_filter: Option<ComponentFilter>,
109    pub time_range: Option<TimeRange>,
110    pub properties: Vec<String>,
111}
112
113#[derive(Debug, Clone)]
114pub struct ComponentFilter {
115    pub name: String,
116    pub is_not_defined: bool,
117    pub time_range: Option<TimeRange>,
118    pub prop_filters: Vec<PropertyFilter>,
119    pub comp_filters: Vec<ComponentFilter>,
120}
121
122/// CalDAV property filter with time-range support
123///
124/// Note: CalDAV property filters include time-range which is not present
125/// in the shared ParameterFilter. CardDAV has a similar struct without time_range.
126#[derive(Debug, Clone)]
127pub struct PropertyFilter {
128    pub name: String,
129    pub is_not_defined: bool,
130    pub text_match: Option<TextMatch>,
131    pub time_range: Option<TimeRange>,
132    pub param_filters: Vec<ParameterFilter>,
133}
134
135#[derive(Debug, Clone)]
136pub struct TimeRange {
137    /// ISO 8601 format
138    pub start: Option<String>,
139    /// ISO 8601 format
140    pub end: Option<String>,
141}
142
143/// CalDAV REPORT request types
144#[derive(Debug, Clone)]
145pub enum CalDavReportType {
146    CalendarQuery(CalendarQuery),
147    CalendarMultiget { hrefs: Vec<String> },
148    FreeBusyQuery { time_range: TimeRange },
149}
150
151/// Helper functions for CalDAV XML generation
152pub fn create_supported_calendar_component_set(components: &[CalendarComponentType]) -> Element {
153    let mut elem = Element::new("C:supported-calendar-component-set");
154    elem.namespace = Some(NS_CALDAV_URI.to_string());
155
156    for comp in components {
157        let mut comp_elem = Element::new("C:comp");
158        comp_elem.namespace = Some(NS_CALDAV_URI.to_string());
159        comp_elem
160            .attributes
161            .insert("name".to_string(), comp.as_str().to_string());
162        elem.children.push(xmltree::XMLNode::Element(comp_elem));
163    }
164
165    elem
166}
167
168pub fn create_supported_calendar_data() -> Element {
169    let mut elem = Element::new("C:supported-calendar-data");
170    elem.namespace = Some(NS_CALDAV_URI.to_string());
171
172    let mut calendar_data = Element::new("C:calendar-data");
173    calendar_data.namespace = Some(NS_CALDAV_URI.to_string());
174    calendar_data
175        .attributes
176        .insert("content-type".to_string(), "text/calendar".to_string());
177    calendar_data
178        .attributes
179        .insert("version".to_string(), "2.0".to_string());
180
181    elem.children.push(xmltree::XMLNode::Element(calendar_data));
182    elem
183}
184
185pub fn create_calendar_home_set(prefix: &str, path: &str) -> Element {
186    let mut elem = Element::new("C:calendar-home-set");
187    elem.namespace = Some(NS_CALDAV_URI.to_string());
188
189    let mut href = Element::new("D:href");
190    href.namespace = Some("DAV:".to_string());
191    href.children
192        .push(xmltree::XMLNode::Text(format!("{prefix}{path}")));
193
194    elem.children.push(xmltree::XMLNode::Element(href));
195    elem
196}
197
198/// Check if a path is within the default CalDAV directory. Expects path without prefix.
199pub(crate) fn is_path_in_caldav_directory(dav_path: &DavPath) -> bool {
200    let path_string = dav_path.to_string();
201    path_string.len() > DEFAULT_CALDAV_DIRECTORY_ENDSLASH.len()
202        && path_string.starts_with(DEFAULT_CALDAV_DIRECTORY_ENDSLASH)
203}
204
205/// Check if a resource is a calendar collection based on resource type
206pub fn is_calendar_collection(resource_type: &[Element]) -> bool {
207    resource_type
208        .iter()
209        .any(|elem| elem.name == "calendar" && elem.namespace.as_deref() == Some(NS_CALDAV_URI))
210}
211
212/// Check if content appears to be iCalendar data
213pub fn is_calendar_data(content: &[u8]) -> bool {
214    if !content.starts_with(b"BEGIN:VCALENDAR") {
215        return false;
216    }
217
218    let trimmed = content.trim_ascii_end();
219    trimmed.ends_with(b"END:VCALENDAR")
220}
221
222/// Validate iCalendar data using the icalendar crate
223///
224/// This function validates that the content is a well-formed iCalendar object.
225/// Use this function in your application layer to validate calendar data
226/// before or after writing to the filesystem.
227///
228/// # Example
229///
230/// ```ignore
231/// use dav_server::caldav::validate_calendar_data;
232///
233/// let ical = "BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\n...";
234/// match validate_calendar_data(ical) {
235///     Ok(_) => println!("Valid iCalendar"),
236///     Err(e) => println!("Invalid iCalendar: {}", e),
237/// }
238/// ```
239#[cfg(feature = "caldav")]
240pub fn validate_calendar_data(content: &str) -> Result<Calendar, String> {
241    content
242        .parse::<Calendar>()
243        .map_err(|e| format!("Invalid iCalendar data: {}", e))
244}
245
246/// Extract the UID from calendar data
247///
248/// Handles both standard `UID:value` and properties with parameters.
249pub fn extract_calendar_uid(content: &str) -> Option<String> {
250    for line in content.lines() {
251        let line = line.trim();
252        // Handle simple UID:VALUE
253        if let Some(value) = line.strip_prefix("UID:") {
254            return Some(value.to_string());
255        }
256        // Handle UID with parameters: UID;PARAMS:VALUE
257        if let Some(rest) = line.strip_prefix("UID;")
258            && let Some(colon_pos) = rest.find(':')
259        {
260            return Some(rest[colon_pos + 1..].to_string());
261        }
262    }
263    None
264}
265
266/// Generate a simple calendar collection resource type XML
267pub fn calendar_resource_type() -> Vec<Element> {
268    let mut collection = Element::new("D:collection");
269    collection.namespace = Some("DAV:".to_string());
270
271    let mut calendar = Element::new("C:calendar");
272    calendar.namespace = Some(NS_CALDAV_URI.to_string());
273
274    vec![collection, calendar]
275}
276
277/// Generate schedule inbox resource type XML
278pub fn schedule_inbox_resource_type() -> Vec<Element> {
279    let mut collection = Element::new("D:collection");
280    collection.namespace = Some("DAV:".to_string());
281
282    let mut schedule_inbox = Element::new("C:schedule-inbox");
283    schedule_inbox.namespace = Some(NS_CALDAV_URI.to_string());
284
285    vec![collection, schedule_inbox]
286}
287
288/// Generate schedule outbox resource type XML
289pub fn schedule_outbox_resource_type() -> Vec<Element> {
290    let mut collection = Element::new("D:collection");
291    collection.namespace = Some("DAV:".to_string());
292
293    let mut schedule_outbox = Element::new("C:schedule-outbox");
294    schedule_outbox.namespace = Some(NS_CALDAV_URI.to_string());
295
296    vec![collection, schedule_outbox]
297}