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