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