//! Implicit scheduling (RFC 6638) as iTIP messages (RFC 5546): what a change //! to a scheduling object sends to whom, and how each copy changes. //! //! Calendar user addresses are compared through closures, so the caller maps //! them onto its principals. use std::collections::HashSet; use calcard::common::PartialDateTime; use calcard::icalendar::{ ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDuration, ICalendarEntry, ICalendarMethod, ICalendarParameter, ICalendarParameterName, ICalendarParameterValue, ICalendarParticipationStatus, ICalendarProperty, ICalendarStatus, ICalendarValue, }; use chrono::{DateTime, Utc}; use xmltree::Element; use crate::xml::{CALDAV, el}; use crate::zone::{Zone, Zones}; /// Whether an address belongs to someone in particular. pub type Is<'a> = &'a dyn Fn(&str) -> bool; /// How the owner of a calendar takes part in one of its objects (RFC 6638, /// 3.1). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Role { Organizer, Attendee, /// Not a scheduling object for this owner. None, } /// A scheduling precondition a PUT fails. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Refused { SameOrganizer, AttendeeChange, } impl Refused { pub fn condition(self) -> Element { match self { Refused::SameOrganizer => el(CALDAV, "same-organizer-in-all-components"), Refused::AttendeeChange => el(CALDAV, "allowed-attendee-scheduling-object-change"), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Method { Request, Cancel, Reply, } #[derive(Debug, Clone)] pub struct Message { /// The recipient's address, as the object writes it. pub to: String, pub method: Method, /// Only the participation of other attendees changed. It updates an /// existing copy, keeps its Schedule-Tag and leaves no inbox entry. pub quiet: bool, /// With METHOD. pub cal: ICalendar, } pub fn role(cal: &ICalendar, owner: Is) -> Result { let obj = Obj::new(cal); let mut organizers = obj .comps() .filter_map(|c| address(c.c.property(&ICalendarProperty::Organizer)?)); let Some(organizer) = organizers.next() else { return Ok(Role::None); }; if organizers.any(|o| !o.eq_ignore_ascii_case(organizer)) { return Err(Refused::SameOrganizer); } if owner(organizer) { return Ok(Role::Organizer); } let attends = obj .comps() .flat_map(|c| attendees(&c.c)) .any(|e| address(e).is_some_and(owner)); Ok(if attends { Role::Attendee } else { Role::None }) } /// An organizer's PUT (`new`) or DELETE (`None`) of a scheduling object. /// `old` is the stored organizer object, if any. Returns what to store and /// what to deliver. pub fn organize( old: Option<&ICalendar>, new: Option, organizer: Is, now: DateTime, ) -> (Option, Vec) { let mut force = Vec::new(); let new = new.map(|n| guard(old, &n, organizer, &mut force)); let messages = messages(old, new.as_ref(), organizer, &force, now); (new, messages) } /// The messages a change of the organizer object sends, without touching /// the attendee state in it. `force` lists attendees who get a REQUEST even /// if nothing changed for them. pub fn messages( old: Option<&ICalendar>, new: Option<&ICalendar>, organizer: Is, force: &[String], now: DateTime, ) -> Vec { let old = old.map(Obj::new); let new = new.map(Obj::new); let mut who: Vec<&str> = Vec::new(); for obj in old.iter().chain(new.iter()) { for e in obj.comps().flat_map(|c| attendees(&c.c)) { if let Some(a) = address(e) && server_agent(e) && !organizer(a) && !who.iter().any(|w| w.eq_ignore_ascii_case(a)) { who.push(a); } } } let mut out = Vec::new(); for a in who { let before = old.as_ref().and_then(|o| Some((o, o.view(a)?))); let after = new.as_ref().and_then(|n| Some((n, n.view(a)?))); let (method, quiet, comps, src) = match (before, after) { (Some((src, b)), None) => (Method::Cancel, false, cancelled(b), src), (None, Some((src, comps))) => (Method::Request, false, comps, src), (Some((_, b)), Some((src, comps))) => { let quiet = if normalized(&b, true) != normalized(&comps, true) { false } else if normalized(&b, false) != normalized(&comps, false) { true } else if force.iter().any(|f| f.eq_ignore_ascii_case(a)) { false } else { continue; }; (Method::Request, quiet, comps, src) } (None, None) => continue, }; out.push(Message { to: a.to_string(), method, quiet, cal: src.envelope(comps, method, now), }); } out } /// Records the delivery status for `to` on its ATTENDEE properties. pub fn set_attendee_status(cal: &mut ICalendar, to: &str, status: &str) { for c in &mut cal.components { for e in c .entries .iter_mut() .filter(|e| e.name == ICalendarProperty::Attendee) { if address(e).is_some_and(|a| a.eq_ignore_ascii_case(to)) { set_param(e, ICalendarParameterName::ScheduleStatus, text(status)); } } } } /// Records the delivery status of a REPLY on the ORGANIZER properties. pub fn set_organizer_status(cal: &mut ICalendar, status: &str) { for c in &mut cal.components { for e in c .entries .iter_mut() .filter(|e| e.name == ICalendarProperty::Organizer) { set_param(e, ICalendarParameterName::ScheduleStatus, text(status)); } } } /// An attendee's PUT over the stored copy. Returns what to store and the /// REPLY, if the attendee's participation changed. pub fn attend( old: &ICalendar, new: ICalendar, me: Is, now: DateTime, ) -> Result<(ICalendar, Option), Refused> { let old = Obj::new(old); if !old.organizer_schedules() { return Ok((new, None)); } let mut next = Obj::new(&new); let master = old.master(); for c in next.comps() { let key = next.key(&c.c); match old.find(key) { Some(oc) => { let same_times = [ ICalendarProperty::Dtstart, ICalendarProperty::Dtend, ICalendarProperty::Duration, ICalendarProperty::Due, ICalendarProperty::Rrule, ICalendarProperty::Rdate, ICalendarProperty::Exrule, ] .iter() .all(|p| old.times(&oc.c, p) == next.times(&c.c, p)); let kept_exdates = old .times(&oc.c, &ICalendarProperty::Exdate) .is_subset(&next.times(&c.c, &ICalendarProperty::Exdate)); let organizer = |c: &ICalendarComponent| { c.property(&ICalendarProperty::Organizer) .and_then(address) .map(str::to_ascii_lowercase) }; if !same_times || !kept_exdates || organizer(&oc.c) != organizer(&c.c) || addresses(&oc.c) != addresses(&c.c) { return Err(Refused::AttendeeChange); } } // An instance the attendee overrides, to set its own status. None => { let start = c.c.property(&ICalendarProperty::Dtstart) .and_then(|e| next.instant(e)); if key.is_none() || master.is_none() || start != key { return Err(Refused::AttendeeChange); } } } } // The state of the others and of the organizer is the server's. let mut force = false; let keys: Vec> = next.comps().map(|c| next.key(&c.c)).collect(); for (c, key) in next.comps_mut().zip(&keys) { let Some(base) = old.find(*key).or(master) else { continue; }; for e in &mut c.c.entries { match e.name { ICalendarProperty::Attendee if !address(e).is_some_and(me) => { if let Some(b) = same_attendee(&base.c, e) { e.params = b.params.clone(); } } ICalendarProperty::Organizer => { force |= param(e, &ICalendarParameterName::ScheduleForceSend) .is_some_and(|v| v.eq_ignore_ascii_case("REPLY")); if let Some(b) = base.c.property(&ICalendarProperty::Organizer) { e.params = b.params.clone(); } } _ => {} } } } let mut replied: Vec = Vec::new(); for (c, key) in next.comps().zip(&keys) { let now_stat = own_partstat(&c.c, me); let before = old .find(*key) .or(master) .and_then(|b| own_partstat(&b.c, me)); if now_stat.is_some() && (force || now_stat != before) { replied.push(reply_part(c, me)); } } if let Some(m) = next.master() { let before = master.map_or_else(HashSet::new, |om| { old.times(&om.c, &ICalendarProperty::Exdate) }); for e in m.c.properties(&ICalendarProperty::Exdate) { for value in &e.values { if before.contains(&next.value_key(e, value)) { continue; } let rid = ICalendarEntry { name: ICalendarProperty::RecurrenceId, params: e.params.clone(), values: vec![value.clone()], }; replied.push(declined_instance(m, rid, me)); } } } let reply = match replied.is_empty() { true => None, false => next.organizer().map(|to| Message { to, method: Method::Reply, quiet: false, cal: next.envelope(replied, Method::Reply, now), }), }; Ok((next.done(), reply)) } /// An attendee's DELETE: the REPLY declining every instance, unless the /// organizer handles scheduling itself or already cancelled. pub fn decline(old: &ICalendar, me: Is, now: DateTime) -> Option { let old = Obj::new(old); if !old.organizer_schedules() { return None; } let cancelled = old.comps().all(|c| { c.c.property(&ICalendarProperty::Status) .and_then(|e| e.values.first()?.as_text()) .is_some_and(|s| s.eq_ignore_ascii_case("CANCELLED")) }); if cancelled { return None; } let comps: Vec = old.comps() .filter(|c| own_partstat(&c.c, me).is_some()) .map(|c| { let mut part = reply_part(c, me); for e in part.c.entries.iter_mut().filter(|e| { e.name == ICalendarProperty::Attendee && address(e).is_some_and(me) }) { set_param( e, ICalendarParameterName::Partstat, partstat(ICalendarParticipationStatus::Declined), ); } part }) .collect(); if comps.is_empty() { return None; } Some(Message { to: old.organizer()?, method: Method::Reply, quiet: false, cal: old.envelope(comps, Method::Reply, now), }) } /// A REQUEST or CANCEL applied to the attendee's copy. `None`: nothing to /// store. pub fn receive(copy: Option<&ICalendar>, msg: &Message) -> Option { match msg.method { Method::Request => { if msg.quiet && copy.is_none() { return None; } let copy = copy.map(Obj::new); let mut next = Obj::new(&msg.cal); next.root .c .entries .retain(|e| e.name != ICalendarProperty::Method); let keys: Vec> = next.comps().map(|c| next.key(&c.c)).collect(); let Some(copy) = copy else { return Some(next.done()); }; // What the attendee may keep for itself (RFC 6638, 3.2.2.1). for (c, key) in next.comps_mut().zip(&keys) { let Some(base) = copy.find(*key).or(copy.master()) else { continue; }; c.children .retain(|n| n.c.component_type != ICalendarComponentType::VAlarm); c.children.extend( base.children .iter() .filter(|n| n.c.component_type == ICalendarComponentType::VAlarm) .cloned(), ); for p in [ ICalendarProperty::Transp, ICalendarProperty::PercentComplete, ICalendarProperty::Completed, ] { c.c.entries.retain(|e| e.name != p); c.c.entries.extend(base.c.properties(&p).cloned()); } let status = base .c .property(&ICalendarProperty::Organizer) .and_then(|e| e.parameter(&ICalendarParameterName::ScheduleStatus)) .cloned(); if let Some(s) = status { for e in c.c.entries .iter_mut() .filter(|e| e.name == ICalendarProperty::Organizer) { set_param(e, ICalendarParameterName::ScheduleStatus, s.clone()); } } } Some(next.done()) } Method::Cancel => { let msg_obj = Obj::new(&msg.cal); let mut next = Obj::new(copy?); let whole = msg_obj.master().is_some(); let gone: Vec<(i64, ICalendarEntry)> = msg_obj .comps() .filter_map(|c| { let rid = c.c.property(&ICalendarProperty::RecurrenceId)?; Some((msg_obj.instant(rid)?, rid.clone())) }) .collect(); let keys: Vec> = next.comps().map(|c| next.key(&c.c)).collect(); for (c, key) in next.comps_mut().zip(&keys) { if whole || key.is_some_and(|k| gone.iter().any(|(g, _)| *g == k)) { set_prop( &mut c.c, ICalendarProperty::Status, ICalendarValue::Status(ICalendarStatus::Cancelled), ); } } if !whole { let missing: Vec = gone .into_iter() .filter(|(k, _)| !keys.contains(&Some(*k))) .map(|(_, rid)| ICalendarEntry { name: ICalendarProperty::Exdate, params: without(rid.params, &ICalendarParameterName::Range), values: rid.values, }) .collect(); if let Some(m) = next .comps_mut() .find(|c| !c.c.has_property(&ICalendarProperty::RecurrenceId)) { m.c.entries.extend(missing); } } Some(next.done()) } Method::Reply => None, } } /// A REPLY applied to the organizer object. `false` if it changed nothing. pub fn apply_reply(org: &mut ICalendar, reply: &ICalendar, replier: Is) -> bool { let rep = Obj::new(reply); let mut next = Obj::new(org); let mut changed = false; for rc in rep.comps() { let key = rep.key(&rc.c); let at = match next.position(key) { Some(at) => at, // A reply for one instance of the series gets its own override. None => { let (Some(_), Some(rid), Some(master)) = ( key, rc.c.property(&ICalendarProperty::RecurrenceId), next.master(), ) else { continue; }; let inst = next.instance(master, rid); next.root.children.push(inst); next.root.children.len() - 1 } }; let target = &mut next.root.children[at]; if sequence(&rc.c) < sequence(&target.c) { continue; } let Some(stat) = rc.c.properties(&ICalendarProperty::Attendee) .find(|e| address(e).is_some_and(replier)) .map(|e| { e.parameter(&ICalendarParameterName::Partstat) .cloned() .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction)) }) else { continue; }; let codes: Vec = rc.c.properties(&ICalendarProperty::RequestStatus) .filter_map(|e| e.values.first()?.as_text()) .map(|s| s.split(';').next().unwrap_or(s).trim().to_string()) .collect(); let status = match codes.is_empty() { true => "2.0".to_string(), false => codes.join(","), }; for e in target.c.entries.iter_mut().filter(|e| { e.name == ICalendarProperty::Attendee && address(e).is_some_and(replier) }) { changed |= e.parameter(&ICalendarParameterName::Partstat) != Some(&stat); set_param(e, ICalendarParameterName::Partstat, stat.clone()); set_param(e, ICalendarParameterName::ScheduleStatus, text(&status)); } } if changed { *org = next.done(); } changed } // --------------------------------------------------------------------------- // The organizer object // --------------------------------------------------------------------------- /// The client's organizer object with the attendee state the server owns: /// their PARTSTAT (reset on a reschedule, RFC 6638 3.2.8) and /// SCHEDULE-STATUS. Collects SCHEDULE-FORCE-SEND=REQUEST into `force`. fn guard( old: Option<&ICalendar>, new: &ICalendar, organizer: Is, force: &mut Vec, ) -> ICalendar { let old = old.map(Obj::new); let mut next = Obj::new(new); let master = old.as_ref().and_then(Obj::master); let keys: Vec> = next.comps().map(|c| next.key(&c.c)).collect(); let moved: Vec = next .comps() .map(|c| match &old { Some(o) => match o.find(next.key(&c.c)) { Some(oc) => rescheduled(o, &oc.c, &next, &c.c), // A new override: rescheduled if it moves its instance. None => { let start = c.c.property(&ICalendarProperty::Dtstart) .and_then(|e| next.instant(e)); next.key(&c.c).is_none() || start != next.key(&c.c) } }, None => true, }) .collect(); let bumps: Vec> = next .comps() .zip(&moved) .map(|(c, moved)| { let oc = old.as_ref()?.find(next.key(&c.c))?; (*moved && sequence(&c.c) <= sequence(&oc.c)).then(|| sequence(&oc.c) + 1) }) .collect(); for ((c, key), (moved, bump)) in next.comps_mut().zip(&keys).zip(moved.iter().zip(&bumps)) { let base = old.as_ref().and_then(|o| o.find(*key)).or(master); for e in &mut c.c.entries { match e.name { ICalendarProperty::Organizer => { remove_param(e, &ICalendarParameterName::ScheduleForceSend); } ICalendarProperty::Attendee => { let forced = param(e, &ICalendarParameterName::ScheduleForceSend) .is_some_and(|v| v.eq_ignore_ascii_case("REQUEST")); remove_param(e, &ICalendarParameterName::ScheduleForceSend); let Some(a) = address(e).map(str::to_string) else { continue; }; if organizer(&a) || !server_agent(e) { continue; } if forced { force.push(a.clone()); } let prev = base.and_then(|b| same_attendee(&b.c, e)); let stat = match (moved, prev) { (false, Some(p)) => p .parameter(&ICalendarParameterName::Partstat) .cloned() .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction)), _ => partstat(ICalendarParticipationStatus::NeedsAction), }; set_param(e, ICalendarParameterName::Partstat, stat); match prev.and_then(|p| p.parameter(&ICalendarParameterName::ScheduleStatus)) { Some(s) => set_param(e, ICalendarParameterName::ScheduleStatus, s.clone()), None => remove_param(e, &ICalendarParameterName::ScheduleStatus), } } _ => {} } } if let Some(n) = bump { set_prop( &mut c.c, ICalendarProperty::Sequence, ICalendarValue::Integer(*n), ); } } next.done() } /// Whether a change moves instances in time (RFC 6638, 3.2.8). Shortening a /// series or excluding instances does not. fn rescheduled(old: &Obj, oc: &ICalendarComponent, new: &Obj, nc: &ICalendarComponent) -> bool { let moved = [ ICalendarProperty::Dtstart, ICalendarProperty::Dtend, ICalendarProperty::Duration, ICalendarProperty::Due, ICalendarProperty::Rdate, ] .iter() .any(|p| old.times(oc, p) != new.times(nc, p)); let reinstated = !old .times(oc, &ICalendarProperty::Exdate) .is_subset(&new.times(nc, &ICalendarProperty::Exdate)); moved || reinstated || rules_grew(oc, nc) } fn rules_grew(oc: &ICalendarComponent, nc: &ICalendarComponent) -> bool { let rules = |c: &ICalendarComponent| -> Vec<_> { c.properties(&ICalendarProperty::Rrule) .filter_map(|e| match e.values.first()? { ICalendarValue::RecurrenceRule(r) => Some((**r).clone()), _ => None, }) .collect() }; let (o, n) = (rules(oc), rules(nc)); if o == n { return false; } let (o, n) = match (&o[..], &n[..]) { // Dropping the rule leaves the first instance only. ([_, ..], []) => return false, ([o], [n]) => (o, n), _ => return true, }; let unbounded = |r: &calcard::icalendar::ICalendarRecurrenceRule| { let mut r = r.clone(); r.until = None; r.count = None; r }; if unbounded(o) != unbounded(n) { return true; } let shorter = match (&o.until, &n.until, o.count, n.count) { (Some(ou), Some(nu), _, _) => nu <= ou, (_, _, Some(oc), Some(nc)) => nc <= oc, (None, _, None, _) => true, _ => false, }; !shorter } fn cancelled(comps: Vec) -> Vec { comps .into_iter() .map(|mut n| { set_prop( &mut n.c, ICalendarProperty::Status, ICalendarValue::Status(ICalendarStatus::Cancelled), ); let seq = sequence(&n.c) + 1; set_prop( &mut n.c, ICalendarProperty::Sequence, ICalendarValue::Integer(seq), ); n }) .collect() } /// For comparing what an attendee would receive: without the stamps a /// client rewrites on every save, and optionally without participation. fn normalized(comps: &[Node], without_partstat: bool) -> Vec { comps .iter() .map(|n| { let mut n = n.clone(); n.c.entries.retain(|e| { !matches!( e.name, ICalendarProperty::Dtstamp | ICalendarProperty::LastModified | ICalendarProperty::Created | ICalendarProperty::Sequence ) && !matches!(&e.name, ICalendarProperty::Other(x) if x.to_ascii_uppercase().starts_with("X-")) }); if without_partstat { for e in n.c.entries.iter_mut().filter(|e| e.name == ICalendarProperty::Attendee) { remove_param(e, &ICalendarParameterName::Partstat); } } n }) .collect() } /// A component of the attendee's REPLY: only its own ATTENDEE, no alarms. fn reply_part(c: &Node, me: Is) -> Node { let mut n = Node { c: c.c.clone(), children: Vec::new(), }; n.c.entries .retain(|e| e.name != ICalendarProperty::Attendee || address(e).is_some_and(me)); strip_scheduling_params(&mut n.c); n } /// The REPLY component for an instance the attendee excluded with EXDATE. fn declined_instance(master: &Node, rid: ICalendarEntry, me: Is) -> Node { let mut c = ICalendarComponent { component_type: master.c.component_type.clone(), entries: Vec::new(), component_ids: Vec::new(), }; for p in [ ICalendarProperty::Uid, ICalendarProperty::Sequence, ICalendarProperty::Organizer, ICalendarProperty::Summary, ] { c.entries.extend(master.c.properties(&p).cloned()); } c.entries.push(ICalendarEntry { name: ICalendarProperty::Dtstart, ..rid.clone() }); c.entries.push(rid); for e in master .c .properties(&ICalendarProperty::Attendee) .filter(|e| address(e).is_some_and(me)) { let mut e = e.clone(); set_param( &mut e, ICalendarParameterName::Partstat, partstat(ICalendarParticipationStatus::Declined), ); c.entries.push(e); } strip_scheduling_params(&mut c); Node { c, children: Vec::new(), } } // --------------------------------------------------------------------------- // The object as a tree // --------------------------------------------------------------------------- /// A component with its sub-components, detached from the flat list calcard /// keeps. #[derive(Debug, Clone, PartialEq)] struct Node { /// `component_ids` is empty; `children` replaces it. c: ICalendarComponent, children: Vec, } fn node(cal: &ICalendar, i: usize) -> Node { let mut c = cal.components[i].clone(); let ids = std::mem::take(&mut c.component_ids); let children = ids .iter() .map(|&id| id as usize) .filter(|&id| id > i && id < cal.components.len()) .map(|id| node(cal, id)) .collect(); Node { c, children } } fn flatten(n: &Node, out: &mut Vec) -> u32 { let at = out.len(); out.push(n.c.clone()); for ch in &n.children { let id = flatten(ch, out); out[at].component_ids.push(id); } at as u32 } /// A calendar object: the VCALENDAR with its time zones and components. struct Obj { root: Node, zones: Zones, } impl Obj { fn new(cal: &ICalendar) -> Self { let root = match cal.components.is_empty() { true => Node { c: ICalendarComponent { component_type: ICalendarComponentType::VCalendar, entries: Vec::new(), component_ids: Vec::new(), }, children: Vec::new(), }, false => node(cal, 0), }; Obj { root, zones: Zones::new(cal, Zone::Utc), } } fn done(self) -> ICalendar { let mut components = Vec::new(); flatten(&self.root, &mut components); ICalendar { components } } fn comps(&self) -> impl Iterator { self.root.children.iter().filter(|n| is_scheduled(&n.c)) } fn comps_mut(&mut self) -> impl Iterator { self.root.children.iter_mut().filter(|n| is_scheduled(&n.c)) } fn master(&self) -> Option<&Node> { self.find(None) } fn find(&self, key: Option) -> Option<&Node> { self.comps().find(|c| self.key(&c.c) == key) } fn position(&self, key: Option) -> Option { self.root .children .iter() .position(|n| is_scheduled(&n.c) && self.key(&n.c) == key) } /// The RECURRENCE-ID as an instant; `None` for the master. fn key(&self, c: &ICalendarComponent) -> Option { self.instant(c.property(&ICalendarProperty::RecurrenceId)?) } /// The first value of a date or date-time property, in UTC seconds. /// Dates count from midnight, whatever the zone. fn instant(&self, e: &ICalendarEntry) -> Option { let v = e.values.first()?.as_partial_date_time()?; self.at(v, e.tz_id()) } fn at(&self, v: &PartialDateTime, tzid: Option<&str>) -> Option { let dt = v.to_date_time()?; Some(match dt.offset { _ if v.hour.is_none() => dt.date_time.and_utc().timestamp(), Some(o) => dt.date_time.and_utc().timestamp() - i64::from(o.local_minus_utc()), None => self.zones.get(tzid).to_utc(dt.date_time).timestamp(), }) } /// Every value of a property, comparable across encodings: instants for /// dates and date-times, the text otherwise. fn times(&self, c: &ICalendarComponent, prop: &ICalendarProperty) -> HashSet { c.properties(prop) .flat_map(|e| e.values.iter().map(move |v| self.value_key(e, v))) .collect() } fn value_key(&self, e: &ICalendarEntry, v: &ICalendarValue) -> String { match v.as_partial_date_time() { Some(p) => match self.at(p, e.tz_id()) { Some(t) if p.hour.is_none() => format!("{t}d"), Some(t) => t.to_string(), None => format!("{v:?}"), }, None => format!("{v:?}"), } } fn organizer(&self) -> Option { self.comps() .find_map(|c| address(c.c.property(&ICalendarProperty::Organizer)?)) .map(str::to_string) } /// Whether the server replies for the attendee (RFC 6638, 3.2.2). fn organizer_schedules(&self) -> bool { self.comps() .filter_map(|c| c.c.property(&ICalendarProperty::Organizer)) .all(server_agent) } /// What attendee `a` gets to see (RFC 6638, 3.2.6): the master with the /// overrides it is in, and EXDATEs for those it is not in; or just the /// overrides it is in. fn view(&self, a: &str) -> Option> { let invited = |c: &ICalendarComponent| { attendees(c) .any(|e| address(e).is_some_and(|x| x.eq_ignore_ascii_case(a)) && server_agent(e)) }; let mut out = Vec::new(); let master = self.master().filter(|m| invited(&m.c)); if let Some(m) = master { let mut m = m.clone(); for o in self .comps() .filter(|c| c.c.has_property(&ICalendarProperty::RecurrenceId)) { if !invited(&o.c) && let Some(rid) = o.c.property(&ICalendarProperty::RecurrenceId) { m.c.entries.push(ICalendarEntry { name: ICalendarProperty::Exdate, params: without(rid.params.clone(), &ICalendarParameterName::Range), values: rid.values.clone(), }); } } out.push(m); } out.extend( self.comps() .filter(|c| c.c.has_property(&ICalendarProperty::RecurrenceId) && invited(&c.c)) .cloned(), ); for n in &mut out { n.children .retain(|ch| ch.c.component_type != ICalendarComponentType::VAlarm); strip_scheduling_params(&mut n.c); } (!out.is_empty()).then_some(out) } /// A message: this object's VCALENDAR and time zones around `comps`. fn envelope(&self, comps: Vec, method: Method, now: DateTime) -> ICalendar { let mut root = Node { c: self.root.c.clone(), children: self .root .children .iter() .filter(|n| n.c.component_type == ICalendarComponentType::VTimezone) .cloned() .collect(), }; let method = match method { Method::Request => ICalendarMethod::Request, Method::Cancel => ICalendarMethod::Cancel, Method::Reply => ICalendarMethod::Reply, }; set_prop( &mut root.c, ICalendarProperty::Method, ICalendarValue::Method(method), ); let stamp = PartialDateTime::from_utc_timestamp(now.timestamp()); for mut n in comps { set_prop( &mut n.c, ICalendarProperty::Dtstamp, ICalendarValue::PartialDateTime(Box::new(stamp.clone())), ); root.children.push(n); } let mut components = Vec::new(); flatten(&root, &mut components); ICalendar { components } } /// An override for one instance of `master`, so it can hold a status of /// its own. Its length becomes a DURATION. fn instance(&self, master: &Node, rid: &ICalendarEntry) -> Node { let mut n = master.clone(); let start = n.c.property(&ICalendarProperty::Dtstart) .and_then(|e| self.instant(e)); let end_prop = match n.c.component_type { ICalendarComponentType::VTodo => ICalendarProperty::Due, _ => ICalendarProperty::Dtend, }; let end = n.c.property(&end_prop).and_then(|e| self.instant(e)); if let (Some(s), Some(e)) = (start, end) { n.c.entries.retain(|x| x.name != end_prop); n.c.entries.push(ICalendarEntry { name: ICalendarProperty::Duration, params: Vec::new(), values: vec![ICalendarValue::Duration(ICalendarDuration::from_seconds( (e - s).max(0), ))], }); } n.c.entries.retain(|e| { !matches!( e.name, ICalendarProperty::Rrule | ICalendarProperty::Rdate | ICalendarProperty::Exdate | ICalendarProperty::Exrule | ICalendarProperty::Dtstart | ICalendarProperty::RecurrenceId ) }); n.c.entries.push(ICalendarEntry { name: ICalendarProperty::Dtstart, params: without(rid.params.clone(), &ICalendarParameterName::Range), values: rid.values.clone(), }); n.c.entries.push(ICalendarEntry { name: ICalendarProperty::RecurrenceId, params: without(rid.params.clone(), &ICalendarParameterName::Range), values: rid.values.clone(), }); n } } // --------------------------------------------------------------------------- // Properties and parameters // --------------------------------------------------------------------------- fn is_scheduled(c: &ICalendarComponent) -> bool { matches!( c.component_type, ICalendarComponentType::VEvent | ICalendarComponentType::VTodo | ICalendarComponentType::VJournal ) } fn address(e: &ICalendarEntry) -> Option<&str> { e.values.first()?.as_text().map(str::trim) } fn attendees(c: &ICalendarComponent) -> impl Iterator { c.properties(&ICalendarProperty::Attendee) } fn addresses(c: &ICalendarComponent) -> Vec { let mut v: Vec = attendees(c) .filter_map(address) .map(str::to_ascii_lowercase) .collect(); v.sort(); v.dedup(); v } fn same_attendee<'a>(c: &'a ICalendarComponent, e: &ICalendarEntry) -> Option<&'a ICalendarEntry> { let a = address(e)?; attendees(c).find(|x| address(x).is_some_and(|b| b.eq_ignore_ascii_case(a))) } fn own_partstat(c: &ICalendarComponent, me: Is) -> Option { attendees(c).find(|e| address(e).is_some_and(me)).map(|e| { param(e, &ICalendarParameterName::Partstat) .unwrap_or("NEEDS-ACTION") .to_ascii_uppercase() }) } /// `SCHEDULE-AGENT` absent or `SERVER`. Unknown values count as `NONE`. fn server_agent(e: &ICalendarEntry) -> bool { param(e, &ICalendarParameterName::ScheduleAgent) .is_none_or(|v| v.eq_ignore_ascii_case("SERVER")) } fn sequence(c: &ICalendarComponent) -> i64 { c.property(&ICalendarProperty::Sequence) .and_then(|e| e.values.first()?.as_integer()) .unwrap_or(0) } fn param<'a>(e: &'a ICalendarEntry, name: &ICalendarParameterName) -> Option<&'a str> { e.parameter(name)?.as_text() } fn set_param(e: &mut ICalendarEntry, name: ICalendarParameterName, value: ICalendarParameterValue) { remove_param(e, &name); e.params.push(ICalendarParameter::new(name, value)); } fn remove_param(e: &mut ICalendarEntry, name: &ICalendarParameterName) { e.params.retain(|p| &p.name != name); } fn without( mut params: Vec, name: &ICalendarParameterName, ) -> Vec { params.retain(|p| &p.name != name); params } fn set_prop(c: &mut ICalendarComponent, name: ICalendarProperty, value: ICalendarValue) { c.entries.retain(|e| e.name != name); c.entries.push(ICalendarEntry { name, params: Vec::new(), values: vec![value], }); } /// Messages carry none of the scheduling parameters (RFC 6638, 7). fn strip_scheduling_params(c: &mut ICalendarComponent) { for e in c.entries.iter_mut().filter(|e| { matches!( e.name, ICalendarProperty::Attendee | ICalendarProperty::Organizer ) }) { for p in [ ICalendarParameterName::ScheduleAgent, ICalendarParameterName::ScheduleStatus, ICalendarParameterName::ScheduleForceSend, ] { remove_param(e, &p); } } } fn text(s: &str) -> ICalendarParameterValue { ICalendarParameterValue::Text(s.to_string()) } fn partstat(p: ICalendarParticipationStatus) -> ICalendarParameterValue { ICalendarParameterValue::Partstat(p) }