itip.rs
⎇
Raw
1//! Implicit scheduling (RFC 6638) as iTIP messages (RFC 5546): what a change
2//! to a scheduling object sends to whom, and how each copy changes.
3//!
4//! Calendar user addresses are compared through closures, so the caller maps
5//! them onto its principals.
6
7use std::cell::{OnceCell, RefCell};
8use std::collections::{HashMap, HashSet};
9
10use calcard::common::PartialDateTime;
11use calcard::icalendar::{
12 ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDuration, ICalendarEntry,
13 ICalendarFrequency, ICalendarMethod, ICalendarParameter, ICalendarParameterName,
14 ICalendarParameterValue, ICalendarParticipationStatus, ICalendarProperty,
15 ICalendarRecurrenceRule, ICalendarStatus, ICalendarValue, ICalendarWeekday, Uri,
16};
17use chrono::{DateTime, TimeDelta, Utc};
18use xmltree::Element;
19
20use crate::expand::expand;
21use crate::filter::TimeRange;
22use crate::freebusy::{Busy, Period, merge};
23use crate::text::{fold, logical_lines, name, param_parts, unfold, value};
24use crate::xml::{CALDAV, el};
25use crate::zone::{Zone, Zones, add_local};
26
27/// Whether an address belongs to someone in particular.
28pub type Is<'a> = &'a dyn Fn(&str) -> bool;
29
30/// How the owner of a calendar takes part in one of its objects (RFC 6638,
31/// 3.1).
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Role {
34 Organizer,
35 Attendee,
36 /// Not a scheduling object for this owner.
37 None,
38}
39
40/// A scheduling precondition a PUT fails.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Refused {
43 SameOrganizer,
44 AttendeeChange,
45}
46
47impl Refused {
48 pub fn condition(self) -> Element {
49 match self {
50 Refused::SameOrganizer => el(CALDAV, "same-organizer-in-all-components"),
51 Refused::AttendeeChange => el(CALDAV, "allowed-attendee-scheduling-object-change"),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum Method {
58 Request,
59 Cancel,
60 Reply,
61}
62
63#[derive(Debug, Clone)]
64pub struct Message {
65 /// The recipient's address, as the object writes it.
66 pub to: String,
67 pub method: Method,
68 /// Only other attendees' answers or the SEQUENCE changed. It updates an
69 /// existing copy, keeps its Schedule-Tag and leaves no inbox entry.
70 pub quiet: bool,
71 /// With METHOD.
72 pub cal: ICalendar,
73}
74
75pub fn role(cal: &ICalendar, owner: Is) -> Result<Role, Refused> {
76 let obj = Obj::new(cal);
77 let mut organizers = obj
78 .comps()
79 .filter_map(|c| address(c.c.property(&ICalendarProperty::Organizer)?));
80 let Some(organizer) = organizers.next() else {
81 return Ok(Role::None);
82 };
83 if organizers.any(|o| !o.eq_ignore_ascii_case(organizer)) {
84 return Err(Refused::SameOrganizer);
85 }
86 if owner(organizer) {
87 return Ok(Role::Organizer);
88 }
89 let attends = obj
90 .comps()
91 .flat_map(|c| attendees(&c.c))
92 .any(|e| address(e).is_some_and(owner));
93 Ok(if attends { Role::Attendee } else { Role::None })
94}
95
96/// The ORGANIZER address of a scheduling object.
97pub fn organizer(cal: &ICalendar) -> Option<&str> {
98 cal.components
99 .iter()
100 .filter(|c| is_scheduled(c))
101 .find_map(|c| address(c.property(&ICalendarProperty::Organizer)?))
102}
103
104/// An organizer's PUT (`new`) or DELETE (`None`) of a scheduling object.
105/// `old` is the stored organizer object, if any. Returns what to store and
106/// what to deliver.
107pub fn organize(
108 old: Option<&ICalendar>,
109 new: Option<ICalendar>,
110 organizer: Is,
111 now: DateTime<Utc>,
112) -> (Option<ICalendar>, Vec<Message>) {
113 let mut force = Vec::new();
114 let new = new.map(|n| guard(old, &n, organizer, &mut force));
115 let messages = messages(old, new.as_ref(), organizer, &force, now);
116 (new, messages)
117}
118
119/// The first half of [`organize`] for a PUT: what to store, with the
120/// attendee state the server owns, and the attendees whose REQUEST is
121/// forced. [`messages`] is the second half.
122pub fn prepare(
123 old: Option<&ICalendar>,
124 new: &ICalendar,
125 organizer: Is,
126) -> (ICalendar, Vec<String>) {
127 let mut force = Vec::new();
128 let store = guard(old, new, organizer, &mut force);
129 (store, force)
130}
131
132/// Names who acted for the owner: `SENT-BY` on the owner's ORGANIZER and
133/// ATTENDEE properties when `sender` is someone else, none when it is the
134/// owner. Only for writes that send a message, so every message names its
135/// real sender.
136pub fn stamp_sender(cal: &mut ICalendar, owner: Is, sender: Option<&str>) {
137 for c in cal.components.iter_mut().filter(|c| is_scheduled(c)) {
138 for e in &mut c.entries {
139 let theirs = matches!(
140 e.name,
141 ICalendarProperty::Organizer | ICalendarProperty::Attendee
142 ) && address(e).is_some_and(owner);
143 match (theirs, sender) {
144 (false, _) => {}
145 (true, Some(s)) => set_param(
146 e,
147 ICalendarParameterName::SentBy,
148 ICalendarParameterValue::Uri(Uri::Location(s.to_string())),
149 ),
150 (true, None) => remove_param(e, &ICalendarParameterName::SentBy),
151 }
152 }
153 }
154}
155
156/// How far ahead a room checks an invitation against its bookings. A
157/// series without end has infinite instances, so it is checked for two years
158/// and accepted beyond. Anything else is checked to its end, at most ten
159/// years ahead.
160pub fn answer_horizon(copy: &ICalendar) -> TimeDelta {
161 let endless = copy.components.iter().any(|c| {
162 is_scheduled(c) && !c.has_property(&ICalendarProperty::RecurrenceId) && endless(c)
163 });
164 TimeDelta::days(if endless { 731 } else { 3653 })
165}
166
167/// Conflicting instances of a series a room declines one by one. Beyond
168/// that it declines the series.
169const MAX_DECLINED_INSTANCES: usize = 100;
170
171/// The answer of a room or resource to the invitation in its copy:
172/// ACCEPTED, and DECLINED where an instance in `window` overlaps `taken`. A
173/// declined instance of a series gets an override of its own.
174pub fn auto_answer(
175 copy: &ICalendar,
176 me: Is,
177 taken: &[Period],
178 window: &TimeRange,
179 floating: &Zone,
180) -> ICalendar {
181 let taken = merge(
182 taken
183 .iter()
184 .map(|p| Period {
185 kind: Busy::Busy,
186 start: p.start,
187 end: p.end,
188 })
189 .collect(),
190 );
191 let conflicts = |s: DateTime<Utc>, e: DateTime<Utc>| {
192 let i = taken.partition_point(|p| p.end <= s);
193 taken
194 .get(i)
195 .is_some_and(|p| p.start < e || (s == e && p.start <= s))
196 };
197 let mut obj = Obj::new(copy);
198 let form = obj
199 .master()
200 .and_then(|m| m.c.property(&ICalendarProperty::Dtstart))
201 .cloned();
202 let mut declined: HashSet<Option<i64>> = HashSet::new();
203 let mut slots: Vec<DateTime<Utc>> = Vec::new();
204 for x in expand(copy, window.clone(), floating.clone()).instances {
205 if !conflicts(x.start, x.end) {
206 continue;
207 }
208 match x.recurrence_id {
209 Some(rid) if form.is_some() => slots.push(rid),
210 _ => {
211 declined.insert(obj.key(&copy.components[x.component]));
212 }
213 }
214 }
215 // One override each would bloat the copy and the REPLY.
216 let all = slots.len() > MAX_DECLINED_INSTANCES;
217 if let (false, Some(form)) = (all, &form) {
218 // In order, so a range moved on past one instance can move again.
219 slots.sort_unstable();
220 for rid in slots {
221 let Some(key) = obj
222 .recurrence_id(form, rid, floating)
223 .and_then(|id| obj.instant(&id))
224 else {
225 continue;
226 };
227 if obj.find(Some(key)).is_some() {
228 obj.narrow(copy, rid, key, floating);
229 } else if let Some(inst) = obj.single(rid, floating) {
230 obj.children_mut().push(inst);
231 } else {
232 continue;
233 }
234 declined.insert(Some(key));
235 }
236 }
237 let keys: Vec<Option<i64>> = obj.comps().map(|c| obj.key(&c.c)).collect();
238 for (c, key) in obj.comps_mut().zip(&keys) {
239 let answer = match all || declined.contains(key) {
240 true => ICalendarParticipationStatus::Declined,
241 false => ICalendarParticipationStatus::Accepted,
242 };
243 for e in
244 c.c.entries
245 .iter_mut()
246 .filter(|e| e.name == ICalendarProperty::Attendee && address(e).is_some_and(me))
247 {
248 set_param(
249 e,
250 ICalendarParameterName::Partstat,
251 partstat(answer.clone()),
252 );
253 }
254 }
255 obj.done()
256}
257
258/// The attendee's copy with `me` answering `answer`: for every component, or
259/// only for the instance at `instance`, which gets an override of its own
260/// when it has none. `floating` reads the instance of an all-day series.
261/// Storing the result through a PUT sends the REPLY. `None` if the series
262/// has no instance at `instance`.
263pub fn respond(
264 copy: &ICalendar,
265 me: Is,
266 answer: ICalendarParticipationStatus,
267 instance: Option<DateTime<Utc>>,
268 floating: &Zone,
269) -> Option<ICalendar> {
270 let mut obj = Obj::new(copy);
271 let target = match instance {
272 None => None,
273 Some(rid) => {
274 let form = obj
275 .master()
276 .and_then(|m| m.c.property(&ICalendarProperty::Dtstart))
277 .or_else(|| {
278 obj.comps()
279 .find_map(|c| c.c.property(&ICalendarProperty::RecurrenceId))
280 })?
281 .clone();
282 let key = obj.instant(&obj.recurrence_id(&form, rid, floating)?)?;
283 if obj.find(Some(key)).is_some() {
284 obj.narrow(copy, rid, key, floating);
285 } else {
286 if !obj.occurs(copy, rid, key, floating) {
287 return None;
288 }
289 let inst = obj.single(rid, floating)?;
290 obj.children_mut().push(inst);
291 }
292 Some(Some(key))
293 }
294 };
295 let keys: Vec<Option<i64>> = obj.comps().map(|c| obj.key(&c.c)).collect();
296 for (c, key) in obj.comps_mut().zip(&keys) {
297 if target.is_some_and(|t| t != *key) {
298 continue;
299 }
300 for e in
301 c.c.entries
302 .iter_mut()
303 .filter(|e| e.name == ICalendarProperty::Attendee && address(e).is_some_and(me))
304 {
305 set_param(
306 e,
307 ICalendarParameterName::Partstat,
308 partstat(answer.clone()),
309 );
310 remove_param(e, &ICalendarParameterName::Rsvp);
311 }
312 }
313 Some(obj.done())
314}
315
316/// The messages a change of the organizer object sends, without touching
317/// the attendee state in it. `force` lists attendees who get a REQUEST even
318/// if nothing changed for them.
319pub fn messages(
320 old: Option<&ICalendar>,
321 new: Option<&ICalendar>,
322 organizer: Is,
323 force: &[String],
324 now: DateTime<Utc>,
325) -> Vec<Message> {
326 let old = old.map(Obj::new);
327 let new = new.map(Obj::new);
328 let mut who: Vec<&str> = Vec::new();
329 let mut seen: HashSet<String> = HashSet::new();
330 for obj in old.iter().chain(new.iter()) {
331 for e in obj.comps().flat_map(|c| attendees(&c.c)) {
332 if let Some(a) = address(e)
333 && server_agent(e)
334 && !organizer(a)
335 && seen.insert(a.to_ascii_lowercase())
336 {
337 who.push(a);
338 }
339 }
340 }
341 // Attendees invited to the same components share one view.
342 let old_inv = old.as_ref().map(Obj::invitations);
343 let new_inv = new.as_ref().map(Obj::invitations);
344 let mut old_views: HashMap<Vec<bool>, Option<Vec<Node>>> = HashMap::new();
345 let mut new_views: HashMap<Vec<bool>, Option<Vec<Node>>> = HashMap::new();
346 let mut changes: HashMap<(Vec<bool>, Vec<bool>), Change> = HashMap::new();
347 let mut out = Vec::new();
348 for a in who {
349 let lower = a.to_ascii_lowercase();
350 let sig = |inv: &Vec<HashSet<String>>| -> Vec<bool> {
351 inv.iter().map(|s| s.contains(&lower)).collect()
352 };
353 let before = old.as_ref().zip(old_inv.as_ref()).and_then(|(o, inv)| {
354 let s = sig(inv);
355 let v = old_views.entry(s.clone()).or_insert_with(|| o.view(&s));
356 Some((o, v.clone()?, s))
357 });
358 let after = new.as_ref().zip(new_inv.as_ref()).and_then(|(n, inv)| {
359 let s = sig(inv);
360 let v = new_views.entry(s.clone()).or_insert_with(|| n.view(&s));
361 Some((n, v.clone()?, s))
362 });
363 let (method, quiet, comps, src) = match (before, after) {
364 (Some((src, b, _)), None) => {
365 // A component that lists them with SCHEDULE-AGENT=CLIENT
366 // now: the client tells them.
367 let lost: Vec<Node> = b
368 .into_iter()
369 .filter(|c| {
370 !new.as_ref()
371 .and_then(|n| n.find(src.key(&c.c)))
372 .is_some_and(|nc| client_scheduled(&nc.c, a))
373 })
374 .collect();
375 if lost.is_empty() {
376 continue;
377 }
378 (Method::Cancel, false, cancelled(lost), src)
379 }
380 (None, Some((src, comps, _))) => (Method::Request, false, comps, src),
381 (Some((_, b, sb)), Some((src, comps, sa))) => {
382 let change = *changes
383 .entry((sb, sa))
384 .or_insert_with(|| change(&b, &comps));
385 let forced = force.iter().any(|f| f.eq_ignore_ascii_case(a));
386 let quiet = match change {
387 Change::Content => false,
388 Change::Answers => true,
389 _ if forced => false,
390 // Copies carry the new SEQUENCE, or their replies count
391 // as stale.
392 Change::Sequence => true,
393 Change::None => continue,
394 };
395 (Method::Request, quiet, comps, src)
396 }
397 (None, None) => continue,
398 };
399 out.push(Message {
400 to: a.to_string(),
401 method,
402 quiet,
403 cal: src.envelope(comps, method, now),
404 });
405 }
406 out
407}
408
409/// Records the delivery status for `to` on its ATTENDEE properties.
410pub fn set_attendee_status(cal: &mut ICalendar, to: &str, status: &str) {
411 for c in &mut cal.components {
412 for e in c
413 .entries
414 .iter_mut()
415 .filter(|e| e.name == ICalendarProperty::Attendee)
416 {
417 if address(e).is_some_and(|a| a.eq_ignore_ascii_case(to)) {
418 set_param(e, ICalendarParameterName::ScheduleStatus, text(status));
419 }
420 }
421 }
422}
423
424/// Records the delivery status of a REPLY on the ORGANIZER properties.
425pub fn set_organizer_status(cal: &mut ICalendar, status: &str) {
426 for c in &mut cal.components {
427 for e in c
428 .entries
429 .iter_mut()
430 .filter(|e| e.name == ICalendarProperty::Organizer)
431 {
432 set_param(e, ICalendarParameterName::ScheduleStatus, text(status));
433 }
434 }
435}
436
437/// An attendee's PUT over the stored copy. Returns what to store and the
438/// REPLY, if the attendee's participation changed.
439pub fn attend(
440 old: &ICalendar,
441 new: ICalendar,
442 me: Is,
443 now: DateTime<Utc>,
444) -> Result<(ICalendar, Option<Message>), Refused> {
445 let old_cal = old;
446 let old = Obj::new(old_cal);
447 if !old.organizer_schedules() {
448 return Ok((new, None));
449 }
450 let mut next = Obj::new(&new);
451 let master = old.master();
452 let added: Vec<i64> = next
453 .comps()
454 .filter_map(|c| next.key(&c.c))
455 .filter(|k| old.find(Some(*k)).is_none())
456 .collect();
457 let instances = old.instances(old_cal, &added);
458
459 for c in next.comps() {
460 let key = next.key(&c.c);
461 match old.find(key) {
462 Some(oc) => {
463 // The end, not its property: clients rewrite DURATION as DTEND.
464 let same_times = [
465 ICalendarProperty::Dtstart,
466 ICalendarProperty::Rrule,
467 ICalendarProperty::Rdate,
468 ICalendarProperty::Exrule,
469 ]
470 .iter()
471 .all(|p| old.times(&oc.c, p) == next.times(&c.c, p))
472 && old.end(&oc.c) == next.end(&c.c);
473 let kept_exdates = old
474 .times(&oc.c, &ICalendarProperty::Exdate)
475 .is_subset(&next.times(&c.c, &ICalendarProperty::Exdate));
476 if !same_times
477 || !kept_exdates
478 || organizer_of(&oc.c) != organizer_of(&c.c)
479 || addresses(&oc.c) != addresses(&c.c)
480 {
481 return Err(Refused::AttendeeChange);
482 }
483 }
484 // An instance the attendee overrides, to set its own status.
485 None => {
486 let instance = |k: i64| instances.as_ref().is_none_or(|s| s.contains(&k));
487 if !key.is_some_and(|k| {
488 master.is_some() && instance(k) && old.plain(&next, &c.c, k, true)
489 }) {
490 return Err(Refused::AttendeeChange);
491 }
492 }
493 }
494 }
495 // Dropping an override is fine where it held no change of the
496 // organizer's, where the attendee deletes that instance with an EXDATE,
497 // and in a copy of single instances.
498 let exdated: HashSet<i64> = next
499 .master()
500 .into_iter()
501 .flat_map(|m| m.c.properties(&ICalendarProperty::Exdate))
502 .flat_map(|e| {
503 e.values
504 .iter()
505 .filter_map(|v| next.at(v.as_partial_date_time()?, e.tz_id()))
506 })
507 .collect();
508 let mut reverted: Vec<(i64, Option<String>)> = Vec::new();
509 for oc in old.comps() {
510 let key = old.key(&oc.c);
511 if next.find(key).is_some() {
512 continue;
513 }
514 let plain = |k: i64| old.plain(&old, &oc.c, k, false);
515 let allowed =
516 |k: i64| master.is_none() || !is_range(&oc.c) && (exdated.contains(&k) || plain(k));
517 if !key.is_some_and(allowed) {
518 return Err(Refused::AttendeeChange);
519 }
520 if let Some(k) = key.filter(|k| master.is_some() && !exdated.contains(k)) {
521 reverted.push((k, own_partstat(&oc.c, me)));
522 }
523 }
524
525 // The state of the others and of the organizer is the server's. A new
526 // override takes it from the THISANDFUTURE override that moves it.
527 let base = |key: Option<i64>| {
528 old.find(key)
529 .or_else(|| old.future(key?).map(|(n, _)| n))
530 .or(master)
531 };
532 let mut force = false;
533 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
534 for (c, key) in next.comps_mut().zip(&keys) {
535 let Some(base) = base(*key) else {
536 continue;
537 };
538 for e in &mut c.c.entries {
539 match e.name {
540 ICalendarProperty::Attendee if !address(e).is_some_and(me) => {
541 if let Some(b) = same_attendee(&base.c, e) {
542 e.params = b.params.clone();
543 }
544 }
545 ICalendarProperty::Organizer => {
546 force |= param(e, &ICalendarParameterName::ScheduleForceSend)
547 .is_some_and(|v| v.eq_ignore_ascii_case("REPLY"));
548 if let Some(b) = base.c.property(&ICalendarProperty::Organizer) {
549 e.params = b.params.clone();
550 }
551 }
552 _ => {}
553 }
554 }
555 }
556
557 let mut replied: Vec<Node> = Vec::new();
558 for (c, key) in next.comps().zip(&keys) {
559 let now_stat = own_partstat(&c.c, me);
560 let before = base(*key).and_then(|b| own_partstat(&b.c, me));
561 if now_stat.is_some() && (force || now_stat != before) {
562 replied.push(reply_part(c, me));
563 }
564 }
565 // A dropped override falls back to the series' answer for that instance.
566 for (k, held) in reverted {
567 let Some(inst) = DateTime::from_timestamp(k, 0).and_then(|t| next.single(t, &Zone::Utc))
568 else {
569 continue;
570 };
571 if own_partstat(&inst.c, me).is_some_and(|now| Some(now) != held) {
572 replied.push(reply_part(&inst, me));
573 }
574 }
575 if let Some(m) = next.master() {
576 let before = master.map_or_else(HashSet::new, |om| {
577 old.times(&om.c, &ICalendarProperty::Exdate)
578 });
579 for e in m.c.properties(&ICalendarProperty::Exdate) {
580 for value in &e.values {
581 if before.contains(&next.value_key(e, value)) {
582 continue;
583 }
584 let rid = ICalendarEntry {
585 name: ICalendarProperty::RecurrenceId,
586 params: e.params.clone(),
587 values: vec![value.clone()],
588 };
589 replied.push(declined_instance(m, rid, me));
590 }
591 }
592 }
593
594 let reply = match replied.is_empty() {
595 true => None,
596 false => next.organizer().map(|to| Message {
597 to,
598 method: Method::Reply,
599 quiet: false,
600 cal: next.envelope(replied, Method::Reply, now),
601 }),
602 };
603 Ok((next.done(), reply))
604}
605
606/// An attendee's DELETE: the REPLY declining every instance, unless the
607/// organizer handles scheduling itself or already cancelled.
608pub fn decline(old: &ICalendar, me: Is, now: DateTime<Utc>) -> Option<Message> {
609 let old = Obj::new(old);
610 if !old.organizer_schedules() {
611 return None;
612 }
613 let cancelled = old.comps().all(|c| {
614 c.c.property(&ICalendarProperty::Status)
615 .and_then(|e| e.values.first()?.as_text())
616 .is_some_and(|s| s.eq_ignore_ascii_case("CANCELLED"))
617 });
618 if cancelled {
619 return None;
620 }
621 let comps: Vec<Node> =
622 old.comps()
623 .filter(|c| own_partstat(&c.c, me).is_some())
624 .map(|c| {
625 let mut part = reply_part(c, me);
626 for e in
627 part.c.entries.iter_mut().filter(|e| {
628 e.name == ICalendarProperty::Attendee && address(e).is_some_and(me)
629 })
630 {
631 set_param(
632 e,
633 ICalendarParameterName::Partstat,
634 partstat(ICalendarParticipationStatus::Declined),
635 );
636 }
637 part
638 })
639 .collect();
640 if comps.is_empty() {
641 return None;
642 }
643 Some(Message {
644 to: old.organizer()?,
645 method: Method::Reply,
646 quiet: false,
647 cal: old.envelope(comps, Method::Reply, now),
648 })
649}
650
651/// A REQUEST or CANCEL applied to the attendee's copy. `None`: nothing to
652/// store.
653pub fn receive(copy: Option<&ICalendar>, msg: &Message) -> Option<ICalendar> {
654 match msg.method {
655 Method::Request => {
656 if msg.quiet && copy.is_none() {
657 return None;
658 }
659 let copy = copy.map(Obj::new);
660 let mut next = Obj::new(&msg.cal);
661 next.root
662 .c
663 .entries
664 .retain(|e| e.name != ICalendarProperty::Method);
665 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
666 let Some(copy) = copy else {
667 return Some(next.done());
668 };
669 // What the attendee may keep for itself (RFC 6638, 3.2.2.1).
670 for (c, key) in next.comps_mut().zip(&keys) {
671 let Some(base) = copy.find(*key).or(copy.master()) else {
672 continue;
673 };
674 c.children
675 .retain(|n| n.c.component_type != ICalendarComponentType::VAlarm);
676 c.children.extend(
677 base.children
678 .iter()
679 .filter(|n| n.c.component_type == ICalendarComponentType::VAlarm)
680 .cloned(),
681 );
682 for p in [
683 ICalendarProperty::Transp,
684 ICalendarProperty::PercentComplete,
685 ICalendarProperty::Completed,
686 ] {
687 c.c.entries.retain(|e| e.name != p);
688 c.c.entries.extend(base.c.properties(&p).cloned());
689 }
690 let status = base
691 .c
692 .property(&ICalendarProperty::Organizer)
693 .and_then(|e| e.parameter(&ICalendarParameterName::ScheduleStatus))
694 .cloned();
695 if let Some(s) = status {
696 for e in
697 c.c.entries
698 .iter_mut()
699 .filter(|e| e.name == ICalendarProperty::Organizer)
700 {
701 set_param(e, ICalendarParameterName::ScheduleStatus, s.clone());
702 }
703 }
704 }
705 Some(next.done())
706 }
707 Method::Cancel => {
708 let msg_obj = Obj::new(&msg.cal);
709 let mut next = Obj::new(copy?);
710 let whole = msg_obj.master().is_some();
711 let gone: Vec<(i64, ICalendarEntry)> = msg_obj
712 .comps()
713 .filter_map(|c| {
714 let rid = c.c.property(&ICalendarProperty::RecurrenceId)?;
715 Some((msg_obj.instant(rid)?, rid.clone()))
716 })
717 .collect();
718 let range =
719 |rid: &ICalendarEntry| rid.parameter(&ICalendarParameterName::Range).cloned();
720 let hit = |k: i64| {
721 gone.iter()
722 .any(|(g, rid)| *g == k || (*g < k && range(rid).is_some()))
723 };
724 // A cancelled range stands for later instances too, which an
725 // EXDATE cannot cover: the override carries the range.
726 let mut missing = Vec::new();
727 for (k, rid) in gone.iter().filter(|_| !whole) {
728 let Some(r) = range(rid) else { continue };
729 let at = next.position(Some(*k)).or_else(|| {
730 let n = next.single(DateTime::from_timestamp(*k, 0)?, &Zone::Utc)?;
731 next.children_mut().push(n);
732 Some(next.root.children.len() - 1)
733 });
734 let entry = at.and_then(|at| {
735 next.children_mut()[at]
736 .c
737 .entries
738 .iter_mut()
739 .find(|e| e.name == ICalendarProperty::RecurrenceId)
740 });
741 match entry {
742 Some(e) => set_param(e, ICalendarParameterName::Range, r),
743 None => missing.push(rid.clone()),
744 }
745 }
746 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
747 for (c, key) in next.comps_mut().zip(&keys) {
748 if whole || key.is_some_and(hit) {
749 set_prop(
750 &mut c.c,
751 ICalendarProperty::Status,
752 ICalendarValue::Status(ICalendarStatus::Cancelled),
753 );
754 }
755 }
756 if !whole {
757 missing.extend(
758 gone.into_iter()
759 .filter(|(k, rid)| range(rid).is_none() && !keys.contains(&Some(*k)))
760 .map(|(_, rid)| rid),
761 );
762 let missing: Vec<ICalendarEntry> = missing
763 .into_iter()
764 .map(|rid| ICalendarEntry {
765 name: ICalendarProperty::Exdate,
766 params: without(rid.params, &ICalendarParameterName::Range),
767 values: rid.values,
768 })
769 .collect();
770 if let Some(m) = next
771 .comps_mut()
772 .find(|c| !c.c.has_property(&ICalendarProperty::RecurrenceId))
773 {
774 m.c.entries.extend(missing);
775 }
776 }
777 Some(next.done())
778 }
779 Method::Reply => None,
780 }
781}
782
783/// A REPLY applied to the organizer object. `false` if it changed nothing.
784pub fn apply_reply(org: &mut ICalendar, reply: &ICalendar, replier: Is) -> bool {
785 let rep = Obj::new(reply);
786 let mut next = Obj::new(org);
787 let mut changed = false;
788 let mut parts: Vec<&Node> = rep.comps().collect();
789 // In order: a range narrowed for one instance moves on to the next.
790 parts.sort_by_key(|c| rep.key(&c.c));
791 let missing: Vec<i64> = parts
792 .iter()
793 .filter_map(|c| rep.key(&c.c))
794 .filter(|k| next.position(Some(*k)).is_none())
795 .collect();
796 // A cut-short expansion over all parts falls back to one per part.
797 let shared = next.instances(org, &missing);
798 for rc in parts {
799 let key = rep.key(&rc.c);
800 let at = match next.position(key) {
801 Some(at) => {
802 if let Some(t) = key
803 && !is_range(&rc.c)
804 && let Some(rid) = DateTime::from_timestamp(t, 0)
805 {
806 next.narrow(org, rid, t, &Zone::Utc);
807 }
808 at
809 }
810 // A reply for one instance of the series gets its own override.
811 // Obj reads floating times in UTC; expand must match.
812 None => {
813 let Some((t, at)) = key.and_then(|t| Some((t, DateTime::from_timestamp(t, 0)?)))
814 else {
815 continue;
816 };
817 let known = match &shared {
818 Some(set) => set.contains(&t),
819 None => next.instances(org, &[t]).is_none_or(|set| set.contains(&t)),
820 };
821 if !known {
822 continue;
823 }
824 let Some(inst) = next.single(at, &Zone::Utc) else {
825 continue;
826 };
827 next.children_mut().push(inst);
828 next.root.children.len() - 1
829 }
830 };
831 let target = &mut next.children_mut()[at];
832 if sequence(&rc.c) < sequence(&target.c) {
833 continue;
834 }
835 let Some(stat) =
836 rc.c.properties(&ICalendarProperty::Attendee)
837 .find(|e| address(e).is_some_and(replier))
838 .map(|e| {
839 e.parameter(&ICalendarParameterName::Partstat)
840 .cloned()
841 .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction))
842 })
843 else {
844 continue;
845 };
846 // The attendee writes these; only digits and dots are a status code.
847 let codes: Vec<String> =
848 rc.c.properties(&ICalendarProperty::RequestStatus)
849 .filter_map(|e| e.values.first()?.as_text())
850 .map(|s| {
851 let code = s.split(';').next().unwrap_or(s);
852 code.chars()
853 .filter(|c| c.is_ascii_digit() || *c == '.')
854 .collect::<String>()
855 })
856 .filter(|c| !c.is_empty())
857 .collect();
858 let status = match codes.is_empty() {
859 true => "2.0".to_string(),
860 false => codes.join(","),
861 };
862 for e in
863 target.c.entries.iter_mut().filter(|e| {
864 e.name == ICalendarProperty::Attendee && address(e).is_some_and(replier)
865 })
866 {
867 changed |= e.parameter(&ICalendarParameterName::Partstat) != Some(&stat);
868 set_param(e, ICalendarParameterName::Partstat, stat.clone());
869 set_param(e, ICalendarParameterName::ScheduleStatus, text(&status));
870 }
871 }
872 if changed {
873 *org = next.done();
874 }
875 changed
876}
877
878// ---------------------------------------------------------------------------
879// The organizer object
880// ---------------------------------------------------------------------------
881
882/// The client's organizer object with the attendee state the server owns:
883/// their PARTSTAT (reset on a reschedule, RFC 6638 3.2.8) and
884/// SCHEDULE-STATUS. Collects SCHEDULE-FORCE-SEND=REQUEST into `force`.
885fn guard(
886 old: Option<&ICalendar>,
887 new: &ICalendar,
888 organizer: Is,
889 force: &mut Vec<String>,
890) -> ICalendar {
891 let old = old.map(Obj::new);
892 let mut next = Obj::new(new);
893 // A new override takes the state of the THISANDFUTURE override that moves
894 // its instance, else of the master.
895 let base_of = |key: Option<i64>| {
896 let o = old.as_ref()?;
897 o.find(key)
898 .or_else(|| o.future(key?).map(|(n, _)| n))
899 .or(o.master())
900 };
901 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
902 let moved: Vec<bool> = next
903 .comps()
904 .map(|c| match &old {
905 Some(o) => match o.find(next.key(&c.c)) {
906 Some(oc) => rescheduled(o, &oc.c, &next, &c.c),
907 // A new override: rescheduled if it moves its instance.
908 None => {
909 let start =
910 c.c.property(&ICalendarProperty::Dtstart)
911 .and_then(|e| next.instant(e));
912 let key = next.key(&c.c);
913 key.is_none()
914 || start != key.map(|k| o.moved(k))
915 || base_of(key).is_some_and(|b| {
916 next.end(&c.c) != start.and_then(|s| o.end_from(&b.c, s))
917 })
918 }
919 },
920 None => true,
921 })
922 .collect();
923 let bumps: Vec<Option<i64>> = keys
924 .iter()
925 .zip(next.comps())
926 .zip(&moved)
927 .map(|((key, c), moved)| {
928 let oc = base_of(*key)?;
929 (*moved && sequence(&c.c) <= sequence(&oc.c)).then(|| sequence(&oc.c).saturating_add(1))
930 })
931 .collect();
932
933 for ((c, key), (moved, bump)) in next.comps_mut().zip(&keys).zip(moved.iter().zip(&bumps)) {
934 let base = base_of(*key);
935 for e in &mut c.c.entries {
936 match e.name {
937 ICalendarProperty::Organizer => {
938 remove_param(e, &ICalendarParameterName::ScheduleForceSend);
939 }
940 ICalendarProperty::Attendee => {
941 let forced = param(e, &ICalendarParameterName::ScheduleForceSend)
942 .is_some_and(|v| v.eq_ignore_ascii_case("REQUEST"));
943 remove_param(e, &ICalendarParameterName::ScheduleForceSend);
944 let Some(a) = address(e).map(str::to_string) else {
945 continue;
946 };
947 if organizer(&a) || !server_agent(e) {
948 continue;
949 }
950 if forced {
951 force.push(a.clone());
952 }
953 let prev = base.and_then(|b| same_attendee(&b.c, e));
954 let stat = match (moved, prev) {
955 (false, Some(p)) => p
956 .parameter(&ICalendarParameterName::Partstat)
957 .cloned()
958 .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction)),
959 _ => partstat(ICalendarParticipationStatus::NeedsAction),
960 };
961 set_param(e, ICalendarParameterName::Partstat, stat);
962 match prev.and_then(|p| p.parameter(&ICalendarParameterName::ScheduleStatus)) {
963 Some(s) => set_param(e, ICalendarParameterName::ScheduleStatus, s.clone()),
964 None => remove_param(e, &ICalendarParameterName::ScheduleStatus),
965 }
966 }
967 _ => {}
968 }
969 }
970 if let Some(n) = bump {
971 set_prop(
972 &mut c.c,
973 ICalendarProperty::Sequence,
974 ICalendarValue::Integer(*n),
975 );
976 }
977 }
978 next.done()
979}
980
981/// Whether a change moves instances in time (RFC 6638, 3.2.8). Shortening a
982/// series or excluding instances does not.
983fn rescheduled(old: &Obj, oc: &ICalendarComponent, new: &Obj, nc: &ICalendarComponent) -> bool {
984 let moved = [ICalendarProperty::Dtstart, ICalendarProperty::Rdate]
985 .iter()
986 .any(|p| old.times(oc, p) != new.times(nc, p))
987 || old.end(oc) != new.end(nc);
988 let reinstated = !old
989 .times(oc, &ICalendarProperty::Exdate)
990 .is_subset(&new.times(nc, &ICalendarProperty::Exdate));
991 // A changed or removed EXRULE can bring instances back; a new one cannot.
992 let exrules = (
993 rules(oc, &ICalendarProperty::Exrule),
994 rules(nc, &ICalendarProperty::Exrule),
995 );
996 let exrule_changed = !exrules.0.is_empty() && exrules.0 != exrules.1;
997 moved || reinstated || exrule_changed || rules_grew(new, oc, nc)
998}
999
1000/// A rule in one form: BY lists sorted, INTERVAL=1 as absent, and WKST
1001/// absent where it cannot change the instances.
1002fn canonical(r: &ICalendarRecurrenceRule) -> ICalendarRecurrenceRule {
1003 fn tidy<T: Ord>(v: &mut Vec<T>) {
1004 v.sort();
1005 v.dedup();
1006 }
1007 let mut r = r.clone();
1008 tidy(&mut r.bysecond);
1009 tidy(&mut r.byminute);
1010 tidy(&mut r.byhour);
1011 tidy(&mut r.byday);
1012 tidy(&mut r.bymonthday);
1013 tidy(&mut r.byyearday);
1014 tidy(&mut r.byweekno);
1015 tidy(&mut r.bymonth);
1016 tidy(&mut r.bysetpos);
1017 r.interval = r.interval.filter(|i| *i > 1);
1018 // WKST only bounds weeks of a WEEKLY rule that skips weeks or picks by
1019 // BYSETPOS, and the weeks of BYWEEKNO (RFC 5545, 3.3.10).
1020 let weeks =
1021 r.freq == ICalendarFrequency::Weekly && (r.interval.is_some() || !r.bysetpos.is_empty());
1022 if !weeks && r.byweekno.is_empty() {
1023 r.wkst = None;
1024 }
1025 r.wkst = r.wkst.filter(|w| *w != ICalendarWeekday::Monday);
1026 r
1027}
1028
1029fn rules(c: &ICalendarComponent, prop: &ICalendarProperty) -> Vec<ICalendarRecurrenceRule> {
1030 let mut v: Vec<_> = c
1031 .properties(prop)
1032 .filter_map(|e| match e.values.first()? {
1033 ICalendarValue::RecurrenceRule(r) => Some(canonical(r)),
1034 _ => None,
1035 })
1036 .collect();
1037 v.sort_by_key(|r| format!("{r:?}"));
1038 v
1039}
1040
1041fn rules_grew(obj: &Obj, oc: &ICalendarComponent, nc: &ICalendarComponent) -> bool {
1042 let rules = |c| rules(c, &ICalendarProperty::Rrule);
1043 let (o, n) = (rules(oc), rules(nc));
1044 if o == n {
1045 return false;
1046 }
1047 let (o, n) = match (&o[..], &n[..]) {
1048 // Dropping the rule leaves the first instance only.
1049 ([_, ..], []) => return false,
1050 ([o], [n]) => (o, n),
1051 _ => return true,
1052 };
1053 let unbounded = |r: &calcard::icalendar::ICalendarRecurrenceRule| {
1054 let mut r = r.clone();
1055 r.until = None;
1056 r.count = None;
1057 r
1058 };
1059 if unbounded(o) != unbounded(n) {
1060 return true;
1061 }
1062 // A DATE UNTIL includes its whole day.
1063 let end =
1064 |u: &PartialDateTime| Some(obj.at(u, None)? + if u.hour.is_none() { 86399 } else { 0 });
1065 let shorter = match (&o.until, &n.until, o.count, n.count) {
1066 (Some(ou), Some(nu), _, _) => end(nu) <= end(ou),
1067 (_, _, Some(oc), Some(nc)) => nc <= oc,
1068 (None, _, None, _) => true,
1069 _ => false,
1070 };
1071 !shorter
1072}
1073
1074fn cancelled(comps: Vec<Node>) -> Vec<Node> {
1075 comps
1076 .into_iter()
1077 .map(|mut n| {
1078 set_prop(
1079 &mut n.c,
1080 ICalendarProperty::Status,
1081 ICalendarValue::Status(ICalendarStatus::Cancelled),
1082 );
1083 let seq = sequence(&n.c).saturating_add(1);
1084 set_prop(
1085 &mut n.c,
1086 ICalendarProperty::Sequence,
1087 ICalendarValue::Integer(seq),
1088 );
1089 n
1090 })
1091 .collect()
1092}
1093
1094/// How an attendee's view changed.
1095#[derive(Clone, Copy)]
1096enum Change {
1097 /// More than the participation of others.
1098 Content,
1099 /// Only the participation of others.
1100 Answers,
1101 /// Only the SEQUENCE.
1102 Sequence,
1103 None,
1104}
1105
1106fn change(before: &[Node], after: &[Node]) -> Change {
1107 let sequences = |comps: &[Node]| {
1108 let mut v: Vec<i64> = comps.iter().map(|n| sequence(&n.c)).collect();
1109 v.sort_unstable();
1110 v
1111 };
1112 if normalized(before, true) != normalized(after, true) {
1113 Change::Content
1114 } else if normalized(before, false) != normalized(after, false) {
1115 Change::Answers
1116 } else if sequences(before) != sequences(after) {
1117 Change::Sequence
1118 } else {
1119 Change::None
1120 }
1121}
1122
1123/// For comparing what an attendee would receive: without the stamps a
1124/// client rewrites on every save, and optionally without participation.
1125fn normalized(comps: &[Node], without_partstat: bool) -> Vec<Node> {
1126 let mut out = comps
1127 .iter()
1128 .map(|n| {
1129 let mut n = n.clone();
1130 n.c.entries.retain(|e| {
1131 !matches!(
1132 e.name,
1133 ICalendarProperty::Dtstamp
1134 | ICalendarProperty::LastModified
1135 | ICalendarProperty::Created
1136 | ICalendarProperty::Sequence
1137 ) && !matches!(&e.name, ICalendarProperty::Other(x) if x.to_ascii_uppercase().starts_with("X-"))
1138 });
1139 // A new SENT-BY alone is no change for an attendee.
1140 for e in n.c.entries.iter_mut().filter(|e| {
1141 matches!(
1142 e.name,
1143 ICalendarProperty::Organizer | ICalendarProperty::Attendee
1144 )
1145 }) {
1146 remove_param(e, &ICalendarParameterName::SentBy);
1147 }
1148 if without_partstat {
1149 for e in n.c.entries.iter_mut().filter(|e| e.name == ICalendarProperty::Attendee) {
1150 remove_param(e, &ICalendarParameterName::Partstat);
1151 }
1152 }
1153 sorted(&mut n);
1154 n
1155 })
1156 .collect::<Vec<_>>();
1157 out.sort_by_cached_key(|n| format!("{n:?}"));
1158 out
1159}
1160
1161/// In one order, so another client's order of properties is no change.
1162fn sorted(n: &mut Node) {
1163 for e in &mut n.c.entries {
1164 e.params.sort_by_cached_key(|p| format!("{p:?}"));
1165 }
1166 n.c.entries.sort_by_cached_key(|e| format!("{e:?}"));
1167 for ch in &mut n.children {
1168 sorted(ch);
1169 }
1170 n.children.sort_by_cached_key(|ch| format!("{ch:?}"));
1171}
1172
1173/// A component of the attendee's REPLY: only its own ATTENDEE, no alarms.
1174fn reply_part(c: &Node, me: Is) -> Node {
1175 let mut n = Node {
1176 c: c.c.clone(),
1177 children: Vec::new(),
1178 };
1179 n.c.entries
1180 .retain(|e| e.name != ICalendarProperty::Attendee || address(e).is_some_and(me));
1181 strip_scheduling_params(&mut n.c);
1182 n
1183}
1184
1185/// The REPLY component for an instance the attendee excluded with EXDATE.
1186fn declined_instance(master: &Node, rid: ICalendarEntry, me: Is) -> Node {
1187 let mut c = ICalendarComponent {
1188 component_type: master.c.component_type.clone(),
1189 entries: Vec::new(),
1190 component_ids: Vec::new(),
1191 };
1192 for p in [
1193 ICalendarProperty::Uid,
1194 ICalendarProperty::Sequence,
1195 ICalendarProperty::Organizer,
1196 ICalendarProperty::Summary,
1197 ] {
1198 c.entries.extend(master.c.properties(&p).cloned());
1199 }
1200 c.entries.push(ICalendarEntry {
1201 name: ICalendarProperty::Dtstart,
1202 ..rid.clone()
1203 });
1204 c.entries.push(rid);
1205 for e in master
1206 .c
1207 .properties(&ICalendarProperty::Attendee)
1208 .filter(|e| address(e).is_some_and(me))
1209 {
1210 let mut e = e.clone();
1211 set_param(
1212 &mut e,
1213 ICalendarParameterName::Partstat,
1214 partstat(ICalendarParticipationStatus::Declined),
1215 );
1216 c.entries.push(e);
1217 }
1218 strip_scheduling_params(&mut c);
1219 Node {
1220 c,
1221 children: Vec::new(),
1222 }
1223}
1224
1225// ---------------------------------------------------------------------------
1226// The object as a tree
1227// ---------------------------------------------------------------------------
1228
1229/// A component with its sub-components, detached from the flat list calcard
1230/// keeps.
1231#[derive(Debug, Clone, PartialEq)]
1232struct Node {
1233 /// `component_ids` is empty; `children` replaces it.
1234 c: ICalendarComponent,
1235 children: Vec<Node>,
1236}
1237
1238fn node(cal: &ICalendar, i: usize) -> Node {
1239 let mut c = cal.components[i].clone();
1240 let ids = std::mem::take(&mut c.component_ids);
1241 let children = ids
1242 .iter()
1243 .map(|&id| id as usize)
1244 .filter(|&id| id > i && id < cal.components.len())
1245 .map(|id| node(cal, id))
1246 .collect();
1247 Node { c, children }
1248}
1249
1250fn flatten(n: &Node, out: &mut Vec<ICalendarComponent>) -> u32 {
1251 let at = out.len();
1252 out.push(n.c.clone());
1253 for ch in &n.children {
1254 let id = flatten(ch, out);
1255 out[at].component_ids.push(id);
1256 }
1257 at as u32
1258}
1259
1260/// A calendar object: the VCALENDAR with its time zones and components.
1261struct Obj {
1262 /// Change its children only through `children_mut` or `comps_mut`, which
1263 /// drop `index`.
1264 root: Node,
1265 zones: Zones,
1266 index: OnceCell<Index>,
1267}
1268
1269/// Lookups over `Obj::root`, built on first use.
1270struct Index {
1271 /// The first scheduled component with each key.
1272 by_key: HashMap<Option<i64>, usize>,
1273 /// THISANDFUTURE overrides by key, ascending.
1274 ranges: Vec<(i64, usize)>,
1275 /// Answers of `Obj::in_series`, which expands.
1276 in_series: RefCell<HashMap<i64, bool>>,
1277}
1278
1279impl Obj {
1280 fn new(cal: &ICalendar) -> Self {
1281 let root = match cal.components.is_empty() {
1282 true => Node {
1283 c: ICalendarComponent {
1284 component_type: ICalendarComponentType::VCalendar,
1285 entries: Vec::new(),
1286 component_ids: Vec::new(),
1287 },
1288 children: Vec::new(),
1289 },
1290 false => node(cal, 0),
1291 };
1292 Obj {
1293 root,
1294 zones: Zones::new(cal, Zone::Utc),
1295 index: OnceCell::new(),
1296 }
1297 }
1298
1299 fn index(&self) -> &Index {
1300 self.index.get_or_init(|| {
1301 let mut by_key = HashMap::new();
1302 let mut ranges = Vec::new();
1303 for (i, n) in self.root.children.iter().enumerate() {
1304 if !is_scheduled(&n.c) {
1305 continue;
1306 }
1307 let key = self.key(&n.c);
1308 by_key.entry(key).or_insert(i);
1309 if let Some(k) = key.filter(|_| is_range(&n.c)) {
1310 ranges.push((k, i));
1311 }
1312 }
1313 ranges.sort_by_key(|(k, _)| *k);
1314 Index {
1315 by_key,
1316 ranges,
1317 in_series: RefCell::default(),
1318 }
1319 })
1320 }
1321
1322 fn children_mut(&mut self) -> &mut Vec<Node> {
1323 self.index.take();
1324 &mut self.root.children
1325 }
1326
1327 fn done(self) -> ICalendar {
1328 let mut components = Vec::new();
1329 flatten(&self.root, &mut components);
1330 ICalendar { components }
1331 }
1332
1333 fn comps(&self) -> impl Iterator<Item = &Node> {
1334 self.root.children.iter().filter(|n| is_scheduled(&n.c))
1335 }
1336
1337 fn comps_mut(&mut self) -> impl Iterator<Item = &mut Node> {
1338 self.children_mut()
1339 .iter_mut()
1340 .filter(|n| is_scheduled(&n.c))
1341 }
1342
1343 fn master(&self) -> Option<&Node> {
1344 self.find(None)
1345 }
1346
1347 fn find(&self, key: Option<i64>) -> Option<&Node> {
1348 Some(&self.root.children[self.position(key)?])
1349 }
1350
1351 fn position(&self, key: Option<i64>) -> Option<usize> {
1352 self.index().by_key.get(&key).copied()
1353 }
1354
1355 /// The RECURRENCE-ID as an instant; `None` for the master.
1356 fn key(&self, c: &ICalendarComponent) -> Option<i64> {
1357 self.instant(c.property(&ICalendarProperty::RecurrenceId)?)
1358 }
1359
1360 /// The first value of a date or date-time property, in UTC seconds.
1361 /// Dates count from midnight, whatever the zone.
1362 fn instant(&self, e: &ICalendarEntry) -> Option<i64> {
1363 let v = e.values.first()?.as_partial_date_time()?;
1364 self.at(v, e.tz_id())
1365 }
1366
1367 fn at(&self, v: &PartialDateTime, tzid: Option<&str>) -> Option<i64> {
1368 let dt = v.to_date_time()?;
1369 Some(match dt.offset {
1370 _ if v.hour.is_none() => dt.date_time.and_utc().timestamp(),
1371 Some(o) => dt.date_time.and_utc().timestamp() - i64::from(o.local_minus_utc()),
1372 None => self.zones.get(tzid).to_utc(dt.date_time).timestamp(),
1373 })
1374 }
1375
1376 /// Every value of a property, comparable across encodings: instants for
1377 /// dates and date-times, the text otherwise.
1378 fn times(&self, c: &ICalendarComponent, prop: &ICalendarProperty) -> HashSet<String> {
1379 c.properties(prop)
1380 .flat_map(|e| e.values.iter().map(move |v| self.value_key(e, v)))
1381 .collect()
1382 }
1383
1384 fn value_key(&self, e: &ICalendarEntry, v: &ICalendarValue) -> String {
1385 match v.as_partial_date_time() {
1386 Some(p) => match self.at(p, e.tz_id()) {
1387 Some(t) if p.hour.is_none() => format!("{t}d"),
1388 Some(t) => t.to_string(),
1389 None => format!("{v:?}"),
1390 },
1391 None => match v {
1392 ICalendarValue::RecurrenceRule(r) => format!("{:?}", canonical(r)),
1393 _ => format!("{v:?}"),
1394 },
1395 }
1396 }
1397
1398 fn organizer(&self) -> Option<String> {
1399 self.comps()
1400 .find_map(|c| address(c.c.property(&ICalendarProperty::Organizer)?))
1401 .map(str::to_string)
1402 }
1403
1404 /// Whether the server replies for the attendee (RFC 6638, 3.2.2).
1405 fn organizer_schedules(&self) -> bool {
1406 self.comps()
1407 .filter_map(|c| c.c.property(&ICalendarProperty::Organizer))
1408 .all(server_agent)
1409 }
1410
1411 /// Per child of `root`, the addresses it invites through the server,
1412 /// lowercase.
1413 fn invitations(&self) -> Vec<HashSet<String>> {
1414 self.root
1415 .children
1416 .iter()
1417 .map(|n| match is_scheduled(&n.c) {
1418 true => attendees(&n.c)
1419 .filter(|e| server_agent(e))
1420 .filter_map(address)
1421 .map(str::to_ascii_lowercase)
1422 .collect(),
1423 false => HashSet::new(),
1424 })
1425 .collect()
1426 }
1427
1428 /// What an attendee sees (RFC 6638, 3.2.6), given which children invite
1429 /// them. Overrides they are not in become EXDATEs. A THISANDFUTURE
1430 /// override they are not in ends their series.
1431 fn view(&self, invited: &[bool]) -> Option<Vec<Node>> {
1432 let overrides = || {
1433 self.root.children.iter().enumerate().filter(|(_, c)| {
1434 is_scheduled(&c.c) && c.c.has_property(&ICalendarProperty::RecurrenceId)
1435 })
1436 };
1437 let mut out = Vec::new();
1438 let master = self
1439 .position(None)
1440 .filter(|i| invited[*i])
1441 .map(|i| &self.root.children[i]);
1442 if let Some(m) = master {
1443 let mut m = m.clone();
1444 let mut end: Option<i64> = None;
1445 for (i, o) in overrides() {
1446 let Some(rid) = o.c.property(&ICalendarProperty::RecurrenceId) else {
1447 continue;
1448 };
1449 if invited[i] {
1450 continue;
1451 }
1452 if is_range(&o.c) {
1453 // ponytail: a later range that invites them again is lost too.
1454 // A stale range past the series' end cuts nothing.
1455 if let Some(t) = self.instant(rid).filter(|t| self.in_series(*t)) {
1456 end = Some(end.map_or(t, |e| e.min(t)));
1457 }
1458 continue;
1459 }
1460 m.c.entries.push(ICalendarEntry {
1461 name: ICalendarProperty::Exdate,
1462 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1463 values: rid.values.clone(),
1464 });
1465 }
1466 let first =
1467 m.c.property(&ICalendarProperty::Dtstart)
1468 .and_then(|e| self.instant(e));
1469 match end {
1470 // The range starts with the first instance: none is left.
1471 Some(t) if first.is_some_and(|f| t <= f) => {}
1472 Some(t) => {
1473 self.end_series(&mut m.c, t);
1474 out.push(m);
1475 }
1476 None => out.push(m),
1477 }
1478 }
1479 out.extend(
1480 overrides()
1481 .filter(|(i, _)| invited[*i])
1482 .map(|(_, c)| c.clone()),
1483 );
1484 for n in &mut out {
1485 n.children
1486 .retain(|ch| ch.c.component_type != ICalendarComponentType::VAlarm);
1487 strip_scheduling_params(&mut n.c);
1488 }
1489 (!out.is_empty()).then_some(out)
1490 }
1491
1492 /// Whether override `c`, read through `obj`, is instance `key` of this
1493 /// series unchanged: its start, length and people, no rules of its own.
1494 fn plain(&self, obj: &Obj, c: &ICalendarComponent, key: i64, added: bool) -> bool {
1495 let Some(base) = self.base(key) else {
1496 return false;
1497 };
1498 let start = c
1499 .property(&ICalendarProperty::Dtstart)
1500 .and_then(|e| obj.instant(e));
1501 start == Some(self.moved(key))
1502 && [
1503 ICalendarProperty::Rrule,
1504 ICalendarProperty::Rdate,
1505 ICalendarProperty::Exrule,
1506 ]
1507 .iter()
1508 .all(|p| !c.has_property(p))
1509 && obj.end(c) == self.end_from(&base.c, self.moved(key))
1510 && organizer_of(c) == organizer_of(&base.c)
1511 && addresses(c) == addresses(&base.c)
1512 && (c.status() == base.c.status()
1513 || own_progress(c, &base.c)
1514 // Some clients mark the instance they answer as confirmed.
1515 || added
1516 && base.c.status().is_none()
1517 && c.status() == Some(&ICalendarStatus::Confirmed))
1518 }
1519
1520 /// Whether the master's own rules have an instance at `key`.
1521 fn in_series(&self, key: i64) -> bool {
1522 *self
1523 .index()
1524 .in_series
1525 .borrow_mut()
1526 .entry(key)
1527 .or_insert_with(|| self.expands_to(key))
1528 }
1529
1530 fn expands_to(&self, key: i64) -> bool {
1531 let (Some(m), Some(at)) = (self.master(), DateTime::from_timestamp(key, 0)) else {
1532 return false;
1533 };
1534 let mut root = Node {
1535 c: self.root.c.clone(),
1536 children: self
1537 .root
1538 .children
1539 .iter()
1540 .filter(|n| n.c.component_type == ICalendarComponentType::VTimezone)
1541 .cloned()
1542 .collect(),
1543 };
1544 root.children.push(m.clone());
1545 let mut components = Vec::new();
1546 flatten(&root, &mut components);
1547 let window = at - TimeDelta::days(1)..at + TimeDelta::days(1);
1548 expand(&ICalendar { components }, window, Zone::Utc)
1549 .instances
1550 .iter()
1551 .any(|i| i.recurrence_id == Some(at))
1552 }
1553
1554 /// What instance `key` copies: the THISANDFUTURE override that moves it,
1555 /// else the master.
1556 fn base(&self, key: i64) -> Option<&Node> {
1557 self.future(key).map(|(n, _)| n).or(self.master())
1558 }
1559
1560 /// When the component ends: DTSTART plus its length, else its DUE.
1561 fn end(&self, c: &ICalendarComponent) -> Option<i64> {
1562 match c.property(&ICalendarProperty::Dtstart) {
1563 Some(s) => self.end_from(c, self.instant(s)?),
1564 None => self.instant(c.property(&ICalendarProperty::Due)?),
1565 }
1566 }
1567
1568 /// When `c` would end if it started at `start`. The days and weeks of a
1569 /// DURATION count in wall-clock time (RFC 5545, 3.3.6).
1570 fn end_from(&self, c: &ICalendarComponent, start: i64) -> Option<i64> {
1571 let duration = c.property(&ICalendarProperty::Duration);
1572 let d = match duration.and_then(|e| e.values.first()) {
1573 Some(ICalendarValue::Duration(d))
1574 if !d.neg
1575 && !c.has_property(&ICalendarProperty::Dtend)
1576 && !c.has_property(&ICalendarProperty::Due) =>
1577 {
1578 d
1579 }
1580 _ => return Some(start + self.length(c)?),
1581 };
1582 let zone = self
1583 .zones
1584 .get(c.property(&ICalendarProperty::Dtstart)?.tz_id());
1585 let days = TimeDelta::try_days(i64::from(d.weeks) * 7 + i64::from(d.days))?;
1586 let local = add_local(zone.to_local(DateTime::from_timestamp(start, 0)?), days);
1587 let exact = i64::from(d.hours) * 3600 + i64::from(d.minutes) * 60 + i64::from(d.seconds);
1588 Some(zone.to_utc(local).timestamp() + exact)
1589 }
1590
1591 /// Which of `keys` are instances of the series in `cal`, from one
1592 /// expansion. `None` if the expansion was cut short and cannot tell.
1593 fn instances(&self, cal: &ICalendar, keys: &[i64]) -> Option<HashSet<i64>> {
1594 let starts: Vec<i64> = keys.iter().map(|k| self.moved(*k)).collect();
1595 let at = |t: i64| DateTime::from_timestamp(t, 0);
1596 // A day either side: an all-day shift is whole days, not 24 hours.
1597 let from = at(*starts.iter().min()?)? - TimeDelta::days(1);
1598 let to = at(*starts.iter().max()?)? + TimeDelta::days(1);
1599 let e = expand(cal, from..to, Zone::Utc);
1600 (!e.truncated).then(|| {
1601 e.instances
1602 .iter()
1603 .filter_map(|i| Some(i.recurrence_id?.timestamp()))
1604 .collect()
1605 })
1606 }
1607
1608 /// Seconds from DTSTART to DTEND or DUE, or the DURATION. Without
1609 /// either, a day for a date and nothing for a date-time (RFC 5545).
1610 fn length(&self, c: &ICalendarComponent) -> Option<i64> {
1611 let start = c.property(&ICalendarProperty::Dtstart)?;
1612 let instant = |p: &ICalendarProperty| c.property(p).and_then(|e| self.instant(e));
1613 match instant(&ICalendarProperty::Dtend).or_else(|| instant(&ICalendarProperty::Due)) {
1614 Some(end) => Some(end - self.instant(start)?),
1615 None => match c.property(&ICalendarProperty::Duration) {
1616 Some(d) => match d.values.first()? {
1617 ICalendarValue::Duration(d) => Some(d.as_seconds()),
1618 _ => None,
1619 },
1620 None => {
1621 let date = start.values.first()?.as_partial_date_time()?.hour.is_none();
1622 Some(if date { 86400 } else { 0 })
1623 }
1624 },
1625 }
1626 }
1627
1628 /// Ends the series of master `m` before instant `at`: UNTIL in the form
1629 /// RFC 5545 asks for, COUNT dropped, later RDATEs removed. `at` is an
1630 /// instance, so COUNT had not ended before it.
1631 fn end_series(&self, m: &mut ICalendarComponent, at: i64) {
1632 let Some(start) = m.property(&ICalendarProperty::Dtstart).cloned() else {
1633 return;
1634 };
1635 let Some(form) = start.values.first().and_then(|v| v.as_partial_date_time()) else {
1636 return;
1637 };
1638 // Obj reads floating times in UTC, so `at` is their wall-clock time.
1639 let until = if form.hour.is_none() {
1640 PartialDateTime::from_date_timestamp(at - 86400)
1641 } else if form.tz_hour.is_none() && start.tz_id().is_none() {
1642 PartialDateTime::from_naive_timestamp(at - 1)
1643 } else {
1644 PartialDateTime::from_utc_timestamp(at - 1)
1645 };
1646 for e in m.entries.iter_mut() {
1647 match e.name {
1648 ICalendarProperty::Rrule => {
1649 for v in &mut e.values {
1650 if let ICalendarValue::RecurrenceRule(r) = v
1651 && !r
1652 .until
1653 .as_ref()
1654 .and_then(|u| self.at(u, start.tz_id()))
1655 .is_some_and(|u| u < at)
1656 {
1657 r.until = Some(until.clone());
1658 r.count = None;
1659 }
1660 }
1661 }
1662 ICalendarProperty::Rdate => {
1663 let tz = e.tz_id().map(str::to_string);
1664 e.values.retain(|v| {
1665 v.as_partial_date_time()
1666 .and_then(|p| self.at(p, tz.as_deref()))
1667 .is_none_or(|t| t < at)
1668 });
1669 }
1670 _ => {}
1671 }
1672 }
1673 m.entries
1674 .retain(|e| e.name != ICalendarProperty::Rdate || !e.values.is_empty());
1675 }
1676
1677 /// A message: this object's VCALENDAR and time zones around `comps`.
1678 fn envelope(&self, comps: Vec<Node>, method: Method, now: DateTime<Utc>) -> ICalendar {
1679 let mut root = Node {
1680 c: self.root.c.clone(),
1681 children: self
1682 .root
1683 .children
1684 .iter()
1685 .filter(|n| n.c.component_type == ICalendarComponentType::VTimezone)
1686 .cloned()
1687 .collect(),
1688 };
1689 let method = match method {
1690 Method::Request => ICalendarMethod::Request,
1691 Method::Cancel => ICalendarMethod::Cancel,
1692 Method::Reply => ICalendarMethod::Reply,
1693 };
1694 set_prop(
1695 &mut root.c,
1696 ICalendarProperty::Method,
1697 ICalendarValue::Method(method),
1698 );
1699 let stamp = PartialDateTime::from_utc_timestamp(now.timestamp());
1700 for mut n in comps {
1701 set_prop(
1702 &mut n.c,
1703 ICalendarProperty::Dtstamp,
1704 ICalendarValue::PartialDateTime(Box::new(stamp.clone())),
1705 );
1706 root.children.push(n);
1707 }
1708 let mut components = Vec::new();
1709 flatten(&root, &mut components);
1710 ICalendar { components }
1711 }
1712
1713 /// A RECURRENCE-ID for the instance at `rid`, in the form of `start`.
1714 fn recurrence_id(
1715 &self,
1716 start: &ICalendarEntry,
1717 rid: DateTime<Utc>,
1718 floating: &Zone,
1719 ) -> Option<ICalendarEntry> {
1720 let v = start.values.first()?.as_partial_date_time()?;
1721 let local = |zone: &Zone| zone.to_local(rid).and_utc().timestamp();
1722 let value = if v.hour.is_none() {
1723 PartialDateTime::from_date_timestamp(local(floating))
1724 } else if v.tz_hour.is_some() {
1725 PartialDateTime::from_utc_timestamp(rid.timestamp())
1726 } else if start.tz_id().is_some() {
1727 PartialDateTime::from_naive_timestamp(local(&self.zones.get(start.tz_id())))
1728 } else {
1729 PartialDateTime::from_naive_timestamp(local(floating))
1730 };
1731 Some(ICalendarEntry {
1732 name: ICalendarProperty::RecurrenceId,
1733 params: without(start.params.clone(), &ICalendarParameterName::Range),
1734 values: vec![ICalendarValue::PartialDateTime(Box::new(value))],
1735 })
1736 }
1737
1738 /// The latest THISANDFUTURE override at or before instance `key`, and
1739 /// its key.
1740 fn future(&self, key: i64) -> Option<(&Node, i64)> {
1741 let ranges = &self.index().ranges;
1742 let (k, at) = ranges[..ranges.partition_point(|(k, _)| *k <= key)].last()?;
1743 Some((&self.root.children[*at], *k))
1744 }
1745
1746 /// Where instance `key` starts once a THISANDFUTURE override moves it.
1747 /// The move is wall-clock time in the zone of the series, as in expand.
1748 fn moved(&self, key: i64) -> i64 {
1749 let shifted = || {
1750 let (n, k) = self.future(key)?;
1751 let dtstart = n.c.property(&ICalendarProperty::Dtstart)?;
1752 let start = self.instant(dtstart)?;
1753 // A copy without its master still has the override's zone.
1754 let series = self
1755 .master()
1756 .and_then(|m| m.c.property(&ICalendarProperty::Dtstart));
1757 let zone = self.zones.get(series.unwrap_or(dtstart).tz_id());
1758 let local = |t: i64| Some(zone.to_local(DateTime::from_timestamp(t, 0)?));
1759 let shift = local(start)? - local(k)?;
1760 Some(zone.to_utc(add_local(local(key)?, shift)).timestamp())
1761 };
1762 shifted().unwrap_or(key)
1763 }
1764
1765 /// Whether the series in `cal` has an instance at `rid`, whose key is
1766 /// `key`.
1767 fn occurs(&self, cal: &ICalendar, rid: DateTime<Utc>, key: i64, floating: &Zone) -> bool {
1768 let Some(at) = DateTime::from_timestamp(self.moved(key), 0) else {
1769 return false;
1770 };
1771 // A day either side: an all-day shift is whole days, not 24 hours.
1772 let window = at - TimeDelta::days(1)..at + TimeDelta::days(1);
1773 expand(cal, window, floating.clone())
1774 .instances
1775 .iter()
1776 .any(|i| i.recurrence_id == Some(rid))
1777 }
1778
1779 /// An override for the instance at `rid`, from the master or from the
1780 /// THISANDFUTURE override that moves it.
1781 fn single(&self, rid: DateTime<Utc>, floating: &Zone) -> Option<Node> {
1782 let master = self.master()?;
1783 let form = master.c.property(&ICalendarProperty::Dtstart)?;
1784 let id = self.recurrence_id(form, rid, floating)?;
1785 let key = self.instant(&id)?;
1786 let Some((base, _)) = self.future(key) else {
1787 return Some(self.instance(master, &id, &id));
1788 };
1789 let at = self.moved(key);
1790 let start = match form.values.first()?.as_partial_date_time()?.hour {
1791 None => ICalendarEntry {
1792 values: vec![ICalendarValue::PartialDateTime(Box::new(
1793 PartialDateTime::from_date_timestamp(at),
1794 ))],
1795 ..id.clone()
1796 },
1797 Some(_) => self.recurrence_id(form, DateTime::from_timestamp(at, 0)?, floating)?,
1798 };
1799 Some(self.instance(base, &id, &start))
1800 }
1801
1802 /// Narrows the THISANDFUTURE override at instance `rid` (key `key`) to
1803 /// that instance. A copy of it carries the range on from the next instance
1804 /// without an override of its own, found by expanding `cal`.
1805 fn narrow(&mut self, cal: &ICalendar, rid: DateTime<Utc>, key: i64, floating: &Zone) {
1806 let Some(at) = self.position(Some(key)) else {
1807 return;
1808 };
1809 let Some(range) = self.root.children[at]
1810 .c
1811 .property(&ICalendarProperty::RecurrenceId)
1812 .and_then(|e| e.parameter(&ICalendarParameterName::Range))
1813 .cloned()
1814 else {
1815 return;
1816 };
1817 let later = self
1818 .comps()
1819 .filter(|c| is_range(&c.c))
1820 .filter_map(|c| self.key(&c.c))
1821 .filter(|k| *k > key)
1822 .min();
1823 let next = self
1824 .next_free(cal, rid, key, floating)
1825 .filter(|(_, k)| later.is_none_or(|l| *k < l))
1826 .and_then(|(r, _)| self.single(r, floating));
1827 if let Some(mut n) = next {
1828 if let Some(e) =
1829 n.c.entries
1830 .iter_mut()
1831 .find(|e| e.name == ICalendarProperty::RecurrenceId)
1832 {
1833 set_param(e, ICalendarParameterName::Range, range);
1834 }
1835 self.children_mut().push(n);
1836 }
1837 if let Some(e) = self.children_mut()[at]
1838 .c
1839 .entries
1840 .iter_mut()
1841 .find(|e| e.name == ICalendarProperty::RecurrenceId)
1842 {
1843 remove_param(e, &ICalendarParameterName::Range);
1844 }
1845 }
1846
1847 /// The first instance after `rid` that has no override, and its key.
1848 // ponytail: looks ten years ahead; a range beyond that falls back to the master.
1849 fn next_free(
1850 &self,
1851 cal: &ICalendar,
1852 rid: DateTime<Utc>,
1853 key: i64,
1854 floating: &Zone,
1855 ) -> Option<(DateTime<Utc>, i64)> {
1856 let form = self.master()?.c.property(&ICalendarProperty::Dtstart)?;
1857 let from = DateTime::from_timestamp(self.moved(key), 0)?;
1858 [1, 32, 400, 3700].into_iter().find_map(|days| {
1859 expand(cal, from..from + TimeDelta::days(days), floating.clone())
1860 .instances
1861 .iter()
1862 .filter_map(|i| i.recurrence_id)
1863 .filter(|r| *r > rid)
1864 .filter_map(|r| Some((r, self.instant(&self.recurrence_id(form, r, floating)?)?)))
1865 .filter(|(_, k)| self.find(Some(*k)).is_none())
1866 .min()
1867 })
1868 }
1869
1870 /// An override of `base` for the instance `rid` starting at `start`, so
1871 /// it can hold a status of its own. Its length becomes a DURATION.
1872 fn instance(&self, base: &Node, rid: &ICalendarEntry, start: &ICalendarEntry) -> Node {
1873 let mut n = base.clone();
1874 let begin =
1875 n.c.property(&ICalendarProperty::Dtstart)
1876 .and_then(|e| self.instant(e));
1877 let end_prop = match n.c.component_type {
1878 ICalendarComponentType::VTodo => ICalendarProperty::Due,
1879 _ => ICalendarProperty::Dtend,
1880 };
1881 let end = n.c.property(&end_prop).and_then(|e| self.instant(e));
1882 if let (Some(s), Some(e)) = (begin, end) {
1883 n.c.entries.retain(|x| x.name != end_prop);
1884 n.c.entries.push(ICalendarEntry {
1885 name: ICalendarProperty::Duration,
1886 params: Vec::new(),
1887 values: vec![ICalendarValue::Duration(ICalendarDuration::from_seconds(
1888 (e - s).max(0),
1889 ))],
1890 });
1891 }
1892 n.c.entries.retain(|e| {
1893 !matches!(
1894 e.name,
1895 ICalendarProperty::Rrule
1896 | ICalendarProperty::Rdate
1897 | ICalendarProperty::Exdate
1898 | ICalendarProperty::Exrule
1899 | ICalendarProperty::Dtstart
1900 | ICalendarProperty::RecurrenceId
1901 )
1902 });
1903 n.c.entries.push(ICalendarEntry {
1904 name: ICalendarProperty::Dtstart,
1905 params: without(start.params.clone(), &ICalendarParameterName::Range),
1906 values: start.values.clone(),
1907 });
1908 n.c.entries.push(ICalendarEntry {
1909 name: ICalendarProperty::RecurrenceId,
1910 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1911 values: rid.values.clone(),
1912 });
1913 n
1914 }
1915}
1916
1917// ---------------------------------------------------------------------------
1918// Properties and parameters
1919// ---------------------------------------------------------------------------
1920
1921fn is_scheduled(c: &ICalendarComponent) -> bool {
1922 matches!(
1923 c.component_type,
1924 ICalendarComponentType::VEvent
1925 | ICalendarComponentType::VTodo
1926 | ICalendarComponentType::VJournal
1927 )
1928}
1929
1930fn address(e: &ICalendarEntry) -> Option<&str> {
1931 e.values.first()?.as_text().map(str::trim)
1932}
1933
1934fn attendees(c: &ICalendarComponent) -> impl Iterator<Item = &ICalendarEntry> {
1935 c.properties(&ICalendarProperty::Attendee)
1936}
1937
1938/// A task's progress is the assignee's to set per instance, unless the
1939/// organizer cancelled it.
1940fn own_progress(c: &ICalendarComponent, base: &ICalendarComponent) -> bool {
1941 use ICalendarStatus::*;
1942 c.component_type == ICalendarComponentType::VTodo
1943 && matches!(c.status(), None | Some(NeedsAction | InProcess | Completed))
1944 && base.status() != Some(&Cancelled)
1945}
1946
1947fn organizer_of(c: &ICalendarComponent) -> Option<String> {
1948 c.property(&ICalendarProperty::Organizer)
1949 .and_then(address)
1950 .map(str::to_ascii_lowercase)
1951}
1952
1953/// Whether `c` repeats without end.
1954pub(crate) fn endless(c: &ICalendarComponent) -> bool {
1955 c.properties(&ICalendarProperty::Rrule).any(|e| {
1956 matches!(e.values.first(), Some(ICalendarValue::RecurrenceRule(r))
1957 if r.count.is_none() && r.until.is_none())
1958 })
1959}
1960
1961fn addresses(c: &ICalendarComponent) -> Vec<String> {
1962 let mut v: Vec<String> = attendees(c)
1963 .filter_map(address)
1964 .map(str::to_ascii_lowercase)
1965 .collect();
1966 v.sort();
1967 v.dedup();
1968 v
1969}
1970
1971fn is_range(c: &ICalendarComponent) -> bool {
1972 c.property(&ICalendarProperty::RecurrenceId)
1973 .is_some_and(|e| e.parameter(&ICalendarParameterName::Range).is_some())
1974}
1975
1976fn client_scheduled(c: &ICalendarComponent, a: &str) -> bool {
1977 attendees(c).any(|e| address(e).is_some_and(|x| x.eq_ignore_ascii_case(a)) && !server_agent(e))
1978}
1979
1980fn same_attendee<'a>(c: &'a ICalendarComponent, e: &ICalendarEntry) -> Option<&'a ICalendarEntry> {
1981 let a = address(e)?;
1982 attendees(c).find(|x| address(x).is_some_and(|b| b.eq_ignore_ascii_case(a)))
1983}
1984
1985fn own_partstat(c: &ICalendarComponent, me: Is) -> Option<String> {
1986 attendees(c).find(|e| address(e).is_some_and(me)).map(|e| {
1987 param(e, &ICalendarParameterName::Partstat)
1988 .unwrap_or("NEEDS-ACTION")
1989 .to_ascii_uppercase()
1990 })
1991}
1992
1993/// `SCHEDULE-AGENT` absent or `SERVER`. Unknown values count as `NONE`.
1994fn server_agent(e: &ICalendarEntry) -> bool {
1995 param(e, &ICalendarParameterName::ScheduleAgent)
1996 .is_none_or(|v| v.eq_ignore_ascii_case("SERVER"))
1997}
1998
1999fn sequence(c: &ICalendarComponent) -> i64 {
2000 c.property(&ICalendarProperty::Sequence)
2001 .and_then(|e| e.values.first()?.as_integer())
2002 .unwrap_or(0)
2003}
2004
2005fn param<'a>(e: &'a ICalendarEntry, name: &ICalendarParameterName) -> Option<&'a str> {
2006 e.parameter(name)?.as_text()
2007}
2008
2009fn set_param(e: &mut ICalendarEntry, name: ICalendarParameterName, value: ICalendarParameterValue) {
2010 remove_param(e, &name);
2011 e.params.push(ICalendarParameter::new(name, value));
2012}
2013
2014fn remove_param(e: &mut ICalendarEntry, name: &ICalendarParameterName) {
2015 e.params.retain(|p| &p.name != name);
2016}
2017
2018fn without(
2019 mut params: Vec<ICalendarParameter>,
2020 name: &ICalendarParameterName,
2021) -> Vec<ICalendarParameter> {
2022 params.retain(|p| &p.name != name);
2023 params
2024}
2025
2026fn set_prop(c: &mut ICalendarComponent, name: ICalendarProperty, value: ICalendarValue) {
2027 c.entries.retain(|e| e.name != name);
2028 c.entries.push(ICalendarEntry {
2029 name,
2030 params: Vec::new(),
2031 values: vec![value],
2032 });
2033}
2034
2035/// Messages carry none of the scheduling parameters (RFC 6638, 7).
2036fn strip_scheduling_params(c: &mut ICalendarComponent) {
2037 for e in c.entries.iter_mut().filter(|e| {
2038 matches!(
2039 e.name,
2040 ICalendarProperty::Attendee | ICalendarProperty::Organizer
2041 )
2042 }) {
2043 for p in [
2044 ICalendarParameterName::ScheduleAgent,
2045 ICalendarParameterName::ScheduleStatus,
2046 ICalendarParameterName::ScheduleForceSend,
2047 ] {
2048 remove_param(e, &p);
2049 }
2050 }
2051}
2052
2053fn text(s: &str) -> ICalendarParameterValue {
2054 ICalendarParameterValue::Text(s.to_string())
2055}
2056
2057fn partstat(p: ICalendarParticipationStatus) -> ICalendarParameterValue {
2058 ICalendarParameterValue::Partstat(p)
2059}
2060
2061/// Makes an object forget a principal that is about to be deleted, so that
2062/// a later principal of the same name is not taken for it. Every ORGANIZER
2063/// and ATTENDEE that `gone` names gets the address `tombstone`, and such an
2064/// ATTENDEE gets SCHEDULE-STATUS 3.7. A component the gone principal
2065/// organized is cancelled. All other lines keep their bytes. `None` when
2066/// nothing names the principal.
2067pub fn forget(text: &str, gone: Is, tombstone: &str) -> Option<String> {
2068 let mut out: Vec<String> = Vec::new();
2069 // Per open component: name, index of its BEGIN line in `out`, index of
2070 // its STATUS line, whether the gone principal organized it.
2071 let mut open: Vec<(String, usize, Option<usize>, bool)> = Vec::new();
2072 let mut changed = false;
2073 for raw in logical_lines(text) {
2074 let eol = match raw.ends_with('\n') {
2075 true if raw.ends_with("\r\n") => "\r\n",
2076 true => "\n",
2077 false => "\r\n",
2078 };
2079 let line = unfold(raw);
2080 let n = name(&line);
2081 match n.as_str() {
2082 "BEGIN" => open.push((
2083 value(&line).trim().to_ascii_uppercase(),
2084 out.len(),
2085 None,
2086 false,
2087 )),
2088 "END" => {
2089 if let Some((comp, begin, status, true)) = open.pop()
2090 && ["VEVENT", "VTODO", "VJOURNAL"].contains(&comp.as_str())
2091 {
2092 let cancelled = format!("STATUS:CANCELLED{eol}");
2093 match status {
2094 Some(i) => out[i] = cancelled,
2095 None => out.insert(begin + 1, cancelled),
2096 }
2097 }
2098 }
2099 "STATUS" => {
2100 if let Some(top) = open.last_mut() {
2101 top.2 = Some(out.len());
2102 }
2103 }
2104 "ORGANIZER" | "ATTENDEE" if gone(value(&line).trim()) => {
2105 let attendee = n == "ATTENDEE";
2106 let mut new = n.clone();
2107 for p in param_parts(&line) {
2108 if !(attendee
2109 && p.trim()
2110 .to_ascii_uppercase()
2111 .starts_with("SCHEDULE-STATUS="))
2112 {
2113 new.push(';');
2114 new.push_str(p);
2115 }
2116 }
2117 if attendee {
2118 new.push_str(";SCHEDULE-STATUS=3.7");
2119 } else if let Some(top) = open.last_mut() {
2120 top.3 = true;
2121 }
2122 new.push(':');
2123 new.push_str(tombstone);
2124 out.push(fold(&new, eol));
2125 changed = true;
2126 continue;
2127 }
2128 _ => {}
2129 }
2130 out.push(raw.to_string());
2131 }
2132 changed.then(|| out.concat())
2133}
2134