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(t), Some(rid), Some(master)) = (
641 key,
642 rc.c.property(&ICalendarProperty::RecurrenceId),
643 next.master(),
644 ) else {
645 continue;
646 };
647 // Obj reads floating times in UTC; expand must match.
648 let occurs = DateTime::from_timestamp(t, 0).is_some_and(|at| {
649 expand(org, at..at + TimeDelta::seconds(1), Zone::Utc)
650 .instances
651 .iter()
652 .any(|i| i.recurrence_id == Some(at))
653 });
654 if !occurs {
655 continue;
656 }
657 let inst = next.instance(master, rid);
658 next.root.children.push(inst);
659 next.root.children.len() - 1
660 }
661 };
662 let target = &mut next.root.children[at];
663 if sequence(&rc.c) < sequence(&target.c) {
664 continue;
665 }
666 let Some(stat) =
667 rc.c.properties(&ICalendarProperty::Attendee)
668 .find(|e| address(e).is_some_and(replier))
669 .map(|e| {
670 e.parameter(&ICalendarParameterName::Partstat)
671 .cloned()
672 .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction))
673 })
674 else {
675 continue;
676 };
677 let codes: Vec<String> =
678 rc.c.properties(&ICalendarProperty::RequestStatus)
679 .filter_map(|e| e.values.first()?.as_text())
680 .map(|s| s.split(';').next().unwrap_or(s).trim().to_string())
681 .collect();
682 let status = match codes.is_empty() {
683 true => "2.0".to_string(),
684 false => codes.join(","),
685 };
686 for e in
687 target.c.entries.iter_mut().filter(|e| {
688 e.name == ICalendarProperty::Attendee && address(e).is_some_and(replier)
689 })
690 {
691 changed |= e.parameter(&ICalendarParameterName::Partstat) != Some(&stat);
692 set_param(e, ICalendarParameterName::Partstat, stat.clone());
693 set_param(e, ICalendarParameterName::ScheduleStatus, text(&status));
694 }
695 }
696 if changed {
697 *org = next.done();
698 }
699 changed
700}
701
702// ---------------------------------------------------------------------------
703// The organizer object
704// ---------------------------------------------------------------------------
705
706/// The client's organizer object with the attendee state the server owns:
707/// their PARTSTAT (reset on a reschedule, RFC 6638 3.2.8) and
708/// SCHEDULE-STATUS. Collects SCHEDULE-FORCE-SEND=REQUEST into `force`.
709fn guard(
710 old: Option<&ICalendar>,
711 new: &ICalendar,
712 organizer: Is,
713 force: &mut Vec<String>,
714) -> ICalendar {
715 let old = old.map(Obj::new);
716 let mut next = Obj::new(new);
717 let master = old.as_ref().and_then(Obj::master);
718 let keys: Vec<Option<i64>> = next.comps().map(|c| next.key(&c.c)).collect();
719 let moved: Vec<bool> = next
720 .comps()
721 .map(|c| match &old {
722 Some(o) => match o.find(next.key(&c.c)) {
723 Some(oc) => rescheduled(o, &oc.c, &next, &c.c),
724 // A new override: rescheduled if it moves its instance.
725 None => {
726 let start =
727 c.c.property(&ICalendarProperty::Dtstart)
728 .and_then(|e| next.instant(e));
729 next.key(&c.c).is_none() || start != next.key(&c.c)
730 }
731 },
732 None => true,
733 })
734 .collect();
735 let bumps: Vec<Option<i64>> = next
736 .comps()
737 .zip(&moved)
738 .map(|(c, moved)| {
739 let oc = old.as_ref()?.find(next.key(&c.c))?;
740 (*moved && sequence(&c.c) <= sequence(&oc.c)).then(|| sequence(&oc.c) + 1)
741 })
742 .collect();
743
744 for ((c, key), (moved, bump)) in next.comps_mut().zip(&keys).zip(moved.iter().zip(&bumps)) {
745 let base = old.as_ref().and_then(|o| o.find(*key)).or(master);
746 for e in &mut c.c.entries {
747 match e.name {
748 ICalendarProperty::Organizer => {
749 remove_param(e, &ICalendarParameterName::ScheduleForceSend);
750 }
751 ICalendarProperty::Attendee => {
752 let forced = param(e, &ICalendarParameterName::ScheduleForceSend)
753 .is_some_and(|v| v.eq_ignore_ascii_case("REQUEST"));
754 remove_param(e, &ICalendarParameterName::ScheduleForceSend);
755 let Some(a) = address(e).map(str::to_string) else {
756 continue;
757 };
758 if organizer(&a) || !server_agent(e) {
759 continue;
760 }
761 if forced {
762 force.push(a.clone());
763 }
764 let prev = base.and_then(|b| same_attendee(&b.c, e));
765 let stat = match (moved, prev) {
766 (false, Some(p)) => p
767 .parameter(&ICalendarParameterName::Partstat)
768 .cloned()
769 .unwrap_or(partstat(ICalendarParticipationStatus::NeedsAction)),
770 _ => partstat(ICalendarParticipationStatus::NeedsAction),
771 };
772 set_param(e, ICalendarParameterName::Partstat, stat);
773 match prev.and_then(|p| p.parameter(&ICalendarParameterName::ScheduleStatus)) {
774 Some(s) => set_param(e, ICalendarParameterName::ScheduleStatus, s.clone()),
775 None => remove_param(e, &ICalendarParameterName::ScheduleStatus),
776 }
777 }
778 _ => {}
779 }
780 }
781 if let Some(n) = bump {
782 set_prop(
783 &mut c.c,
784 ICalendarProperty::Sequence,
785 ICalendarValue::Integer(*n),
786 );
787 }
788 }
789 next.done()
790}
791
792/// Whether a change moves instances in time (RFC 6638, 3.2.8). Shortening a
793/// series or excluding instances does not.
794fn rescheduled(old: &Obj, oc: &ICalendarComponent, new: &Obj, nc: &ICalendarComponent) -> bool {
795 let moved = [
796 ICalendarProperty::Dtstart,
797 ICalendarProperty::Dtend,
798 ICalendarProperty::Duration,
799 ICalendarProperty::Due,
800 ICalendarProperty::Rdate,
801 ]
802 .iter()
803 .any(|p| old.times(oc, p) != new.times(nc, p));
804 let reinstated = !old
805 .times(oc, &ICalendarProperty::Exdate)
806 .is_subset(&new.times(nc, &ICalendarProperty::Exdate));
807 moved || reinstated || rules_grew(oc, nc)
808}
809
810fn rules_grew(oc: &ICalendarComponent, nc: &ICalendarComponent) -> bool {
811 let rules = |c: &ICalendarComponent| -> Vec<_> {
812 c.properties(&ICalendarProperty::Rrule)
813 .filter_map(|e| match e.values.first()? {
814 ICalendarValue::RecurrenceRule(r) => Some((**r).clone()),
815 _ => None,
816 })
817 .collect()
818 };
819 let (o, n) = (rules(oc), rules(nc));
820 if o == n {
821 return false;
822 }
823 let (o, n) = match (&o[..], &n[..]) {
824 // Dropping the rule leaves the first instance only.
825 ([_, ..], []) => return false,
826 ([o], [n]) => (o, n),
827 _ => return true,
828 };
829 let unbounded = |r: &calcard::icalendar::ICalendarRecurrenceRule| {
830 let mut r = r.clone();
831 r.until = None;
832 r.count = None;
833 r
834 };
835 if unbounded(o) != unbounded(n) {
836 return true;
837 }
838 let shorter = match (&o.until, &n.until, o.count, n.count) {
839 (Some(ou), Some(nu), _, _) => nu <= ou,
840 (_, _, Some(oc), Some(nc)) => nc <= oc,
841 (None, _, None, _) => true,
842 _ => false,
843 };
844 !shorter
845}
846
847fn cancelled(comps: Vec<Node>) -> Vec<Node> {
848 comps
849 .into_iter()
850 .map(|mut n| {
851 set_prop(
852 &mut n.c,
853 ICalendarProperty::Status,
854 ICalendarValue::Status(ICalendarStatus::Cancelled),
855 );
856 let seq = sequence(&n.c) + 1;
857 set_prop(
858 &mut n.c,
859 ICalendarProperty::Sequence,
860 ICalendarValue::Integer(seq),
861 );
862 n
863 })
864 .collect()
865}
866
867/// For comparing what an attendee would receive: without the stamps a
868/// client rewrites on every save, and optionally without participation.
869fn normalized(comps: &[Node], without_partstat: bool) -> Vec<Node> {
870 comps
871 .iter()
872 .map(|n| {
873 let mut n = n.clone();
874 n.c.entries.retain(|e| {
875 !matches!(
876 e.name,
877 ICalendarProperty::Dtstamp
878 | ICalendarProperty::LastModified
879 | ICalendarProperty::Created
880 | ICalendarProperty::Sequence
881 ) && !matches!(&e.name, ICalendarProperty::Other(x) if x.to_ascii_uppercase().starts_with("X-"))
882 });
883 // A new SENT-BY alone is no change for an attendee.
884 for e in n.c.entries.iter_mut().filter(|e| {
885 matches!(
886 e.name,
887 ICalendarProperty::Organizer | ICalendarProperty::Attendee
888 )
889 }) {
890 remove_param(e, &ICalendarParameterName::SentBy);
891 }
892 if without_partstat {
893 for e in n.c.entries.iter_mut().filter(|e| e.name == ICalendarProperty::Attendee) {
894 remove_param(e, &ICalendarParameterName::Partstat);
895 }
896 }
897 n
898 })
899 .collect()
900}
901
902/// A component of the attendee's REPLY: only its own ATTENDEE, no alarms.
903fn reply_part(c: &Node, me: Is) -> Node {
904 let mut n = Node {
905 c: c.c.clone(),
906 children: Vec::new(),
907 };
908 n.c.entries
909 .retain(|e| e.name != ICalendarProperty::Attendee || address(e).is_some_and(me));
910 strip_scheduling_params(&mut n.c);
911 n
912}
913
914/// The REPLY component for an instance the attendee excluded with EXDATE.
915fn declined_instance(master: &Node, rid: ICalendarEntry, me: Is) -> Node {
916 let mut c = ICalendarComponent {
917 component_type: master.c.component_type.clone(),
918 entries: Vec::new(),
919 component_ids: Vec::new(),
920 };
921 for p in [
922 ICalendarProperty::Uid,
923 ICalendarProperty::Sequence,
924 ICalendarProperty::Organizer,
925 ICalendarProperty::Summary,
926 ] {
927 c.entries.extend(master.c.properties(&p).cloned());
928 }
929 c.entries.push(ICalendarEntry {
930 name: ICalendarProperty::Dtstart,
931 ..rid.clone()
932 });
933 c.entries.push(rid);
934 for e in master
935 .c
936 .properties(&ICalendarProperty::Attendee)
937 .filter(|e| address(e).is_some_and(me))
938 {
939 let mut e = e.clone();
940 set_param(
941 &mut e,
942 ICalendarParameterName::Partstat,
943 partstat(ICalendarParticipationStatus::Declined),
944 );
945 c.entries.push(e);
946 }
947 strip_scheduling_params(&mut c);
948 Node {
949 c,
950 children: Vec::new(),
951 }
952}
953
954// ---------------------------------------------------------------------------
955// The object as a tree
956// ---------------------------------------------------------------------------
957
958/// A component with its sub-components, detached from the flat list calcard
959/// keeps.
960#[derive(Debug, Clone, PartialEq)]
961struct Node {
962 /// `component_ids` is empty; `children` replaces it.
963 c: ICalendarComponent,
964 children: Vec<Node>,
965}
966
967fn node(cal: &ICalendar, i: usize) -> Node {
968 let mut c = cal.components[i].clone();
969 let ids = std::mem::take(&mut c.component_ids);
970 let children = ids
971 .iter()
972 .map(|&id| id as usize)
973 .filter(|&id| id > i && id < cal.components.len())
974 .map(|id| node(cal, id))
975 .collect();
976 Node { c, children }
977}
978
979fn flatten(n: &Node, out: &mut Vec<ICalendarComponent>) -> u32 {
980 let at = out.len();
981 out.push(n.c.clone());
982 for ch in &n.children {
983 let id = flatten(ch, out);
984 out[at].component_ids.push(id);
985 }
986 at as u32
987}
988
989/// A calendar object: the VCALENDAR with its time zones and components.
990struct Obj {
991 root: Node,
992 zones: Zones,
993}
994
995impl Obj {
996 fn new(cal: &ICalendar) -> Self {
997 let root = match cal.components.is_empty() {
998 true => Node {
999 c: ICalendarComponent {
1000 component_type: ICalendarComponentType::VCalendar,
1001 entries: Vec::new(),
1002 component_ids: Vec::new(),
1003 },
1004 children: Vec::new(),
1005 },
1006 false => node(cal, 0),
1007 };
1008 Obj {
1009 root,
1010 zones: Zones::new(cal, Zone::Utc),
1011 }
1012 }
1013
1014 fn done(self) -> ICalendar {
1015 let mut components = Vec::new();
1016 flatten(&self.root, &mut components);
1017 ICalendar { components }
1018 }
1019
1020 fn comps(&self) -> impl Iterator<Item = &Node> {
1021 self.root.children.iter().filter(|n| is_scheduled(&n.c))
1022 }
1023
1024 fn comps_mut(&mut self) -> impl Iterator<Item = &mut Node> {
1025 self.root.children.iter_mut().filter(|n| is_scheduled(&n.c))
1026 }
1027
1028 fn master(&self) -> Option<&Node> {
1029 self.find(None)
1030 }
1031
1032 fn find(&self, key: Option<i64>) -> Option<&Node> {
1033 self.comps().find(|c| self.key(&c.c) == key)
1034 }
1035
1036 fn position(&self, key: Option<i64>) -> Option<usize> {
1037 self.root
1038 .children
1039 .iter()
1040 .position(|n| is_scheduled(&n.c) && self.key(&n.c) == key)
1041 }
1042
1043 /// The RECURRENCE-ID as an instant; `None` for the master.
1044 fn key(&self, c: &ICalendarComponent) -> Option<i64> {
1045 self.instant(c.property(&ICalendarProperty::RecurrenceId)?)
1046 }
1047
1048 /// The first value of a date or date-time property, in UTC seconds.
1049 /// Dates count from midnight, whatever the zone.
1050 fn instant(&self, e: &ICalendarEntry) -> Option<i64> {
1051 let v = e.values.first()?.as_partial_date_time()?;
1052 self.at(v, e.tz_id())
1053 }
1054
1055 fn at(&self, v: &PartialDateTime, tzid: Option<&str>) -> Option<i64> {
1056 let dt = v.to_date_time()?;
1057 Some(match dt.offset {
1058 _ if v.hour.is_none() => dt.date_time.and_utc().timestamp(),
1059 Some(o) => dt.date_time.and_utc().timestamp() - i64::from(o.local_minus_utc()),
1060 None => self.zones.get(tzid).to_utc(dt.date_time).timestamp(),
1061 })
1062 }
1063
1064 /// Every value of a property, comparable across encodings: instants for
1065 /// dates and date-times, the text otherwise.
1066 fn times(&self, c: &ICalendarComponent, prop: &ICalendarProperty) -> HashSet<String> {
1067 c.properties(prop)
1068 .flat_map(|e| e.values.iter().map(move |v| self.value_key(e, v)))
1069 .collect()
1070 }
1071
1072 fn value_key(&self, e: &ICalendarEntry, v: &ICalendarValue) -> String {
1073 match v.as_partial_date_time() {
1074 Some(p) => match self.at(p, e.tz_id()) {
1075 Some(t) if p.hour.is_none() => format!("{t}d"),
1076 Some(t) => t.to_string(),
1077 None => format!("{v:?}"),
1078 },
1079 None => format!("{v:?}"),
1080 }
1081 }
1082
1083 fn organizer(&self) -> Option<String> {
1084 self.comps()
1085 .find_map(|c| address(c.c.property(&ICalendarProperty::Organizer)?))
1086 .map(str::to_string)
1087 }
1088
1089 /// Whether the server replies for the attendee (RFC 6638, 3.2.2).
1090 fn organizer_schedules(&self) -> bool {
1091 self.comps()
1092 .filter_map(|c| c.c.property(&ICalendarProperty::Organizer))
1093 .all(server_agent)
1094 }
1095
1096 /// What attendee `a` gets to see (RFC 6638, 3.2.6): the master with the
1097 /// overrides it is in, and EXDATEs for those it is not in; or just the
1098 /// overrides it is in.
1099 fn view(&self, a: &str) -> Option<Vec<Node>> {
1100 let invited = |c: &ICalendarComponent| {
1101 attendees(c)
1102 .any(|e| address(e).is_some_and(|x| x.eq_ignore_ascii_case(a)) && server_agent(e))
1103 };
1104 let mut out = Vec::new();
1105 let master = self.master().filter(|m| invited(&m.c));
1106 if let Some(m) = master {
1107 let mut m = m.clone();
1108 for o in self
1109 .comps()
1110 .filter(|c| c.c.has_property(&ICalendarProperty::RecurrenceId))
1111 {
1112 if !invited(&o.c)
1113 && let Some(rid) = o.c.property(&ICalendarProperty::RecurrenceId)
1114 {
1115 m.c.entries.push(ICalendarEntry {
1116 name: ICalendarProperty::Exdate,
1117 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1118 values: rid.values.clone(),
1119 });
1120 }
1121 }
1122 out.push(m);
1123 }
1124 out.extend(
1125 self.comps()
1126 .filter(|c| c.c.has_property(&ICalendarProperty::RecurrenceId) && invited(&c.c))
1127 .cloned(),
1128 );
1129 for n in &mut out {
1130 n.children
1131 .retain(|ch| ch.c.component_type != ICalendarComponentType::VAlarm);
1132 strip_scheduling_params(&mut n.c);
1133 }
1134 (!out.is_empty()).then_some(out)
1135 }
1136
1137 /// A message: this object's VCALENDAR and time zones around `comps`.
1138 fn envelope(&self, comps: Vec<Node>, method: Method, now: DateTime<Utc>) -> ICalendar {
1139 let mut root = Node {
1140 c: self.root.c.clone(),
1141 children: self
1142 .root
1143 .children
1144 .iter()
1145 .filter(|n| n.c.component_type == ICalendarComponentType::VTimezone)
1146 .cloned()
1147 .collect(),
1148 };
1149 let method = match method {
1150 Method::Request => ICalendarMethod::Request,
1151 Method::Cancel => ICalendarMethod::Cancel,
1152 Method::Reply => ICalendarMethod::Reply,
1153 };
1154 set_prop(
1155 &mut root.c,
1156 ICalendarProperty::Method,
1157 ICalendarValue::Method(method),
1158 );
1159 let stamp = PartialDateTime::from_utc_timestamp(now.timestamp());
1160 for mut n in comps {
1161 set_prop(
1162 &mut n.c,
1163 ICalendarProperty::Dtstamp,
1164 ICalendarValue::PartialDateTime(Box::new(stamp.clone())),
1165 );
1166 root.children.push(n);
1167 }
1168 let mut components = Vec::new();
1169 flatten(&root, &mut components);
1170 ICalendar { components }
1171 }
1172
1173 /// A RECURRENCE-ID for the instance of `master` at `rid`, in the form of
1174 /// the master's DTSTART.
1175 fn recurrence_id(
1176 &self,
1177 master: &Node,
1178 rid: DateTime<Utc>,
1179 floating: &Zone,
1180 ) -> Option<ICalendarEntry> {
1181 let start = master.c.property(&ICalendarProperty::Dtstart)?;
1182 let v = start.values.first()?.as_partial_date_time()?;
1183 let local = |zone: &Zone| zone.to_local(rid).and_utc().timestamp();
1184 let value = if v.hour.is_none() {
1185 PartialDateTime::from_date_timestamp(local(floating))
1186 } else if v.tz_hour.is_some() {
1187 PartialDateTime::from_utc_timestamp(rid.timestamp())
1188 } else if start.tz_id().is_some() {
1189 PartialDateTime::from_naive_timestamp(local(&self.zones.get(start.tz_id())))
1190 } else {
1191 PartialDateTime::from_naive_timestamp(local(floating))
1192 };
1193 Some(ICalendarEntry {
1194 name: ICalendarProperty::RecurrenceId,
1195 params: start.params.clone(),
1196 values: vec![ICalendarValue::PartialDateTime(Box::new(value))],
1197 })
1198 }
1199
1200 /// An override for one instance of `master`, so it can hold a status of
1201 /// its own. Its length becomes a DURATION.
1202 fn instance(&self, master: &Node, rid: &ICalendarEntry) -> Node {
1203 let mut n = master.clone();
1204 let start =
1205 n.c.property(&ICalendarProperty::Dtstart)
1206 .and_then(|e| self.instant(e));
1207 let end_prop = match n.c.component_type {
1208 ICalendarComponentType::VTodo => ICalendarProperty::Due,
1209 _ => ICalendarProperty::Dtend,
1210 };
1211 let end = n.c.property(&end_prop).and_then(|e| self.instant(e));
1212 if let (Some(s), Some(e)) = (start, end) {
1213 n.c.entries.retain(|x| x.name != end_prop);
1214 n.c.entries.push(ICalendarEntry {
1215 name: ICalendarProperty::Duration,
1216 params: Vec::new(),
1217 values: vec![ICalendarValue::Duration(ICalendarDuration::from_seconds(
1218 (e - s).max(0),
1219 ))],
1220 });
1221 }
1222 n.c.entries.retain(|e| {
1223 !matches!(
1224 e.name,
1225 ICalendarProperty::Rrule
1226 | ICalendarProperty::Rdate
1227 | ICalendarProperty::Exdate
1228 | ICalendarProperty::Exrule
1229 | ICalendarProperty::Dtstart
1230 | ICalendarProperty::RecurrenceId
1231 )
1232 });
1233 n.c.entries.push(ICalendarEntry {
1234 name: ICalendarProperty::Dtstart,
1235 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1236 values: rid.values.clone(),
1237 });
1238 n.c.entries.push(ICalendarEntry {
1239 name: ICalendarProperty::RecurrenceId,
1240 params: without(rid.params.clone(), &ICalendarParameterName::Range),
1241 values: rid.values.clone(),
1242 });
1243 n
1244 }
1245}
1246
1247// ---------------------------------------------------------------------------
1248// Properties and parameters
1249// ---------------------------------------------------------------------------
1250
1251fn is_scheduled(c: &ICalendarComponent) -> bool {
1252 matches!(
1253 c.component_type,
1254 ICalendarComponentType::VEvent
1255 | ICalendarComponentType::VTodo
1256 | ICalendarComponentType::VJournal
1257 )
1258}
1259
1260fn address(e: &ICalendarEntry) -> Option<&str> {
1261 e.values.first()?.as_text().map(str::trim)
1262}
1263
1264fn attendees(c: &ICalendarComponent) -> impl Iterator<Item = &ICalendarEntry> {
1265 c.properties(&ICalendarProperty::Attendee)
1266}
1267
1268fn addresses(c: &ICalendarComponent) -> Vec<String> {
1269 let mut v: Vec<String> = attendees(c)
1270 .filter_map(address)
1271 .map(str::to_ascii_lowercase)
1272 .collect();
1273 v.sort();
1274 v.dedup();
1275 v
1276}
1277
1278fn same_attendee<'a>(c: &'a ICalendarComponent, e: &ICalendarEntry) -> Option<&'a ICalendarEntry> {
1279 let a = address(e)?;
1280 attendees(c).find(|x| address(x).is_some_and(|b| b.eq_ignore_ascii_case(a)))
1281}
1282
1283fn own_partstat(c: &ICalendarComponent, me: Is) -> Option<String> {
1284 attendees(c).find(|e| address(e).is_some_and(me)).map(|e| {
1285 param(e, &ICalendarParameterName::Partstat)
1286 .unwrap_or("NEEDS-ACTION")
1287 .to_ascii_uppercase()
1288 })
1289}
1290
1291/// `SCHEDULE-AGENT` absent or `SERVER`. Unknown values count as `NONE`.
1292fn server_agent(e: &ICalendarEntry) -> bool {
1293 param(e, &ICalendarParameterName::ScheduleAgent)
1294 .is_none_or(|v| v.eq_ignore_ascii_case("SERVER"))
1295}
1296
1297fn sequence(c: &ICalendarComponent) -> i64 {
1298 c.property(&ICalendarProperty::Sequence)
1299 .and_then(|e| e.values.first()?.as_integer())
1300 .unwrap_or(0)
1301}
1302
1303fn param<'a>(e: &'a ICalendarEntry, name: &ICalendarParameterName) -> Option<&'a str> {
1304 e.parameter(name)?.as_text()
1305}
1306
1307fn set_param(e: &mut ICalendarEntry, name: ICalendarParameterName, value: ICalendarParameterValue) {
1308 remove_param(e, &name);
1309 e.params.push(ICalendarParameter::new(name, value));
1310}
1311
1312fn remove_param(e: &mut ICalendarEntry, name: &ICalendarParameterName) {
1313 e.params.retain(|p| &p.name != name);
1314}
1315
1316fn without(
1317 mut params: Vec<ICalendarParameter>,
1318 name: &ICalendarParameterName,
1319) -> Vec<ICalendarParameter> {
1320 params.retain(|p| &p.name != name);
1321 params
1322}
1323
1324fn set_prop(c: &mut ICalendarComponent, name: ICalendarProperty, value: ICalendarValue) {
1325 c.entries.retain(|e| e.name != name);
1326 c.entries.push(ICalendarEntry {
1327 name,
1328 params: Vec::new(),
1329 values: vec![value],
1330 });
1331}
1332
1333/// Messages carry none of the scheduling parameters (RFC 6638, 7).
1334fn strip_scheduling_params(c: &mut ICalendarComponent) {
1335 for e in c.entries.iter_mut().filter(|e| {
1336 matches!(
1337 e.name,
1338 ICalendarProperty::Attendee | ICalendarProperty::Organizer
1339 )
1340 }) {
1341 for p in [
1342 ICalendarParameterName::ScheduleAgent,
1343 ICalendarParameterName::ScheduleStatus,
1344 ICalendarParameterName::ScheduleForceSend,
1345 ] {
1346 remove_param(e, &p);
1347 }
1348 }
1349}
1350
1351fn text(s: &str) -> ICalendarParameterValue {
1352 ICalendarParameterValue::Text(s.to_string())
1353}
1354
1355fn partstat(p: ICalendarParticipationStatus) -> ICalendarParameterValue {
1356 ICalendarParameterValue::Partstat(p)
1357}
1358
1359/// Makes an object forget a principal that is about to be deleted, so that
1360/// a later principal of the same name is not taken for it. Every ORGANIZER
1361/// and ATTENDEE that `gone` names gets the address `tombstone`, and such an
1362/// ATTENDEE gets SCHEDULE-STATUS 3.7. A component the gone principal
1363/// organized is cancelled. All other lines keep their bytes. `None` when
1364/// nothing names the principal.
1365pub fn forget(text: &str, gone: Is, tombstone: &str) -> Option<String> {
1366 let mut out: Vec<String> = Vec::new();
1367 // Per open component: name, index of its BEGIN line in `out`, index of
1368 // its STATUS line, whether the gone principal organized it.
1369 let mut open: Vec<(String, usize, Option<usize>, bool)> = Vec::new();
1370 let mut changed = false;
1371 for raw in logical_lines(text) {
1372 let eol = match raw.ends_with('\n') {
1373 true if raw.ends_with("\r\n") => "\r\n",
1374 true => "\n",
1375 false => "\r\n",
1376 };
1377 let line = unfold(raw);
1378 let n = name(&line);
1379 match n.as_str() {
1380 "BEGIN" => open.push((
1381 value(&line).trim().to_ascii_uppercase(),
1382 out.len(),
1383 None,
1384 false,
1385 )),
1386 "END" => {
1387 if let Some((comp, begin, status, true)) = open.pop()
1388 && ["VEVENT", "VTODO", "VJOURNAL"].contains(&comp.as_str())
1389 {
1390 let cancelled = format!("STATUS:CANCELLED{eol}");
1391 match status {
1392 Some(i) => out[i] = cancelled,
1393 None => out.insert(begin + 1, cancelled),
1394 }
1395 }
1396 }
1397 "STATUS" => {
1398 if let Some(top) = open.last_mut() {
1399 top.2 = Some(out.len());
1400 }
1401 }
1402 "ORGANIZER" | "ATTENDEE" if gone(value(&line).trim()) => {
1403 let attendee = n == "ATTENDEE";
1404 let mut new = n.clone();
1405 for p in param_parts(&line) {
1406 if !(attendee
1407 && p.trim()
1408 .to_ascii_uppercase()
1409 .starts_with("SCHEDULE-STATUS="))
1410 {
1411 new.push(';');
1412 new.push_str(p);
1413 }
1414 }
1415 if attendee {
1416 new.push_str(";SCHEDULE-STATUS=3.7");
1417 } else if let Some(top) = open.last_mut() {
1418 top.3 = true;
1419 }
1420 new.push(':');
1421 new.push_str(tombstone);
1422 out.push(fold(&new, eol));
1423 changed = true;
1424 continue;
1425 }
1426 _ => {}
1427 }
1428 out.push(raw.to_string());
1429 }
1430 changed.then(|| out.concat())
1431}
1432