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_cal = old;
422 let old = Obj::new(old_cal);
423 if !old.organizer_schedules() {
424 return Ok((new, None));
425 }
426 let mut next = Obj::new(&new);
427 let master = old.master();
428 let added: Vec<i64> = next
429 .comps()
430 .filter_map(|c| next.key(&c.c))
431 .filter(|k| old.find(Some(*k)).is_none())
432 .collect();
433 let instances = old.instances(old_cal, &added);
434
435 for c in next.comps() {
436 let key = next.key(&c.c);
437 match old.find(key) {
438 Some(oc) => {
439 // The end, not its property: clients rewrite DURATION as DTEND.
440 let same_times = [
441 ICalendarProperty::Dtstart,
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 && old.end(&oc.c) == next.end(&c.c);
449 let kept_exdates = old
450 .times(&oc.c, &ICalendarProperty::Exdate)
451 .is_subset(&next.times(&c.c, &ICalendarProperty::Exdate));
452 if !same_times
453 || !kept_exdates
454 || organizer_of(&oc.c) != organizer_of(&c.c)
455 || addresses(&oc.c) != addresses(&c.c)
456 {
457 return Err(Refused::AttendeeChange);
458 }
459 }
460 // An instance the attendee overrides, to set its own status.
461 None => {
462 let instance = |k: i64| instances.as_ref().is_none_or(|s| s.contains(&k));
463 if !key
464 .is_some_and(|k| master.is_some() && instance(k) && old.plain(&next, &c.c, k))
465 {
466 return Err(Refused::AttendeeChange);
467 }
468 }
469 }
470 }
471 // Dropping an override is fine where it held no change of the
472 // organizer's, where the attendee deletes that instance with an EXDATE,
473 // and in a copy of single instances.
474 let exdated: HashSet<i64> = next
475 .master()
476 .into_iter()
477 .flat_map(|m| m.c.properties(&ICalendarProperty::Exdate))
478 .flat_map(|e| {
479 e.values
480 .iter()
481 .filter_map(|v| next.at(v.as_partial_date_time()?, e.tz_id()))
482 })
483 .collect();
484 let mut reverted: Vec<(i64, Option<String>)> = Vec::new();
485 for oc in old.comps() {
486 let key = old.key(&oc.c);
487 if next.find(key).is_some() {
488 continue;
489 }
490 let plain = |k: i64| old.plain(&old, &oc.c, k);
491 let allowed =
492 |k: i64| master.is_none() || !is_range(&oc.c) && (exdated.contains(&k) || plain(k));
493 if !key.is_some_and(allowed) {
494 return Err(Refused::AttendeeChange);
495 }
496 if let Some(k) = key.filter(|k| master.is_some() && !exdated.contains(k)) {
497 reverted.push((k, own_partstat(&oc.c, me)));
498 }
499 }
500
501 // The state of the others and of the organizer is the server's. A new
502 // override takes it from the THISANDFUTURE override that moves it.
503 let base = |key: Option<i64>| {
504 old.find(key)
505 .or_else(|| old.future(key?).map(|(n, _)| n))
506 .or(master)
507 };
508 let mut force = false;
509 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
510 for (c, key) in next.comps_mut().zip(&keys) {
511 let Some(base) = base(*key) else {
512 continue;
513 };
514 for e in &mut c.c.entries {
515 match e.name {
516 ICalendarProperty::Attendee if !address(e).is_some_and(me) => {
517 if let Some(b) = same_attendee(&base.c, e) {
518 e.params = b.params.clone();
519 }
520 }
521 ICalendarProperty::Organizer => {
522 force |= param(e, &ICalendarParameterName::ScheduleForceSend)
523 .is_some_and(|v| v.eq_ignore_ascii_case("REPLY"));
524 if let Some(b) = base.c.property(&ICalendarProperty::Organizer) {
525 e.params = b.params.clone();
526 }
527 }
528 _ => {}
529 }
530 }
531 }
532
533 let mut replied: Vec<Node> = Vec::new();
534 for (c, key) in next.comps().zip(&keys) {
535 let now_stat = own_partstat(&c.c, me);
536 let before = base(*key).and_then(|b| own_partstat(&b.c, me));
537 if now_stat.is_some() && (force || now_stat != before) {
538 replied.push(reply_part(c, me));
539 }
540 }
541 // A dropped override falls back to the series' answer for that instance.
542 for (k, held) in reverted {
543 let Some(inst) = DateTime::from_timestamp(k, 0).and_then(|t| next.single(t, &Zone::Utc))
544 else {
545 continue;
546 };
547 if own_partstat(&inst.c, me).is_some_and(|now| Some(now) != held) {
548 replied.push(reply_part(&inst, me));
549 }
550 }
551 if let Some(m) = next.master() {
552 let before = master.map_or_else(HashSet::new, |om| {
553 old.times(&om.c, &ICalendarProperty::Exdate)
554 });
555 for e in m.c.properties(&ICalendarProperty::Exdate) {
556 for value in &e.values {
557 if before.contains(&next.value_key(e, value)) {
558 continue;
559 }
560 let rid = ICalendarEntry {
561 name: ICalendarProperty::RecurrenceId,
562 params: e.params.clone(),
563 values: vec![value.clone()],
564 };
565 replied.push(declined_instance(m, rid, me));
566 }
567 }
568 }
569
570 let reply = match replied.is_empty() {
571 true => None,
572 false => next.organizer().map(|to| Message {
573 to,
574 method: Method::Reply,
575 quiet: false,
576 cal: next.envelope(replied, Method::Reply, now),
577 }),
578 };
579 Ok((next.done(), reply))
580}
581
582/// An attendee's DELETE: the REPLY declining every instance, unless the
583/// organizer handles scheduling itself or already cancelled.
584pub fn decline(old: &ICalendar, me: Is, now: DateTime<Utc>) -> Option<Message> {
585 let old = Obj::new(old);
586 if !old.organizer_schedules() {
587 return None;
588 }
589 let cancelled = old.comps().all(|c| {
590 c.c.property(&ICalendarProperty::Status)
591 .and_then(|e| e.values.first()?.as_text())
592 .is_some_and(|s| s.eq_ignore_ascii_case("CANCELLED"))
593 });
594 if cancelled {
595 return None;
596 }
597 let comps: Vec<Node> =
598 old.comps()
599 .filter(|c| own_partstat(&c.c, me).is_some())
600 .map(|c| {
601 let mut part = reply_part(c, me);
602 for e in
603 part.c.entries.iter_mut().filter(|e| {
604 e.name == ICalendarProperty::Attendee && address(e).is_some_and(me)
605 })
606 {
607 set_param(
608 e,
609 ICalendarParameterName::Partstat,
610 partstat(ICalendarParticipationStatus::Declined),
611 );
612 }
613 part
614 })
615 .collect();
616 if comps.is_empty() {
617 return None;
618 }
619 Some(Message {
620 to: old.organizer()?,
621 method: Method::Reply,
622 quiet: false,
623 cal: old.envelope(comps, Method::Reply, now),
624 })
625}
626
627/// A REQUEST or CANCEL applied to the attendee's copy. `None`: nothing to
628/// store.
629pub fn receive(copy: Option<&ICalendar>, msg: &Message) -> Option<ICalendar> {
630 match msg.method {
631 Method::Request => {
632 if msg.quiet && copy.is_none() {
633 return None;
634 }
635 let copy = copy.map(Obj::new);
636 let mut next = Obj::new(&msg.cal);
637 next.root
638 .c
639 .entries
640 .retain(|e| e.name != ICalendarProperty::Method);
641 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
642 let Some(copy) = copy else {
643 return Some(next.done());
644 };
645 // What the attendee may keep for itself (RFC 6638, 3.2.2.1).
646 for (c, key) in next.comps_mut().zip(&keys) {
647 let Some(base) = copy.find(*key).or(copy.master()) else {
648 continue;
649 };
650 c.children
651 .retain(|n| n.c.component_type != ICalendarComponentType::VAlarm);
652 c.children.extend(
653 base.children
654 .iter()
655 .filter(|n| n.c.component_type == ICalendarComponentType::VAlarm)
656 .cloned(),
657 );
658 for p in [
659 ICalendarProperty::Transp,
660 ICalendarProperty::PercentComplete,
661 ICalendarProperty::Completed,
662 ] {
663 c.c.entries.retain(|e| e.name != p);
664 c.c.entries.extend(base.c.properties(&p).cloned());
665 }
666 let status = base
667 .c
668 .property(&ICalendarProperty::Organizer)
669 .and_then(|e| e.parameter(&ICalendarParameterName::ScheduleStatus))
670 .cloned();
671 if let Some(s) = status {
672 for e in
673 c.c.entries
674 .iter_mut()
675 .filter(|e| e.name == ICalendarProperty::Organizer)
676 {
677 set_param(e, ICalendarParameterName::ScheduleStatus, s.clone());
678 }
679 }
680 }
681 Some(next.done())
682 }
683 Method::Cancel => {
684 let msg_obj = Obj::new(&msg.cal);
685 let mut next = Obj::new(copy?);
686 let whole = msg_obj.master().is_some();
687 let gone: Vec<(i64, ICalendarEntry)> = msg_obj
688 .comps()
689 .filter_map(|c| {
690 let rid = c.c.property(&ICalendarProperty::RecurrenceId)?;
691 Some((msg_obj.instant(rid)?, rid.clone()))
692 })
693 .collect();
694 let range =
695 |rid: &ICalendarEntry| rid.parameter(&ICalendarParameterName::Range).cloned();
696 let hit = |k: i64| {
697 gone.iter()
698 .any(|(g, rid)| *g == k || (*g < k && range(rid).is_some()))
699 };
700 // A cancelled range stands for later instances too, which an
701 // EXDATE cannot cover: the override carries the range.
702 let mut missing = Vec::new();
703 for (k, rid) in gone.iter().filter(|_| !whole) {
704 let Some(r) = range(rid) else { continue };
705 let at = next.position(Some(*k)).or_else(|| {
706 let n = next.single(DateTime::from_timestamp(*k, 0)?, &Zone::Utc)?;
707 next.root.children.push(n);
708 Some(next.root.children.len() - 1)
709 });
710 let entry = at.and_then(|at| {
711 next.root.children[at]
712 .c
713 .entries
714 .iter_mut()
715 .find(|e| e.name == ICalendarProperty::RecurrenceId)
716 });
717 match entry {
718 Some(e) => set_param(e, ICalendarParameterName::Range, r),
719 None => missing.push(rid.clone()),
720 }
721 }
722 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
723 for (c, key) in next.comps_mut().zip(&keys) {
724 if whole || key.is_some_and(hit) {
725 set_prop(
726 &mut c.c,
727 ICalendarProperty::Status,
728 ICalendarValue::Status(ICalendarStatus::Cancelled),
729 );
730 }
731 }
732 if !whole {
733 missing.extend(
734 gone.into_iter()
735 .filter(|(k, rid)| range(rid).is_none() && !keys.contains(&Some(*k)))
736 .map(|(_, rid)| rid),
737 );
738 let missing: Vec<ICalendarEntry> = missing
739 .into_iter()
740 .map(|rid| ICalendarEntry {
741 name: ICalendarProperty::Exdate,
742 params: without(rid.params, &ICalendarParameterName::Range),
743 values: rid.values,
744 })
745 .collect();
746 if let Some(m) = next
747 .comps_mut()
748 .find(|c| !c.c.has_property(&ICalendarProperty::RecurrenceId))
749 {
750 m.c.entries.extend(missing);
751 }
752 }
753 Some(next.done())
754 }
755 Method::Reply => None,
756 }
757}
758
759/// A REPLY applied to the organizer object. `false` if it changed nothing.
760pub fn apply_reply(org: &mut ICalendar, reply: &ICalendar, replier: Is) -> bool {
761 let rep = Obj::new(reply);
762 let mut next = Obj::new(org);
763 let mut changed = false;
764 let mut parts: Vec<&Node> = rep.comps().collect();
765 // In order: a range narrowed for one instance moves on to the next.
766 parts.sort_by_key(|c| rep.key(&c.c));
767 let missing: Vec<i64> = parts
768 .iter()
769 .filter_map(|c| rep.key(&c.c))
770 .filter(|k| next.position(Some(*k)).is_none())
771 .collect();
772 // A cut-short expansion over all parts falls back to one per part.
773 let shared = next.instances(org, &missing);
774 for rc in parts {
775 let key = rep.key(&rc.c);
776 let at = match next.position(key) {
777 Some(at) => {
778 if let Some(t) = key
779 && !is_range(&rc.c)
780 && let Some(rid) = DateTime::from_timestamp(t, 0)
781 {
782 next.narrow(org, rid, t, &Zone::Utc);
783 }
784 at
785 }
786 // A reply for one instance of the series gets its own override.
787 // Obj reads floating times in UTC; expand must match.
788 None => {
789 let Some((t, at)) = key.and_then(|t| Some((t, DateTime::from_timestamp(t, 0)?)))
790 else {
791 continue;
792 };
793 let known = match &shared {
794 Some(set) => set.contains(&t),
795 None => next.instances(org, &[t]).is_none_or(|set| set.contains(&t)),
796 };
797 if !known {
798 continue;
799 }
800 let Some(inst) = next.single(at, &Zone::Utc) else {
801 continue;
802 };
803 next.root.children.push(inst);
804 next.root.children.len() - 1
805 }
806 };
807 let target = &mut next.root.children[at];
808 if sequence(&rc.c) < sequence(&target.c) {
809 continue;
810 }
811 let Some(stat) =
812 rc.c.properties(&ICalendarProperty::Attendee)
813 .find(|e| address(e).is_some_and(replier))
814 .map(|e| {
815 e.parameter(&ICalendarParameterName::Partstat)
816 .cloned()
817 .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction))
818 })
819 else {
820 continue;
821 };
822 // The attendee writes these; only digits and dots are a status code.
823 let codes: Vec<String> =
824 rc.c.properties(&ICalendarProperty::RequestStatus)
825 .filter_map(|e| e.values.first()?.as_text())
826 .map(|s| {
827 let code = s.split(';').next().unwrap_or(s);
828 code.chars()
829 .filter(|c| c.is_ascii_digit() || *c == '.')
830 .collect::<String>()
831 })
832 .filter(|c| !c.is_empty())
833 .collect();
834 let status = match codes.is_empty() {
835 true => "2.0".to_string(),
836 false => codes.join(","),
837 };
838 for e in
839 target.c.entries.iter_mut().filter(|e| {
840 e.name == ICalendarProperty::Attendee && address(e).is_some_and(replier)
841 })
842 {
843 changed |= e.parameter(&ICalendarParameterName::Partstat) != Some(&stat);
844 set_param(e, ICalendarParameterName::Partstat, stat.clone());
845 set_param(e, ICalendarParameterName::ScheduleStatus, text(&status));
846 }
847 }
848 if changed {
849 *org = next.done();
850 }
851 changed
852}
853
854// ---------------------------------------------------------------------------
855// The organizer object
856// ---------------------------------------------------------------------------
857
858/// The client's organizer object with the attendee state the server owns:
859/// their PARTSTAT (reset on a reschedule, RFC 6638 3.2.8) and
860/// SCHEDULE-STATUS. Collects SCHEDULE-FORCE-SEND=REQUEST into `force`.
861fn guard(
862 old: Option<&ICalendar>,
863 new: &ICalendar,
864 organizer: Is,
865 force: &mut Vec<String>,
866) -> ICalendar {
867 let old = old.map(Obj::new);
868 let mut next = Obj::new(new);
869 // A new override takes the state of the THISANDFUTURE override that moves
870 // its instance, else of the master.
871 let base_of = |key: Option<i64>| {
872 let o = old.as_ref()?;
873 o.find(key)
874 .or_else(|| o.future(key?).map(|(n, _)| n))
875 .or(o.master())
876 };
877 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
878 let moved: Vec<bool> = next
879 .comps()
880 .map(|c| match &old {
881 Some(o) => match o.find(next.key(&c.c)) {
882 Some(oc) => rescheduled(o, &oc.c, &next, &c.c),
883 // A new override: rescheduled if it moves its instance.
884 None => {
885 let start =
886 c.c.property(&ICalendarProperty::Dtstart)
887 .and_then(|e| next.instant(e));
888 let key = next.key(&c.c);
889 key.is_none()
890 || start != key.map(|k| o.moved(k))
891 || base_of(key).is_some_and(|b| {
892 next.end(&c.c) != start.and_then(|s| o.end_from(&b.c, s))
893 })
894 }
895 },
896 None => true,
897 })
898 .collect();
899 let bumps: Vec<Option<i64>> = keys
900 .iter()
901 .zip(next.comps())
902 .zip(&moved)
903 .map(|((key, c), moved)| {
904 let oc = base_of(*key)?;
905 (*moved && sequence(&c.c) <= sequence(&oc.c)).then(|| sequence(&oc.c).saturating_add(1))
906 })
907 .collect();
908
909 for ((c, key), (moved, bump)) in next.comps_mut().zip(&keys).zip(moved.iter().zip(&bumps)) {
910 let base = base_of(*key);
911 for e in &mut c.c.entries {
912 match e.name {
913 ICalendarProperty::Organizer => {
914 remove_param(e, &ICalendarParameterName::ScheduleForceSend);
915 }
916 ICalendarProperty::Attendee => {
917 let forced = param(e, &ICalendarParameterName::ScheduleForceSend)
918 .is_some_and(|v| v.eq_ignore_ascii_case("REQUEST"));
919 remove_param(e, &ICalendarParameterName::ScheduleForceSend);
920 let Some(a) = address(e).map(str::to_string) else {
921 continue;
922 };
923 if organizer(&a) || !server_agent(e) {
924 continue;
925 }
926 if forced {
927 force.push(a.clone());
928 }
929 let prev = base.and_then(|b| same_attendee(&b.c, e));
930 let stat = match (moved, prev) {
931 (false, Some(p)) => p
932 .parameter(&ICalendarParameterName::Partstat)
933 .cloned()
934 .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction)),
935 _ => partstat(ICalendarParticipationStatus::NeedsAction),
936 };
937 set_param(e, ICalendarParameterName::Partstat, stat);
938 match prev.and_then(|p| p.parameter(&ICalendarParameterName::ScheduleStatus)) {
939 Some(s) => set_param(e, ICalendarParameterName::ScheduleStatus, s.clone()),
940 None => remove_param(e, &ICalendarParameterName::ScheduleStatus),
941 }
942 }
943 _ => {}
944 }
945 }
946 if let Some(n) = bump {
947 set_prop(
948 &mut c.c,
949 ICalendarProperty::Sequence,
950 ICalendarValue::Integer(*n),
951 );
952 }
953 }
954 next.done()
955}
956
957/// Whether a change moves instances in time (RFC 6638, 3.2.8). Shortening a
958/// series or excluding instances does not.
959fn rescheduled(old: &Obj, oc: &ICalendarComponent, new: &Obj, nc: &ICalendarComponent) -> bool {
960 let moved = [ICalendarProperty::Dtstart, ICalendarProperty::Rdate]
961 .iter()
962 .any(|p| old.times(oc, p) != new.times(nc, p))
963 || old.end(oc) != new.end(nc);
964 let reinstated = !old
965 .times(oc, &ICalendarProperty::Exdate)
966 .is_subset(&new.times(nc, &ICalendarProperty::Exdate));
967 // A changed or removed EXRULE can bring instances back; a new one cannot.
968 let exrules = (
969 rules(oc, &ICalendarProperty::Exrule),
970 rules(nc, &ICalendarProperty::Exrule),
971 );
972 let exrule_changed = !exrules.0.is_empty() && exrules.0 != exrules.1;
973 moved || reinstated || exrule_changed || rules_grew(new, oc, nc)
974}
975
976/// A rule in one form: BY lists sorted, INTERVAL=1 and WKST=MO as absent.
977fn canonical(r: &ICalendarRecurrenceRule) -> ICalendarRecurrenceRule {
978 fn tidy<T: Ord>(v: &mut Vec<T>) {
979 v.sort();
980 v.dedup();
981 }
982 let mut r = r.clone();
983 tidy(&mut r.bysecond);
984 tidy(&mut r.byminute);
985 tidy(&mut r.byhour);
986 tidy(&mut r.byday);
987 tidy(&mut r.bymonthday);
988 tidy(&mut r.byyearday);
989 tidy(&mut r.byweekno);
990 tidy(&mut r.bymonth);
991 tidy(&mut r.bysetpos);
992 r.interval = r.interval.filter(|i| *i > 1);
993 r.wkst = r.wkst.filter(|w| *w != ICalendarWeekday::Monday);
994 r
995}
996
997fn rules(c: &ICalendarComponent, prop: &ICalendarProperty) -> Vec<ICalendarRecurrenceRule> {
998 let mut v: Vec<_> = c
999 .properties(prop)
1000 .filter_map(|e| match e.values.first()? {
1001 ICalendarValue::RecurrenceRule(r) => Some(canonical(r)),
1002 _ => None,
1003 })
1004 .collect();
1005 v.sort_by_key(|r| format!("{r:?}"));
1006 v
1007}
1008
1009fn rules_grew(obj: &Obj, oc: &ICalendarComponent, nc: &ICalendarComponent) -> bool {
1010 let rules = |c| rules(c, &ICalendarProperty::Rrule);
1011 let (o, n) = (rules(oc), rules(nc));
1012 if o == n {
1013 return false;
1014 }
1015 let (o, n) = match (&o[..], &n[..]) {
1016 // Dropping the rule leaves the first instance only.
1017 ([_, ..], []) => return false,
1018 ([o], [n]) => (o, n),
1019 _ => return true,
1020 };
1021 let unbounded = |r: &calcard::icalendar::ICalendarRecurrenceRule| {
1022 let mut r = r.clone();
1023 r.until = None;
1024 r.count = None;
1025 r
1026 };
1027 if unbounded(o) != unbounded(n) {
1028 return true;
1029 }
1030 // A DATE UNTIL includes its whole day.
1031 let end =
1032 |u: &PartialDateTime| Some(obj.at(u, None)? + if u.hour.is_none() { 86399 } else { 0 });
1033 let shorter = match (&o.until, &n.until, o.count, n.count) {
1034 (Some(ou), Some(nu), _, _) => end(nu) <= end(ou),
1035 (_, _, Some(oc), Some(nc)) => nc <= oc,
1036 (None, _, None, _) => true,
1037 _ => false,
1038 };
1039 !shorter
1040}
1041
1042fn cancelled(comps: Vec<Node>) -> Vec<Node> {
1043 comps
1044 .into_iter()
1045 .map(|mut n| {
1046 set_prop(
1047 &mut n.c,
1048 ICalendarProperty::Status,
1049 ICalendarValue::Status(ICalendarStatus::Cancelled),
1050 );
1051 let seq = sequence(&n.c).saturating_add(1);
1052 set_prop(
1053 &mut n.c,
1054 ICalendarProperty::Sequence,
1055 ICalendarValue::Integer(seq),
1056 );
1057 n
1058 })
1059 .collect()
1060}
1061
1062/// For comparing what an attendee would receive: without the stamps a
1063/// client rewrites on every save, and optionally without participation.
1064fn normalized(comps: &[Node], without_partstat: bool) -> Vec<Node> {
1065 let mut out = comps
1066 .iter()
1067 .map(|n| {
1068 let mut n = n.clone();
1069 n.c.entries.retain(|e| {
1070 !matches!(
1071 e.name,
1072 ICalendarProperty::Dtstamp
1073 | ICalendarProperty::LastModified
1074 | ICalendarProperty::Created
1075 | ICalendarProperty::Sequence
1076 ) && !matches!(&e.name, ICalendarProperty::Other(x) if x.to_ascii_uppercase().starts_with("X-"))
1077 });
1078 // A new SENT-BY alone is no change for an attendee.
1079 for e in n.c.entries.iter_mut().filter(|e| {
1080 matches!(
1081 e.name,
1082 ICalendarProperty::Organizer | ICalendarProperty::Attendee
1083 )
1084 }) {
1085 remove_param(e, &ICalendarParameterName::SentBy);
1086 }
1087 if without_partstat {
1088 for e in n.c.entries.iter_mut().filter(|e| e.name == ICalendarProperty::Attendee) {
1089 remove_param(e, &ICalendarParameterName::Partstat);
1090 }
1091 }
1092 sorted(&mut n);
1093 n
1094 })
1095 .collect::<Vec<_>>();
1096 out.sort_by_cached_key(|n| format!("{n:?}"));
1097 out
1098}
1099
1100/// In one order, so another client's order of properties is no change.
1101fn sorted(n: &mut Node) {
1102 for e in &mut n.c.entries {
1103 e.params.sort_by_cached_key(|p| format!("{p:?}"));
1104 }
1105 n.c.entries.sort_by_cached_key(|e| format!("{e:?}"));
1106 for ch in &mut n.children {
1107 sorted(ch);
1108 }
1109 n.children.sort_by_cached_key(|ch| format!("{ch:?}"));
1110}
1111
1112/// A component of the attendee's REPLY: only its own ATTENDEE, no alarms.
1113fn reply_part(c: &Node, me: Is) -> Node {
1114 let mut n = Node {
1115 c: c.c.clone(),
1116 children: Vec::new(),
1117 };
1118 n.c.entries
1119 .retain(|e| e.name != ICalendarProperty::Attendee || address(e).is_some_and(me));
1120 strip_scheduling_params(&mut n.c);
1121 n
1122}
1123
1124/// The REPLY component for an instance the attendee excluded with EXDATE.
1125fn declined_instance(master: &Node, rid: ICalendarEntry, me: Is) -> Node {
1126 let mut c = ICalendarComponent {
1127 component_type: master.c.component_type.clone(),
1128 entries: Vec::new(),
1129 component_ids: Vec::new(),
1130 };
1131 for p in [
1132 ICalendarProperty::Uid,
1133 ICalendarProperty::Sequence,
1134 ICalendarProperty::Organizer,
1135 ICalendarProperty::Summary,
1136 ] {
1137 c.entries.extend(master.c.properties(&p).cloned());
1138 }
1139 c.entries.push(ICalendarEntry {
1140 name: ICalendarProperty::Dtstart,
1141 ..rid.clone()
1142 });
1143 c.entries.push(rid);
1144 for e in master
1145 .c
1146 .properties(&ICalendarProperty::Attendee)
1147 .filter(|e| address(e).is_some_and(me))
1148 {
1149 let mut e = e.clone();
1150 set_param(
1151 &mut e,
1152 ICalendarParameterName::Partstat,
1153 partstat(ICalendarParticipationStatus::Declined),
1154 );
1155 c.entries.push(e);
1156 }
1157 strip_scheduling_params(&mut c);
1158 Node {
1159 c,
1160 children: Vec::new(),
1161 }
1162}
1163
1164// ---------------------------------------------------------------------------
1165// The object as a tree
1166// ---------------------------------------------------------------------------
1167
1168/// A component with its sub-components, detached from the flat list calcard
1169/// keeps.
1170#[derive(Debug, Clone, PartialEq)]
1171struct Node {
1172 /// `component_ids` is empty; `children` replaces it.
1173 c: ICalendarComponent,
1174 children: Vec<Node>,
1175}
1176
1177fn node(cal: &ICalendar, i: usize) -> Node {
1178 let mut c = cal.components[i].clone();
1179 let ids = std::mem::take(&mut c.component_ids);
1180 let children = ids
1181 .iter()
1182 .map(|&id| id as usize)
1183 .filter(|&id| id > i && id < cal.components.len())
1184 .map(|id| node(cal, id))
1185 .collect();
1186 Node { c, children }
1187}
1188
1189fn flatten(n: &Node, out: &mut Vec<ICalendarComponent>) -> u32 {
1190 let at = out.len();
1191 out.push(n.c.clone());
1192 for ch in &n.children {
1193 let id = flatten(ch, out);
1194 out[at].component_ids.push(id);
1195 }
1196 at as u32
1197}
1198
1199/// A calendar object: the VCALENDAR with its time zones and components.
1200struct Obj {
1201 root: Node,
1202 zones: Zones,
1203}
1204
1205impl Obj {
1206 fn new(cal: &ICalendar) -> Self {
1207 let root = match cal.components.is_empty() {
1208 true => Node {
1209 c: ICalendarComponent {
1210 component_type: ICalendarComponentType::VCalendar,
1211 entries: Vec::new(),
1212 component_ids: Vec::new(),
1213 },
1214 children: Vec::new(),
1215 },
1216 false => node(cal, 0),
1217 };
1218 Obj {
1219 root,
1220 zones: Zones::new(cal, Zone::Utc),
1221 }
1222 }
1223
1224 fn done(self) -> ICalendar {
1225 let mut components = Vec::new();
1226 flatten(&self.root, &mut components);
1227 ICalendar { components }
1228 }
1229
1230 fn comps(&self) -> impl Iterator<Item = &Node> {
1231 self.root.children.iter().filter(|n| is_scheduled(&n.c))
1232 }
1233
1234 fn comps_mut(&mut self) -> impl Iterator<Item = &mut Node> {
1235 self.root.children.iter_mut().filter(|n| is_scheduled(&n.c))
1236 }
1237
1238 fn master(&self) -> Option<&Node> {
1239 self.find(None)
1240 }
1241
1242 fn find(&self, key: Option<i64>) -> Option<&Node> {
1243 self.comps().find(|c| self.key(&c.c) == key)
1244 }
1245
1246 fn position(&self, key: Option<i64>) -> Option<usize> {
1247 self.root
1248 .children
1249 .iter()
1250 .position(|n| is_scheduled(&n.c) && self.key(&n.c) == key)
1251 }
1252
1253 /// The RECURRENCE-ID as an instant; `None` for the master.
1254 fn key(&self, c: &ICalendarComponent) -> Option<i64> {
1255 self.instant(c.property(&ICalendarProperty::RecurrenceId)?)
1256 }
1257
1258 /// The first value of a date or date-time property, in UTC seconds.
1259 /// Dates count from midnight, whatever the zone.
1260 fn instant(&self, e: &ICalendarEntry) -> Option<i64> {
1261 let v = e.values.first()?.as_partial_date_time()?;
1262 self.at(v, e.tz_id())
1263 }
1264
1265 fn at(&self, v: &PartialDateTime, tzid: Option<&str>) -> Option<i64> {
1266 let dt = v.to_date_time()?;
1267 Some(match dt.offset {
1268 _ if v.hour.is_none() => dt.date_time.and_utc().timestamp(),
1269 Some(o) => dt.date_time.and_utc().timestamp() - i64::from(o.local_minus_utc()),
1270 None => self.zones.get(tzid).to_utc(dt.date_time).timestamp(),
1271 })
1272 }
1273
1274 /// Every value of a property, comparable across encodings: instants for
1275 /// dates and date-times, the text otherwise.
1276 fn times(&self, c: &ICalendarComponent, prop: &ICalendarProperty) -> HashSet<String> {
1277 c.properties(prop)
1278 .flat_map(|e| e.values.iter().map(move |v| self.value_key(e, v)))
1279 .collect()
1280 }
1281
1282 fn value_key(&self, e: &ICalendarEntry, v: &ICalendarValue) -> String {
1283 match v.as_partial_date_time() {
1284 Some(p) => match self.at(p, e.tz_id()) {
1285 Some(t) if p.hour.is_none() => format!("{t}d"),
1286 Some(t) => t.to_string(),
1287 None => format!("{v:?}"),
1288 },
1289 None => match v {
1290 ICalendarValue::RecurrenceRule(r) => format!("{:?}", canonical(r)),
1291 _ => format!("{v:?}"),
1292 },
1293 }
1294 }
1295
1296 fn organizer(&self) -> Option<String> {
1297 self.comps()
1298 .find_map(|c| address(c.c.property(&ICalendarProperty::Organizer)?))
1299 .map(str::to_string)
1300 }
1301
1302 /// Whether the server replies for the attendee (RFC 6638, 3.2.2).
1303 fn organizer_schedules(&self) -> bool {
1304 self.comps()
1305 .filter_map(|c| c.c.property(&ICalendarProperty::Organizer))
1306 .all(server_agent)
1307 }
1308
1309 /// What attendee `a` gets to see (RFC 6638, 3.2.6): the master with the
1310 /// overrides it is in, and EXDATEs for those it is not in; or just the
1311 /// overrides it is in. A THISANDFUTURE override it is not in ends its
1312 /// series there.
1313 fn view(&self, a: &str) -> Option<Vec<Node>> {
1314 let invited = |c: &ICalendarComponent| {
1315 attendees(c)
1316 .any(|e| address(e).is_some_and(|x| x.eq_ignore_ascii_case(a)) && server_agent(e))
1317 };
1318 let mut out = Vec::new();
1319 let master = self.master().filter(|m| invited(&m.c));
1320 if let Some(m) = master {
1321 let mut m = m.clone();
1322 let mut end: Option<i64> = None;
1323 for o in self
1324 .comps()
1325 .filter(|c| c.c.has_property(&ICalendarProperty::RecurrenceId))
1326 {
1327 let Some(rid) = o.c.property(&ICalendarProperty::RecurrenceId) else {
1328 continue;
1329 };
1330 if invited(&o.c) {
1331 continue;
1332 }
1333 if is_range(&o.c) {
1334 // ponytail: a later range that invites them again is lost too.
1335 // A stale range past the series' end cuts nothing.
1336 if let Some(t) = self.instant(rid).filter(|t| self.in_series(*t)) {
1337 end = Some(end.map_or(t, |e| e.min(t)));
1338 }
1339 continue;
1340 }
1341 m.c.entries.push(ICalendarEntry {
1342 name: ICalendarProperty::Exdate,
1343 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1344 values: rid.values.clone(),
1345 });
1346 }
1347 let first =
1348 m.c.property(&ICalendarProperty::Dtstart)
1349 .and_then(|e| self.instant(e));
1350 match end {
1351 // The range starts with the first instance: none is left.
1352 Some(t) if first.is_some_and(|f| t <= f) => {}
1353 Some(t) => {
1354 self.end_series(&mut m.c, t);
1355 out.push(m);
1356 }
1357 None => out.push(m),
1358 }
1359 }
1360 out.extend(
1361 self.comps()
1362 .filter(|c| c.c.has_property(&ICalendarProperty::RecurrenceId) && invited(&c.c))
1363 .cloned(),
1364 );
1365 for n in &mut out {
1366 n.children
1367 .retain(|ch| ch.c.component_type != ICalendarComponentType::VAlarm);
1368 strip_scheduling_params(&mut n.c);
1369 }
1370 (!out.is_empty()).then_some(out)
1371 }
1372
1373 /// Whether override `c`, read through `obj`, is instance `key` of this
1374 /// series unchanged: its start, length and people, no rules of its own.
1375 fn plain(&self, obj: &Obj, c: &ICalendarComponent, key: i64) -> bool {
1376 let Some(base) = self.base(key) else {
1377 return false;
1378 };
1379 let start = c
1380 .property(&ICalendarProperty::Dtstart)
1381 .and_then(|e| obj.instant(e));
1382 start == Some(self.moved(key))
1383 && [
1384 ICalendarProperty::Rrule,
1385 ICalendarProperty::Rdate,
1386 ICalendarProperty::Exrule,
1387 ]
1388 .iter()
1389 .all(|p| !c.has_property(p))
1390 && obj.end(c) == self.end_from(&base.c, self.moved(key))
1391 && organizer_of(c) == organizer_of(&base.c)
1392 && addresses(c) == addresses(&base.c)
1393 && (c.status() == base.c.status() || own_progress(c, &base.c))
1394 }
1395
1396 /// Whether the master's own rules have an instance at `key`.
1397 fn in_series(&self, key: i64) -> bool {
1398 let (Some(m), Some(at)) = (self.master(), DateTime::from_timestamp(key, 0)) else {
1399 return false;
1400 };
1401 let mut root = Node {
1402 c: self.root.c.clone(),
1403 children: self
1404 .root
1405 .children
1406 .iter()
1407 .filter(|n| n.c.component_type == ICalendarComponentType::VTimezone)
1408 .cloned()
1409 .collect(),
1410 };
1411 root.children.push(m.clone());
1412 let mut components = Vec::new();
1413 flatten(&root, &mut components);
1414 let window = at - TimeDelta::days(1)..at + TimeDelta::days(1);
1415 expand(&ICalendar { components }, window, Zone::Utc)
1416 .instances
1417 .iter()
1418 .any(|i| i.recurrence_id == Some(at))
1419 }
1420
1421 /// What instance `key` copies: the THISANDFUTURE override that moves it,
1422 /// else the master.
1423 fn base(&self, key: i64) -> Option<&Node> {
1424 self.future(key).map(|(n, _)| n).or(self.master())
1425 }
1426
1427 /// When the component ends: DTSTART plus its length, else its DUE.
1428 fn end(&self, c: &ICalendarComponent) -> Option<i64> {
1429 match c.property(&ICalendarProperty::Dtstart) {
1430 Some(s) => self.end_from(c, self.instant(s)?),
1431 None => self.instant(c.property(&ICalendarProperty::Due)?),
1432 }
1433 }
1434
1435 /// When `c` would end if it started at `start`. The days and weeks of a
1436 /// DURATION count in wall-clock time (RFC 5545, 3.3.6).
1437 fn end_from(&self, c: &ICalendarComponent, start: i64) -> Option<i64> {
1438 let duration = c.property(&ICalendarProperty::Duration);
1439 let d = match duration.and_then(|e| e.values.first()) {
1440 Some(ICalendarValue::Duration(d))
1441 if !d.neg
1442 && !c.has_property(&ICalendarProperty::Dtend)
1443 && !c.has_property(&ICalendarProperty::Due) =>
1444 {
1445 d
1446 }
1447 _ => return Some(start + self.length(c)?),
1448 };
1449 let zone = self
1450 .zones
1451 .get(c.property(&ICalendarProperty::Dtstart)?.tz_id());
1452 let days = i64::from(d.weeks) * 7 + i64::from(d.days);
1453 let local = zone.to_local(DateTime::from_timestamp(start, 0)?) + TimeDelta::days(days);
1454 let exact = i64::from(d.hours) * 3600 + i64::from(d.minutes) * 60 + i64::from(d.seconds);
1455 Some(zone.to_utc(local).timestamp() + exact)
1456 }
1457
1458 /// Which of `keys` are instances of the series in `cal`, from one
1459 /// expansion. `None` if the expansion was cut short and cannot tell.
1460 fn instances(&self, cal: &ICalendar, keys: &[i64]) -> Option<HashSet<i64>> {
1461 let starts: Vec<i64> = keys.iter().map(|k| self.moved(*k)).collect();
1462 let at = |t: i64| DateTime::from_timestamp(t, 0);
1463 // A day either side: an all-day shift is whole days, not 24 hours.
1464 let from = at(*starts.iter().min()?)? - TimeDelta::days(1);
1465 let to = at(*starts.iter().max()?)? + TimeDelta::days(1);
1466 let e = expand(cal, from..to, Zone::Utc);
1467 (!e.truncated).then(|| {
1468 e.instances
1469 .iter()
1470 .filter_map(|i| Some(i.recurrence_id?.timestamp()))
1471 .collect()
1472 })
1473 }
1474
1475 /// Seconds from DTSTART to DTEND or DUE, or the DURATION. Without
1476 /// either, a day for a date and nothing for a date-time (RFC 5545).
1477 fn length(&self, c: &ICalendarComponent) -> Option<i64> {
1478 let start = c.property(&ICalendarProperty::Dtstart)?;
1479 let instant = |p: &ICalendarProperty| c.property(p).and_then(|e| self.instant(e));
1480 match instant(&ICalendarProperty::Dtend).or_else(|| instant(&ICalendarProperty::Due)) {
1481 Some(end) => Some(end - self.instant(start)?),
1482 None => match c.property(&ICalendarProperty::Duration) {
1483 Some(d) => match d.values.first()? {
1484 ICalendarValue::Duration(d) => Some(d.as_seconds()),
1485 _ => None,
1486 },
1487 None => {
1488 let date = start.values.first()?.as_partial_date_time()?.hour.is_none();
1489 Some(if date { 86400 } else { 0 })
1490 }
1491 },
1492 }
1493 }
1494
1495 /// Ends the series of master `m` before instant `at`: UNTIL in the form
1496 /// RFC 5545 asks for, COUNT dropped, later RDATEs removed. `at` is an
1497 /// instance, so COUNT had not ended before it.
1498 fn end_series(&self, m: &mut ICalendarComponent, at: i64) {
1499 let Some(start) = m.property(&ICalendarProperty::Dtstart).cloned() else {
1500 return;
1501 };
1502 let Some(form) = start.values.first().and_then(|v| v.as_partial_date_time()) else {
1503 return;
1504 };
1505 // Obj reads floating times in UTC, so `at` is their wall-clock time.
1506 let until = if form.hour.is_none() {
1507 PartialDateTime::from_date_timestamp(at - 86400)
1508 } else if form.tz_hour.is_none() && start.tz_id().is_none() {
1509 PartialDateTime::from_naive_timestamp(at - 1)
1510 } else {
1511 PartialDateTime::from_utc_timestamp(at - 1)
1512 };
1513 for e in m.entries.iter_mut() {
1514 match e.name {
1515 ICalendarProperty::Rrule => {
1516 for v in &mut e.values {
1517 if let ICalendarValue::RecurrenceRule(r) = v
1518 && !r
1519 .until
1520 .as_ref()
1521 .and_then(|u| self.at(u, start.tz_id()))
1522 .is_some_and(|u| u < at)
1523 {
1524 r.until = Some(until.clone());
1525 r.count = None;
1526 }
1527 }
1528 }
1529 ICalendarProperty::Rdate => {
1530 let tz = e.tz_id().map(str::to_string);
1531 e.values.retain(|v| {
1532 v.as_partial_date_time()
1533 .and_then(|p| self.at(p, tz.as_deref()))
1534 .is_none_or(|t| t < at)
1535 });
1536 }
1537 _ => {}
1538 }
1539 }
1540 m.entries
1541 .retain(|e| e.name != ICalendarProperty::Rdate || !e.values.is_empty());
1542 }
1543
1544 /// A message: this object's VCALENDAR and time zones around `comps`.
1545 fn envelope(&self, comps: Vec<Node>, method: Method, now: DateTime<Utc>) -> ICalendar {
1546 let mut root = Node {
1547 c: self.root.c.clone(),
1548 children: self
1549 .root
1550 .children
1551 .iter()
1552 .filter(|n| n.c.component_type == ICalendarComponentType::VTimezone)
1553 .cloned()
1554 .collect(),
1555 };
1556 let method = match method {
1557 Method::Request => ICalendarMethod::Request,
1558 Method::Cancel => ICalendarMethod::Cancel,
1559 Method::Reply => ICalendarMethod::Reply,
1560 };
1561 set_prop(
1562 &mut root.c,
1563 ICalendarProperty::Method,
1564 ICalendarValue::Method(method),
1565 );
1566 let stamp = PartialDateTime::from_utc_timestamp(now.timestamp());
1567 for mut n in comps {
1568 set_prop(
1569 &mut n.c,
1570 ICalendarProperty::Dtstamp,
1571 ICalendarValue::PartialDateTime(Box::new(stamp.clone())),
1572 );
1573 root.children.push(n);
1574 }
1575 let mut components = Vec::new();
1576 flatten(&root, &mut components);
1577 ICalendar { components }
1578 }
1579
1580 /// A RECURRENCE-ID for the instance at `rid`, in the form of `start`.
1581 fn recurrence_id(
1582 &self,
1583 start: &ICalendarEntry,
1584 rid: DateTime<Utc>,
1585 floating: &Zone,
1586 ) -> Option<ICalendarEntry> {
1587 let v = start.values.first()?.as_partial_date_time()?;
1588 let local = |zone: &Zone| zone.to_local(rid).and_utc().timestamp();
1589 let value = if v.hour.is_none() {
1590 PartialDateTime::from_date_timestamp(local(floating))
1591 } else if v.tz_hour.is_some() {
1592 PartialDateTime::from_utc_timestamp(rid.timestamp())
1593 } else if start.tz_id().is_some() {
1594 PartialDateTime::from_naive_timestamp(local(&self.zones.get(start.tz_id())))
1595 } else {
1596 PartialDateTime::from_naive_timestamp(local(floating))
1597 };
1598 Some(ICalendarEntry {
1599 name: ICalendarProperty::RecurrenceId,
1600 params: without(start.params.clone(), &ICalendarParameterName::Range),
1601 values: vec![ICalendarValue::PartialDateTime(Box::new(value))],
1602 })
1603 }
1604
1605 /// The latest THISANDFUTURE override at or before instance `key`, and
1606 /// its key.
1607 fn future(&self, key: i64) -> Option<(&Node, i64)> {
1608 self.comps()
1609 .filter(|c| is_range(&c.c))
1610 .filter_map(|c| Some((c, self.key(&c.c)?)))
1611 .filter(|(_, k)| *k <= key)
1612 .max_by_key(|(_, k)| *k)
1613 }
1614
1615 /// Where instance `key` starts once a THISANDFUTURE override moves it.
1616 /// The move is wall-clock time in the zone of the series, as in expand.
1617 fn moved(&self, key: i64) -> i64 {
1618 let shifted = || {
1619 let (n, k) = self.future(key)?;
1620 let dtstart = n.c.property(&ICalendarProperty::Dtstart)?;
1621 let start = self.instant(dtstart)?;
1622 // A copy without its master still has the override's zone.
1623 let series = self
1624 .master()
1625 .and_then(|m| m.c.property(&ICalendarProperty::Dtstart));
1626 let zone = self.zones.get(series.unwrap_or(dtstart).tz_id());
1627 let local = |t: i64| Some(zone.to_local(DateTime::from_timestamp(t, 0)?));
1628 let shift = local(start)? - local(k)?;
1629 Some(zone.to_utc(add_local(local(key)?, shift)).timestamp())
1630 };
1631 shifted().unwrap_or(key)
1632 }
1633
1634 /// Whether the series in `cal` has an instance at `rid`, whose key is
1635 /// `key`.
1636 fn occurs(&self, cal: &ICalendar, rid: DateTime<Utc>, key: i64, floating: &Zone) -> bool {
1637 let Some(at) = DateTime::from_timestamp(self.moved(key), 0) else {
1638 return false;
1639 };
1640 // A day either side: an all-day shift is whole days, not 24 hours.
1641 let window = at - TimeDelta::days(1)..at + TimeDelta::days(1);
1642 expand(cal, window, floating.clone())
1643 .instances
1644 .iter()
1645 .any(|i| i.recurrence_id == Some(rid))
1646 }
1647
1648 /// An override for the instance at `rid`, from the master or from the
1649 /// THISANDFUTURE override that moves it.
1650 fn single(&self, rid: DateTime<Utc>, floating: &Zone) -> Option<Node> {
1651 let master = self.master()?;
1652 let form = master.c.property(&ICalendarProperty::Dtstart)?;
1653 let id = self.recurrence_id(form, rid, floating)?;
1654 let key = self.instant(&id)?;
1655 let Some((base, _)) = self.future(key) else {
1656 return Some(self.instance(master, &id, &id));
1657 };
1658 let at = self.moved(key);
1659 let start = match form.values.first()?.as_partial_date_time()?.hour {
1660 None => ICalendarEntry {
1661 values: vec![ICalendarValue::PartialDateTime(Box::new(
1662 PartialDateTime::from_date_timestamp(at),
1663 ))],
1664 ..id.clone()
1665 },
1666 Some(_) => self.recurrence_id(form, DateTime::from_timestamp(at, 0)?, floating)?,
1667 };
1668 Some(self.instance(base, &id, &start))
1669 }
1670
1671 /// Narrows the THISANDFUTURE override at instance `rid` (key `key`) to
1672 /// that instance. A copy of it carries the range on from the next instance
1673 /// without an override of its own, found by expanding `cal`.
1674 fn narrow(&mut self, cal: &ICalendar, rid: DateTime<Utc>, key: i64, floating: &Zone) {
1675 let Some(at) = self.position(Some(key)) else {
1676 return;
1677 };
1678 let Some(range) = self.root.children[at]
1679 .c
1680 .property(&ICalendarProperty::RecurrenceId)
1681 .and_then(|e| e.parameter(&ICalendarParameterName::Range))
1682 .cloned()
1683 else {
1684 return;
1685 };
1686 let later = self
1687 .comps()
1688 .filter(|c| is_range(&c.c))
1689 .filter_map(|c| self.key(&c.c))
1690 .filter(|k| *k > key)
1691 .min();
1692 let next = self
1693 .next_free(cal, rid, key, floating)
1694 .filter(|(_, k)| later.is_none_or(|l| *k < l))
1695 .and_then(|(r, _)| self.single(r, floating));
1696 if let Some(mut n) = next {
1697 if let Some(e) =
1698 n.c.entries
1699 .iter_mut()
1700 .find(|e| e.name == ICalendarProperty::RecurrenceId)
1701 {
1702 set_param(e, ICalendarParameterName::Range, range);
1703 }
1704 self.root.children.push(n);
1705 }
1706 if let Some(e) = self.root.children[at]
1707 .c
1708 .entries
1709 .iter_mut()
1710 .find(|e| e.name == ICalendarProperty::RecurrenceId)
1711 {
1712 remove_param(e, &ICalendarParameterName::Range);
1713 }
1714 }
1715
1716 /// The first instance after `rid` that has no override, and its key.
1717 // ponytail: looks ten years ahead; a range beyond that falls back to the master.
1718 fn next_free(
1719 &self,
1720 cal: &ICalendar,
1721 rid: DateTime<Utc>,
1722 key: i64,
1723 floating: &Zone,
1724 ) -> Option<(DateTime<Utc>, i64)> {
1725 let form = self.master()?.c.property(&ICalendarProperty::Dtstart)?;
1726 let from = DateTime::from_timestamp(self.moved(key), 0)?;
1727 [1, 32, 400, 3700].into_iter().find_map(|days| {
1728 expand(cal, from..from + TimeDelta::days(days), floating.clone())
1729 .instances
1730 .iter()
1731 .filter_map(|i| i.recurrence_id)
1732 .filter(|r| *r > rid)
1733 .filter_map(|r| Some((r, self.instant(&self.recurrence_id(form, r, floating)?)?)))
1734 .filter(|(_, k)| self.find(Some(*k)).is_none())
1735 .min()
1736 })
1737 }
1738
1739 /// An override of `base` for the instance `rid` starting at `start`, so
1740 /// it can hold a status of its own. Its length becomes a DURATION.
1741 fn instance(&self, base: &Node, rid: &ICalendarEntry, start: &ICalendarEntry) -> Node {
1742 let mut n = base.clone();
1743 let begin =
1744 n.c.property(&ICalendarProperty::Dtstart)
1745 .and_then(|e| self.instant(e));
1746 let end_prop = match n.c.component_type {
1747 ICalendarComponentType::VTodo => ICalendarProperty::Due,
1748 _ => ICalendarProperty::Dtend,
1749 };
1750 let end = n.c.property(&end_prop).and_then(|e| self.instant(e));
1751 if let (Some(s), Some(e)) = (begin, end) {
1752 n.c.entries.retain(|x| x.name != end_prop);
1753 n.c.entries.push(ICalendarEntry {
1754 name: ICalendarProperty::Duration,
1755 params: Vec::new(),
1756 values: vec![ICalendarValue::Duration(ICalendarDuration::from_seconds(
1757 (e - s).max(0),
1758 ))],
1759 });
1760 }
1761 n.c.entries.retain(|e| {
1762 !matches!(
1763 e.name,
1764 ICalendarProperty::Rrule
1765 | ICalendarProperty::Rdate
1766 | ICalendarProperty::Exdate
1767 | ICalendarProperty::Exrule
1768 | ICalendarProperty::Dtstart
1769 | ICalendarProperty::RecurrenceId
1770 )
1771 });
1772 n.c.entries.push(ICalendarEntry {
1773 name: ICalendarProperty::Dtstart,
1774 params: without(start.params.clone(), &ICalendarParameterName::Range),
1775 values: start.values.clone(),
1776 });
1777 n.c.entries.push(ICalendarEntry {
1778 name: ICalendarProperty::RecurrenceId,
1779 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1780 values: rid.values.clone(),
1781 });
1782 n
1783 }
1784}
1785
1786// ---------------------------------------------------------------------------
1787// Properties and parameters
1788// ---------------------------------------------------------------------------
1789
1790fn is_scheduled(c: &ICalendarComponent) -> bool {
1791 matches!(
1792 c.component_type,
1793 ICalendarComponentType::VEvent
1794 | ICalendarComponentType::VTodo
1795 | ICalendarComponentType::VJournal
1796 )
1797}
1798
1799fn address(e: &ICalendarEntry) -> Option<&str> {
1800 e.values.first()?.as_text().map(str::trim)
1801}
1802
1803fn attendees(c: &ICalendarComponent) -> impl Iterator<Item = &ICalendarEntry> {
1804 c.properties(&ICalendarProperty::Attendee)
1805}
1806
1807/// A task's progress is the assignee's to set per instance, unless the
1808/// organizer cancelled it.
1809fn own_progress(c: &ICalendarComponent, base: &ICalendarComponent) -> bool {
1810 use ICalendarStatus::*;
1811 c.component_type == ICalendarComponentType::VTodo
1812 && matches!(c.status(), None | Some(NeedsAction | InProcess | Completed))
1813 && base.status() != Some(&Cancelled)
1814}
1815
1816fn organizer_of(c: &ICalendarComponent) -> Option<String> {
1817 c.property(&ICalendarProperty::Organizer)
1818 .and_then(address)
1819 .map(str::to_ascii_lowercase)
1820}
1821
1822/// Whether `c` repeats without end.
1823pub(crate) fn endless(c: &ICalendarComponent) -> bool {
1824 c.properties(&ICalendarProperty::Rrule).any(|e| {
1825 matches!(e.values.first(), Some(ICalendarValue::RecurrenceRule(r))
1826 if r.count.is_none() && r.until.is_none())
1827 })
1828}
1829
1830fn addresses(c: &ICalendarComponent) -> Vec<String> {
1831 let mut v: Vec<String> = attendees(c)
1832 .filter_map(address)
1833 .map(str::to_ascii_lowercase)
1834 .collect();
1835 v.sort();
1836 v.dedup();
1837 v
1838}
1839
1840fn is_range(c: &ICalendarComponent) -> bool {
1841 c.property(&ICalendarProperty::RecurrenceId)
1842 .is_some_and(|e| e.parameter(&ICalendarParameterName::Range).is_some())
1843}
1844
1845fn client_scheduled(c: &ICalendarComponent, a: &str) -> bool {
1846 attendees(c).any(|e| address(e).is_some_and(|x| x.eq_ignore_ascii_case(a)) && !server_agent(e))
1847}
1848
1849fn same_attendee<'a>(c: &'a ICalendarComponent, e: &ICalendarEntry) -> Option<&'a ICalendarEntry> {
1850 let a = address(e)?;
1851 attendees(c).find(|x| address(x).is_some_and(|b| b.eq_ignore_ascii_case(a)))
1852}
1853
1854fn own_partstat(c: &ICalendarComponent, me: Is) -> Option<String> {
1855 attendees(c).find(|e| address(e).is_some_and(me)).map(|e| {
1856 param(e, &ICalendarParameterName::Partstat)
1857 .unwrap_or("NEEDS-ACTION")
1858 .to_ascii_uppercase()
1859 })
1860}
1861
1862/// `SCHEDULE-AGENT` absent or `SERVER`. Unknown values count as `NONE`.
1863fn server_agent(e: &ICalendarEntry) -> bool {
1864 param(e, &ICalendarParameterName::ScheduleAgent)
1865 .is_none_or(|v| v.eq_ignore_ascii_case("SERVER"))
1866}
1867
1868fn sequence(c: &ICalendarComponent) -> i64 {
1869 c.property(&ICalendarProperty::Sequence)
1870 .and_then(|e| e.values.first()?.as_integer())
1871 .unwrap_or(0)
1872}
1873
1874fn param<'a>(e: &'a ICalendarEntry, name: &ICalendarParameterName) -> Option<&'a str> {
1875 e.parameter(name)?.as_text()
1876}
1877
1878fn set_param(e: &mut ICalendarEntry, name: ICalendarParameterName, value: ICalendarParameterValue) {
1879 remove_param(e, &name);
1880 e.params.push(ICalendarParameter::new(name, value));
1881}
1882
1883fn remove_param(e: &mut ICalendarEntry, name: &ICalendarParameterName) {
1884 e.params.retain(|p| &p.name != name);
1885}
1886
1887fn without(
1888 mut params: Vec<ICalendarParameter>,
1889 name: &ICalendarParameterName,
1890) -> Vec<ICalendarParameter> {
1891 params.retain(|p| &p.name != name);
1892 params
1893}
1894
1895fn set_prop(c: &mut ICalendarComponent, name: ICalendarProperty, value: ICalendarValue) {
1896 c.entries.retain(|e| e.name != name);
1897 c.entries.push(ICalendarEntry {
1898 name,
1899 params: Vec::new(),
1900 values: vec![value],
1901 });
1902}
1903
1904/// Messages carry none of the scheduling parameters (RFC 6638, 7).
1905fn strip_scheduling_params(c: &mut ICalendarComponent) {
1906 for e in c.entries.iter_mut().filter(|e| {
1907 matches!(
1908 e.name,
1909 ICalendarProperty::Attendee | ICalendarProperty::Organizer
1910 )
1911 }) {
1912 for p in [
1913 ICalendarParameterName::ScheduleAgent,
1914 ICalendarParameterName::ScheduleStatus,
1915 ICalendarParameterName::ScheduleForceSend,
1916 ] {
1917 remove_param(e, &p);
1918 }
1919 }
1920}
1921
1922fn text(s: &str) -> ICalendarParameterValue {
1923 ICalendarParameterValue::Text(s.to_string())
1924}
1925
1926fn partstat(p: ICalendarParticipationStatus) -> ICalendarParameterValue {
1927 ICalendarParameterValue::Partstat(p)
1928}
1929
1930/// Makes an object forget a principal that is about to be deleted, so that
1931/// a later principal of the same name is not taken for it. Every ORGANIZER
1932/// and ATTENDEE that `gone` names gets the address `tombstone`, and such an
1933/// ATTENDEE gets SCHEDULE-STATUS 3.7. A component the gone principal
1934/// organized is cancelled. All other lines keep their bytes. `None` when
1935/// nothing names the principal.
1936pub fn forget(text: &str, gone: Is, tombstone: &str) -> Option<String> {
1937 let mut out: Vec<String> = Vec::new();
1938 // Per open component: name, index of its BEGIN line in `out`, index of
1939 // its STATUS line, whether the gone principal organized it.
1940 let mut open: Vec<(String, usize, Option<usize>, bool)> = Vec::new();
1941 let mut changed = false;
1942 for raw in logical_lines(text) {
1943 let eol = match raw.ends_with('\n') {
1944 true if raw.ends_with("\r\n") => "\r\n",
1945 true => "\n",
1946 false => "\r\n",
1947 };
1948 let line = unfold(raw);
1949 let n = name(&line);
1950 match n.as_str() {
1951 "BEGIN" => open.push((
1952 value(&line).trim().to_ascii_uppercase(),
1953 out.len(),
1954 None,
1955 false,
1956 )),
1957 "END" => {
1958 if let Some((comp, begin, status, true)) = open.pop()
1959 && ["VEVENT", "VTODO", "VJOURNAL"].contains(&comp.as_str())
1960 {
1961 let cancelled = format!("STATUS:CANCELLED{eol}");
1962 match status {
1963 Some(i) => out[i] = cancelled,
1964 None => out.insert(begin + 1, cancelled),
1965 }
1966 }
1967 }
1968 "STATUS" => {
1969 if let Some(top) = open.last_mut() {
1970 top.2 = Some(out.len());
1971 }
1972 }
1973 "ORGANIZER" | "ATTENDEE" if gone(value(&line).trim()) => {
1974 let attendee = n == "ATTENDEE";
1975 let mut new = n.clone();
1976 for p in param_parts(&line) {
1977 if !(attendee
1978 && p.trim()
1979 .to_ascii_uppercase()
1980 .starts_with("SCHEDULE-STATUS="))
1981 {
1982 new.push(';');
1983 new.push_str(p);
1984 }
1985 }
1986 if attendee {
1987 new.push_str(";SCHEDULE-STATUS=3.7");
1988 } else if let Some(top) = open.last_mut() {
1989 top.3 = true;
1990 }
1991 new.push(':');
1992 new.push_str(tombstone);
1993 out.push(fold(&new, eol));
1994 changed = true;
1995 continue;
1996 }
1997 _ => {}
1998 }
1999 out.push(raw.to_string());
2000 }
2001 changed.then(|| out.concat())
2002}
2003