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