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