From 20bab962d47033391d214914c92c3dacc98635e0 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Mon, 14 Jul 2025 16:44:34 +0200 Subject: [PATCH] Include DTSTART/DTEND properties in iMIP CANCEL messages (fixes #1775) --- .../groupware/src/scheduling/event_cancel.rs | 73 ++++++++++++------- crates/groupware/src/scheduling/organizer.rs | 15 +--- .../itip/rfc5546_event_recurring.txt | 21 ++++-- tests/resources/itip/rfc5546_event_single.txt | 22 ++++-- tests/resources/itip/rfc5546_todo.txt | 7 +- tests/src/webdav/cal_itip.rs | 2 +- 6 files changed, 81 insertions(+), 59 deletions(-) diff --git a/crates/groupware/src/scheduling/event_cancel.rs b/crates/groupware/src/scheduling/event_cancel.rs index c082d969..16fa91c3 100644 --- a/crates/groupware/src/scheduling/event_cancel.rs +++ b/crates/groupware/src/scheduling/event_cancel.rs @@ -5,7 +5,7 @@ */ use crate::scheduling::{ - InstanceId, ItipError, ItipMessage, ItipSnapshots, ItipSummary, + InstanceId, ItipError, ItipMessage, ItipSummary, attendee::attendee_decline, itip::{itip_add_tz, itip_build_envelope}, snapshot::itip_snapshot, @@ -18,7 +18,6 @@ use calcard::{ ICalendarParticipationStatus, ICalendarProperty, ICalendarStatus, ICalendarValue, }, }; -use std::fmt::Display; pub fn itip_cancel( ical: &ICalendar, @@ -58,19 +57,20 @@ pub fn itip_cancel( } if !recipients.is_empty() && component_type != &ICalendarComponentType::VFreebusy { + let instance = itip.main_instance_or_default(); message.components.push(build_cancel_component( - component_type.clone(), - &itip, + instance.comp, sequence, dt_stamp, - cancel_guests.iter(), + &[], )); + // Add timezones + itip_add_tz(&mut message, ical); + Ok(ItipMessage { to: recipients.into_iter().collect(), - summary: ItipSummary::Cancel( - itip.main_instance_or_default().build_summary(None, &[]), - ), + summary: ItipSummary::Cancel(instance.build_summary(None, &[])), from: itip.organizer.email.email, from_organizer: true, message, @@ -121,19 +121,14 @@ pub fn itip_cancel( } } -pub(crate) fn build_cancel_component( - component_type: ICalendarComponentType, - itip: &ItipSnapshots<'_>, +pub(crate) fn build_cancel_component( + component: &ICalendarComponent, sequence: i64, dt_stamp: PartialDateTime, - cancel_guests: T, -) -> ICalendarComponent -where - T: Iterator, - I: Display, -{ + attendees: &[&str], +) -> ICalendarComponent { let mut cancel_comp = ICalendarComponent { - component_type, + component_type: component.component_type.clone(), entries: Vec::with_capacity(7), component_ids: vec![], }; @@ -143,17 +138,39 @@ where ); cancel_comp.add_dtstamp(dt_stamp); cancel_comp.add_sequence(sequence); - cancel_comp.add_uid(itip.uid); - cancel_comp.add_property( - ICalendarProperty::Organizer, - ICalendarValue::Text(itip.organizer.email.to_string()), + cancel_comp.entries.extend( + component + .entries + .iter() + .filter(|e| match e.name { + ICalendarProperty::Organizer + | ICalendarProperty::Uid + | ICalendarProperty::Summary + | ICalendarProperty::Dtstart + | ICalendarProperty::Dtend + | ICalendarProperty::Due + | ICalendarProperty::RecurrenceId + | ICalendarProperty::Created + | ICalendarProperty::LastModified + | ICalendarProperty::Description + | ICalendarProperty::Location => true, + ICalendarProperty::Attendee => { + attendees.is_empty() + || e.values + .first() + .and_then(|v| v.as_text()) + .is_some_and(|email| { + attendees.iter().any(|attendee| { + email + .strip_suffix(attendee) + .is_some_and(|v| v.ends_with(':') || v.is_empty()) + }) + }) + } + _ => false, + }) + .cloned(), ); - for email in cancel_guests { - cancel_comp.add_property( - ICalendarProperty::Attendee, - ICalendarValue::Text(email.to_string()), - ); - } cancel_comp } diff --git a/crates/groupware/src/scheduling/organizer.rs b/crates/groupware/src/scheduling/organizer.rs index cce4c941..1d2a5800 100644 --- a/crates/groupware/src/scheduling/organizer.rs +++ b/crates/groupware/src/scheduling/organizer.rs @@ -264,20 +264,7 @@ pub(crate) fn organizer_handle_update( ItipExportAs::Organizer(&ICalendarParticipationStatus::NeedsAction), ) } else { - let mut cancel_comp = build_cancel_component( - orig_component.component_type.clone(), - itip, - sequence, - dt_stamp.clone(), - emails.iter().map(|e| format!("mailto:{}", e)), - ); - - if let InstanceId::Recurrence(recurrence_id) = instance_id { - cancel_comp - .entries - .push(orig_component.entries[recurrence_id.entry_id as usize].clone()); - } - cancel_comp + build_cancel_component(orig_component, sequence, dt_stamp.clone(), &emails) }; // Add component to message diff --git a/tests/resources/itip/rfc5546_event_recurring.txt b/tests/resources/itip/rfc5546_event_recurring.txt index 5701984f..ac3e746f 100644 --- a/tests/resources/itip/rfc5546_event_recurring.txt +++ b/tests/resources/itip/rfc5546_event_recurring.txt @@ -541,11 +541,16 @@ METHOD:CANCEL PRODID:-//Stalwart Labs LLC//Stalwart Server//EN VERSION:2.0 BEGIN:VEVENT +DESCRIPTION:IETF-C&S Conference Call +LOCATION:Conference Call STATUS:CANCELLED -ATTENDEE:mailto:a@example.com +SUMMARY:IETF Calendaring Working Group Meeting +DTEND:19970601T220000Z +DTSTART:19970601T210000Z ATTENDEE:mailto:b@example.com ATTENDEE:mailto:c@example.com ATTENDEE:mailto:d@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com ORGANIZER:mailto:a@example.com UID:guid-1@example.com DTSTAMP:0 @@ -569,10 +574,10 @@ STATUS:CANCELLED SUMMARY:IETF Calendaring Working Group Meeting DTEND:19970601T220000Z DTSTART:19970601T210000Z -ATTENDEE:mailto:a@example.com ATTENDEE:mailto:b@example.com ATTENDEE:mailto:c@example.com ATTENDEE:mailto:d@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com ORGANIZER:mailto:a@example.com UID:guid-1@example.com RRULE:FREQ=MONTHLY;UNTIL=19980901T210000Z;BYMONTHDAY=1 @@ -586,10 +591,10 @@ STATUS:CANCELLED SUMMARY:IETF Calendaring Working Group Meeting DTEND:19970703T220000Z DTSTART:19970703T210000Z -ATTENDEE:mailto:a@example.com ATTENDEE:mailto:b@example.com ATTENDEE:mailto:c@example.com ATTENDEE:mailto:d@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com ORGANIZER:mailto:a@example.com RECURRENCE-ID:19970701T210000Z UID:guid-1@example.com @@ -598,10 +603,10 @@ SEQUENCE:2 END:VEVENT BEGIN:VEVENT STATUS:CANCELLED -ATTENDEE:mailto:a@example.com ATTENDEE:mailto:b@example.com ATTENDEE:mailto:c@example.com ATTENDEE:mailto:d@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com ORGANIZER:mailto:a@example.com RECURRENCE-ID:19970801T210000Z UID:guid-1@example.com @@ -616,10 +621,10 @@ STATUS:CANCELLED SUMMARY:IETF Calendaring Working Group Meeting DTEND:19970901T220000Z DTSTART:19970901T210000Z -ATTENDEE:mailto:a@example.com ATTENDEE:mailto:b@example.com ATTENDEE:mailto:c@example.com ATTENDEE:mailto:d@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com ORGANIZER:mailto:a@example.com RECURRENCE-ID;THISANDFUTURE:19970901T210000Z UID:guid-1@example.com @@ -768,8 +773,10 @@ METHOD:CANCEL PRODID:-//Stalwart Labs LLC//Stalwart Server//EN VERSION:2.0 BEGIN:VEVENT +LOCATION:The Red Room STATUS:CANCELLED -ATTENDEE:mailto:c@example.com +SUMMARY:Review Accounts +ATTENDEE;RSVP=TRUE:mailto:c@example.com ORGANIZER:mailto:a@example.com RECURRENCE-ID;THISANDFUTURE:19970901T210000Z UID:123456789@example.com @@ -811,7 +818,7 @@ BEGIN:VEVENT LOCATION:The Red Room STATUS:CANCELLED SUMMARY:Review Accounts -ATTENDEE:mailto:c@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com ORGANIZER:mailto:a@example.com RECURRENCE-ID;THISANDFUTURE:19970901T210000Z UID:123456789@example.com diff --git a/tests/resources/itip/rfc5546_event_single.txt b/tests/resources/itip/rfc5546_event_single.txt index 013b67a6..bb7bb1b4 100644 --- a/tests/resources/itip/rfc5546_event_single.txt +++ b/tests/resources/itip/rfc5546_event_single.txt @@ -658,7 +658,10 @@ PRODID:-//Stalwart Labs LLC//Stalwart Server//EN VERSION:2.0 BEGIN:VEVENT STATUS:CANCELLED -ATTENDEE:mailto:b@example.com +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:b@example.com ORGANIZER:mailto:a@example.com UID:calsrv.example.com-873970198738777@example.com DTSTAMP:0 @@ -679,7 +682,7 @@ STATUS:CANCELLED SUMMARY:Phone Conference DTEND:19970701T190000Z DTSTART:19970701T180000Z -ATTENDEE:mailto:b@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:b@example.com ORGANIZER:mailto:a@example.com UID:calsrv.example.com-873970198738777@example.com DTSTAMP:0 @@ -701,11 +704,16 @@ PRODID:-//Stalwart Labs LLC//Stalwart Server//EN VERSION:2.0 BEGIN:VEVENT STATUS:CANCELLED -ATTENDEE:mailto:a@example.com -ATTENDEE:mailto:c@example.com -ATTENDEE:mailto:conf@example.com -ATTENDEE:mailto:d@example.com -ATTENDEE:mailto:e@example.com +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@exam + ple.com";SCHEDULE-STATUS=2.0:mailto:c@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE;CUTYPE=ROOM:mailto:conf@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal:mailto:d@example.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=DECLINED; + SCHEDULE-STATUS=2.0:mailto:e@example.com ORGANIZER:mailto:a@example.com UID:calsrv.example.com-873970198738777@example.com DTSTAMP:0 diff --git a/tests/resources/itip/rfc5546_todo.txt b/tests/resources/itip/rfc5546_todo.txt index abb28439..db578403 100644 --- a/tests/resources/itip/rfc5546_todo.txt +++ b/tests/resources/itip/rfc5546_todo.txt @@ -345,7 +345,10 @@ PRODID:-//Stalwart Labs LLC//Stalwart Server//EN VERSION:2.0 BEGIN:VTODO STATUS:CANCELLED -ATTENDEE:mailto:c@example.com +SUMMARY:Create the requirements document +DUE:19970722T170000Z +DTSTART:19970701T170000Z +ATTENDEE;RSVP=TRUE:mailto:c@example.com ORGANIZER:mailto:a@example.com UID:calsrv.example.com-873970198738777-00@example.com DTSTAMP:0 @@ -366,7 +369,7 @@ STATUS:CANCELLED SUMMARY:Create the requirements document DUE:19970722T170000Z DTSTART:19970701T170000Z -ATTENDEE:mailto:c@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com ORGANIZER:mailto:a@example.com UID:calsrv.example.com-873970198738777-00@example.com DTSTAMP:0 diff --git a/tests/src/webdav/cal_itip.rs b/tests/src/webdav/cal_itip.rs index f2c94b9c..c78638c3 100644 --- a/tests/src/webdav/cal_itip.rs +++ b/tests/src/webdav/cal_itip.rs @@ -230,7 +230,7 @@ pub fn test() { assert_eq!( command.payload.trim(), last_itip_str.trim(), - "iTIP message mismatch for {} at line {}: expected {}, got {}", + "iTIP message mismatch for {} at line {}\nEXPECTED {}\n\nRECEIVED {}", command.test_name, command.line_num, command.payload,