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_dav_push::DavPushStore;
11use rustical_ical::CalendarObjectType;
12use rustical_store::auth::Principal;
13use rustical_store::{Calendar, CalendarMetadata, CalendarStore};
14use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
15use std::str::FromStr;
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 #[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, DP: DavPushStore>(
69 Path((principal, cal_id)): Path<(String, String)>,
70 user: Principal,
71 State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, DP>>,
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(|err| {
103 tracing::error!(%err);
104 Error::PreconditionFailed(Precondition::CalendarTimezone("Error parsing timezone"))
105 })
106 .inspect_err(|e| tracing::error!(%e))?;
107
108 let timezone = calendar
109 .vtimezones
110 .values()
111 .next()
112 .ok_or(Error::PreconditionFailed(Precondition::CalendarTimezone(
113 "No timezone data provided",
114 )))?;
115 let timezone: Option<chrono_tz::Tz> = timezone.into();
116 let timezone = timezone.ok_or(Error::PreconditionFailed(
117 Precondition::CalendarTimezone("No timezone data provided"),
118 ))?;
119
120 Some(timezone.name().to_owned())
121 } else {
122 None
123 };
124
125 let calendar = Calendar {
126 id: cal_id.clone(),
127 principal: principal.clone(),
128 meta: CalendarMetadata {
129 order: request.calendar_order.unwrap_or(0),
130 displayname: request.displayname,
131 color: request.calendar_color,
132 description: request.calendar_description,
133 },
134 timezone_id,
135 deleted_at: None,
136 synctoken: 0,
137 subscription_url: request.source.map(|href| href.href.to_string()),
138 push_topic: uuid::Uuid::new_v4().to_string(),
139 components: request.supported_calendar_component_set.map_or_else(
140 || {
141 vec![
142 CalendarObjectType::Event,
143 CalendarObjectType::Todo,
144 CalendarObjectType::Journal,
145 ]
146 },
147 Into::into,
148 ),
149 };
150
151 match cal_store.insert_calendar(calendar).await {
152 Err(rustical_store::Error::AlreadyExists) => {
156 return Ok((
157 StatusCode::METHOD_NOT_ALLOWED,
158 "A calendar already exists at this URI",
159 )
160 .into_response());
161 }
162 Err(err) => return Err(err.into()),
163 _ => {}
164 }
165 Ok(StatusCode::CREATED.into_response())
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_xml_mkcalendar() {
175 MkcalendarRequest::parse_str(r#"
176 <?xml version='1.0' encoding='UTF-8' ?>
177 <CAL:mkcalendar xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
178 <set>
179 <prop>
180 <resourcetype>
181 <collection />
182 <CAL:calendar />
183 </resourcetype>
184 <displayname>jfs</displayname>
185 <CAL:calendar-description>rggg</CAL:calendar-description>
186 <n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">#FFF8DCFF</n0:calendar-color>
187 <CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
188 <CAL:supported-calendar-component-set>
189 <CAL:comp name="VEVENT"/>
190 <CAL:comp name="VTODO"/>
191 <CAL:comp name="VJOURNAL"/>
192 </CAL:supported-calendar-component-set>
193 <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>
194 </prop>
195 </set>
196 </CAL:mkcalendar>
197 "#).unwrap();
198 }
199
200 #[test]
201 fn test_xml_mkcol() {
202 MkcolRequest::parse_str(r#"
203 <?xml version='1.0' encoding='UTF-8' ?>
204 <mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
205 <set>
206 <prop>
207 <resourcetype>
208 <collection />
209 <CAL:calendar />
210 </resourcetype>
211 <displayname>jfs</displayname>
212 <CAL:calendar-description>rggg</CAL:calendar-description>
213 <n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">#FFF8DCFF</n0:calendar-color>
214 <CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
215 <CAL:supported-calendar-component-set>
216 <CAL:comp name="VEVENT"/>
217 <CAL:comp name="VTODO"/>
218 <CAL:comp name="VJOURNAL"/>
219 </CAL:supported-calendar-component-set>
220 <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>
221 </prop>
222 </set>
223 </mkcol>
224 "#).unwrap();
225 }
226
227 #[test]
228 fn test_xml_mkcol_apple() {
229 let mkcol = MkcalendarRequest::parse_str(
230 r#"
231<?xml version="1.0" encoding="UTF-8"?>
232<B:mkcalendar xmlns:B="urn:ietf:params:xml:ns:caldav">
233 <A:set xmlns:A="DAV:">
234 <A:prop>
235 <B:calendar-free-busy-set>
236 <NO/>
237 </B:calendar-free-busy-set>
238 <G:calendar-color xmlns:G="http://apple.com/ns/ical/" symbolic-color="custom">#0088FF</G:calendar-color>
239 <A:displayname>rdgrdgre</A:displayname>
240 <B:supported-calendar-component-set>
241 <B:comp name="VEVENT"/>
242 </B:supported-calendar-component-set>
243 <B:calendar-timezone>BEGIN:VCALENDAR
244VERSION:2.0
245CALSCALE:GREGORIAN
246BEGIN:VTIMEZONE
247TZID:Europe/Berlin
248BEGIN:DAYLIGHT
249TZOFFSETFROM:+0100
250RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
251DTSTART:19810329T020000
252TZNAME:CEST
253TZOFFSETTO:+0200
254END:DAYLIGHT
255BEGIN:STANDARD
256TZOFFSETFROM:+0200
257RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
258DTSTART:19961027T030000
259TZNAME:CET
260TZOFFSETTO:+0100
261END:STANDARD
262END:VTIMEZONE
263END:VCALENDAR
264</B:calendar-timezone>
265 <G:calendar-order xmlns:G="http://apple.com/ns/ical/">5116</G:calendar-order>
266 </A:prop>
267 </A:set>
268</B:mkcalendar>
269 "#,
270 )
271 .unwrap();
272
273 insta::assert_debug_snapshot!(mkcol);
274 }
275}