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::collections::HashSet;
8
9use calcard::common::PartialDateTime;
10use calcard::icalendar::{
11 ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDuration, ICalendarEntry,
12 ICalendarMethod, ICalendarParameter, ICalendarParameterName, ICalendarParameterValue,
13 ICalendarParticipationStatus, ICalendarProperty, ICalendarStatus, ICalendarValue, Uri,
14};
15use chrono::{DateTime, TimeDelta, Utc};
16use xmltree::Element;
17
18use crate::expand::expand;
19use crate::filter::TimeRange;
20use crate::freebusy::Period;
21use crate::text::{fold, logical_lines, name, param_parts, unfold, value};
22use crate::xml::{CALDAV, el};
23use crate::zone::{Zone, Zones};
24
25/// Whether an address belongs to someone in particular.
26pub type Is<'a> = &'a dyn Fn(&str) -> bool;
27
28/// How the owner of a calendar takes part in one of its objects (RFC 6638,
29/// 3.1).
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Role {
32 Organizer,
33 Attendee,
34 /// Not a scheduling object for this owner.
35 None,
36}
37
38/// A scheduling precondition a PUT fails.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Refused {
41 SameOrganizer,
42 AttendeeChange,
43}
44
45impl Refused {
46 pub fn condition(self) -> Element {
47 match self {
48 Refused::SameOrganizer => el(CALDAV, "same-organizer-in-all-components"),
49 Refused::AttendeeChange => el(CALDAV, "allowed-attendee-scheduling-object-change"),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum Method {
56 Request,
57 Cancel,
58 Reply,
59}
60
61#[derive(Debug, Clone)]
62pub struct Message {
63 /// The recipient's address, as the object writes it.
64 pub to: String,
65 pub method: Method,
66 /// Only the participation of other attendees changed. It updates an
67 /// existing copy, keeps its Schedule-Tag and leaves no inbox entry.
68 pub quiet: bool,
69 /// With METHOD.
70 pub cal: ICalendar,
71}
72
73pub fn role(cal: &ICalendar, owner: Is) -> Result<Role, Refused> {
74 let obj = Obj::new(cal);
75 let mut organizers = obj
76 .comps()
77 .filter_map(|c| address(c.c.property(&ICalendarProperty::Organizer)?));
78 let Some(organizer) = organizers.next() else {
79 return Ok(Role::None);
80 };
81 if organizers.any(|o| !o.eq_ignore_ascii_case(organizer)) {
82 return Err(Refused::SameOrganizer);
83 }
84 if owner(organizer) {
85 return Ok(Role::Organizer);
86 }
87 let attends = obj
88 .comps()
89 .flat_map(|c| attendees(&c.c))
90 .any(|e| address(e).is_some_and(owner));
91 Ok(if attends { Role::Attendee } else { Role::None })
92}
93
94/// An organizer's PUT (`new`) or DELETE (`None`) of a scheduling object.
95/// `old` is the stored organizer object, if any. Returns what to store and
96/// what to deliver.
97pub fn organize(
98 old: Option<&ICalendar>,
99 new: Option<ICalendar>,
100 organizer: Is,
101 now: DateTime<Utc>,
102) -> (Option<ICalendar>, Vec<Message>) {
103 let mut force = Vec::new();
104 let new = new.map(|n| guard(old, &n, organizer, &mut force));
105 let messages = messages(old, new.as_ref(), organizer, &force, now);
106 (new, messages)
107}
108
109/// The first half of [`organize`] for a PUT: what to store, with the
110/// attendee state the server owns, and the attendees whose REQUEST is
111/// forced. [`messages`] is the second half.
112pub fn prepare(
113 old: Option<&ICalendar>,
114 new: &ICalendar,
115 organizer: Is,
116) -> (ICalendar, Vec<String>) {
117 let mut force = Vec::new();
118 let store = guard(old, new, organizer, &mut force);
119 (store, force)
120}
121
122/// Names who acted for the owner: `SENT-BY` on the owner's ORGANIZER and
123/// ATTENDEE properties when `sender` is someone else, none when it is the
124/// owner. Only for writes that send a message, so every message names its
125/// real sender.
126pub fn stamp_sender(cal: &mut ICalendar, owner: Is, sender: Option<&str>) {
127 for c in cal.components.iter_mut().filter(|c| is_scheduled(c)) {
128 for e in &mut c.entries {
129 let theirs = matches!(
130 e.name,
131 ICalendarProperty::Organizer | ICalendarProperty::Attendee
132 ) && address(e).is_some_and(owner);
133 match (theirs, sender) {
134 (false, _) => {}
135 (true, Some(s)) => set_param(
136 e,
137 ICalendarParameterName::SentBy,
138 ICalendarParameterValue::Uri(Uri::Location(s.to_string())),
139 ),
140 (true, None) => remove_param(e, &ICalendarParameterName::SentBy),
141 }
142 }
143 }
144}
145
146/// How far ahead a room checks an invitation against its bookings. A
147/// series without end has infinite instances, so it is checked for two years
148/// and accepted beyond. Anything else is checked to its end, at most ten
149/// years ahead.
150pub fn answer_horizon(copy: &ICalendar) -> TimeDelta {
151 let endless = copy.components.iter().any(|c| {
152 is_scheduled(c)
153 && !c.has_property(&ICalendarProperty::RecurrenceId)
154 && c.properties(&ICalendarProperty::Rrule).any(|e| {
155 matches!(e.values.first(), Some(ICalendarValue::RecurrenceRule(r))
156 if r.count.is_none() && r.until.is_none())
157 })
158 });
159 TimeDelta::days(if endless { 731 } else { 3653 })
160}
161
162/// The answer of a room or resource to the invitation in its copy:
163/// ACCEPTED, and DECLINED where an instance in `window` overlaps `taken`. A
164/// declined instance of a series gets an override of its own.
165pub fn auto_answer(
166 copy: &ICalendar,
167 me: Is,
168 taken: &[Period],
169 window: &TimeRange,
170 floating: &Zone,
171) -> ICalendar {
172 let conflicts = |s: DateTime<Utc>, e: DateTime<Utc>| {
173 taken.iter().any(|p| match s == e {
174 true => p.start <= s && s < p.end,
175 false => s < p.end && e > p.start,
176 })
177 };
178 let mut obj = Obj::new(copy);
179 let mut declined: Vec<Option<i64>> = Vec::new();
180 let mut instances: Vec<DateTime<Utc>> = Vec::new();
181 for x in expand(copy, window.clone(), floating.clone()).instances {
182 if !conflicts(x.start, x.end) {
183 continue;
184 }
185 match (obj.key(&copy.components[x.component]), x.recurrence_id) {
186 (None, Some(rid)) => instances.push(rid),
187 (key, _) => declined.push(key),
188 }
189 }
190 if let Some(master) = obj.master().cloned() {
191 for rid in instances {
192 if let Some(entry) = obj.recurrence_id(&master, rid, floating) {
193 let inst = obj.instance(&master, &entry);
194 obj.root.children.push(inst);
195 declined.push(obj.key(&obj.root.children.last().expect("pushed").c));
196 }
197 }
198 }
199 let keys: Vec<Option<i64>> = obj.comps().map(|c| obj.key(&c.c)).collect();
200 for (c, key) in obj.comps_mut().zip(&keys) {
201 let answer = match declined.contains(key) {
202 true => ICalendarParticipationStatus::Declined,
203 false => ICalendarParticipationStatus::Accepted,
204 };
205 for e in
206 c.c.entries
207 .iter_mut()
208 .filter(|e| e.name == ICalendarProperty::Attendee && address(e).is_some_and(me))
209 {
210 set_param(
211 e,
212 ICalendarParameterName::Partstat,
213 partstat(answer.clone()),
214 );
215 }
216 }
217 obj.done()
218}
219
220/// The messages a change of the organizer object sends, without touching
221/// the attendee state in it. `force` lists attendees who get a REQUEST even
222/// if nothing changed for them.
223pub fn messages(
224 old: Option<&ICalendar>,
225 new: Option<&ICalendar>,
226 organizer: Is,
227 force: &[String],
228 now: DateTime<Utc>,
229) -> Vec<Message> {
230 let old = old.map(Obj::new);
231 let new = new.map(Obj::new);
232 let mut who: Vec<&str> = Vec::new();
233 for obj in old.iter().chain(new.iter()) {
234 for e in obj.comps().flat_map(|c| attendees(&c.c)) {
235 if let Some(a) = address(e)
236 && server_agent(e)
237 && !organizer(a)
238 && !who.iter().any(|w| w.eq_ignore_ascii_case(a))
239 {
240 who.push(a);
241 }
242 }
243 }
244 let mut out = Vec::new();
245 for a in who {
246 let before = old.as_ref().and_then(|o| Some((o, o.view(a)?)));
247 let after = new.as_ref().and_then(|n| Some((n, n.view(a)?)));
248 let (method, quiet, comps, src) = match (before, after) {
249 (Some((src, b)), None) => (Method::Cancel, false, cancelled(b), src),
250 (None, Some((src, comps))) => (Method::Request, false, comps, src),
251 (Some((_, b)), Some((src, comps))) => {
252 let quiet = if normalized(&b, true) != normalized(&comps, true) {
253 false
254 } else if normalized(&b, false) != normalized(&comps, false) {
255 true
256 } else if force.iter().any(|f| f.eq_ignore_ascii_case(a)) {
257 false
258 } else {
259 continue;
260 };
261 (Method::Request, quiet, comps, src)
262 }
263 (None, None) => continue,
264 };
265 out.push(Message {
266 to: a.to_string(),
267 method,
268 quiet,
269 cal: src.envelope(comps, method, now),
270 });
271 }
272 out
273}
274
275/// Records the delivery status for `to` on its ATTENDEE properties.
276pub fn set_attendee_status(cal: &mut ICalendar, to: &str, status: &str) {
277 for c in &mut cal.components {
278 for e in c
279 .entries
280 .iter_mut()
281 .filter(|e| e.name == ICalendarProperty::Attendee)
282 {
283 if address(e).is_some_and(|a| a.eq_ignore_ascii_case(to)) {
284 set_param(e, ICalendarParameterName::ScheduleStatus, text(status));
285 }
286 }
287 }
288}
289
290/// Records the delivery status of a REPLY on the ORGANIZER properties.
291pub fn set_organizer_status(cal: &mut ICalendar, status: &str) {
292 for c in &mut cal.components {
293 for e in c
294 .entries
295 .iter_mut()
296 .filter(|e| e.name == ICalendarProperty::Organizer)
297 {
298 set_param(e, ICalendarParameterName::ScheduleStatus, text(status));
299 }
300 }
301}
302
303/// An attendee's PUT over the stored copy. Returns what to store and the
304/// REPLY, if the attendee's participation changed.
305pub fn attend(
306 old: &ICalendar,
307 new: ICalendar,
308 me: Is,
309 now: DateTime<Utc>,
310) -> Result<(ICalendar, Option<Message>), Refused> {
311 let old = Obj::new(old);
312 if !old.organizer_schedules() {
313 return Ok((new, None));
314 }
315 let mut next = Obj::new(&new);
316 let master = old.master();
317
318 for c in next.comps() {
319 let key = next.key(&c.c);
320 match old.find(key) {
321 Some(oc) => {
322 let same_times = [
323 ICalendarProperty::Dtstart,
324 ICalendarProperty::Dtend,
325 ICalendarProperty::Duration,
326 ICalendarProperty::Due,
327 ICalendarProperty::Rrule,
328 ICalendarProperty::Rdate,
329 ICalendarProperty::Exrule,
330 ]
331 .iter()
332 .all(|p| old.times(&oc.c, p) == next.times(&c.c, p));
333 let kept_exdates = old
334 .times(&oc.c, &ICalendarProperty::Exdate)
335 .is_subset(&next.times(&c.c, &ICalendarProperty::Exdate));
336 let organizer = |c: &ICalendarComponent| {
337 c.property(&ICalendarProperty::Organizer)
338 .and_then(address)
339 .map(str::to_ascii_lowercase)
340 };
341 if !same_times
342 || !kept_exdates
343 || organizer(&oc.c) != organizer(&c.c)
344 || addresses(&oc.c) != addresses(&c.c)
345 {
346 return Err(Refused::AttendeeChange);
347 }
348 }
349 // An instance the attendee overrides, to set its own status.
350 None => {
351 let start =
352 c.c.property(&ICalendarProperty::Dtstart)
353 .and_then(|e| next.instant(e));
354 if key.is_none() || master.is_none() || start != key {
355 return Err(Refused::AttendeeChange);
356 }
357 }
358 }
359 }
360
361 // The state of the others and of the organizer is the server's.
362 let mut force = false;
363 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
364 for (c, key) in next.comps_mut().zip(&keys) {
365 let Some(base) = old.find(*key).or(master) else {
366 continue;
367 };
368 for e in &mut c.c.entries {
369 match e.name {
370 ICalendarProperty::Attendee if !address(e).is_some_and(me) => {
371 if let Some(b) = same_attendee(&base.c, e) {
372 e.params = b.params.clone();
373 }
374 }
375 ICalendarProperty::Organizer => {
376 force |= param(e, &ICalendarParameterName::ScheduleForceSend)
377 .is_some_and(|v| v.eq_ignore_ascii_case("REPLY"));
378 if let Some(b) = base.c.property(&ICalendarProperty::Organizer) {
379 e.params = b.params.clone();
380 }
381 }
382 _ => {}
383 }
384 }
385 }
386
387 let mut replied: Vec<Node> = Vec::new();
388 for (c, key) in next.comps().zip(&keys) {
389 let now_stat = own_partstat(&c.c, me);
390 let before = old
391 .find(*key)
392 .or(master)
393 .and_then(|b| own_partstat(&b.c, me));
394 if now_stat.is_some() && (force || now_stat != before) {
395 replied.push(reply_part(c, me));
396 }
397 }
398 if let Some(m) = next.master() {
399 let before = master.map_or_else(HashSet::new, |om| {
400 old.times(&om.c, &ICalendarProperty::Exdate)
401 });
402 for e in m.c.properties(&ICalendarProperty::Exdate) {
403 for value in &e.values {
404 if before.contains(&next.value_key(e, value)) {
405 continue;
406 }
407 let rid = ICalendarEntry {
408 name: ICalendarProperty::RecurrenceId,
409 params: e.params.clone(),
410 values: vec![value.clone()],
411 };
412 replied.push(declined_instance(m, rid, me));
413 }
414 }
415 }
416
417 let reply = match replied.is_empty() {
418 true => None,
419 false => next.organizer().map(|to| Message {
420 to,
421 method: Method::Reply,
422 quiet: false,
423 cal: next.envelope(replied, Method::Reply, now),
424 }),
425 };
426 Ok((next.done(), reply))
427}
428
429/// An attendee's DELETE: the REPLY declining every instance, unless the
430/// organizer handles scheduling itself or already cancelled.
431pub fn decline(old: &ICalendar, me: Is, now: DateTime<Utc>) -> Option<Message> {
432 let old = Obj::new(old);
433 if !old.organizer_schedules() {
434 return None;
435 }
436 let cancelled = old.comps().all(|c| {
437 c.c.property(&ICalendarProperty::Status)
438 .and_then(|e| e.values.first()?.as_text())
439 .is_some_and(|s| s.eq_ignore_ascii_case("CANCELLED"))
440 });
441 if cancelled {
442 return None;
443 }
444 let comps: Vec<Node> =
445 old.comps()
446 .filter(|c| own_partstat(&c.c, me).is_some())
447 .map(|c| {
448 let mut part = reply_part(c, me);
449 for e in
450 part.c.entries.iter_mut().filter(|e| {
451 e.name == ICalendarProperty::Attendee && address(e).is_some_and(me)
452 })
453 {
454 set_param(
455 e,
456 ICalendarParameterName::Partstat,
457 partstat(ICalendarParticipationStatus::Declined),
458 );
459 }
460 part
461 })
462 .collect();
463 if comps.is_empty() {
464 return None;
465 }
466 Some(Message {
467 to: old.organizer()?,
468 method: Method::Reply,
469 quiet: false,
470 cal: old.envelope(comps, Method::Reply, now),
471 })
472}
473
474/// A REQUEST or CANCEL applied to the attendee's copy. `None`: nothing to
475/// store.
476pub fn receive(copy: Option<&ICalendar>, msg: &Message) -> Option<ICalendar> {
477 match msg.method {
478 Method::Request => {
479 if msg.quiet && copy.is_none() {
480 return None;
481 }
482 let copy = copy.map(Obj::new);
483 let mut next = Obj::new(&msg.cal);
484 next.root
485 .c
486 .entries
487 .retain(|e| e.name != ICalendarProperty::Method);
488 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
489 let Some(copy) = copy else {
490 return Some(next.done());
491 };
492 // What the attendee may keep for itself (RFC 6638, 3.2.2.1).
493 for (c, key) in next.comps_mut().zip(&keys) {
494 let Some(base) = copy.find(*key).or(copy.master()) else {
495 continue;
496 };
497 c.children
498 .retain(|n| n.c.component_type != ICalendarComponentType::VAlarm);
499 c.children.extend(
500 base.children
501 .iter()
502 .filter(|n| n.c.component_type == ICalendarComponentType::VAlarm)
503 .cloned(),
504 );
505 for p in [
506 ICalendarProperty::Transp,
507 ICalendarProperty::PercentComplete,
508 ICalendarProperty::Completed,
509 ] {
510 c.c.entries.retain(|e| e.name != p);
511 c.c.entries.extend(base.c.properties(&p).cloned());
512 }
513 let status = base
514 .c
515 .property(&ICalendarProperty::Organizer)
516 .and_then(|e| e.parameter(&ICalendarParameterName::ScheduleStatus))
517 .cloned();
518 if let Some(s) = status {
519 for e in
520 c.c.entries
521 .iter_mut()
522 .filter(|e| e.name == ICalendarProperty::Organizer)
523 {
524 set_param(e, ICalendarParameterName::ScheduleStatus, s.clone());
525 }
526 }
527 }
528 Some(next.done())
529 }
530 Method::Cancel => {
531 let msg_obj = Obj::new(&msg.cal);
532 let mut next = Obj::new(copy?);
533 let whole = msg_obj.master().is_some();
534 let gone: Vec<(i64, ICalendarEntry)> = msg_obj
535 .comps()
536 .filter_map(|c| {
537 let rid = c.c.property(&ICalendarProperty::RecurrenceId)?;
538 Some((msg_obj.instant(rid)?, rid.clone()))
539 })
540 .collect();
541 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
542 for (c, key) in next.comps_mut().zip(&keys) {
543 if whole || key.is_some_and(|k| gone.iter().any(|(g, _)| *g == k)) {
544 set_prop(
545 &mut c.c,
546 ICalendarProperty::Status,
547 ICalendarValue::Status(ICalendarStatus::Cancelled),
548 );
549 }
550 }
551 if !whole {
552 let missing: Vec<ICalendarEntry> = gone
553 .into_iter()
554 .filter(|(k, _)| !keys.contains(&Some(*k)))
555 .map(|(_, rid)| ICalendarEntry {
556 name: ICalendarProperty::Exdate,
557 params: without(rid.params, &ICalendarParameterName::Range),
558 values: rid.values,
559 })
560 .collect();
561 if let Some(m) = next
562 .comps_mut()
563 .find(|c| !c.c.has_property(&ICalendarProperty::RecurrenceId))
564 {
565 m.c.entries.extend(missing);
566 }
567 }
568 Some(next.done())
569 }
570 Method::Reply => None,
571 }
572}
573
574/// A REPLY applied to the organizer object. `false` if it changed nothing.
575pub fn apply_reply(org: &mut ICalendar, reply: &ICalendar, replier: Is) -> bool {
576 let rep = Obj::new(reply);
577 let mut next = Obj::new(org);
578 let mut changed = false;
579 for rc in rep.comps() {
580 let key = rep.key(&rc.c);
581 let at = match next.position(key) {
582 Some(at) => at,
583 // A reply for one instance of the series gets its own override.
584 None => {
585 let (Some(_), Some(rid), Some(master)) = (
586 key,
587 rc.c.property(&ICalendarProperty::RecurrenceId),
588 next.master(),
589 ) else {
590 continue;
591 };
592 let inst = next.instance(master, rid);
593 next.root.children.push(inst);
594 next.root.children.len() - 1
595 }
596 };
597 let target = &mut next.root.children[at];
598 if sequence(&rc.c) < sequence(&target.c) {
599 continue;
600 }
601 let Some(stat) =
602 rc.c.properties(&ICalendarProperty::Attendee)
603 .find(|e| address(e).is_some_and(replier))
604 .map(|e| {
605 e.parameter(&ICalendarParameterName::Partstat)
606 .cloned()
607 .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction))
608 })
609 else {
610 continue;
611 };
612 let codes: Vec<String> =
613 rc.c.properties(&ICalendarProperty::RequestStatus)
614 .filter_map(|e| e.values.first()?.as_text())
615 .map(|s| s.split(';').next().unwrap_or(s).trim().to_string())
616 .collect();
617 let status = match codes.is_empty() {
618 true => "2.0".to_string(),
619 false => codes.join(","),
620 };
621 for e in
622 target.c.entries.iter_mut().filter(|e| {
623 e.name == ICalendarProperty::Attendee && address(e).is_some_and(replier)
624 })
625 {
626 changed |= e.parameter(&ICalendarParameterName::Partstat) != Some(&stat);
627 set_param(e, ICalendarParameterName::Partstat, stat.clone());
628 set_param(e, ICalendarParameterName::ScheduleStatus, text(&status));
629 }
630 }
631 if changed {
632 *org = next.done();
633 }
634 changed
635}
636
637// ---------------------------------------------------------------------------
638// The organizer object
639// ---------------------------------------------------------------------------
640
641/// The client's organizer object with the attendee state the server owns:
642/// their PARTSTAT (reset on a reschedule, RFC 6638 3.2.8) and
643/// SCHEDULE-STATUS. Collects SCHEDULE-FORCE-SEND=REQUEST into `force`.
644fn guard(
645 old: Option<&ICalendar>,
646 new: &ICalendar,
647 organizer: Is,
648 force: &mut Vec<String>,
649) -> ICalendar {
650 let old = old.map(Obj::new);
651 let mut next = Obj::new(new);
652 let master = old.as_ref().and_then(Obj::master);
653 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
654 let moved: Vec<bool> = next
655 .comps()
656 .map(|c| match &old {
657 Some(o) => match o.find(next.key(&c.c)) {
658 Some(oc) => rescheduled(o, &oc.c, &next, &c.c),
659 // A new override: rescheduled if it moves its instance.
660 None => {
661 let start =
662 c.c.property(&ICalendarProperty::Dtstart)
663 .and_then(|e| next.instant(e));
664 next.key(&c.c).is_none() || start != next.key(&c.c)
665 }
666 },
667 None => true,
668 })
669 .collect();
670 let bumps: Vec<Option<i64>> = next
671 .comps()
672 .zip(&moved)
673 .map(|(c, moved)| {
674 let oc = old.as_ref()?.find(next.key(&c.c))?;
675 (*moved && sequence(&c.c) <= sequence(&oc.c)).then(|| sequence(&oc.c) + 1)
676 })
677 .collect();
678
679 for ((c, key), (moved, bump)) in next.comps_mut().zip(&keys).zip(moved.iter().zip(&bumps)) {
680 let base = old.as_ref().and_then(|o| o.find(*key)).or(master);
681 for e in &mut c.c.entries {
682 match e.name {
683 ICalendarProperty::Organizer => {
684 remove_param(e, &ICalendarParameterName::ScheduleForceSend);
685 }
686 ICalendarProperty::Attendee => {
687 let forced = param(e, &ICalendarParameterName::ScheduleForceSend)
688 .is_some_and(|v| v.eq_ignore_ascii_case("REQUEST"));
689 remove_param(e, &ICalendarParameterName::ScheduleForceSend);
690 let Some(a) = address(e).map(str::to_string) else {
691 continue;
692 };
693 if organizer(&a) || !server_agent(e) {
694 continue;
695 }
696 if forced {
697 force.push(a.clone());
698 }
699 let prev = base.and_then(|b| same_attendee(&b.c, e));
700 let stat = match (moved, prev) {
701 (false, Some(p)) => p
702 .parameter(&ICalendarParameterName::Partstat)
703 .cloned()
704 .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction)),
705 _ => partstat(ICalendarParticipationStatus::NeedsAction),
706 };
707 set_param(e, ICalendarParameterName::Partstat, stat);
708 match prev.and_then(|p| p.parameter(&ICalendarParameterName::ScheduleStatus)) {
709 Some(s) => set_param(e, ICalendarParameterName::ScheduleStatus, s.clone()),
710 None => remove_param(e, &ICalendarParameterName::ScheduleStatus),
711 }
712 }
713 _ => {}
714 }
715 }
716 if let Some(n) = bump {
717 set_prop(
718 &mut c.c,
719 ICalendarProperty::Sequence,
720 ICalendarValue::Integer(*n),
721 );
722 }
723 }
724 next.done()
725}
726
727/// Whether a change moves instances in time (RFC 6638, 3.2.8). Shortening a
728/// series or excluding instances does not.
729fn rescheduled(old: &Obj, oc: &ICalendarComponent, new: &Obj, nc: &ICalendarComponent) -> bool {
730 let moved = [
731 ICalendarProperty::Dtstart,
732 ICalendarProperty::Dtend,
733 ICalendarProperty::Duration,
734 ICalendarProperty::Due,
735 ICalendarProperty::Rdate,
736 ]
737 .iter()
738 .any(|p| old.times(oc, p) != new.times(nc, p));
739 let reinstated = !old
740 .times(oc, &ICalendarProperty::Exdate)
741 .is_subset(&new.times(nc, &ICalendarProperty::Exdate));
742 moved || reinstated || rules_grew(oc, nc)
743}
744
745fn rules_grew(oc: &ICalendarComponent, nc: &ICalendarComponent) -> bool {
746 let rules = |c: &ICalendarComponent| -> Vec<_> {
747 c.properties(&ICalendarProperty::Rrule)
748 .filter_map(|e| match e.values.first()? {
749 ICalendarValue::RecurrenceRule(r) => Some((**r).clone()),
750 _ => None,
751 })
752 .collect()
753 };
754 let (o, n) = (rules(oc), rules(nc));
755 if o == n {
756 return false;
757 }
758 let (o, n) = match (&o[..], &n[..]) {
759 // Dropping the rule leaves the first instance only.
760 ([_, ..], []) => return false,
761 ([o], [n]) => (o, n),
762 _ => return true,
763 };
764 let unbounded = |r: &calcard::icalendar::ICalendarRecurrenceRule| {
765 let mut r = r.clone();
766 r.until = None;
767 r.count = None;
768 r
769 };
770 if unbounded(o) != unbounded(n) {
771 return true;
772 }
773 let shorter = match (&o.until, &n.until, o.count, n.count) {
774 (Some(ou), Some(nu), _, _) => nu <= ou,
775 (_, _, Some(oc), Some(nc)) => nc <= oc,
776 (None, _, None, _) => true,
777 _ => false,
778 };
779 !shorter
780}
781
782fn cancelled(comps: Vec<Node>) -> Vec<Node> {
783 comps
784 .into_iter()
785 .map(|mut n| {
786 set_prop(
787 &mut n.c,
788 ICalendarProperty::Status,
789 ICalendarValue::Status(ICalendarStatus::Cancelled),
790 );
791 let seq = sequence(&n.c) + 1;
792 set_prop(
793 &mut n.c,
794 ICalendarProperty::Sequence,
795 ICalendarValue::Integer(seq),
796 );
797 n
798 })
799 .collect()
800}
801
802/// For comparing what an attendee would receive: without the stamps a
803/// client rewrites on every save, and optionally without participation.
804fn normalized(comps: &[Node], without_partstat: bool) -> Vec<Node> {
805 comps
806 .iter()
807 .map(|n| {
808 let mut n = n.clone();
809 n.c.entries.retain(|e| {
810 !matches!(
811 e.name,
812 ICalendarProperty::Dtstamp
813 | ICalendarProperty::LastModified
814 | ICalendarProperty::Created
815 | ICalendarProperty::Sequence
816 ) && !matches!(&e.name, ICalendarProperty::Other(x) if x.to_ascii_uppercase().starts_with("X-"))
817 });
818 // A new SENT-BY alone is no change for an attendee.
819 for e in n.c.entries.iter_mut().filter(|e| {
820 matches!(
821 e.name,
822 ICalendarProperty::Organizer | ICalendarProperty::Attendee
823 )
824 }) {
825 remove_param(e, &ICalendarParameterName::SentBy);
826 }
827 if without_partstat {
828 for e in n.c.entries.iter_mut().filter(|e| e.name == ICalendarProperty::Attendee) {
829 remove_param(e, &ICalendarParameterName::Partstat);
830 }
831 }
832 n
833 })
834 .collect()
835}
836
837/// A component of the attendee's REPLY: only its own ATTENDEE, no alarms.
838fn reply_part(c: &Node, me: Is) -> Node {
839 let mut n = Node {
840 c: c.c.clone(),
841 children: Vec::new(),
842 };
843 n.c.entries
844 .retain(|e| e.name != ICalendarProperty::Attendee || address(e).is_some_and(me));
845 strip_scheduling_params(&mut n.c);
846 n
847}
848
849/// The REPLY component for an instance the attendee excluded with EXDATE.
850fn declined_instance(master: &Node, rid: ICalendarEntry, me: Is) -> Node {
851 let mut c = ICalendarComponent {
852 component_type: master.c.component_type.clone(),
853 entries: Vec::new(),
854 component_ids: Vec::new(),
855 };
856 for p in [
857 ICalendarProperty::Uid,
858 ICalendarProperty::Sequence,
859 ICalendarProperty::Organizer,
860 ICalendarProperty::Summary,
861 ] {
862 c.entries.extend(master.c.properties(&p).cloned());
863 }
864 c.entries.push(ICalendarEntry {
865 name: ICalendarProperty::Dtstart,
866 ..rid.clone()
867 });
868 c.entries.push(rid);
869 for e in master
870 .c
871 .properties(&ICalendarProperty::Attendee)
872 .filter(|e| address(e).is_some_and(me))
873 {
874 let mut e = e.clone();
875 set_param(
876 &mut e,
877 ICalendarParameterName::Partstat,
878 partstat(ICalendarParticipationStatus::Declined),
879 );
880 c.entries.push(e);
881 }
882 strip_scheduling_params(&mut c);
883 Node {
884 c,
885 children: Vec::new(),
886 }
887}
888
889// ---------------------------------------------------------------------------
890// The object as a tree
891// ---------------------------------------------------------------------------
892
893/// A component with its sub-components, detached from the flat list calcard
894/// keeps.
895#[derive(Debug, Clone, PartialEq)]
896struct Node {
897 /// `component_ids` is empty; `children` replaces it.
898 c: ICalendarComponent,
899 children: Vec<Node>,
900}
901
902fn node(cal: &ICalendar, i: usize) -> Node {
903 let mut c = cal.components[i].clone();
904 let ids = std::mem::take(&mut c.component_ids);
905 let children = ids
906 .iter()
907 .map(|&id| id as usize)
908 .filter(|&id| id > i && id < cal.components.len())
909 .map(|id| node(cal, id))
910 .collect();
911 Node { c, children }
912}
913
914fn flatten(n: &Node, out: &mut Vec<ICalendarComponent>) -> u32 {
915 let at = out.len();
916 out.push(n.c.clone());
917 for ch in &n.children {
918 let id = flatten(ch, out);
919 out[at].component_ids.push(id);
920 }
921 at as u32
922}
923
924/// A calendar object: the VCALENDAR with its time zones and components.
925struct Obj {
926 root: Node,
927 zones: Zones,
928}
929
930impl Obj {
931 fn new(cal: &ICalendar) -> Self {
932 let root = match cal.components.is_empty() {
933 true => Node {
934 c: ICalendarComponent {
935 component_type: ICalendarComponentType::VCalendar,
936 entries: Vec::new(),
937 component_ids: Vec::new(),
938 },
939 children: Vec::new(),
940 },
941 false => node(cal, 0),
942 };
943 Obj {
944 root,
945 zones: Zones::new(cal, Zone::Utc),
946 }
947 }
948
949 fn done(self) -> ICalendar {
950 let mut components = Vec::new();
951 flatten(&self.root, &mut components);
952 ICalendar { components }
953 }
954
955 fn comps(&self) -> impl Iterator<Item = &Node> {
956 self.root.children.iter().filter(|n| is_scheduled(&n.c))
957 }
958
959 fn comps_mut(&mut self) -> impl Iterator<Item = &mut Node> {
960 self.root.children.iter_mut().filter(|n| is_scheduled(&n.c))
961 }
962
963 fn master(&self) -> Option<&Node> {
964 self.find(None)
965 }
966
967 fn find(&self, key: Option<i64>) -> Option<&Node> {
968 self.comps().find(|c| self.key(&c.c) == key)
969 }
970
971 fn position(&self, key: Option<i64>) -> Option<usize> {
972 self.root
973 .children
974 .iter()
975 .position(|n| is_scheduled(&n.c) && self.key(&n.c) == key)
976 }
977
978 /// The RECURRENCE-ID as an instant; `None` for the master.
979 fn key(&self, c: &ICalendarComponent) -> Option<i64> {
980 self.instant(c.property(&ICalendarProperty::RecurrenceId)?)
981 }
982
983 /// The first value of a date or date-time property, in UTC seconds.
984 /// Dates count from midnight, whatever the zone.
985 fn instant(&self, e: &ICalendarEntry) -> Option<i64> {
986 let v = e.values.first()?.as_partial_date_time()?;
987 self.at(v, e.tz_id())
988 }
989
990 fn at(&self, v: &PartialDateTime, tzid: Option<&str>) -> Option<i64> {
991 let dt = v.to_date_time()?;
992 Some(match dt.offset {
993 _ if v.hour.is_none() => dt.date_time.and_utc().timestamp(),
994 Some(o) => dt.date_time.and_utc().timestamp() - i64::from(o.local_minus_utc()),
995 None => self.zones.get(tzid).to_utc(dt.date_time).timestamp(),
996 })
997 }
998
999 /// Every value of a property, comparable across encodings: instants for
1000 /// dates and date-times, the text otherwise.
1001 fn times(&self, c: &ICalendarComponent, prop: &ICalendarProperty) -> HashSet<String> {
1002 c.properties(prop)
1003 .flat_map(|e| e.values.iter().map(move |v| self.value_key(e, v)))
1004 .collect()
1005 }
1006
1007 fn value_key(&self, e: &ICalendarEntry, v: &ICalendarValue) -> String {
1008 match v.as_partial_date_time() {
1009 Some(p) => match self.at(p, e.tz_id()) {
1010 Some(t) if p.hour.is_none() => format!("{t}d"),
1011 Some(t) => t.to_string(),
1012 None => format!("{v:?}"),
1013 },
1014 None => format!("{v:?}"),
1015 }
1016 }
1017
1018 fn organizer(&self) -> Option<String> {
1019 self.comps()
1020 .find_map(|c| address(c.c.property(&ICalendarProperty::Organizer)?))
1021 .map(str::to_string)
1022 }
1023
1024 /// Whether the server replies for the attendee (RFC 6638, 3.2.2).
1025 fn organizer_schedules(&self) -> bool {
1026 self.comps()
1027 .filter_map(|c| c.c.property(&ICalendarProperty::Organizer))
1028 .all(server_agent)
1029 }
1030
1031 /// What attendee `a` gets to see (RFC 6638, 3.2.6): the master with the
1032 /// overrides it is in, and EXDATEs for those it is not in; or just the
1033 /// overrides it is in.
1034 fn view(&self, a: &str) -> Option<Vec<Node>> {
1035 let invited = |c: &ICalendarComponent| {
1036 attendees(c)
1037 .any(|e| address(e).is_some_and(|x| x.eq_ignore_ascii_case(a)) && server_agent(e))
1038 };
1039 let mut out = Vec::new();
1040 let master = self.master().filter(|m| invited(&m.c));
1041 if let Some(m) = master {
1042 let mut m = m.clone();
1043 for o in self
1044 .comps()
1045 .filter(|c| c.c.has_property(&ICalendarProperty::RecurrenceId))
1046 {
1047 if !invited(&o.c)
1048 && let Some(rid) = o.c.property(&ICalendarProperty::RecurrenceId)
1049 {
1050 m.c.entries.push(ICalendarEntry {
1051 name: ICalendarProperty::Exdate,
1052 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1053 values: rid.values.clone(),
1054 });
1055 }
1056 }
1057 out.push(m);
1058 }
1059 out.extend(
1060 self.comps()
1061 .filter(|c| c.c.has_property(&ICalendarProperty::RecurrenceId) && invited(&c.c))
1062 .cloned(),
1063 );
1064 for n in &mut out {
1065 n.children
1066 .retain(|ch| ch.c.component_type != ICalendarComponentType::VAlarm);
1067 strip_scheduling_params(&mut n.c);
1068 }
1069 (!out.is_empty()).then_some(out)
1070 }
1071
1072 /// A message: this object's VCALENDAR and time zones around `comps`.
1073 fn envelope(&self, comps: Vec<Node>, method: Method, now: DateTime<Utc>) -> ICalendar {
1074 let mut root = Node {
1075 c: self.root.c.clone(),
1076 children: self
1077 .root
1078 .children
1079 .iter()
1080 .filter(|n| n.c.component_type == ICalendarComponentType::VTimezone)
1081 .cloned()
1082 .collect(),
1083 };
1084 let method = match method {
1085 Method::Request => ICalendarMethod::Request,
1086 Method::Cancel => ICalendarMethod::Cancel,
1087 Method::Reply => ICalendarMethod::Reply,
1088 };
1089 set_prop(
1090 &mut root.c,
1091 ICalendarProperty::Method,
1092 ICalendarValue::Method(method),
1093 );
1094 let stamp = PartialDateTime::from_utc_timestamp(now.timestamp());
1095 for mut n in comps {
1096 set_prop(
1097 &mut n.c,
1098 ICalendarProperty::Dtstamp,
1099 ICalendarValue::PartialDateTime(Box::new(stamp.clone())),
1100 );
1101 root.children.push(n);
1102 }
1103 let mut components = Vec::new();
1104 flatten(&root, &mut components);
1105 ICalendar { components }
1106 }
1107
1108 /// A RECURRENCE-ID for the instance of `master` at `rid`, in the form of
1109 /// the master's DTSTART.
1110 fn recurrence_id(
1111 &self,
1112 master: &Node,
1113 rid: DateTime<Utc>,
1114 floating: &Zone,
1115 ) -> Option<ICalendarEntry> {
1116 let start = master.c.property(&ICalendarProperty::Dtstart)?;
1117 let v = start.values.first()?.as_partial_date_time()?;
1118 let local = |zone: &Zone| zone.to_local(rid).and_utc().timestamp();
1119 let value = if v.hour.is_none() {
1120 PartialDateTime::from_date_timestamp(local(floating))
1121 } else if v.tz_hour.is_some() {
1122 PartialDateTime::from_utc_timestamp(rid.timestamp())
1123 } else if start.tz_id().is_some() {
1124 PartialDateTime::from_naive_timestamp(local(&self.zones.get(start.tz_id())))
1125 } else {
1126 PartialDateTime::from_naive_timestamp(local(floating))
1127 };
1128 Some(ICalendarEntry {
1129 name: ICalendarProperty::RecurrenceId,
1130 params: start.params.clone(),
1131 values: vec![ICalendarValue::PartialDateTime(Box::new(value))],
1132 })
1133 }
1134
1135 /// An override for one instance of `master`, so it can hold a status of
1136 /// its own. Its length becomes a DURATION.
1137 fn instance(&self, master: &Node, rid: &ICalendarEntry) -> Node {
1138 let mut n = master.clone();
1139 let start =
1140 n.c.property(&ICalendarProperty::Dtstart)
1141 .and_then(|e| self.instant(e));
1142 let end_prop = match n.c.component_type {
1143 ICalendarComponentType::VTodo => ICalendarProperty::Due,
1144 _ => ICalendarProperty::Dtend,
1145 };
1146 let end = n.c.property(&end_prop).and_then(|e| self.instant(e));
1147 if let (Some(s), Some(e)) = (start, end) {
1148 n.c.entries.retain(|x| x.name != end_prop);
1149 n.c.entries.push(ICalendarEntry {
1150 name: ICalendarProperty::Duration,
1151 params: Vec::new(),
1152 values: vec![ICalendarValue::Duration(ICalendarDuration::from_seconds(
1153 (e - s).max(0),
1154 ))],
1155 });
1156 }
1157 n.c.entries.retain(|e| {
1158 !matches!(
1159 e.name,
1160 ICalendarProperty::Rrule
1161 | ICalendarProperty::Rdate
1162 | ICalendarProperty::Exdate
1163 | ICalendarProperty::Exrule
1164 | ICalendarProperty::Dtstart
1165 | ICalendarProperty::RecurrenceId
1166 )
1167 });
1168 n.c.entries.push(ICalendarEntry {
1169 name: ICalendarProperty::Dtstart,
1170 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1171 values: rid.values.clone(),
1172 });
1173 n.c.entries.push(ICalendarEntry {
1174 name: ICalendarProperty::RecurrenceId,
1175 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1176 values: rid.values.clone(),
1177 });
1178 n
1179 }
1180}
1181
1182// ---------------------------------------------------------------------------
1183// Properties and parameters
1184// ---------------------------------------------------------------------------
1185
1186fn is_scheduled(c: &ICalendarComponent) -> bool {
1187 matches!(
1188 c.component_type,
1189 ICalendarComponentType::VEvent
1190 | ICalendarComponentType::VTodo
1191 | ICalendarComponentType::VJournal
1192 )
1193}
1194
1195fn address(e: &ICalendarEntry) -> Option<&str> {
1196 e.values.first()?.as_text().map(str::trim)
1197}
1198
1199fn attendees(c: &ICalendarComponent) -> impl Iterator<Item = &ICalendarEntry> {
1200 c.properties(&ICalendarProperty::Attendee)
1201}
1202
1203fn addresses(c: &ICalendarComponent) -> Vec<String> {
1204 let mut v: Vec<String> = attendees(c)
1205 .filter_map(address)
1206 .map(str::to_ascii_lowercase)
1207 .collect();
1208 v.sort();
1209 v.dedup();
1210 v
1211}
1212
1213fn same_attendee<'a>(c: &'a ICalendarComponent, e: &ICalendarEntry) -> Option<&'a ICalendarEntry> {
1214 let a = address(e)?;
1215 attendees(c).find(|x| address(x).is_some_and(|b| b.eq_ignore_ascii_case(a)))
1216}
1217
1218fn own_partstat(c: &ICalendarComponent, me: Is) -> Option<String> {
1219 attendees(c).find(|e| address(e).is_some_and(me)).map(|e| {
1220 param(e, &ICalendarParameterName::Partstat)
1221 .unwrap_or("NEEDS-ACTION")
1222 .to_ascii_uppercase()
1223 })
1224}
1225
1226/// `SCHEDULE-AGENT` absent or `SERVER`. Unknown values count as `NONE`.
1227fn server_agent(e: &ICalendarEntry) -> bool {
1228 param(e, &ICalendarParameterName::ScheduleAgent)
1229 .is_none_or(|v| v.eq_ignore_ascii_case("SERVER"))
1230}
1231
1232fn sequence(c: &ICalendarComponent) -> i64 {
1233 c.property(&ICalendarProperty::Sequence)
1234 .and_then(|e| e.values.first()?.as_integer())
1235 .unwrap_or(0)
1236}
1237
1238fn param<'a>(e: &'a ICalendarEntry, name: &ICalendarParameterName) -> Option<&'a str> {
1239 e.parameter(name)?.as_text()
1240}
1241
1242fn set_param(e: &mut ICalendarEntry, name: ICalendarParameterName, value: ICalendarParameterValue) {
1243 remove_param(e, &name);
1244 e.params.push(ICalendarParameter::new(name, value));
1245}
1246
1247fn remove_param(e: &mut ICalendarEntry, name: &ICalendarParameterName) {
1248 e.params.retain(|p| &p.name != name);
1249}
1250
1251fn without(
1252 mut params: Vec<ICalendarParameter>,
1253 name: &ICalendarParameterName,
1254) -> Vec<ICalendarParameter> {
1255 params.retain(|p| &p.name != name);
1256 params
1257}
1258
1259fn set_prop(c: &mut ICalendarComponent, name: ICalendarProperty, value: ICalendarValue) {
1260 c.entries.retain(|e| e.name != name);
1261 c.entries.push(ICalendarEntry {
1262 name,
1263 params: Vec::new(),
1264 values: vec![value],
1265 });
1266}
1267
1268/// Messages carry none of the scheduling parameters (RFC 6638, 7).
1269fn strip_scheduling_params(c: &mut ICalendarComponent) {
1270 for e in c.entries.iter_mut().filter(|e| {
1271 matches!(
1272 e.name,
1273 ICalendarProperty::Attendee | ICalendarProperty::Organizer
1274 )
1275 }) {
1276 for p in [
1277 ICalendarParameterName::ScheduleAgent,
1278 ICalendarParameterName::ScheduleStatus,
1279 ICalendarParameterName::ScheduleForceSend,
1280 ] {
1281 remove_param(e, &p);
1282 }
1283 }
1284}
1285
1286fn text(s: &str) -> ICalendarParameterValue {
1287 ICalendarParameterValue::Text(s.to_string())
1288}
1289
1290fn partstat(p: ICalendarParticipationStatus) -> ICalendarParameterValue {
1291 ICalendarParameterValue::Partstat(p)
1292}
1293
1294/// Makes an object forget a principal that is about to be deleted, so that
1295/// a later principal of the same name is not taken for it. Every ORGANIZER
1296/// and ATTENDEE that `gone` names gets the address `tombstone`, and such an
1297/// ATTENDEE gets SCHEDULE-STATUS 3.7. A component the gone principal
1298/// organized is cancelled. All other lines keep their bytes. `None` when
1299/// nothing names the principal.
1300pub fn forget(text: &str, gone: Is, tombstone: &str) -> Option<String> {
1301 let mut out: Vec<String> = Vec::new();
1302 // Per open component: name, index of its BEGIN line in `out`, index of
1303 // its STATUS line, whether the gone principal organized it.
1304 let mut open: Vec<(String, usize, Option<usize>, bool)> = Vec::new();
1305 let mut changed = false;
1306 for raw in logical_lines(text) {
1307 let eol = match raw.ends_with('\n') {
1308 true if raw.ends_with("\r\n") => "\r\n",
1309 true => "\n",
1310 false => "\r\n",
1311 };
1312 let line = unfold(raw);
1313 let n = name(&line);
1314 match n.as_str() {
1315 "BEGIN" => open.push((
1316 value(&line).trim().to_ascii_uppercase(),
1317 out.len(),
1318 None,
1319 false,
1320 )),
1321 "END" => {
1322 if let Some((comp, begin, status, true)) = open.pop()
1323 && ["VEVENT", "VTODO", "VJOURNAL"].contains(&comp.as_str())
1324 {
1325 let cancelled = format!("STATUS:CANCELLED{eol}");
1326 match status {
1327 Some(i) => out[i] = cancelled,
1328 None => out.insert(begin + 1, cancelled),
1329 }
1330 }
1331 }
1332 "STATUS" => {
1333 if let Some(top) = open.last_mut() {
1334 top.2 = Some(out.len());
1335 }
1336 }
1337 "ORGANIZER" | "ATTENDEE" if gone(value(&line).trim()) => {
1338 let attendee = n == "ATTENDEE";
1339 let mut new = n.clone();
1340 for p in param_parts(&line) {
1341 if !(attendee
1342 && p.trim()
1343 .to_ascii_uppercase()
1344 .starts_with("SCHEDULE-STATUS="))
1345 {
1346 new.push(';');
1347 new.push_str(p);
1348 }
1349 }
1350 if attendee {
1351 new.push_str(";SCHEDULE-STATUS=3.7");
1352 } else if let Some(top) = open.last_mut() {
1353 top.3 = true;
1354 }
1355 new.push(':');
1356 new.push_str(tombstone);
1357 out.push(fold(&new, eol));
1358 changed = true;
1359 continue;
1360 }
1361 _ => {}
1362 }
1363 out.push(raw.to_string());
1364 }
1365 changed.then(|| out.concat())
1366}
1367