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