//! 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, ICalendarRecurrenceRule, ICalendarStatus, ICalendarValue, ICalendarWeekday, Uri, }; use chrono::{DateTime, TimeDelta, Utc}; use xmltree::Element; use crate::expand::expand; use crate::filter::TimeRange; use crate::freebusy::{Busy, Period, merge}; use crate::text::{fold, logical_lines, name, param_parts, unfold, value}; use crate::xml::{CALDAV, el}; use crate::zone::{Zone, Zones, add_local}; /// 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 }) } /// The ORGANIZER address of a scheduling object. pub fn organizer(cal: &ICalendar) -> Option<&str> { cal.components .iter() .filter(|c| is_scheduled(c)) .find_map(|c| address(c.property(&ICalendarProperty::Organizer)?)) } /// 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 first half of [`organize`] for a PUT: what to store, with the /// attendee state the server owns, and the attendees whose REQUEST is /// forced. [`messages`] is the second half. pub fn prepare( old: Option<&ICalendar>, new: &ICalendar, organizer: Is, ) -> (ICalendar, Vec) { let mut force = Vec::new(); let store = guard(old, new, organizer, &mut force); (store, force) } /// Names who acted for the owner: `SENT-BY` on the owner's ORGANIZER and /// ATTENDEE properties when `sender` is someone else, none when it is the /// owner. Only for writes that send a message, so every message names its /// real sender. pub fn stamp_sender(cal: &mut ICalendar, owner: Is, sender: Option<&str>) { for c in cal.components.iter_mut().filter(|c| is_scheduled(c)) { for e in &mut c.entries { let theirs = matches!( e.name, ICalendarProperty::Organizer | ICalendarProperty::Attendee ) && address(e).is_some_and(owner); match (theirs, sender) { (false, _) => {} (true, Some(s)) => set_param( e, ICalendarParameterName::SentBy, ICalendarParameterValue::Uri(Uri::Location(s.to_string())), ), (true, None) => remove_param(e, &ICalendarParameterName::SentBy), } } } } /// How far ahead a room checks an invitation against its bookings. A /// series without end has infinite instances, so it is checked for two years /// and accepted beyond. Anything else is checked to its end, at most ten /// years ahead. pub fn answer_horizon(copy: &ICalendar) -> TimeDelta { let endless = copy.components.iter().any(|c| { is_scheduled(c) && !c.has_property(&ICalendarProperty::RecurrenceId) && endless(c) }); TimeDelta::days(if endless { 731 } else { 3653 }) } /// Conflicting instances of a series a room declines one by one. Beyond /// that it declines the series. const MAX_DECLINED_INSTANCES: usize = 100; /// The answer of a room or resource to the invitation in its copy: /// ACCEPTED, and DECLINED where an instance in `window` overlaps `taken`. A /// declined instance of a series gets an override of its own. pub fn auto_answer( copy: &ICalendar, me: Is, taken: &[Period], window: &TimeRange, floating: &Zone, ) -> ICalendar { let taken = merge( taken .iter() .map(|p| Period { kind: Busy::Busy, start: p.start, end: p.end, }) .collect(), ); let conflicts = |s: DateTime, e: DateTime| { let i = taken.partition_point(|p| p.end <= s); taken .get(i) .is_some_and(|p| p.start < e || (s == e && p.start <= s)) }; let mut obj = Obj::new(copy); let form = obj .master() .and_then(|m| m.c.property(&ICalendarProperty::Dtstart)) .cloned(); let mut declined: HashSet> = HashSet::new(); let mut slots: Vec> = Vec::new(); for x in expand(copy, window.clone(), floating.clone()).instances { if !conflicts(x.start, x.end) { continue; } match x.recurrence_id { Some(rid) if form.is_some() => slots.push(rid), _ => { declined.insert(obj.key(©.components[x.component])); } } } // One override each would bloat the copy and the REPLY. let all = slots.len() > MAX_DECLINED_INSTANCES; if let (false, Some(form)) = (all, &form) { // In order, so a range moved on past one instance can move again. slots.sort_unstable(); for rid in slots { let Some(key) = obj .recurrence_id(form, rid, floating) .and_then(|id| obj.instant(&id)) else { continue; }; if obj.find(Some(key)).is_some() { obj.narrow(copy, rid, key, floating); } else if let Some(inst) = obj.single(rid, floating) { obj.root.children.push(inst); } else { continue; } declined.insert(Some(key)); } } let keys: Vec> = obj.comps().map(|c| obj.key(&c.c)).collect(); for (c, key) in obj.comps_mut().zip(&keys) { let answer = match all || declined.contains(key) { true => ICalendarParticipationStatus::Declined, false => ICalendarParticipationStatus::Accepted, }; for e in c.c.entries .iter_mut() .filter(|e| e.name == ICalendarProperty::Attendee && address(e).is_some_and(me)) { set_param( e, ICalendarParameterName::Partstat, partstat(answer.clone()), ); } } obj.done() } /// The attendee's copy with `me` answering `answer`: for every component, or /// only for the instance at `instance`, which gets an override of its own /// when it has none. `floating` reads the instance of an all-day series. /// Storing the result through a PUT sends the REPLY. `None` if the series /// has no instance at `instance`. pub fn respond( copy: &ICalendar, me: Is, answer: ICalendarParticipationStatus, instance: Option>, floating: &Zone, ) -> Option { let mut obj = Obj::new(copy); let target = match instance { None => None, Some(rid) => { let form = obj .master() .and_then(|m| m.c.property(&ICalendarProperty::Dtstart)) .or_else(|| { obj.comps() .find_map(|c| c.c.property(&ICalendarProperty::RecurrenceId)) })? .clone(); let key = obj.instant(&obj.recurrence_id(&form, rid, floating)?)?; if obj.find(Some(key)).is_some() { obj.narrow(copy, rid, key, floating); } else { if !obj.occurs(copy, rid, key, floating) { return None; } let inst = obj.single(rid, floating)?; obj.root.children.push(inst); } Some(Some(key)) } }; let keys: Vec> = obj.comps().map(|c| obj.key(&c.c)).collect(); for (c, key) in obj.comps_mut().zip(&keys) { if target.is_some_and(|t| t != *key) { continue; } for e in c.c.entries .iter_mut() .filter(|e| e.name == ICalendarProperty::Attendee && address(e).is_some_and(me)) { set_param( e, ICalendarParameterName::Partstat, partstat(answer.clone()), ); remove_param(e, &ICalendarParameterName::Rsvp); } } Some(obj.done()) } /// 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) => { // A component that lists them with SCHEDULE-AGENT=CLIENT // now: the client tells them. let lost: Vec = b .into_iter() .filter(|c| { !new.as_ref() .and_then(|n| n.find(src.key(&c.c))) .is_some_and(|nc| client_scheduled(&nc.c, a)) }) .collect(); if lost.is_empty() { continue; } (Method::Cancel, false, cancelled(lost), 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_cal = old; let old = Obj::new(old_cal); if !old.organizer_schedules() { return Ok((new, None)); } let mut next = Obj::new(&new); let master = old.master(); let added: Vec = next .comps() .filter_map(|c| next.key(&c.c)) .filter(|k| old.find(Some(*k)).is_none()) .collect(); let instances = old.instances(old_cal, &added); for c in next.comps() { let key = next.key(&c.c); match old.find(key) { Some(oc) => { // The end, not its property: clients rewrite DURATION as DTEND. let same_times = [ ICalendarProperty::Dtstart, ICalendarProperty::Rrule, ICalendarProperty::Rdate, ICalendarProperty::Exrule, ] .iter() .all(|p| old.times(&oc.c, p) == next.times(&c.c, p)) && old.end(&oc.c) == next.end(&c.c); let kept_exdates = old .times(&oc.c, &ICalendarProperty::Exdate) .is_subset(&next.times(&c.c, &ICalendarProperty::Exdate)); if !same_times || !kept_exdates || organizer_of(&oc.c) != organizer_of(&c.c) || addresses(&oc.c) != addresses(&c.c) { return Err(Refused::AttendeeChange); } } // An instance the attendee overrides, to set its own status. None => { let instance = |k: i64| instances.as_ref().is_none_or(|s| s.contains(&k)); if !key .is_some_and(|k| master.is_some() && instance(k) && old.plain(&next, &c.c, k)) { return Err(Refused::AttendeeChange); } } } } // Dropping an override is fine where it held no change of the // organizer's, where the attendee deletes that instance with an EXDATE, // and in a copy of single instances. let exdated: HashSet = next .master() .into_iter() .flat_map(|m| m.c.properties(&ICalendarProperty::Exdate)) .flat_map(|e| { e.values .iter() .filter_map(|v| next.at(v.as_partial_date_time()?, e.tz_id())) }) .collect(); let mut reverted: Vec<(i64, Option)> = Vec::new(); for oc in old.comps() { let key = old.key(&oc.c); if next.find(key).is_some() { continue; } let plain = |k: i64| old.plain(&old, &oc.c, k); let allowed = |k: i64| master.is_none() || !is_range(&oc.c) && (exdated.contains(&k) || plain(k)); if !key.is_some_and(allowed) { return Err(Refused::AttendeeChange); } if let Some(k) = key.filter(|k| master.is_some() && !exdated.contains(k)) { reverted.push((k, own_partstat(&oc.c, me))); } } // The state of the others and of the organizer is the server's. A new // override takes it from the THISANDFUTURE override that moves it. let base = |key: Option| { old.find(key) .or_else(|| old.future(key?).map(|(n, _)| n)) .or(master) }; 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) = base(*key) 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 = base(*key).and_then(|b| own_partstat(&b.c, me)); if now_stat.is_some() && (force || now_stat != before) { replied.push(reply_part(c, me)); } } // A dropped override falls back to the series' answer for that instance. for (k, held) in reverted { let Some(inst) = DateTime::from_timestamp(k, 0).and_then(|t| next.single(t, &Zone::Utc)) else { continue; }; if own_partstat(&inst.c, me).is_some_and(|now| Some(now) != held) { replied.push(reply_part(&inst, 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 range = |rid: &ICalendarEntry| rid.parameter(&ICalendarParameterName::Range).cloned(); let hit = |k: i64| { gone.iter() .any(|(g, rid)| *g == k || (*g < k && range(rid).is_some())) }; // A cancelled range stands for later instances too, which an // EXDATE cannot cover: the override carries the range. let mut missing = Vec::new(); for (k, rid) in gone.iter().filter(|_| !whole) { let Some(r) = range(rid) else { continue }; let at = next.position(Some(*k)).or_else(|| { let n = next.single(DateTime::from_timestamp(*k, 0)?, &Zone::Utc)?; next.root.children.push(n); Some(next.root.children.len() - 1) }); let entry = at.and_then(|at| { next.root.children[at] .c .entries .iter_mut() .find(|e| e.name == ICalendarProperty::RecurrenceId) }); match entry { Some(e) => set_param(e, ICalendarParameterName::Range, r), None => missing.push(rid.clone()), } } 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(hit) { set_prop( &mut c.c, ICalendarProperty::Status, ICalendarValue::Status(ICalendarStatus::Cancelled), ); } } if !whole { missing.extend( gone.into_iter() .filter(|(k, rid)| range(rid).is_none() && !keys.contains(&Some(*k))) .map(|(_, rid)| rid), ); let missing: Vec = missing .into_iter() .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; let mut parts: Vec<&Node> = rep.comps().collect(); // In order: a range narrowed for one instance moves on to the next. parts.sort_by_key(|c| rep.key(&c.c)); let missing: Vec = parts .iter() .filter_map(|c| rep.key(&c.c)) .filter(|k| next.position(Some(*k)).is_none()) .collect(); // A cut-short expansion over all parts falls back to one per part. let shared = next.instances(org, &missing); for rc in parts { let key = rep.key(&rc.c); let at = match next.position(key) { Some(at) => { if let Some(t) = key && !is_range(&rc.c) && let Some(rid) = DateTime::from_timestamp(t, 0) { next.narrow(org, rid, t, &Zone::Utc); } at } // A reply for one instance of the series gets its own override. // Obj reads floating times in UTC; expand must match. None => { let Some((t, at)) = key.and_then(|t| Some((t, DateTime::from_timestamp(t, 0)?))) else { continue; }; let known = match &shared { Some(set) => set.contains(&t), None => next.instances(org, &[t]).is_none_or(|set| set.contains(&t)), }; if !known { continue; } let Some(inst) = next.single(at, &Zone::Utc) else { continue; }; 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; }; // The attendee writes these; only digits and dots are a status code. let codes: Vec = rc.c.properties(&ICalendarProperty::RequestStatus) .filter_map(|e| e.values.first()?.as_text()) .map(|s| { let code = s.split(';').next().unwrap_or(s); code.chars() .filter(|c| c.is_ascii_digit() || *c == '.') .collect::() }) .filter(|c| !c.is_empty()) .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); // A new override takes the state of the THISANDFUTURE override that moves // its instance, else of the master. let base_of = |key: Option| { let o = old.as_ref()?; o.find(key) .or_else(|| o.future(key?).map(|(n, _)| n)) .or(o.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)); let key = next.key(&c.c); key.is_none() || start != key.map(|k| o.moved(k)) || base_of(key).is_some_and(|b| { next.end(&c.c) != start.and_then(|s| o.end_from(&b.c, s)) }) } }, None => true, }) .collect(); let bumps: Vec> = keys .iter() .zip(next.comps()) .zip(&moved) .map(|((key, c), moved)| { let oc = base_of(*key)?; (*moved && sequence(&c.c) <= sequence(&oc.c)).then(|| sequence(&oc.c).saturating_add(1)) }) .collect(); for ((c, key), (moved, bump)) in next.comps_mut().zip(&keys).zip(moved.iter().zip(&bumps)) { let base = base_of(*key); 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::Rdate] .iter() .any(|p| old.times(oc, p) != new.times(nc, p)) || old.end(oc) != new.end(nc); let reinstated = !old .times(oc, &ICalendarProperty::Exdate) .is_subset(&new.times(nc, &ICalendarProperty::Exdate)); // A changed or removed EXRULE can bring instances back; a new one cannot. let exrules = ( rules(oc, &ICalendarProperty::Exrule), rules(nc, &ICalendarProperty::Exrule), ); let exrule_changed = !exrules.0.is_empty() && exrules.0 != exrules.1; moved || reinstated || exrule_changed || rules_grew(new, oc, nc) } /// A rule in one form: BY lists sorted, INTERVAL=1 and WKST=MO as absent. fn canonical(r: &ICalendarRecurrenceRule) -> ICalendarRecurrenceRule { fn tidy(v: &mut Vec) { v.sort(); v.dedup(); } let mut r = r.clone(); tidy(&mut r.bysecond); tidy(&mut r.byminute); tidy(&mut r.byhour); tidy(&mut r.byday); tidy(&mut r.bymonthday); tidy(&mut r.byyearday); tidy(&mut r.byweekno); tidy(&mut r.bymonth); tidy(&mut r.bysetpos); r.interval = r.interval.filter(|i| *i > 1); r.wkst = r.wkst.filter(|w| *w != ICalendarWeekday::Monday); r } fn rules(c: &ICalendarComponent, prop: &ICalendarProperty) -> Vec { let mut v: Vec<_> = c .properties(prop) .filter_map(|e| match e.values.first()? { ICalendarValue::RecurrenceRule(r) => Some(canonical(r)), _ => None, }) .collect(); v.sort_by_key(|r| format!("{r:?}")); v } fn rules_grew(obj: &Obj, oc: &ICalendarComponent, nc: &ICalendarComponent) -> bool { let rules = |c| rules(c, &ICalendarProperty::Rrule); 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; } // A DATE UNTIL includes its whole day. let end = |u: &PartialDateTime| Some(obj.at(u, None)? + if u.hour.is_none() { 86399 } else { 0 }); let shorter = match (&o.until, &n.until, o.count, n.count) { (Some(ou), Some(nu), _, _) => end(nu) <= end(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).saturating_add(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 { let mut out = 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-")) }); // A new SENT-BY alone is no change for an attendee. for e in n.c.entries.iter_mut().filter(|e| { matches!( e.name, ICalendarProperty::Organizer | ICalendarProperty::Attendee ) }) { remove_param(e, &ICalendarParameterName::SentBy); } if without_partstat { for e in n.c.entries.iter_mut().filter(|e| e.name == ICalendarProperty::Attendee) { remove_param(e, &ICalendarParameterName::Partstat); } } sorted(&mut n); n }) .collect::>(); out.sort_by_cached_key(|n| format!("{n:?}")); out } /// In one order, so another client's order of properties is no change. fn sorted(n: &mut Node) { for e in &mut n.c.entries { e.params.sort_by_cached_key(|p| format!("{p:?}")); } n.c.entries.sort_by_cached_key(|e| format!("{e:?}")); for ch in &mut n.children { sorted(ch); } n.children.sort_by_cached_key(|ch| format!("{ch:?}")); } /// 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 => match v { ICalendarValue::RecurrenceRule(r) => format!("{:?}", canonical(r)), _ => 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. A THISANDFUTURE override it is not in ends its /// series there. 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(); let mut end: Option = None; for o in self .comps() .filter(|c| c.c.has_property(&ICalendarProperty::RecurrenceId)) { let Some(rid) = o.c.property(&ICalendarProperty::RecurrenceId) else { continue; }; if invited(&o.c) { continue; } if is_range(&o.c) { // ponytail: a later range that invites them again is lost too. // A stale range past the series' end cuts nothing. if let Some(t) = self.instant(rid).filter(|t| self.in_series(*t)) { end = Some(end.map_or(t, |e| e.min(t))); } continue; } m.c.entries.push(ICalendarEntry { name: ICalendarProperty::Exdate, params: without(rid.params.clone(), &ICalendarParameterName::Range), values: rid.values.clone(), }); } let first = m.c.property(&ICalendarProperty::Dtstart) .and_then(|e| self.instant(e)); match end { // The range starts with the first instance: none is left. Some(t) if first.is_some_and(|f| t <= f) => {} Some(t) => { self.end_series(&mut m.c, t); out.push(m); } None => 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) } /// Whether override `c`, read through `obj`, is instance `key` of this /// series unchanged: its start, length and people, no rules of its own. fn plain(&self, obj: &Obj, c: &ICalendarComponent, key: i64) -> bool { let Some(base) = self.base(key) else { return false; }; let start = c .property(&ICalendarProperty::Dtstart) .and_then(|e| obj.instant(e)); start == Some(self.moved(key)) && [ ICalendarProperty::Rrule, ICalendarProperty::Rdate, ICalendarProperty::Exrule, ] .iter() .all(|p| !c.has_property(p)) && obj.end(c) == self.end_from(&base.c, self.moved(key)) && organizer_of(c) == organizer_of(&base.c) && addresses(c) == addresses(&base.c) && (c.status() == base.c.status() || own_progress(c, &base.c)) } /// Whether the master's own rules have an instance at `key`. fn in_series(&self, key: i64) -> bool { let (Some(m), Some(at)) = (self.master(), DateTime::from_timestamp(key, 0)) else { return false; }; let mut root = Node { c: self.root.c.clone(), children: self .root .children .iter() .filter(|n| n.c.component_type == ICalendarComponentType::VTimezone) .cloned() .collect(), }; root.children.push(m.clone()); let mut components = Vec::new(); flatten(&root, &mut components); let window = at - TimeDelta::days(1)..at + TimeDelta::days(1); expand(&ICalendar { components }, window, Zone::Utc) .instances .iter() .any(|i| i.recurrence_id == Some(at)) } /// What instance `key` copies: the THISANDFUTURE override that moves it, /// else the master. fn base(&self, key: i64) -> Option<&Node> { self.future(key).map(|(n, _)| n).or(self.master()) } /// When the component ends: DTSTART plus its length, else its DUE. fn end(&self, c: &ICalendarComponent) -> Option { match c.property(&ICalendarProperty::Dtstart) { Some(s) => self.end_from(c, self.instant(s)?), None => self.instant(c.property(&ICalendarProperty::Due)?), } } /// When `c` would end if it started at `start`. The days and weeks of a /// DURATION count in wall-clock time (RFC 5545, 3.3.6). fn end_from(&self, c: &ICalendarComponent, start: i64) -> Option { let duration = c.property(&ICalendarProperty::Duration); let d = match duration.and_then(|e| e.values.first()) { Some(ICalendarValue::Duration(d)) if !d.neg && !c.has_property(&ICalendarProperty::Dtend) && !c.has_property(&ICalendarProperty::Due) => { d } _ => return Some(start + self.length(c)?), }; let zone = self .zones .get(c.property(&ICalendarProperty::Dtstart)?.tz_id()); let days = i64::from(d.weeks) * 7 + i64::from(d.days); let local = zone.to_local(DateTime::from_timestamp(start, 0)?) + TimeDelta::days(days); let exact = i64::from(d.hours) * 3600 + i64::from(d.minutes) * 60 + i64::from(d.seconds); Some(zone.to_utc(local).timestamp() + exact) } /// Which of `keys` are instances of the series in `cal`, from one /// expansion. `None` if the expansion was cut short and cannot tell. fn instances(&self, cal: &ICalendar, keys: &[i64]) -> Option> { let starts: Vec = keys.iter().map(|k| self.moved(*k)).collect(); let at = |t: i64| DateTime::from_timestamp(t, 0); // A day either side: an all-day shift is whole days, not 24 hours. let from = at(*starts.iter().min()?)? - TimeDelta::days(1); let to = at(*starts.iter().max()?)? + TimeDelta::days(1); let e = expand(cal, from..to, Zone::Utc); (!e.truncated).then(|| { e.instances .iter() .filter_map(|i| Some(i.recurrence_id?.timestamp())) .collect() }) } /// Seconds from DTSTART to DTEND or DUE, or the DURATION. Without /// either, a day for a date and nothing for a date-time (RFC 5545). fn length(&self, c: &ICalendarComponent) -> Option { let start = c.property(&ICalendarProperty::Dtstart)?; let instant = |p: &ICalendarProperty| c.property(p).and_then(|e| self.instant(e)); match instant(&ICalendarProperty::Dtend).or_else(|| instant(&ICalendarProperty::Due)) { Some(end) => Some(end - self.instant(start)?), None => match c.property(&ICalendarProperty::Duration) { Some(d) => match d.values.first()? { ICalendarValue::Duration(d) => Some(d.as_seconds()), _ => None, }, None => { let date = start.values.first()?.as_partial_date_time()?.hour.is_none(); Some(if date { 86400 } else { 0 }) } }, } } /// Ends the series of master `m` before instant `at`: UNTIL in the form /// RFC 5545 asks for, COUNT dropped, later RDATEs removed. `at` is an /// instance, so COUNT had not ended before it. fn end_series(&self, m: &mut ICalendarComponent, at: i64) { let Some(start) = m.property(&ICalendarProperty::Dtstart).cloned() else { return; }; let Some(form) = start.values.first().and_then(|v| v.as_partial_date_time()) else { return; }; // Obj reads floating times in UTC, so `at` is their wall-clock time. let until = if form.hour.is_none() { PartialDateTime::from_date_timestamp(at - 86400) } else if form.tz_hour.is_none() && start.tz_id().is_none() { PartialDateTime::from_naive_timestamp(at - 1) } else { PartialDateTime::from_utc_timestamp(at - 1) }; for e in m.entries.iter_mut() { match e.name { ICalendarProperty::Rrule => { for v in &mut e.values { if let ICalendarValue::RecurrenceRule(r) = v && !r .until .as_ref() .and_then(|u| self.at(u, start.tz_id())) .is_some_and(|u| u < at) { r.until = Some(until.clone()); r.count = None; } } } ICalendarProperty::Rdate => { let tz = e.tz_id().map(str::to_string); e.values.retain(|v| { v.as_partial_date_time() .and_then(|p| self.at(p, tz.as_deref())) .is_none_or(|t| t < at) }); } _ => {} } } m.entries .retain(|e| e.name != ICalendarProperty::Rdate || !e.values.is_empty()); } /// 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 } } /// A RECURRENCE-ID for the instance at `rid`, in the form of `start`. fn recurrence_id( &self, start: &ICalendarEntry, rid: DateTime, floating: &Zone, ) -> Option { let v = start.values.first()?.as_partial_date_time()?; let local = |zone: &Zone| zone.to_local(rid).and_utc().timestamp(); let value = if v.hour.is_none() { PartialDateTime::from_date_timestamp(local(floating)) } else if v.tz_hour.is_some() { PartialDateTime::from_utc_timestamp(rid.timestamp()) } else if start.tz_id().is_some() { PartialDateTime::from_naive_timestamp(local(&self.zones.get(start.tz_id()))) } else { PartialDateTime::from_naive_timestamp(local(floating)) }; Some(ICalendarEntry { name: ICalendarProperty::RecurrenceId, params: without(start.params.clone(), &ICalendarParameterName::Range), values: vec![ICalendarValue::PartialDateTime(Box::new(value))], }) } /// The latest THISANDFUTURE override at or before instance `key`, and /// its key. fn future(&self, key: i64) -> Option<(&Node, i64)> { self.comps() .filter(|c| is_range(&c.c)) .filter_map(|c| Some((c, self.key(&c.c)?))) .filter(|(_, k)| *k <= key) .max_by_key(|(_, k)| *k) } /// Where instance `key` starts once a THISANDFUTURE override moves it. /// The move is wall-clock time in the zone of the series, as in expand. fn moved(&self, key: i64) -> i64 { let shifted = || { let (n, k) = self.future(key)?; let dtstart = n.c.property(&ICalendarProperty::Dtstart)?; let start = self.instant(dtstart)?; // A copy without its master still has the override's zone. let series = self .master() .and_then(|m| m.c.property(&ICalendarProperty::Dtstart)); let zone = self.zones.get(series.unwrap_or(dtstart).tz_id()); let local = |t: i64| Some(zone.to_local(DateTime::from_timestamp(t, 0)?)); let shift = local(start)? - local(k)?; Some(zone.to_utc(add_local(local(key)?, shift)).timestamp()) }; shifted().unwrap_or(key) } /// Whether the series in `cal` has an instance at `rid`, whose key is /// `key`. fn occurs(&self, cal: &ICalendar, rid: DateTime, key: i64, floating: &Zone) -> bool { let Some(at) = DateTime::from_timestamp(self.moved(key), 0) else { return false; }; // A day either side: an all-day shift is whole days, not 24 hours. let window = at - TimeDelta::days(1)..at + TimeDelta::days(1); expand(cal, window, floating.clone()) .instances .iter() .any(|i| i.recurrence_id == Some(rid)) } /// An override for the instance at `rid`, from the master or from the /// THISANDFUTURE override that moves it. fn single(&self, rid: DateTime, floating: &Zone) -> Option { let master = self.master()?; let form = master.c.property(&ICalendarProperty::Dtstart)?; let id = self.recurrence_id(form, rid, floating)?; let key = self.instant(&id)?; let Some((base, _)) = self.future(key) else { return Some(self.instance(master, &id, &id)); }; let at = self.moved(key); let start = match form.values.first()?.as_partial_date_time()?.hour { None => ICalendarEntry { values: vec![ICalendarValue::PartialDateTime(Box::new( PartialDateTime::from_date_timestamp(at), ))], ..id.clone() }, Some(_) => self.recurrence_id(form, DateTime::from_timestamp(at, 0)?, floating)?, }; Some(self.instance(base, &id, &start)) } /// Narrows the THISANDFUTURE override at instance `rid` (key `key`) to /// that instance. A copy of it carries the range on from the next instance /// without an override of its own, found by expanding `cal`. fn narrow(&mut self, cal: &ICalendar, rid: DateTime, key: i64, floating: &Zone) { let Some(at) = self.position(Some(key)) else { return; }; let Some(range) = self.root.children[at] .c .property(&ICalendarProperty::RecurrenceId) .and_then(|e| e.parameter(&ICalendarParameterName::Range)) .cloned() else { return; }; let later = self .comps() .filter(|c| is_range(&c.c)) .filter_map(|c| self.key(&c.c)) .filter(|k| *k > key) .min(); let next = self .next_free(cal, rid, key, floating) .filter(|(_, k)| later.is_none_or(|l| *k < l)) .and_then(|(r, _)| self.single(r, floating)); if let Some(mut n) = next { if let Some(e) = n.c.entries .iter_mut() .find(|e| e.name == ICalendarProperty::RecurrenceId) { set_param(e, ICalendarParameterName::Range, range); } self.root.children.push(n); } if let Some(e) = self.root.children[at] .c .entries .iter_mut() .find(|e| e.name == ICalendarProperty::RecurrenceId) { remove_param(e, &ICalendarParameterName::Range); } } /// The first instance after `rid` that has no override, and its key. // ponytail: looks ten years ahead; a range beyond that falls back to the master. fn next_free( &self, cal: &ICalendar, rid: DateTime, key: i64, floating: &Zone, ) -> Option<(DateTime, i64)> { let form = self.master()?.c.property(&ICalendarProperty::Dtstart)?; let from = DateTime::from_timestamp(self.moved(key), 0)?; [1, 32, 400, 3700].into_iter().find_map(|days| { expand(cal, from..from + TimeDelta::days(days), floating.clone()) .instances .iter() .filter_map(|i| i.recurrence_id) .filter(|r| *r > rid) .filter_map(|r| Some((r, self.instant(&self.recurrence_id(form, r, floating)?)?))) .filter(|(_, k)| self.find(Some(*k)).is_none()) .min() }) } /// An override of `base` for the instance `rid` starting at `start`, so /// it can hold a status of its own. Its length becomes a DURATION. fn instance(&self, base: &Node, rid: &ICalendarEntry, start: &ICalendarEntry) -> Node { let mut n = base.clone(); let begin = 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)) = (begin, 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(start.params.clone(), &ICalendarParameterName::Range), values: start.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) } /// A task's progress is the assignee's to set per instance, unless the /// organizer cancelled it. fn own_progress(c: &ICalendarComponent, base: &ICalendarComponent) -> bool { use ICalendarStatus::*; c.component_type == ICalendarComponentType::VTodo && matches!(c.status(), None | Some(NeedsAction | InProcess | Completed)) && base.status() != Some(&Cancelled) } fn organizer_of(c: &ICalendarComponent) -> Option { c.property(&ICalendarProperty::Organizer) .and_then(address) .map(str::to_ascii_lowercase) } /// Whether `c` repeats without end. pub(crate) fn endless(c: &ICalendarComponent) -> bool { c.properties(&ICalendarProperty::Rrule).any(|e| { matches!(e.values.first(), Some(ICalendarValue::RecurrenceRule(r)) if r.count.is_none() && r.until.is_none()) }) } 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 is_range(c: &ICalendarComponent) -> bool { c.property(&ICalendarProperty::RecurrenceId) .is_some_and(|e| e.parameter(&ICalendarParameterName::Range).is_some()) } fn client_scheduled(c: &ICalendarComponent, a: &str) -> bool { attendees(c).any(|e| address(e).is_some_and(|x| x.eq_ignore_ascii_case(a)) && !server_agent(e)) } 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) } /// Makes an object forget a principal that is about to be deleted, so that /// a later principal of the same name is not taken for it. Every ORGANIZER /// and ATTENDEE that `gone` names gets the address `tombstone`, and such an /// ATTENDEE gets SCHEDULE-STATUS 3.7. A component the gone principal /// organized is cancelled. All other lines keep their bytes. `None` when /// nothing names the principal. pub fn forget(text: &str, gone: Is, tombstone: &str) -> Option { let mut out: Vec = Vec::new(); // Per open component: name, index of its BEGIN line in `out`, index of // its STATUS line, whether the gone principal organized it. let mut open: Vec<(String, usize, Option, bool)> = Vec::new(); let mut changed = false; for raw in logical_lines(text) { let eol = match raw.ends_with('\n') { true if raw.ends_with("\r\n") => "\r\n", true => "\n", false => "\r\n", }; let line = unfold(raw); let n = name(&line); match n.as_str() { "BEGIN" => open.push(( value(&line).trim().to_ascii_uppercase(), out.len(), None, false, )), "END" => { if let Some((comp, begin, status, true)) = open.pop() && ["VEVENT", "VTODO", "VJOURNAL"].contains(&comp.as_str()) { let cancelled = format!("STATUS:CANCELLED{eol}"); match status { Some(i) => out[i] = cancelled, None => out.insert(begin + 1, cancelled), } } } "STATUS" => { if let Some(top) = open.last_mut() { top.2 = Some(out.len()); } } "ORGANIZER" | "ATTENDEE" if gone(value(&line).trim()) => { let attendee = n == "ATTENDEE"; let mut new = n.clone(); for p in param_parts(&line) { if !(attendee && p.trim() .to_ascii_uppercase() .starts_with("SCHEDULE-STATUS=")) { new.push(';'); new.push_str(p); } } if attendee { new.push_str(";SCHEDULE-STATUS=3.7"); } else if let Some(top) = open.last_mut() { top.3 = true; } new.push(':'); new.push_str(tombstone); out.push(fold(&new, eol)); changed = true; continue; } _ => {} } out.push(raw.to_string()); } changed.then(|| out.concat()) }