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