//! The `calendar-data` and `address-data` a REPORT returns: the whole //! object, a part of it, or its recurrence set expanded (RFC 4791, 9.6; //! RFC 6352, 10.4). use std::collections::HashSet; use calcard::common::PartialDateTime; use calcard::icalendar::{ ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarParameter, ICalendarParameterName, ICalendarParameterValue, ICalendarProperty, ICalendarValue, ICalendarValueType, }; use calcard::vcard::{VCard, VCardVersion}; use chrono::{DateTime, Utc}; use xmltree::Element; use crate::expand::expand; use crate::filter::{TimeRange, time_range}; use crate::freebusy::period; use crate::report::Refused; use crate::xml::{CALDAV, CARDDAV, Name, elements}; use crate::zone::{Zone, Zones}; /// Instances one object may expand into in one response. const MAX_EXPANDED: usize = 10_000; #[derive(Debug, Clone, Default, PartialEq)] pub struct CalendarData { /// `None` returns every component and property. pub comp: Option, pub expand: Option, pub limit_recurrence: Option, pub limit_freebusy: Option, } #[derive(Debug, Clone, PartialEq)] pub struct CompSelect { /// Upper case. pub name: String, /// `None` for `allprop`. pub props: Option>, /// `None` for `allcomp`. pub comps: Option>, } #[derive(Debug, Clone, PartialEq)] pub struct PropSelect { pub name: String, pub novalue: bool, } #[derive(Debug, Clone, Default, PartialEq)] pub struct AddressData { /// `None` for `allprop`. pub props: Option>, pub version: Option, } /// The expansion would exceed [`MAX_EXPANDED`] instances. #[derive(Debug, PartialEq, Eq)] pub struct TooManyInstances; pub fn calendar_request(e: &Element) -> Result { let content_type = e.attributes.get("content-type").map(String::as_str); let version = e.attributes.get("version").map(String::as_str); if content_type.is_some_and(|t| t != "text/calendar") || version.is_some_and(|v| v != "2.0") { return Err(Refused::Condition(Name::new( CALDAV, "supported-calendar-data", ))); } let mut d = CalendarData::default(); for c in elements(e).filter(|c| Name::of(c).ns == CALDAV) { match c.name.as_str() { "comp" => d.comp = Some(comp_select(c)?), "expand" => d.expand = Some(time_range(c)?), "limit-recurrence-set" => d.limit_recurrence = Some(time_range(c)?), "limit-freebusy-set" => d.limit_freebusy = Some(time_range(c)?), _ => {} } } Ok(d) } fn comp_select(e: &Element) -> Result { let name = e.attributes.get("name").ok_or(Refused::Invalid)?; let has = |local: &str| elements(e).any(|c| Name::of(c).is(CALDAV, local)); let props = (!has("allprop")).then(|| prop_selects(e, CALDAV)); let comps = match has("allcomp") { true => None, false => Some( elements(e) .filter(|c| Name::of(c).is(CALDAV, "comp")) .map(comp_select) .collect::>()?, ), }; Ok(CompSelect { name: name.to_ascii_uppercase(), props, comps, }) } fn prop_selects(e: &Element, ns: &str) -> Vec { elements(e) .filter(|c| Name::of(c).is(ns, "prop")) .filter_map(|c| { Some(PropSelect { name: c.attributes.get("name")?.clone(), novalue: c.attributes.get("novalue").map(String::as_str) == Some("yes"), }) }) .collect() } pub fn address_request(e: &Element) -> Result { let unsupported = || Refused::Condition(Name::new(CARDDAV, "supported-address-data")); if e.attributes .get("content-type") .is_some_and(|t| t != "text/vcard") { return Err(unsupported()); } // RFC 6352, 10.4: without a version, 3.0. let version = match e.attributes.get("version").map(String::as_str) { None | Some("3.0") => Some(VCardVersion::V3_0), Some("4.0") => Some(VCardVersion::V4_0), Some(_) => return Err(unsupported()), }; let all = elements(e).any(|c| Name::of(c).is(CARDDAV, "allprop")); let props = prop_selects(e, CARDDAV); Ok(AddressData { props: (!all && !props.is_empty()).then_some(props), version, }) } /// The calendar-data of one object. The stored text is returned unchanged /// unless the request narrows or expands it. pub fn calendar_data( raw: &str, req: &CalendarData, floating: &Zone, ) -> Result { if *req == CalendarData::default() { return Ok(raw.to_string()); } let Ok(mut cal) = ICalendar::parse(raw) else { return Ok(raw.to_string()); }; if let Some(r) = &req.expand { cal = expanded(&cal, r, floating)?; } if let Some(r) = &req.limit_recurrence { cal = limit_recurrence(&cal, r, floating); } if let Some(r) = &req.limit_freebusy { limit_freebusy(&mut cal, r, floating); } if let Some(sel) = &req.comp { let mut out = Vec::new(); copy(&cal, 0, Some(sel), &mut out); cal = ICalendar { components: out }; } Ok(cal.to_string()) } /// One component per instance in `range`, in UTC, with a RECURRENCE-ID and /// without recurrence rules (RFC 4791, 9.6.5). Dates stay dates. fn expanded( cal: &ICalendar, range: &TimeRange, floating: &Zone, ) -> Result { let exp = expand(cal, range.clone(), floating.clone()); if exp.truncated || exp.instances.len() > MAX_EXPANDED { return Err(TooManyInstances); } let mut out = vec![root(cal)]; for x in exp.instances { let src = &cal.components[x.component]; let date = src .property(&ICalendarProperty::Dtstart) .and_then(|e| e.values.first()?.as_partial_date_time()) .is_some_and(|p| p.hour.is_none()); let at = copy(cal, x.component, None, &mut out); let c = &mut out[at as usize]; c.entries.retain(|e| { !matches!( e.name, ICalendarProperty::Rrule | ICalendarProperty::Rdate | ICalendarProperty::Exdate | ICalendarProperty::Exrule | ICalendarProperty::Dtstart | ICalendarProperty::Dtend | ICalendarProperty::Due | ICalendarProperty::RecurrenceId ) }); let when = |name: ICalendarProperty, t: DateTime| { let (value, params) = if date { let day = floating.to_local(t).and_utc().timestamp(); let param = ICalendarParameter::new( ICalendarParameterName::Value, ICalendarParameterValue::Value(ICalendarValueType::Date), ); (PartialDateTime::from_date_timestamp(day), vec![param]) } else { (PartialDateTime::from_utc_timestamp(t.timestamp()), vec![]) }; ICalendarEntry { name, params, values: vec![ICalendarValue::PartialDateTime(Box::new(value))], } }; c.entries.push(when(ICalendarProperty::Dtstart, x.start)); if src.has_property(&ICalendarProperty::Dtend) { c.entries.push(when(ICalendarProperty::Dtend, x.end)); } if src.has_property(&ICalendarProperty::Due) { c.entries.push(when(ICalendarProperty::Due, x.end)); } if let Some(rid) = x.recurrence_id { c.entries.push(when(ICalendarProperty::RecurrenceId, rid)); } out[0].component_ids.push(at); } Ok(ICalendar { components: out }) } /// The masters, and only the overrides that affect `range` (RFC 4791, /// 9.6.6). fn limit_recurrence(cal: &ICalendar, range: &TimeRange, floating: &Zone) -> ICalendar { let exp = expand(cal, range.clone(), floating.clone()); let used: HashSet = exp.instances.iter().map(|x| x.component).collect(); let mut out = vec![root(cal)]; for &i in cal.components.first().map_or(&[][..], |r| &r.component_ids) { let c = &cal.components[i as usize]; let keep = match c.property(&ICalendarProperty::RecurrenceId) { None => true, Some(rid) => { exp.truncated || used.contains(&(i as usize)) || rid.parameter(&ICalendarParameterName::Range).is_some() } }; if keep { let at = copy(cal, i as usize, None, &mut out); out[0].component_ids.push(at); } } ICalendar { components: out } } /// Drops the FREEBUSY periods outside `range` (RFC 4791, 9.6.7). fn limit_freebusy(cal: &mut ICalendar, range: &TimeRange, floating: &Zone) { let zones = Zones::new(cal, floating.clone()); for c in cal .components .iter_mut() .filter(|c| c.component_type == ICalendarComponentType::VFreebusy) { for e in c .entries .iter_mut() .filter(|e| e.name == ICalendarProperty::Freebusy) { let tzid = e.tz_id().map(str::to_string); e.values.retain(|v| { period(&zones, tzid.as_deref(), v) .is_some_and(|(s, e)| s < range.end && e > range.start) }); } c.entries .retain(|e| e.name != ICalendarProperty::Freebusy || !e.values.is_empty()); } } /// The VCALENDAR with its properties and no components. fn root(cal: &ICalendar) -> ICalendarComponent { ICalendarComponent { component_type: ICalendarComponentType::VCalendar, entries: cal .components .first() .map(|c| c.entries.clone()) .unwrap_or_default(), component_ids: Vec::new(), } } /// Appends component `i` of `src` and its sub-components, narrowed by `sel`, /// to `out`. Returns the new index. fn copy( src: &ICalendar, i: usize, sel: Option<&CompSelect>, out: &mut Vec, ) -> u32 { let c = &src.components[i]; let entries = match sel.and_then(|s| s.props.as_ref()) { None => c.entries.clone(), Some(props) => c .entries .iter() .filter_map(|e| { let p = props .iter() .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?; let mut e = e.clone(); if p.novalue { e.values.clear(); } Some(e) }) .collect(), }; let at = out.len(); out.push(ICalendarComponent { component_type: c.component_type.clone(), entries, component_ids: Vec::new(), }); for &child in &c.component_ids { let child = child as usize; let child_sel = match sel.map(|s| &s.comps) { None | Some(None) => None, Some(Some(list)) => { let name = src.components[child].component_type.as_str(); match list.iter().find(|s| s.name.eq_ignore_ascii_case(name)) { Some(s) => Some(s), None => continue, } } }; let id = copy(src, child, child_sel, out); out[at].component_ids.push(id); } at as u32 } /// The address-data of one card. The stored text is returned unchanged /// unless the request narrows it or asks for another version. pub fn address_data(raw: &str, req: &AddressData) -> String { if *req == AddressData::default() { return raw.to_string(); } let Ok(card) = VCard::parse(raw) else { return raw.to_string(); }; let version = req.version.or(card.version()).unwrap_or_default(); if req.props.is_none() && Some(version) == card.version() { return raw.to_string(); } // calcard's 4.0 writer drops a `pref` type, so map before parsing. let card = match version { VCardVersion::V4_0 => VCard::parse(apple_forms(raw, version)).unwrap_or(card), _ => card, }; let entries = match &req.props { None => card.entries, Some(props) => card .entries .into_iter() .filter_map(|mut e| { let p = props .iter() .find(|p| p.name.eq_ignore_ascii_case(e.name.as_str()))?; if p.novalue { e.values.clear(); } Some(e) }) .collect(), }; let mut out = String::new(); let _ = VCard { entries }.write_to(&mut out, version); match version { VCardVersion::V4_0 => out, _ => apple_forms(&out, version), } } /// The version a GET asks for in its `Accept` header: 4.0 when named, /// else 3.0, which RFC 6352 makes the default and Apple Contacts needs. pub fn accepted_version(accept: Option<&str>) -> VCardVersion { match accept.is_some_and(|a| a.contains("version=4.0")) { true => VCardVersion::V4_0, false => VCardVersion::V3_0, } } /// vCard 4.0 groups and companies in the vCard 3.0 extensions Apple /// Contacts reads, and back: `KIND:group` and `MEMBER` as /// `X-ADDRESSBOOKSERVER-KIND` and `X-ADDRESSBOOKSERVER-MEMBER`, `KIND:org` /// as `X-ABSHOWAS:COMPANY`, and `PREF=1` as `TYPE=pref`. sabre/vobject /// converts the same way. fn apple_forms(text: &str, version: VCardVersion) -> String { let v3 = version == VCardVersion::V3_0; let eol = if text.contains("\r\n") { "\r\n" } else { "\n" }; let mut out = String::with_capacity(text.len()); for raw in crate::text::logical_lines(text) { let line = crate::text::unfold(raw); let start = crate::text::value_start(&line); let head = &line[..start.saturating_sub(1)]; let head_name = head.split(';').next().unwrap_or_default(); let (group, name) = match head_name.rsplit_once('.') { Some((g, n)) => (&head_name[..=g.len()], n.to_ascii_uppercase()), None => ("", head_name.to_ascii_uppercase()), }; let value = &line[start..]; let lower = value.trim().to_ascii_lowercase(); let renamed = match (v3, name.as_str(), lower.as_str()) { (true, "KIND", "group") => Some(("X-ADDRESSBOOKSERVER-KIND", "group")), (true, "KIND", "org") => Some(("X-ABSHOWAS", "COMPANY")), (true, "KIND", "individual") => continue, (true, "MEMBER", _) => Some(("X-ADDRESSBOOKSERVER-MEMBER", value)), (false, "X-ADDRESSBOOKSERVER-KIND", "group") => Some(("KIND", "group")), (false, "X-ABSHOWAS", "company") => Some(("KIND", "org")), (false, "X-ADDRESSBOOKSERVER-MEMBER", _) => Some(("MEMBER", value)), _ => None, }; let params = pref_params(crate::text::param_parts(&line), v3); let (name, value) = renamed.unwrap_or((&name, value)); let mut new = format!("{group}{name}"); for p in ¶ms { new.push(';'); new.push_str(p); } new.push(':'); new.push_str(value); if new == line { out.push_str(raw); } else { out.push_str(&crate::text::fold(&new, eol)); } } out } /// `PREF=1` as a `pref` type for vCard 3.0, and back for 4.0. fn pref_params(params: Vec<&str>, v3: bool) -> Vec { let mut out = Vec::with_capacity(params.len()); let mut pref = false; for p in params { let (key, value) = p.split_once('=').unwrap_or((p, "")); match (v3, key.to_ascii_uppercase().as_str()) { (true, "PREF") => pref |= value.trim() == "1", (false, "TYPE") => { let (prefs, rest): (Vec<&str>, Vec<&str>) = value .trim_matches('"') .split(',') .partition(|t| t.eq_ignore_ascii_case("pref")); pref |= !prefs.is_empty(); if !rest.is_empty() { out.push(format!("{key}={}", rest.join(","))); } } _ => out.push(p.to_string()), } } if pref { out.push(if v3 { "TYPE=pref" } else { "PREF=1" }.to_string()); } out }