rustical_caldav/calendar/methods/
mkcalendar.rs1use 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 #[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 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}