rustical_caldav/calendar/methods/
mkcalendar.rs1use crate::Error;
2use crate::calendar::CalendarResourceService;
3use crate::calendar::prop::SupportedCalendarComponentSet;
4use crate::error::Precondition;
5use axum::extract::{Path, State};
6use axum::response::{IntoResponse, Response};
7use caldata::IcalParser;
8use http::{Method, StatusCode};
9use rustical_dav::xml::HrefElement;
10use rustical_ical::CalendarObjectType;
11use rustical_store::auth::Principal;
12use rustical_store::{Calendar, CalendarMetadata, CalendarStore, SubscriptionStore};
13use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
14use std::str::FromStr;
15use tracing::instrument;
16
17#[derive(XmlDeserialize, Clone, Debug)]
18pub struct MkcolCalendarProp {
19 #[xml(ns = "rustical_dav::namespace::NS_DAV")]
20 displayname: Option<String>,
21 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
22 calendar_description: Option<String>,
23 #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
24 calendar_color: Option<String>,
25 #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
26 calendar_order: Option<i64>,
27 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
28 calendar_timezone: Option<String>,
29 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
30 calendar_timezone_id: Option<String>,
31 #[xml(ns = "rustical_dav::namespace::NS_DAV")]
32 #[allow(dead_code)]
33 resourcetype: Option<Unparsed>,
34 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
35 supported_calendar_component_set: Option<SupportedCalendarComponentSet>,
36 #[xml(ns = "rustical_dav::namespace::NS_CALENDARSERVER")]
37 source: Option<HrefElement>,
38 #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
40 #[allow(dead_code)]
41 calendar_free_busy_set: Option<Unparsed>,
42}
43
44#[derive(XmlDeserialize, Clone, Debug)]
45pub struct PropElement {
46 #[xml(ns = "rustical_dav::namespace::NS_DAV")]
47 prop: MkcolCalendarProp,
48}
49
50#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
51#[xml(root = "mkcalendar")]
52#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
53struct MkcalendarRequest {
54 #[xml(ns = "rustical_dav::namespace::NS_DAV")]
55 set: PropElement,
56}
57
58#[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
59#[xml(root = "mkcol")]
60#[xml(ns = "rustical_dav::namespace::NS_DAV")]
61struct MkcolRequest {
62 #[xml(ns = "rustical_dav::namespace::NS_DAV")]
63 set: PropElement,
64}
65
66#[instrument(skip(cal_store))]
67pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
68 Path((principal, cal_id)): Path<(String, String)>,
69 user: Principal,
70 State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
71 method: Method,
72 body: String,
73) -> Result<Response, Error> {
74 if !user.is_principal(&principal) {
75 return Err(Error::Unauthorized);
76 }
77
78 let mut request = match method.as_str() {
79 "MKCALENDAR" => MkcalendarRequest::parse_str(&body)?.set.prop,
80 "MKCOL" => MkcolRequest::parse_str(&body)?.set.prop,
81 _ => unreachable!("We never call with another method"),
82 };
83
84 if request.displayname.as_deref() == Some("") {
85 request.displayname = None;
86 }
87
88 let timezone_id = if let Some(tzid) = request.calendar_timezone_id {
89 if chrono_tz::Tz::from_str(&tzid).is_err() {
90 return Err(Error::PreconditionFailed(Precondition::CalendarTimezone(
91 "Invalid timezone ID in calendar-timezone-id",
92 )));
93 }
94 Some(tzid)
95 } else if let Some(tz) = request.calendar_timezone {
96 let calendar = IcalParser::from_slice(tz.as_bytes())
97 .next()
98 .ok_or(Error::PreconditionFailed(Precondition::CalendarTimezone(
99 "No timezone data provided",
100 )))?
101 .map_err(|err| {
102 tracing::error!(%err);
103 Error::PreconditionFailed(Precondition::CalendarTimezone("Error parsing timezone"))
104 })
105 .inspect_err(|e| tracing::error!(%e))?;
106
107 let timezone = calendar
108 .vtimezones
109 .values()
110 .next()
111 .ok_or(Error::PreconditionFailed(Precondition::CalendarTimezone(
112 "No timezone data provided",
113 )))?;
114 let timezone: Option<chrono_tz::Tz> = timezone.into();
115 let timezone = timezone.ok_or(Error::PreconditionFailed(
116 Precondition::CalendarTimezone("No timezone data provided"),
117 ))?;
118
119 Some(timezone.name().to_owned())
120 } else {
121 None
122 };
123
124 let calendar = Calendar {
125 id: cal_id.clone(),
126 principal: principal.clone(),
127 meta: CalendarMetadata {
128 order: request.calendar_order.unwrap_or(0),
129 displayname: request.displayname,
130 color: request.calendar_color,
131 description: request.calendar_description,
132 },
133 timezone_id,
134 deleted_at: None,
135 synctoken: 0,
136 subscription_url: request.source.map(|href| href.href),
137 push_topic: uuid::Uuid::new_v4().to_string(),
138 components: request.supported_calendar_component_set.map_or_else(
139 || {
140 vec![
141 CalendarObjectType::Event,
142 CalendarObjectType::Todo,
143 CalendarObjectType::Journal,
144 ]
145 },
146 Into::into,
147 ),
148 };
149
150 cal_store.insert_calendar(calendar).await?;
151 Ok(StatusCode::CREATED.into_response())
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_xml_mkcalendar() {
161 MkcalendarRequest::parse_str(r#"
162 <?xml version='1.0' encoding='UTF-8' ?>
163 <CAL:mkcalendar xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
164 <set>
165 <prop>
166 <resourcetype>
167 <collection />
168 <CAL:calendar />
169 </resourcetype>
170 <displayname>jfs</displayname>
171 <CAL:calendar-description>rggg</CAL:calendar-description>
172 <n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">#FFF8DCFF</n0:calendar-color>
173 <CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
174 <CAL:supported-calendar-component-set>
175 <CAL:comp name="VEVENT"/>
176 <CAL:comp name="VTODO"/>
177 <CAL:comp name="VJOURNAL"/>
178 </CAL:supported-calendar-component-set>
179 <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>
180 </prop>
181 </set>
182 </CAL:mkcalendar>
183 "#).unwrap();
184 }
185
186 #[test]
187 fn test_xml_mkcol() {
188 MkcolRequest::parse_str(r#"
189 <?xml version='1.0' encoding='UTF-8' ?>
190 <mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
191 <set>
192 <prop>
193 <resourcetype>
194 <collection />
195 <CAL:calendar />
196 </resourcetype>
197 <displayname>jfs</displayname>
198 <CAL:calendar-description>rggg</CAL:calendar-description>
199 <n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">#FFF8DCFF</n0:calendar-color>
200 <CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
201 <CAL:supported-calendar-component-set>
202 <CAL:comp name="VEVENT"/>
203 <CAL:comp name="VTODO"/>
204 <CAL:comp name="VJOURNAL"/>
205 </CAL:supported-calendar-component-set>
206 <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>
207 </prop>
208 </set>
209 </mkcol>
210 "#).unwrap();
211 }
212
213 #[test]
214 fn test_xml_mkcol_apple() {
215 let mkcol = MkcalendarRequest::parse_str(
216 r#"
217<?xml version="1.0" encoding="UTF-8"?>
218<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
219 <A:set xmlns:A="DAV:">
220 <A:prop>
221 <B:calendar-free-busy-set>
222 <NO/>
223 </B:calendar-free-busy-set>
224 <G:calendar-color xmlns:G="http://apple.com/ns/ical/" symbolic-color="custom">#0088FF</G:calendar-color>
225 <A:displayname>rdgrdgre</A:displayname>
226 <B:supported-calendar-component-set>
227 <B:comp name="VEVENT"/>
228 </B:supported-calendar-component-set>
229 <B:calendar-timezone>BEGIN:VCALENDAR
230VERSION:2.0
231CALSCALE:GREGORIAN
232BEGIN:VTIMEZONE
233TZID:Europe/Berlin
234BEGIN:DAYLIGHT
235TZOFFSETFROM:+0100
236RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
237DTSTART:19810329T020000
238TZNAME:CEST
239TZOFFSETTO:+0200
240END:DAYLIGHT
241BEGIN:STANDARD
242TZOFFSETFROM:+0200
243RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
244DTSTART:19961027T030000
245TZNAME:CET
246TZOFFSETTO:+0100
247END:STANDARD
248END:VTIMEZONE
249END:VCALENDAR
250</B:calendar-timezone>
251 <G:calendar-order xmlns:G="http://apple.com/ns/ical/">5116</G:calendar-order>
252 </A:prop>
253 </A:set>
254</B:mkcalendar>
255 "#,
256 )
257 .unwrap();
258
259 insta::assert_debug_snapshot!(mkcol);
260 }
261}