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