rustical_caldav/calendar/methods/
mkcalendar.rs

1use std::str::FromStr;
2
3use crate::Error;
4use crate::calendar::CalendarResourceService;
5use crate::calendar::prop::SupportedCalendarComponentSet;
6use crate::error::Precondition;
7use axum::extract::{Path, State};
8use axum::response::{IntoResponse, Response};
9use caldata::IcalParser;
10use http::{Method, StatusCode};
11use rustical_dav::xml::HrefElement;
12use rustical_ical::CalendarObjectType;
13use rustical_store::auth::Principal;
14use rustical_store::{Calendar, CalendarMetadata, CalendarStore, SubscriptionStore};
15use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
16use tracing::instrument;
17
18#[derive(XmlDeserialize, Clone, Debug)]
19pub struct MkcolCalendarProp {
20    #[xml(ns = "rustical_dav::namespace::NS_DAV")]
21    displayname: Option<String>,
22    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
23    calendar_description: Option<String>,
24    #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
25    calendar_color: Option<String>,
26    #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
27    calendar_order: Option<i64>,
28    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
29    calendar_timezone: Option<String>,
30    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
31    calendar_timezone_id: Option<String>,
32    #[xml(ns = "rustical_dav::namespace::NS_DAV")]
33    #[allow(dead_code)]
34    resourcetype: Option<Unparsed>,
35    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
36    supported_calendar_component_set: Option<SupportedCalendarComponentSet>,
37    #[xml(ns = "rustical_dav::namespace::NS_CALENDARSERVER")]
38    source: Option<HrefElement>,
39    // Ignore that property, we don't support it but also don't want to throw an error
40    #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
41    #[allow(dead_code)]
42    calendar_free_busy_set: Option<Unparsed>,
43}
44
45#[derive(XmlDeserialize, Clone, Debug)]
46pub struct PropElement {
47    #[xml(ns = "rustical_dav::namespace::NS_DAV")]
48    prop: MkcolCalendarProp,
49}
50
51#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
52#[xml(root = "mkcalendar")]
53#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
54struct MkcalendarRequest {
55    #[xml(ns = "rustical_dav::namespace::NS_DAV")]
56    set: PropElement,
57}
58
59#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
60#[xml(root = "mkcol")]
61#[xml(ns = "rustical_dav::namespace::NS_DAV")]
62struct MkcolRequest {
63    #[xml(ns = "rustical_dav::namespace::NS_DAV")]
64    set: PropElement,
65}
66
67#[instrument(skip(cal_store))]
68pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
69    Path((principal, cal_id)): Path<(String, String)>,
70    user: Principal,
71    State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
72    method: Method,
73    body: String,
74) -> Result<Response, Error> {
75    if !user.is_principal(&principal) {
76        return Err(Error::Unauthorized);
77    }
78
79    let mut request = match method.as_str() {
80        "MKCALENDAR" => MkcalendarRequest::parse_str(&body)?.set.prop,
81        "MKCOL" => MkcolRequest::parse_str(&body)?.set.prop,
82        _ => unreachable!("We never call with another method"),
83    };
84
85    if request.displayname.as_deref() == Some("") {
86        request.displayname = None;
87    }
88
89    let timezone_id = if let Some(tzid) = request.calendar_timezone_id {
90        if chrono_tz::Tz::from_str(&tzid).is_err() {
91            return Err(Error::PreconditionFailed(Precondition::CalendarTimezone(
92                "Invalid timezone ID in calendar-timezone-id",
93            )));
94        }
95        Some(tzid)
96    } else if let Some(tz) = request.calendar_timezone {
97        let calendar = IcalParser::from_slice(tz.as_bytes())
98            .next()
99            .ok_or(Error::PreconditionFailed(Precondition::CalendarTimezone(
100                "No timezone data provided",
101            )))?
102            .map_err(|_| {
103                Error::PreconditionFailed(Precondition::CalendarTimezone("Error parsing timezone"))
104            })?;
105
106        let timezone = calendar
107            .vtimezones
108            .values()
109            .next()
110            .ok_or(Error::PreconditionFailed(Precondition::CalendarTimezone(
111                "No timezone data provided",
112            )))?;
113        let timezone: Option<chrono_tz::Tz> = timezone.into();
114        let timezone = timezone.ok_or(Error::PreconditionFailed(
115            Precondition::CalendarTimezone("No timezone data provided"),
116        ))?;
117
118        Some(timezone.name().to_owned())
119    } else {
120        None
121    };
122
123    let calendar = Calendar {
124        id: cal_id.clone(),
125        principal: principal.clone(),
126        meta: CalendarMetadata {
127            order: request.calendar_order.unwrap_or(0),
128            displayname: request.displayname,
129            color: request.calendar_color,
130            description: request.calendar_description,
131        },
132        timezone_id,
133        deleted_at: None,
134        synctoken: 0,
135        subscription_url: request.source.map(|href| href.href),
136        push_topic: uuid::Uuid::new_v4().to_string(),
137        components: request.supported_calendar_component_set.map_or_else(
138            || {
139                vec![
140                    CalendarObjectType::Event,
141                    CalendarObjectType::Todo,
142                    CalendarObjectType::Journal,
143                ]
144            },
145            Into::into,
146        ),
147    };
148
149    cal_store.insert_calendar(calendar).await?;
150    // The spec says we don't have to return a response everything was successful
151    Ok(StatusCode::CREATED.into_response())
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_xml_mkcalendar() {
160        MkcalendarRequest::parse_str(r#"
161            <?xml version='1.0' encoding='UTF-8' ?>
162            <CAL:mkcalendar xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
163                <set>
164                    <prop>
165                        <resourcetype>
166                            <collection />
167                            <CAL:calendar />
168                        </resourcetype>
169                        <displayname>jfs</displayname>
170                        <CAL:calendar-description>rggg</CAL:calendar-description>
171                        <n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">#FFF8DCFF</n0:calendar-color>
172                        <CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
173                        <CAL:supported-calendar-component-set>
174                            <CAL:comp name="VEVENT"/>
175                            <CAL:comp name="VTODO"/>
176                            <CAL:comp name="VJOURNAL"/>
177                        </CAL:supported-calendar-component-set>
178                        <CAL:calendar-timezone>BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nLAST-MODIFIED:20240422T053450Z\r\nTZURL:https://www.tzurl.org/zoneinfo/Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nX-PROLEPTIC-TZNAME:LMT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+005328\r\nTZOFFSETTO:+0100\r\nDTSTART:18930401T000632\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19160430T230000\r\nRDATE:19400401T020000\r\nRDATE:19430329T020000\r\nRDATE:19460414T020000\r\nRDATE:19470406T030000\r\nRDATE:19480418T020000\r\nRDATE:19490410T020000\r\nRDATE:19800406T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19161001T010000\r\nRDATE:19421102T030000\r\nRDATE:19431004T030000\r\nRDATE:19441002T030000\r\nRDATE:19451118T030000\r\nRDATE:19461007T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19170416T020000\r\nRRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19170917T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19440403T020000\r\nRRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEMT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nDTSTART:19450524T000000\r\nRDATE:19470511T010000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nDTSTART:19450924T030000\r\nRDATE:19470629T030000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nDTSTART:19460101T000000\r\nRDATE:19800101T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19471005T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19800928T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19810329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19961027T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR\r\n</CAL:calendar-timezone>
179                    </prop>
180                </set>
181            </CAL:mkcalendar>
182    "#).unwrap();
183    }
184
185    #[test]
186    fn test_xml_mkcol() {
187        MkcolRequest::parse_str(r#"
188            <?xml version='1.0' encoding='UTF-8' ?>
189            <mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
190                <set>
191                    <prop>
192                        <resourcetype>
193                            <collection />
194                            <CAL:calendar />
195                        </resourcetype>
196                        <displayname>jfs</displayname>
197                        <CAL:calendar-description>rggg</CAL:calendar-description>
198                        <n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">#FFF8DCFF</n0:calendar-color>
199                        <CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
200                        <CAL:supported-calendar-component-set>
201                            <CAL:comp name="VEVENT"/>
202                            <CAL:comp name="VTODO"/>
203                            <CAL:comp name="VJOURNAL"/>
204                        </CAL:supported-calendar-component-set>
205                        <CAL:calendar-timezone>BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nLAST-MODIFIED:20240422T053450Z\r\nTZURL:https://www.tzurl.org/zoneinfo/Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nX-PROLEPTIC-TZNAME:LMT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+005328\r\nTZOFFSETTO:+0100\r\nDTSTART:18930401T000632\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19160430T230000\r\nRDATE:19400401T020000\r\nRDATE:19430329T020000\r\nRDATE:19460414T020000\r\nRDATE:19470406T030000\r\nRDATE:19480418T020000\r\nRDATE:19490410T020000\r\nRDATE:19800406T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19161001T010000\r\nRDATE:19421102T030000\r\nRDATE:19431004T030000\r\nRDATE:19441002T030000\r\nRDATE:19451118T030000\r\nRDATE:19461007T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19170416T020000\r\nRRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19170917T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19440403T020000\r\nRRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEMT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nDTSTART:19450524T000000\r\nRDATE:19470511T010000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nDTSTART:19450924T030000\r\nRDATE:19470629T030000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nDTSTART:19460101T000000\r\nRDATE:19800101T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19471005T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19800928T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19810329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19961027T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR\r\n</CAL:calendar-timezone>
206                    </prop>
207                </set>
208            </mkcol>
209    "#).unwrap();
210    }
211}