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