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