report.rs
⎇
Raw
1//! REPORT request bodies.
2
3use xmltree::Element;
4
5use crate::filter::{CardFilter, CompFilter, TimeRange, calendar_filter, card_filter, time_range};
6use crate::principal::{Search, calendarserver_search, property_search};
7use crate::render::{AddressData, CalendarData, address_request, calendar_request};
8use crate::xml::{CALDAV, CALSERVER, CARDDAV, DAV, Name, Propfind, child, elements, text};
9use crate::zone::{self, Zone};
10
11/// Why a REPORT body is refused.
12#[derive(Debug, Clone, PartialEq)]
13pub enum Refused {
14 /// Malformed: 400.
15 Invalid,
16 /// Understood but not served: 403 naming this precondition.
17 Condition(Name),
18}
19
20#[derive(Debug, Clone, PartialEq)]
21pub struct Props {
22 pub find: Propfind,
23 /// The request of a `calendar-data` element among the properties.
24 pub calendar: Option<CalendarData>,
25 pub address: Option<AddressData>,
26}
27
28#[derive(Debug, Clone)]
29pub enum Report {
30 CalendarMultiget {
31 props: Props,
32 hrefs: Vec<String>,
33 },
34 AddressbookMultiget {
35 props: Props,
36 hrefs: Vec<String>,
37 },
38 CalendarQuery {
39 props: Props,
40 filter: CompFilter,
41 /// From `timezone` or `timezone-id`, for floating times.
42 timezone: Option<Zone>,
43 },
44 AddressbookQuery {
45 props: Props,
46 filter: CardFilter,
47 limit: Option<usize>,
48 },
49 SyncCollection {
50 /// Empty for the first sync.
51 token: String,
52 props: Props,
53 limit: Option<usize>,
54 },
55 FreeBusy(TimeRange),
56 PrincipalSearch(Search),
57 PrincipalSearchPropertySet,
58}
59
60pub fn parse(body: &[u8]) -> Result<Report, Refused> {
61 let root = Element::parse(body).map_err(|_| Refused::Invalid)?;
62 let hrefs = || {
63 elements(&root)
64 .filter(|e| Name::of(e).is(DAV, "href"))
65 .map(text)
66 .collect()
67 };
68 let name = Name::of(&root);
69 Ok(match (name.ns.as_str(), name.local.as_str()) {
70 (CALDAV, "calendar-multiget") => Report::CalendarMultiget {
71 props: props(&root)?,
72 hrefs: hrefs(),
73 },
74 (CARDDAV, "addressbook-multiget") => Report::AddressbookMultiget {
75 props: props(&root)?,
76 hrefs: hrefs(),
77 },
78 (CALDAV, "calendar-query") => {
79 let filter = child(&root, CALDAV, "filter").ok_or(Refused::Invalid)?;
80 let timezone = match (
81 child(&root, CALDAV, "timezone"),
82 child(&root, CALDAV, "timezone-id"),
83 ) {
84 (Some(tz), _) => {
85 Some(zone::from_vtimezone(&text(tz)).ok_or_else(|| {
86 Refused::Condition(Name::new(CALDAV, "valid-calendar-data"))
87 })?)
88 }
89 (None, Some(id)) => Some(
90 zone::named(&text(id))
91 .ok_or_else(|| Refused::Condition(Name::new(CALDAV, "valid-timezone")))?,
92 ),
93 (None, None) => None,
94 };
95 Report::CalendarQuery {
96 props: props(&root)?,
97 filter: calendar_filter(filter).map_err(|r| invalid_filter(r, CALDAV))?,
98 timezone,
99 }
100 }
101 (CARDDAV, "addressbook-query") => Report::AddressbookQuery {
102 props: props(&root)?,
103 filter: card_filter(child(&root, CARDDAV, "filter").ok_or(Refused::Invalid)?)
104 .map_err(|r| invalid_filter(r, CARDDAV))?,
105 limit: limit(&root, CARDDAV)?,
106 },
107 (DAV, "sync-collection") => {
108 let level = child(&root, DAV, "sync-level").map(text);
109 // Collections here hold no collections, so both levels agree.
110 if !matches!(level.as_deref(), None | Some("1" | "infinite")) {
111 return Err(Refused::Invalid);
112 }
113 Report::SyncCollection {
114 token: child(&root, DAV, "sync-token")
115 .map(text)
116 .unwrap_or_default(),
117 props: props(&root)?,
118 limit: limit(&root, DAV)?,
119 }
120 }
121 (CALDAV, "free-busy-query") => {
122 let range = time_range(child(&root, CALDAV, "time-range").ok_or(Refused::Invalid)?)?;
123 let open =
124 chrono::DateTime::<chrono::Utc>::MIN_UTC..chrono::DateTime::<chrono::Utc>::MAX_UTC;
125 if range.start == open.start || range.end == open.end {
126 return Err(Refused::Invalid);
127 }
128 Report::FreeBusy(range)
129 }
130 (DAV, "principal-property-search") => Report::PrincipalSearch(property_search(&root)?),
131 (CALSERVER, "calendarserver-principal-search") => {
132 Report::PrincipalSearch(calendarserver_search(&root)?)
133 }
134 (DAV, "principal-search-property-set") => Report::PrincipalSearchPropertySet,
135 _ => return Err(Refused::Condition(Name::new(DAV, "supported-report"))),
136 })
137}
138
139/// A filter that does not parse is `valid-filter` in the namespace `ns`.
140fn invalid_filter(r: Refused, ns: &str) -> Refused {
141 match r {
142 Refused::Invalid => Refused::Condition(Name::new(ns, "valid-filter")),
143 c => c,
144 }
145}
146
147/// The requested properties. Without any, only the hrefs come back.
148fn props(root: &Element) -> Result<Props, Refused> {
149 let mut out = Props {
150 find: Propfind::Prop(Vec::new()),
151 calendar: None,
152 address: None,
153 };
154 for e in elements(root).filter(|e| Name::of(e).ns == DAV) {
155 match e.name.as_str() {
156 "allprop" => out.find = Propfind::AllProp(Vec::new()),
157 "propname" => out.find = Propfind::PropName,
158 "prop" => {
159 let mut names = Vec::new();
160 for p in elements(e) {
161 let n = Name::of(p);
162 if n.is(CALDAV, "calendar-data") {
163 out.calendar = Some(calendar_request(p)?);
164 } else if n.is(CARDDAV, "address-data") {
165 out.address = Some(address_request(p)?);
166 }
167 names.push(n);
168 }
169 out.find = Propfind::Prop(names);
170 }
171 _ => {}
172 }
173 }
174 Ok(out)
175}
176
177/// `<limit><nresults>n</nresults></limit>` in the namespace `ns`.
178fn limit(root: &Element, ns: &str) -> Result<Option<usize>, Refused> {
179 child(root, ns, "limit")
180 .and_then(|l| child(l, ns, "nresults"))
181 .map(|n| text(n).parse().map_err(|_| Refused::Invalid))
182 .transpose()
183}
184