//! REPORT request bodies. use xmltree::Element; use crate::filter::{CardFilter, CompFilter, TimeRange, calendar_filter, card_filter, time_range}; use crate::principal::{Search, calendarserver_search, property_search}; use crate::render::{AddressData, CalendarData, address_request, calendar_request}; use crate::xml::{CALDAV, CALSERVER, CARDDAV, DAV, Name, Propfind, child, elements, text}; use crate::zone::{self, Zone}; /// Why a REPORT body is refused. #[derive(Debug, Clone, PartialEq)] pub enum Refused { /// Malformed: 400. Invalid, /// Understood but not served: 403 naming this precondition. Condition(Name), } #[derive(Debug, Clone, PartialEq)] pub struct Props { pub find: Propfind, /// The request of a `calendar-data` element among the properties. pub calendar: Option, pub address: Option, } #[derive(Debug, Clone)] pub enum Report { CalendarMultiget { props: Props, hrefs: Vec, }, AddressbookMultiget { props: Props, hrefs: Vec, }, CalendarQuery { props: Props, filter: CompFilter, /// From `timezone` or `timezone-id`, for floating times. timezone: Option, }, AddressbookQuery { props: Props, filter: CardFilter, limit: Option, }, SyncCollection { /// Empty for the first sync. token: String, props: Props, limit: Option, }, FreeBusy(TimeRange), PrincipalSearch(Search), PrincipalSearchPropertySet, } pub fn parse(body: &[u8]) -> Result { let root = Element::parse(body).map_err(|_| Refused::Invalid)?; let hrefs = || { elements(&root) .filter(|e| Name::of(e).is(DAV, "href")) .map(text) .collect() }; let name = Name::of(&root); Ok(match (name.ns.as_str(), name.local.as_str()) { (CALDAV, "calendar-multiget") => Report::CalendarMultiget { props: props(&root)?, hrefs: hrefs(), }, (CARDDAV, "addressbook-multiget") => Report::AddressbookMultiget { props: props(&root)?, hrefs: hrefs(), }, (CALDAV, "calendar-query") => { let filter = child(&root, CALDAV, "filter").ok_or(Refused::Invalid)?; let timezone = match ( child(&root, CALDAV, "timezone"), child(&root, CALDAV, "timezone-id"), ) { (Some(tz), _) => { Some(zone::from_vtimezone(&text(tz)).ok_or_else(|| { Refused::Condition(Name::new(CALDAV, "valid-calendar-data")) })?) } (None, Some(id)) => Some( zone::named(&text(id)) .ok_or_else(|| Refused::Condition(Name::new(CALDAV, "valid-timezone")))?, ), (None, None) => None, }; Report::CalendarQuery { props: props(&root)?, filter: calendar_filter(filter).map_err(|r| invalid_filter(r, CALDAV))?, timezone, } } (CARDDAV, "addressbook-query") => Report::AddressbookQuery { props: props(&root)?, filter: card_filter(child(&root, CARDDAV, "filter").ok_or(Refused::Invalid)?) .map_err(|r| invalid_filter(r, CARDDAV))?, limit: limit(&root, CARDDAV)?, }, (DAV, "sync-collection") => { let level = child(&root, DAV, "sync-level").map(text); // Collections here hold no collections, so both levels agree. if !matches!(level.as_deref(), None | Some("1" | "infinite")) { return Err(Refused::Invalid); } Report::SyncCollection { token: child(&root, DAV, "sync-token") .map(text) .unwrap_or_default(), props: props(&root)?, limit: limit(&root, DAV)?, } } (CALDAV, "free-busy-query") => { let range = time_range(child(&root, CALDAV, "time-range").ok_or(Refused::Invalid)?)?; let open = chrono::DateTime::::MIN_UTC..chrono::DateTime::::MAX_UTC; if range.start == open.start || range.end == open.end { return Err(Refused::Invalid); } Report::FreeBusy(range) } (DAV, "principal-property-search") => Report::PrincipalSearch(property_search(&root)?), (CALSERVER, "calendarserver-principal-search") => { Report::PrincipalSearch(calendarserver_search(&root)?) } (DAV, "principal-search-property-set") => Report::PrincipalSearchPropertySet, _ => return Err(Refused::Condition(Name::new(DAV, "supported-report"))), }) } /// A filter that does not parse is `valid-filter` in the namespace `ns`. fn invalid_filter(r: Refused, ns: &str) -> Refused { match r { Refused::Invalid => Refused::Condition(Name::new(ns, "valid-filter")), c => c, } } /// The requested properties. Without any, only the hrefs come back. fn props(root: &Element) -> Result { let mut out = Props { find: Propfind::Prop(Vec::new()), calendar: None, address: None, }; for e in elements(root).filter(|e| Name::of(e).ns == DAV) { match e.name.as_str() { "allprop" => out.find = Propfind::AllProp(Vec::new()), "propname" => out.find = Propfind::PropName, "prop" => { let mut names = Vec::new(); for p in elements(e) { let n = Name::of(p); if n.is(CALDAV, "calendar-data") { out.calendar = Some(calendar_request(p)?); } else if n.is(CARDDAV, "address-data") { out.address = Some(address_request(p)?); } names.push(n); } out.find = Propfind::Prop(names); } _ => {} } } Ok(out) } /// `n` in the namespace `ns`. fn limit(root: &Element, ns: &str) -> Result, Refused> { child(root, ns, "limit") .and_then(|l| child(l, ns, "nresults")) .map(|n| text(n).parse().map_err(|_| Refused::Invalid)) .transpose() }