diff --git a/Cargo.lock b/Cargo.lock index d06b51cc..5a9b61f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -989,9 +989,9 @@ dependencies = [ [[package]] name = "calcard" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6387223a016027ad73f3482d6e58f804d22edcc8bff1a31df877094850621a6" +checksum = "720e412adf25f179f643b0753108cb308b812f82e1d34131c06b015c806e3f3c" dependencies = [ "ahash", "chrono", @@ -2843,6 +2843,7 @@ dependencies = [ name = "groupware" version = "0.12.4" dependencies = [ + "ahash", "calcard", "chrono", "common", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 17f497d1..3d684493 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -20,7 +20,7 @@ mail-auth = { version = "0.7.1" } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } smtp-proto = { version = "0.1", features = ["rkyv"] } dns-update = { version = "0.1" } -calcard = { version = "0.1.2", features = ["rkyv"] } +calcard = { version = "0.1.3", features = ["rkyv"] } ahash = { version = "0.8.2", features = ["serde"] } parking_lot = "0.12.1" regex = "1.7.0" diff --git a/crates/dav-proto/Cargo.toml b/crates/dav-proto/Cargo.toml index 5d00048c..afe8f989 100644 --- a/crates/dav-proto/Cargo.toml +++ b/crates/dav-proto/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" trc = { path = "../trc" } hashify = "0.2.6" quick-xml = "0.37.2" -calcard = { version = "0.1.2", features = ["rkyv"] } +calcard = { version = "0.1.3", features = ["rkyv"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } hyper = "1.6.0" rkyv = { version = "0.8.10", features = ["little_endian"] } @@ -15,7 +15,7 @@ chrono = { version = "0.4.40", features = ["serde"], optional = true } compact_str = "0.9.0" [dev-dependencies] -calcard = { version = "0.1.2", features = ["serde", "rkyv"] } +calcard = { version = "0.1.3", features = ["serde", "rkyv"] } serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.138" chrono = { version = "0.4.40", features = ["serde"] } diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index f7261873..797f633c 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -14,7 +14,7 @@ directory = { path = "../directory" } http_proto = { path = "../http-proto" } jmap_proto = { path = "../jmap-proto" } trc = { path = "../trc" } -calcard = { version = "0.1.2", features = ["rkyv"] } +calcard = { version = "0.1.3", features = ["rkyv"] } hashify = { version = "0.2" } hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } percent-encoding = "2.3.1" diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index 356dcdb7..8044f415 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -12,12 +12,13 @@ jmap_proto = { path = "../jmap-proto" } trc = { path = "../trc" } directory = { path = "../directory" } dav-proto = { path = "../dav-proto" } -calcard = { version = "0.1.2", features = ["rkyv"] } +calcard = { version = "0.1.3", features = ["rkyv"] } hashify = "0.2" tokio = { version = "1.45", features = ["net", "macros"] } rkyv = { version = "0.8.10", features = ["little_endian"] } percent-encoding = "2.3.1" compact_str = "0.9.0" +ahash = { version = "0.8" } chrono = "0.4.40" [features] diff --git a/crates/groupware/src/lib.rs b/crates/groupware/src/lib.rs index 417e7514..c64e20b2 100644 --- a/crates/groupware/src/lib.rs +++ b/crates/groupware/src/lib.rs @@ -13,6 +13,7 @@ pub mod cache; pub mod calendar; pub mod contact; pub mod file; +pub mod scheduling; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DavResourceName { diff --git a/crates/groupware/src/scheduling/attendee.rs b/crates/groupware/src/scheduling/attendee.rs index 72b96db1..75af26e2 100644 --- a/crates/groupware/src/scheduling/attendee.rs +++ b/crates/groupware/src/scheduling/attendee.rs @@ -4,25 +4,28 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - common::PartialDateTime, - icalendar::{ - ICalendar, ICalendarComponent, ICalendarMethod, ICalendarParameter, - ICalendarParticipationStatus, ICalendarProperty, ICalendarValue, - }, - scheduling::{ - itip::{itip_add_tz, itip_build_envelope, itip_export_component, ItipExportAs}, - Email, InstanceId, ItipEntryValue, ItipError, ItipMessage, ItipSnapshot, ItipSnapshots, +use crate::scheduling::{ + Email, InstanceId, ItipEntryValue, ItipError, ItipMessage, ItipSnapshot, ItipSnapshots, + itip::{ + ItipExportAs, can_attendee_modify_property, itip_add_tz, itip_build_envelope, + itip_export_component, }, + organizer::organizer_request_full, }; use ahash::AHashSet; +use calcard::{ + common::PartialDateTime, + icalendar::{ + ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod, ICalendarParameter, + ICalendarParticipationStatus, ICalendarProperty, ICalendarValue, + }, +}; pub(crate) fn attendee_handle_update( - old_ical: &ICalendar, new_ical: &ICalendar, old_itip: ItipSnapshots<'_>, new_itip: ItipSnapshots<'_>, -) -> Result { +) -> Result, ItipError> { let dt_stamp = PartialDateTime::now(); let mut message = ICalendar { components: Vec::with_capacity(2), @@ -33,19 +36,22 @@ pub(crate) fn attendee_handle_update( let mut mail_from = None; let mut email_rcpt = AHashSet::new(); + let mut new_delegates = AHashSet::new(); for (instance_id, instance) in &new_itip.components { if let Some(old_instance) = old_itip.components.get(instance_id) { match (instance.local_attendee(), old_instance.local_attendee()) { - (Some(attendee), Some(old_attendee)) if attendee.email == old_attendee.email => { + (Some(local_attendee), Some(old_local_attendee)) + if local_attendee.email == old_local_attendee.email => + { // Check added fields + let mut send_update = false; for new_entry in instance.entries.difference(&old_instance.entries) { match (new_entry.name, &new_entry.value) { (ICalendarProperty::Exdate, ItipEntryValue::DateTime(date)) if instance_id == &InstanceId::Main => { if let Some((mut cancel_comp, attendee_email)) = attendee_decline( - old_ical, instance_id, &old_itip, old_instance, @@ -64,84 +70,124 @@ pub(crate) fn attendee_handle_update( mail_from = Some(&attendee_email.email); } } - ( - ICalendarProperty::Exdate - | ICalendarProperty::Summary - | ICalendarProperty::Description, - _, - ) => {} _ => { - // Adding these properties is not allowed - return Err(ItipError::ChangeNotAllowed); + // Changing these properties is not allowed + if !can_attendee_modify_property( + &instance.comp.component_type, + new_entry.name, + ) { + return Err(ItipError::CannotModifyProperty( + new_entry.name.clone(), + )); + } else { + send_update = send_update + || (instance.comp.component_type + == ICalendarComponentType::VTodo + && matches!( + new_entry.name, + ICalendarProperty::Status + | ICalendarProperty::PercentComplete + | ICalendarProperty::Completed + )); + } } } } // Send participation status update - if attendee.is_server_scheduling - && ((attendee.part_stat != old_attendee.part_stat) - || attendee.force_send.is_some()) + if local_attendee.is_server_scheduling + && ((local_attendee.part_stat != old_local_attendee.part_stat) + || local_attendee.force_send.is_some() + || send_update) { - // A new instance has been added + // Build the attendee list + let mut attendee_entry_uids = vec![local_attendee.entry_id]; + let old_delegates = old_instance + .external_attendees() + .filter(|a| a.is_delegated_from(old_local_attendee)) + .map(|a| a.email.email.as_str()) + .collect::>(); + for external_attendee in instance.external_attendees() { + if external_attendee.is_delegated_from(local_attendee) { + if external_attendee.send_invite_messages() + && !old_delegates + .contains(&external_attendee.email.email.as_str()) + { + new_delegates.insert(external_attendee.email.email.as_str()); + } + } else if external_attendee.is_delegated_to(local_attendee) { + if external_attendee.send_update_messages() { + email_rcpt.insert(external_attendee.email.email.as_str()); + } + } else { + continue; + } + attendee_entry_uids.push(external_attendee.entry_id); + } + let comp_id = message.components.len() as u16; message.components[0].component_ids.push(comp_id); message.components.push(itip_export_component( - &new_ical.components[instance.comp_id as usize], + instance.comp, new_itip.uid, &dt_stamp, instance.sequence.unwrap_or_default(), - ItipExportAs::Attendee( - instance - .attendee_delegates(attendee) - .chain([attendee]) - .map(|a| a.entry_id) - .collect(), - ), + ItipExportAs::Attendee(attendee_entry_uids), )); - mail_from = Some(&attendee.email.email); + mail_from = Some(&local_attendee.email.email); } // Check removed fields for removed_entry in old_instance.entries.difference(&instance.entries) { - if !matches!( + if !can_attendee_modify_property( + &instance.comp.component_type, removed_entry.name, - ICalendarProperty::Exdate - | ICalendarProperty::Summary - | ICalendarProperty::Description ) { // Removing these properties is not allowed - return Err(ItipError::ChangeNotAllowed); + return Err(ItipError::CannotModifyProperty( + removed_entry.name.clone(), + )); } } } _ => { // Change in local attendee email is not allowed - return Err(ItipError::ChangeNotAllowed); + return Err(ItipError::CannotModifyAddress); } } } else if let Some(local_attendee) = instance .local_attendee() .filter(|_| instance_id != &InstanceId::Main) { + let mut attendee_entry_uids = vec![local_attendee.entry_id]; + for external_attendee in instance.external_attendees() { + if external_attendee.is_delegated_from(local_attendee) { + if external_attendee.send_invite_messages() { + new_delegates.insert(external_attendee.email.email.as_str()); + } + } else if external_attendee.is_delegated_to(local_attendee) { + if external_attendee.send_update_messages() { + email_rcpt.insert(external_attendee.email.email.as_str()); + } + } else { + continue; + } + attendee_entry_uids.push(external_attendee.entry_id); + } + // A new instance has been added let comp_id = message.components.len() as u16; message.components[0].component_ids.push(comp_id); message.components.push(itip_export_component( - &new_ical.components[instance.comp_id as usize], + instance.comp, new_itip.uid, &dt_stamp, instance.sequence.unwrap_or_default(), - ItipExportAs::Attendee( - instance - .attendee_delegates(local_attendee) - .chain([local_attendee]) - .map(|a| a.entry_id) - .collect(), - ), + ItipExportAs::Attendee(attendee_entry_uids), )); mail_from = Some(&local_attendee.email.email); } else { - return Err(ItipError::ChangeNotAllowed); + return Err(ItipError::CannotModifyInstance); } } @@ -150,7 +196,6 @@ pub(crate) fn attendee_handle_update( if instance_id != &InstanceId::Main && old_instance.has_local_attendee() { // Send cancel message for removed instances if let Some((cancel_comp, attendee_email)) = attendee_decline( - old_ical, instance_id, &old_itip, old_instance, @@ -165,7 +210,7 @@ pub(crate) fn attendee_handle_update( } } else { // Removing instances is not allowed - return Err(ItipError::ChangeNotAllowed); + return Err(ItipError::CannotModifyInstance); } } } @@ -176,27 +221,39 @@ pub(crate) fn attendee_handle_update( // Add timezones if needed itip_add_tz(&mut message, new_ical); - Ok(ItipMessage { + let mut responses = vec![ItipMessage { method: ICalendarMethod::Reply, from: from.to_string(), to: email_rcpt.into_iter().map(|e| e.to_string()).collect(), changed_properties: vec![], message, - }) + }]; + + // Invite new delegates + if !new_delegates.is_empty() { + let from = from.to_string(); + let new_delegates = new_delegates.into_iter().map(|e| e.to_string()).collect(); + if let Ok(mut message) = organizer_request_full(new_ical, &new_itip, None, true) { + message.from = from; + message.to = new_delegates; + responses.push(message); + } + } + + Ok(responses) } else { Err(ItipError::NothingToSend) } } pub(crate) fn attendee_decline<'x>( - ical: &'x ICalendar, instance_id: &'x InstanceId, itip: &'x ItipSnapshots<'x>, comp: &'x ItipSnapshot<'x>, dt_stamp: &'x PartialDateTime, email_rcpt: &mut AHashSet<&'x str>, ) -> Option<(ICalendarComponent, &'x Email)> { - let component = &ical.components[comp.comp_id as usize]; + let component = comp.comp; let mut cancel_comp = ICalendarComponent { component_type: component.component_type.clone(), entries: Vec::with_capacity(5), diff --git a/crates/groupware/src/scheduling/event_cancel.rs b/crates/groupware/src/scheduling/event_cancel.rs index 187d7bd5..735face4 100644 --- a/crates/groupware/src/scheduling/event_cancel.rs +++ b/crates/groupware/src/scheduling/event_cancel.rs @@ -4,20 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ +use crate::scheduling::{ + InstanceId, ItipError, ItipMessage, ItipSnapshots, + attendee::attendee_decline, + itip::{itip_add_tz, itip_build_envelope, itip_finalize}, + snapshot::itip_snapshot, +}; +use ahash::AHashSet; +use calcard::{ common::PartialDateTime, icalendar::{ ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod, ICalendarProperty, - ICalendarValue, - }, - scheduling::{ - attendee::attendee_decline, - itip::{itip_add_tz, itip_build_envelope, itip_finalize}, - snapshot::itip_snapshot, - Email, InstanceId, ItipError, ItipMessage, ItipSnapshot, ItipSnapshots, + ICalendarStatus, ICalendarValue, }, }; -use ahash::AHashSet; +use std::fmt::Display; pub fn itip_cancel( ical: &mut ICalendar, @@ -43,7 +44,7 @@ pub fn itip_cancel( let mut increment_sequences = Vec::new(); let mut sequence = 0; for (instance_id, comp) in &itip.components { - component_type = &ical.components[comp.comp_id as usize].component_type; + component_type = &comp.comp.component_type; for attendee in &comp.attendees { if attendee.send_update_messages() { recipients.insert(attendee.email.email.clone()); @@ -64,7 +65,7 @@ pub fn itip_cancel( &itip, sequence, dt_stamp, - cancel_guests, + cancel_guests.iter(), )); let message = ItipMessage { method: ICalendarMethod::Cancel, @@ -91,7 +92,7 @@ pub fn itip_cancel( let mut email_rcpt = AHashSet::new(); for (instance_id, comp) in &itip.components { if let Some((cancel_comp, attendee_email)) = - attendee_decline(ical, instance_id, &itip, comp, &dt_stamp, &mut email_rcpt) + attendee_decline(instance_id, &itip, comp, &dt_stamp, &mut email_rcpt) { // Add cancel component let comp_id = message.components.len() as u16; @@ -123,54 +124,25 @@ pub fn itip_cancel( } } -pub(crate) fn cancel_component<'x>( - ical: &'x ICalendar, - itip: &'x ItipSnapshots<'x>, - comp: &'x ItipSnapshot<'x>, - sequence: i64, - dt_stamp: PartialDateTime, - recipients: &mut AHashSet<&'x str>, -) -> Option { - let component_type = &ical.components[comp.comp_id as usize].component_type; - let mut cancel_guests = AHashSet::new(); - let mut has_recipients = false; - - for attendee in &comp.attendees { - if attendee.send_update_messages() { - recipients.insert(attendee.email.email.as_str()); - has_recipients = true; - } - cancel_guests.insert(&attendee.email); - } - - if has_recipients && component_type != &ICalendarComponentType::VFreebusy { - Some(build_cancel_component( - component_type.clone(), - itip, - sequence, - dt_stamp, - cancel_guests, - )) - } else { - None - } -} - -fn build_cancel_component( +pub(crate) fn build_cancel_component( component_type: ICalendarComponentType, itip: &ItipSnapshots<'_>, sequence: i64, dt_stamp: PartialDateTime, - cancel_guests: AHashSet<&Email>, -) -> ICalendarComponent { + cancel_guests: T, +) -> ICalendarComponent +where + T: Iterator, + I: Display, +{ let mut cancel_comp = ICalendarComponent { component_type, - entries: Vec::with_capacity(cancel_guests.len() + 5), + entries: Vec::with_capacity(7), component_ids: vec![], }; cancel_comp.add_property( - ICalendarProperty::Method, - ICalendarValue::Method(ICalendarMethod::Cancel), + ICalendarProperty::Status, + ICalendarValue::Status(ICalendarStatus::Cancelled), ); cancel_comp.add_dtstamp(dt_stamp); cancel_comp.add_sequence(sequence); diff --git a/crates/groupware/src/scheduling/event_create.rs b/crates/groupware/src/scheduling/event_create.rs index 98a57a77..f6682fac 100644 --- a/crates/groupware/src/scheduling/event_create.rs +++ b/crates/groupware/src/scheduling/event_create.rs @@ -4,13 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - icalendar::ICalendar, - scheduling::{ - itip::itip_finalize, organizer::organizer_request_full, snapshot::itip_snapshot, ItipError, - ItipMessage, - }, +use crate::scheduling::{ + ItipError, ItipMessage, itip::itip_finalize, organizer::organizer_request_full, + snapshot::itip_snapshot, }; +use calcard::icalendar::ICalendar; pub fn itip_create( ical: &mut ICalendar, @@ -23,7 +21,7 @@ pub fn itip_create( Err(ItipError::NotOrganizer) } else { let mut sequences = Vec::new(); - organizer_request_full(ical, itip, Some(&mut sequences), true).inspect(|_| { + organizer_request_full(ical, &itip, Some(&mut sequences), true).inspect(|_| { itip_finalize(ical, &sequences); }) } diff --git a/crates/groupware/src/scheduling/event_update.rs b/crates/groupware/src/scheduling/event_update.rs index 71b8e91f..c1d8fc8a 100644 --- a/crates/groupware/src/scheduling/event_update.rs +++ b/crates/groupware/src/scheduling/event_update.rs @@ -4,19 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - icalendar::ICalendar, - scheduling::{ - attendee::attendee_handle_update, event_cancel::itip_cancel, itip::itip_finalize, - organizer::organizer_handle_update, snapshot::itip_snapshot, ItipError, ItipMessage, - }, +use crate::scheduling::{ + ItipError, ItipMessage, attendee::attendee_handle_update, event_cancel::itip_cancel, + itip::itip_finalize, organizer::organizer_handle_update, snapshot::itip_snapshot, }; +use calcard::icalendar::ICalendar; pub fn itip_update( ical: &mut ICalendar, old_ical: &mut ICalendar, account_emails: &[&str], -) -> Result { +) -> Result, ItipError> { let old_itip = itip_snapshot(old_ical, account_emails, false)?; match itip_snapshot(ical, account_emails, false) { Ok(new_itip) => { @@ -27,7 +25,7 @@ pub fn itip_update( } else if old_itip.organizer.email.is_local { organizer_handle_update(old_ical, ical, old_itip, new_itip, &mut sequences) } else { - attendee_handle_update(old_ical, ical, old_itip, new_itip) + attendee_handle_update(ical, old_itip, new_itip) } .inspect(|_| { itip_finalize(ical, &sequences); @@ -41,9 +39,9 @@ pub fn itip_update( | ItipError::OtherSchedulingAgent => { if old_itip.organizer.email.is_local { // RFC 6638 does not support replacing the organizer, so we cancel the event - itip_cancel(old_ical, account_emails) + itip_cancel(old_ical, account_emails).map(|message| vec![message]) } else { - Err(ItipError::ChangeNotAllowed) + Err(ItipError::CannotModifyAddress) } } _ => Err(err), diff --git a/crates/groupware/src/scheduling/inbound.rs b/crates/groupware/src/scheduling/inbound.rs index bf46cb8c..19d0fb8d 100644 --- a/crates/groupware/src/scheduling/inbound.rs +++ b/crates/groupware/src/scheduling/inbound.rs @@ -4,17 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::scheduling::{ + InstanceId, ItipError, ItipMessage, ItipSnapshots, organizer::organizer_request_full, +}; use ahash::AHashSet; - -use crate::{ - icalendar::{ - ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod, - ICalendarParameter, ICalendarParameterName, ICalendarProperty, ICalendarStatus, - ICalendarValue, Uri, - }, - scheduling::{ - organizer::organizer_request_full, InstanceId, ItipError, ItipMessage, ItipSnapshots, - }, +use calcard::icalendar::{ + ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod, + ICalendarParameter, ICalendarParameterName, ICalendarProperty, ICalendarStatus, ICalendarValue, + Uri, }; #[derive(Debug)] @@ -72,168 +69,10 @@ pub fn itip_process_message( } match method { ICalendarMethod::Reply => { - for (instance_id, itip_snapshot) in &itip_snapshots.components { - if let Some(snapshot) = snapshots.components.get(instance_id) { - if let (Some(attendee), Some(updated_attendee)) = ( - snapshot.attendee_by_email(&sender), - itip_snapshot.attendee_by_email(&sender), - ) { - let itip_component = &itip.components[itip_snapshot.comp_id as usize]; - let changed_part_stat = - attendee.part_stat != updated_attendee.part_stat; - let changed_rsvp = attendee.rsvp != updated_attendee.rsvp; - let changed_delegated_to = - attendee.delegated_to != updated_attendee.delegated_to; - let has_request_status = !itip_snapshot.request_status.is_empty(); - - if changed_part_stat - || changed_rsvp - || changed_delegated_to - || has_request_status - { - // Update participant status - let mut add_parameters = Vec::new(); - let mut remove_parameters = Vec::new(); - if changed_part_stat { - remove_parameters.push(ICalendarParameterName::Partstat); - if let Some(part_stat) = updated_attendee.part_stat { - add_parameters - .push(ICalendarParameter::Partstat(part_stat.clone())); - } - } - - if changed_rsvp { - remove_parameters.push(ICalendarParameterName::Rsvp); - if let Some(rsvp) = updated_attendee.rsvp { - add_parameters.push(ICalendarParameter::Rsvp(rsvp)); - } - } - - if changed_delegated_to { - remove_parameters.push(ICalendarParameterName::DelegatedTo); - if !updated_attendee.delegated_to.is_empty() { - add_parameters.push(ICalendarParameter::DelegatedTo( - updated_attendee - .delegated_to - .iter() - .map(|email| Uri::Location(email.to_string())) - .collect::>(), - )); - } - } - - if has_request_status { - remove_parameters.push(ICalendarParameterName::ScheduleStatus); - add_parameters.push(ICalendarParameter::ScheduleStatus( - itip_snapshot.request_status.join(","), - )); - } - - merge_actions.push(MergeAction::RemoveParameters { - component_id: snapshot.comp_id, - entry_id: attendee.entry_id, - parameters: remove_parameters, - }); - merge_actions.push(MergeAction::AddParameters { - component_id: snapshot.comp_id, - entry_id: attendee.entry_id, - parameters: add_parameters, - }); - - // Add unknown delegated attendees - for delegated_to in &updated_attendee.delegated_to { - if snapshot.attendee_by_email(&delegated_to.email).is_none() { - if let Some(delegated_attendee) = - itip_snapshot.attendee_by_email(&delegated_to.email) - { - merge_actions.push(MergeAction::AddEntries { - component_id: snapshot.comp_id, - entries: vec![itip_component.entries - [delegated_attendee.entry_id as usize] - .clone()], - }); - } - } - } - } - - // Add changed properties for VTODO - if ical.components[snapshot.comp_id as usize].component_type - == ICalendarComponentType::VTodo - { - let changed_entries = itip_snapshot - .entries - .symmetric_difference(&snapshot.entries) - .filter(|entry| { - matches!( - entry.name, - ICalendarProperty::PercentComplete - | ICalendarProperty::Status - | ICalendarProperty::Completed - ) - }) - .map(|entry| entry.name.clone()) - .collect::>(); - - if !changed_entries.is_empty() { - merge_actions.push(MergeAction::AddEntries { - component_id: snapshot.comp_id, - entries: itip_component - .entries - .iter() - .filter(|entry| changed_entries.contains(&entry.name)) - .cloned() - .collect(), - }); - merge_actions.push(MergeAction::RemoveEntries { - component_id: snapshot.comp_id, - entries: changed_entries, - }); - } - } - } else { - return Err(ItipError::SenderIsNotParticipant(sender.clone())); - } - } else if itip_snapshot.attendee_by_email(&sender).is_some() { - // Add component - let itip_component = &itip.components[itip_snapshot.comp_id as usize]; - let is_todo = - itip_component.component_type == ICalendarComponentType::VTodo; - merge_actions.push(MergeAction::AddComponent { - component: ICalendarComponent { - component_type: itip_component.component_type.clone(), - entries: itip_component - .entries - .iter() - .filter(|entry| { - matches!( - entry.name, - ICalendarProperty::Organizer - | ICalendarProperty::Attendee - | ICalendarProperty::Uid - | ICalendarProperty::Dtstamp - | ICalendarProperty::Sequence - | ICalendarProperty::RecurrenceId - ) || (is_todo - && matches!( - entry.name, - ICalendarProperty::PercentComplete - | ICalendarProperty::Status - | ICalendarProperty::Completed - )) - }) - .cloned() - .collect(), - component_ids: vec![], - }, - }); - } else { - return Err(ItipError::SenderIsNotParticipant(sender.clone())); - } - } + handle_reply(&snapshots, &itip_snapshots, &sender, &mut merge_actions)?; } ICalendarMethod::Refresh => { - return organizer_request_full(ical, snapshots, None, false).map(|mut message| { + return organizer_request_full(ical, &snapshots, None, false).map(|mut message| { message.to = vec![sender]; MergeResult::Message(message) }); @@ -241,7 +80,7 @@ pub fn itip_process_message( _ => return Err(ItipError::UnsupportedMethod(method.clone())), } } else { - // Handle organizer updates + // Handle organizer and attendees updates match method { ICalendarMethod::Request => { let mut is_full_update = false; @@ -270,19 +109,20 @@ pub fn itip_process_message( changed_entries.insert(ICalendarProperty::Sequence); if !changed_entries.is_empty() { - merge_actions.push(MergeAction::AddEntries { - component_id: snapshot.comp_id, - entries: itip_component - .entries - .iter() - .filter(|entry| changed_entries.contains(&entry.name)) - .cloned() - .collect(), - }); + let entries = itip_component + .entries + .iter() + .filter(|entry| changed_entries.contains(&entry.name)) + .cloned() + .collect(); merge_actions.push(MergeAction::RemoveEntries { component_id: snapshot.comp_id, entries: changed_entries, }); + merge_actions.push(MergeAction::AddEntries { + component_id: snapshot.comp_id, + entries, + }); } } else { return Err(ItipError::OutOfSequence); @@ -339,13 +179,14 @@ pub fn itip_process_message( } } ICalendarMethod::Cancel => { + let mut cancel_all_instances = false; for (instance_id, itip_snapshot) in &itip_snapshots.components { if let Some(snapshot) = snapshots.components.get(instance_id) { if itip_snapshot.sequence.unwrap_or_default() >= snapshot.sequence.unwrap_or_default() { // Cancel instance - let itip_component = &itip.components[itip_snapshot.comp_id as usize]; + let itip_component = itip_snapshot.comp; merge_actions.push(MergeAction::RemoveEntries { component_id: snapshot.comp_id, entries: [ @@ -379,11 +220,90 @@ pub fn itip_process_message( }]) .collect(), }); + cancel_all_instances = + cancel_all_instances || instance_id == &InstanceId::Main; } else { return Err(ItipError::OutOfSequence); } + } else { + let itip_component = itip_snapshot.comp; + merge_actions.push(MergeAction::AddComponent { + component: ICalendarComponent { + component_type: itip_component.component_type.clone(), + entries: itip_component + .entries + .iter() + .filter(|entry| { + !matches!( + entry.name, + ICalendarProperty::Status | ICalendarProperty::Other(_) + ) + }) + .cloned() + .chain([ICalendarEntry { + name: ICalendarProperty::Status, + params: vec![], + values: vec![ICalendarValue::Status( + ICalendarStatus::Cancelled, + )], + }]) + .collect(), + component_ids: vec![], + }, + }); } } + + if cancel_all_instances { + // Remove all instances + let itip_main = itip_snapshots.components.get(&InstanceId::Main).unwrap(); + let itip_component = itip_main.comp; + for (instance_id, snapshot) in &snapshots.components { + if !itip_snapshots.components.contains_key(instance_id) { + merge_actions.push(MergeAction::RemoveEntries { + component_id: snapshot.comp_id, + entries: [ + ICalendarProperty::Organizer, + ICalendarProperty::Attendee, + ICalendarProperty::Status, + ] + .into_iter() + .collect(), + }); + merge_actions.push(MergeAction::AddEntries { + component_id: snapshot.comp_id, + entries: itip_component + .entries + .iter() + .filter(|entry| { + matches!( + entry.name, + ICalendarProperty::Organizer + | ICalendarProperty::Attendee + ) + }) + .cloned() + .chain([ICalendarEntry { + name: ICalendarProperty::Status, + params: vec![], + values: vec![ICalendarValue::Status( + ICalendarStatus::Cancelled, + )], + }]) + .collect(), + }); + } + } + } + } + ICalendarMethod::Reply + if itip_snapshots.components.values().any(|snapshot| { + snapshot.external_attendees().any(|a| { + a.email.email == sender && a.delegated_from.iter().any(|a| a.is_local) + }) + }) => + { + handle_reply(&snapshots, &itip_snapshots, &sender, &mut merge_actions)?; } _ => return Err(ItipError::UnsupportedMethod(method.clone())), } @@ -396,9 +316,224 @@ pub fn itip_process_message( } } -pub fn itip_merge_changes(ical: &mut ICalendar, changes: Vec) { - let c = println!("Merging changes: {:?}", changes); +pub fn itip_import_message(ical: &mut ICalendar) -> Result<(), ItipError> { + let todo = "use before insert"; + let todo = "sender must not be organizer"; + let mut expect_object_type = None; + for comp in ical.components.iter_mut() { + if comp.component_type.is_scheduling_object() { + match expect_object_type { + Some(expected) if expected != &comp.component_type => { + return Err(ItipError::MultipleObjectTypes); + } + None => { + expect_object_type = Some(&comp.component_type); + } + _ => {} + } + } else if comp.component_type == ICalendarComponentType::VCalendar { + comp.entries + .retain(|entry| !matches!(entry.name, ICalendarProperty::Method)); + } + } + Ok(()) +} + +fn handle_reply( + snapshots: &ItipSnapshots<'_>, + itip_snapshots: &ItipSnapshots<'_>, + sender: &str, + merge_actions: &mut Vec, +) -> Result<(), ItipError> { + for (instance_id, itip_snapshot) in &itip_snapshots.components { + if let Some(snapshot) = snapshots.components.get(instance_id) { + if let (Some(attendee), Some(updated_attendee)) = ( + snapshot.attendee_by_email(sender), + itip_snapshot.attendee_by_email(sender), + ) { + let itip_component = itip_snapshot.comp; + let changed_part_stat = attendee.part_stat != updated_attendee.part_stat; + let changed_rsvp = attendee.rsvp != updated_attendee.rsvp; + let changed_delegated_to = attendee.delegated_to != updated_attendee.delegated_to; + let has_request_status = !itip_snapshot.request_status.is_empty(); + + if changed_part_stat || changed_rsvp || changed_delegated_to || has_request_status { + // Update participant status + let mut add_parameters = Vec::new(); + let mut remove_parameters = Vec::new(); + if changed_part_stat { + remove_parameters.push(ICalendarParameterName::Partstat); + if let Some(part_stat) = updated_attendee.part_stat { + add_parameters.push(ICalendarParameter::Partstat(part_stat.clone())); + } + } + + if changed_rsvp { + remove_parameters.push(ICalendarParameterName::Rsvp); + if let Some(rsvp) = updated_attendee.rsvp { + add_parameters.push(ICalendarParameter::Rsvp(rsvp)); + } + } + + if changed_delegated_to { + remove_parameters.push(ICalendarParameterName::DelegatedTo); + if !updated_attendee.delegated_to.is_empty() { + add_parameters.push(ICalendarParameter::DelegatedTo( + updated_attendee + .delegated_to + .iter() + .map(|email| Uri::Location(email.to_string())) + .collect::>(), + )); + } + } + + if has_request_status { + remove_parameters.push(ICalendarParameterName::ScheduleStatus); + add_parameters.push(ICalendarParameter::ScheduleStatus( + itip_snapshot.request_status.join(","), + )); + } + + merge_actions.push(MergeAction::RemoveParameters { + component_id: snapshot.comp_id, + entry_id: attendee.entry_id, + parameters: remove_parameters, + }); + merge_actions.push(MergeAction::AddParameters { + component_id: snapshot.comp_id, + entry_id: attendee.entry_id, + parameters: add_parameters, + }); + + // Add unknown delegated attendees + for delegated_to in &updated_attendee.delegated_to { + if let Some(itip_delegated) = + itip_snapshot.attendee_by_email(&delegated_to.email) + { + if let Some(delegated) = snapshot.attendee_by_email(&delegated_to.email) + { + if delegated != itip_delegated { + merge_actions.push(MergeAction::RemoveParameters { + component_id: snapshot.comp_id, + entry_id: delegated.entry_id, + parameters: vec![ + ICalendarParameterName::DelegatedTo, + ICalendarParameterName::DelegatedFrom, + ICalendarParameterName::Partstat, + ICalendarParameterName::Rsvp, + ICalendarParameterName::ScheduleStatus, + ICalendarParameterName::Role, + ], + }); + merge_actions.push(MergeAction::AddParameters { + component_id: snapshot.comp_id, + entry_id: delegated.entry_id, + parameters: itip_component.entries + [itip_delegated.entry_id as usize] + .params + .iter() + .filter(|param| { + matches!( + param, + ICalendarParameter::DelegatedTo(_) + | ICalendarParameter::DelegatedFrom(_) + | ICalendarParameter::Partstat(_) + | ICalendarParameter::Rsvp(_) + | ICalendarParameter::ScheduleStatus(_) + | ICalendarParameter::Role(_) + ) + }) + .cloned() + .collect(), + }); + } + } else { + merge_actions.push(MergeAction::AddEntries { + component_id: snapshot.comp_id, + entries: vec![ + itip_component.entries[itip_delegated.entry_id as usize] + .clone(), + ], + }); + } + } + } + } + + // Add changed properties for VTODO + if snapshot.comp.component_type == ICalendarComponentType::VTodo { + let mut remove_entries = AHashSet::new(); + let mut add_entries = Vec::new(); + + for entry in itip_component.entries.iter() { + if matches!( + entry.name, + ICalendarProperty::PercentComplete + | ICalendarProperty::Status + | ICalendarProperty::Completed + ) { + remove_entries.insert(entry.name.clone()); + add_entries.push(entry.clone()); + } + } + + if !add_entries.is_empty() { + merge_actions.push(MergeAction::RemoveEntries { + component_id: snapshot.comp_id, + entries: remove_entries, + }); + merge_actions.push(MergeAction::AddEntries { + component_id: snapshot.comp_id, + entries: add_entries, + }); + } + } + } else { + return Err(ItipError::SenderIsNotParticipant(sender.to_string())); + } + } else if itip_snapshot.attendee_by_email(sender).is_some() { + // Add component + let itip_component = itip_snapshot.comp; + let is_todo = itip_component.component_type == ICalendarComponentType::VTodo; + merge_actions.push(MergeAction::AddComponent { + component: ICalendarComponent { + component_type: itip_component.component_type.clone(), + entries: itip_component + .entries + .iter() + .filter(|entry| { + matches!( + entry.name, + ICalendarProperty::Organizer + | ICalendarProperty::Attendee + | ICalendarProperty::Uid + | ICalendarProperty::Dtstamp + | ICalendarProperty::Sequence + | ICalendarProperty::RecurrenceId + ) || (is_todo + && matches!( + entry.name, + ICalendarProperty::PercentComplete + | ICalendarProperty::Status + | ICalendarProperty::Completed + )) + }) + .cloned() + .collect(), + component_ids: vec![], + }, + }); + } else { + return Err(ItipError::SenderIsNotParticipant(sender.to_string())); + } + } + + Ok(()) +} + +pub fn itip_merge_changes(ical: &mut ICalendar, changes: Vec) { let mut remove_component_ids = Vec::new(); for action in changes { match action { diff --git a/crates/groupware/src/scheduling/itip.rs b/crates/groupware/src/scheduling/itip.rs index c752321c..47c1366a 100644 --- a/crates/groupware/src/scheduling/itip.rs +++ b/crates/groupware/src/scheduling/itip.rs @@ -4,17 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ +use calcard::{ common::PartialDateTime, icalendar::{ ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod, ICalendarParameter, ICalendarParticipationStatus, ICalendarProperty, ICalendarValue, }, - scheduling::ItipError, }; +use common::PROD_ID; pub(crate) fn itip_build_envelope(method: ICalendarMethod) -> ICalendarComponent { - let todo = "fix prodid"; ICalendarComponent { component_type: ICalendarComponentType::VCalendar, entries: vec![ @@ -26,9 +25,7 @@ pub(crate) fn itip_build_envelope(method: ICalendarMethod) -> ICalendarComponent ICalendarEntry { name: ICalendarProperty::Prodid, params: vec![], - values: vec![ICalendarValue::Text( - "-//Stalwart Labs LLC//Stalwart Server//EN".to_string(), - )], + values: vec![ICalendarValue::Text(PROD_ID.to_string())], }, ICalendarEntry { name: ICalendarProperty::Method, @@ -52,6 +49,7 @@ pub(crate) fn itip_export_component( sequence: i64, export_as: ItipExportAs<'_>, ) -> ICalendarComponent { + let is_todo = component.component_type == ICalendarComponentType::VTodo; let mut comp = ICalendarComponent { component_type: component.component_type.clone(), entries: Vec::with_capacity(component.entries.len() + 1), @@ -72,6 +70,7 @@ pub(crate) fn itip_export_component( values: entry.values.clone(), }; let mut has_partstat = false; + let mut rsvp = true; for entry in &entry.params { match entry { @@ -79,14 +78,22 @@ pub(crate) fn itip_export_component( | ICalendarParameter::ScheduleAgent(_) | ICalendarParameter::ScheduleForceSend(_) => {} _ => { - has_partstat = has_partstat - || matches!(entry, ICalendarParameter::Partstat(_)); + match entry { + ICalendarParameter::Rsvp(false) => { + rsvp = false; + } + ICalendarParameter::Partstat(_) => { + has_partstat = true; + } + _ => {} + } + new_entry.params.push(entry.clone()) } } } - if !has_partstat && entry.name == ICalendarProperty::Attendee { + if !has_partstat && rsvp && entry.name == ICalendarProperty::Attendee { new_entry .params .push(ICalendarParameter::Partstat((*partstat).clone())); @@ -125,6 +132,13 @@ pub(crate) fn itip_export_component( _ => { if matches!(export_as, ItipExportAs::Organizer(_)) || matches!(entry.name, ICalendarProperty::RecurrenceId) + || (is_todo + && matches!( + entry.name, + ICalendarProperty::Status + | ICalendarProperty::PercentComplete + | ICalendarProperty::Completed + )) { comp.entries.push(entry.clone()); } @@ -184,28 +198,6 @@ pub(crate) fn itip_finalize(ical: &mut ICalendar, scheduling_object_ids: &[u16]) } } -pub fn itip_import_message(ical: &mut ICalendar) -> Result<(), ItipError> { - let mut expect_object_type = None; - for comp in ical.components.iter_mut() { - if comp.component_type.is_scheduling_object() { - match expect_object_type { - Some(expected) if expected != &comp.component_type => { - return Err(ItipError::MultipleObjectTypes); - } - None => { - expect_object_type = Some(&comp.component_type); - } - _ => {} - } - } else if comp.component_type == ICalendarComponentType::VCalendar { - comp.entries - .retain(|entry| !matches!(entry.name, ICalendarProperty::Method)); - } - } - - Ok(()) -} - pub(crate) fn itip_add_tz(message: &mut ICalendar, ical: &ICalendar) { let mut has_timezones = false; @@ -223,3 +215,32 @@ pub(crate) fn itip_add_tz(message: &mut ICalendar, ical: &ICalendar) { message.copy_timezones(ical); } } + +#[inline] +pub(crate) fn can_attendee_modify_property( + component_type: &ICalendarComponentType, + property: &ICalendarProperty, +) -> bool { + match component_type { + ICalendarComponentType::VEvent | ICalendarComponentType::VJournal => { + matches!( + property, + ICalendarProperty::Exdate + | ICalendarProperty::Summary + | ICalendarProperty::Description + | ICalendarProperty::Comment + ) + } + ICalendarComponentType::VTodo => matches!( + property, + ICalendarProperty::Exdate + | ICalendarProperty::Summary + | ICalendarProperty::Description + | ICalendarProperty::Status + | ICalendarProperty::PercentComplete + | ICalendarProperty::Completed + | ICalendarProperty::Comment + ), + _ => false, + } +} diff --git a/crates/groupware/src/scheduling/mod.rs b/crates/groupware/src/scheduling/mod.rs index b98c5bb0..16068231 100644 --- a/crates/groupware/src/scheduling/mod.rs +++ b/crates/groupware/src/scheduling/mod.rs @@ -4,16 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ +use ahash::{AHashMap, AHashSet}; +use calcard::{ common::PartialDateTime, icalendar::{ - ICalendar, ICalendarComponentType, ICalendarDuration, ICalendarEntry, ICalendarMethod, + ICalendar, ICalendarComponent, ICalendarDuration, ICalendarEntry, ICalendarMethod, ICalendarParameter, ICalendarParticipationRole, ICalendarParticipationStatus, ICalendarPeriod, ICalendarProperty, ICalendarRecurrenceRule, ICalendarScheduleForceSendValue, ICalendarStatus, ICalendarUserTypes, ICalendarValue, Uri, }, }; -use ahash::{AHashMap, AHashSet}; use std::{fmt::Display, hash::Hash}; pub mod attendee; @@ -32,9 +32,10 @@ pub struct ItipSnapshots<'x> { pub components: AHashMap>, } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct ItipSnapshot<'x> { pub comp_id: u16, + pub comp: &'x ICalendarComponent, pub attendees: AHashSet>, pub dtstamp: Option<&'x PartialDateTime>, pub entries: AHashSet>, @@ -66,13 +67,13 @@ pub struct ItipDateTime<'x> { pub timestamp: i64, } -#[derive(Debug, PartialEq, Eq, Hash)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum InstanceId { Main, Recurrence(RecurrenceId), } -#[derive(Debug)] +#[derive(Debug, PartialOrd, Ord)] pub struct RecurrenceId { pub entry_id: u16, pub date: i64, @@ -120,7 +121,9 @@ pub enum ItipError { MultipleOrganizer, MultipleObjectTypes, MultipleObjectInstances, - ChangeNotAllowed, + CannotModifyProperty(ICalendarProperty), + CannotModifyInstance, + CannotModifyAddress, OrganizerMismatch, MissingMethod, InvalidComponentType, @@ -139,18 +142,6 @@ pub struct ItipMessage { pub message: ICalendar, } -impl ICalendarComponentType { - pub fn is_scheduling_object(&self) -> bool { - matches!( - self, - ICalendarComponentType::VEvent - | ICalendarComponentType::VTodo - | ICalendarComponentType::VJournal - | ICalendarComponentType::VFreebusy - ) - } -} - impl ItipSnapshot<'_> { pub fn has_local_attendee(&self) -> bool { self.attendees @@ -164,17 +155,8 @@ impl ItipSnapshot<'_> { .find(|attendee| attendee.email.is_local) } - pub fn attendee_delegates<'x>( - &'x self, - attendee: &'x Attendee<'x>, - ) -> impl Iterator> + 'x { - self.attendees.iter().filter(|item| { - !item.email.is_local - && attendee - .delegated_from - .iter() - .any(|d| d.email == attendee.email.email) - }) + pub fn external_attendees(&self) -> impl Iterator> + '_ { + self.attendees.iter().filter(|item| !item.email.is_local) } } @@ -198,6 +180,18 @@ impl Attendee<'_> { .part_stat .is_none_or(|part_stat| part_stat != &ICalendarParticipationStatus::Declined)) } + + pub fn is_delegated_from(&self, attendee: &Attendee<'_>) -> bool { + self.delegated_from + .iter() + .any(|d| d.email == attendee.email.email) + } + + pub fn is_delegated_to(&self, attendee: &Attendee<'_>) -> bool { + self.delegated_to + .iter() + .any(|d| d.email == attendee.email.email) + } } impl Email { @@ -304,366 +298,3 @@ impl ItipDateTime<'_> { } } } - -#[cfg(test)] -mod tests { - use crate::{ - common::PartialDateTime, - icalendar::{ICalendar, ICalendarProperty, ICalendarValue}, - scheduling::{ - event_cancel::itip_cancel, - event_create::itip_create, - event_update::itip_update, - inbound::{itip_merge_changes, itip_process_message, MergeResult}, - itip::itip_import_message, - snapshot::itip_snapshot, - ItipMessage, - }, - }; - use ahash::AHashMap; - use std::collections::hash_map::Entry; - - struct Test { - test_name: String, - command: Command, - line_num: usize, - parameters: Vec, - payload: String, - } - - #[derive(Debug, PartialEq, Eq)] - enum Command { - Put, - Get, - Delete, - Expect, - Send, - } - - #[test] - fn scheduling_tests() { - for entry in std::fs::read_dir("resources/scheduling").unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - if path.extension().is_none_or(|ext| ext != "txt") { - continue; - } - let file_name = path.file_name().unwrap().to_str().unwrap(); - let rules = std::fs::read_to_string(&path).unwrap(); - let mut last_comment = ""; - let mut last_command = ""; - let mut last_line_num = 0; - let mut payload = String::new(); - let mut commands = Vec::new(); - - for (line_num, line) in rules.lines().enumerate() { - if line.starts_with('#') { - last_comment = line.trim_start_matches('#').trim(); - } else if let Some(command) = line.strip_prefix("> ") { - last_command = command.trim(); - last_line_num = line_num; - } else if !line.is_empty() { - payload.push_str(line); - payload.push('\n'); - } else { - if last_command.is_empty() && payload.is_empty() { - continue; - } - let mut command_and_args = last_command.split_whitespace(); - let command = match command_and_args - .next() - .expect("Command should not be empty") - { - "put" => Command::Put, - "get" => Command::Get, - "expect" => Command::Expect, - "send" => Command::Send, - "delete" => Command::Delete, - _ => panic!("Unknown command: {}", last_command), - }; - - commands.push(Test { - command, - test_name: last_comment.to_string(), - line_num: last_line_num, - parameters: command_and_args.map(String::from).collect(), - payload: payload.trim().to_string(), - }); - - last_command = ""; - last_line_num = 0; - payload.clear(); - } - } - - if commands.is_empty() { - panic!("No commands found in file: {}", file_name); - } else if !last_command.is_empty() { - panic!( - "File ended with command '{}' at line {} without payload", - last_command, last_line_num - ); - } - - println!("====== Running test: {} ======", file_name); - - let mut store: AHashMap> = AHashMap::new(); - let mut dtstamp_map: AHashMap = AHashMap::new(); - let mut last_itip = None; - - for command in &commands { - if command.command != Command::Put { - println!("{} (line {})", command.test_name, command.line_num); - } - match command.command { - Command::Put => { - let account = command - .parameters - .first() - .expect("Account parameter is required"); - let name = command - .parameters - .get(1) - .expect("Name parameter is required"); - let mut ical = ICalendar::parse(&command.payload) - .expect("Failed to parse iCalendar payload"); - match store - .entry(account.to_string()) - .or_default() - .entry(name.to_string()) - { - Entry::Occupied(mut entry) => { - last_itip = - Some(itip_update(&mut ical, entry.get_mut(), &[account])); - entry.insert(ical); - } - Entry::Vacant(entry) => { - last_itip = Some(itip_create(&mut ical, &[account])); - entry.insert(ical); - } - } - } - Command::Get => { - let account = command - .parameters - .first() - .expect("Account parameter is required") - .as_str(); - let name = command - .parameters - .get(1) - .expect("Name parameter is required") - .as_str(); - let ical = ICalendar::parse(&command.payload) - .expect("Failed to parse iCalendar payload") - .to_string() - .replace("\r\n", "\n"); - store - .get(account) - .and_then(|account_store| account_store.get(name)) - .map(|stored_ical| { - let stored_ical = - normalize_ical(stored_ical.clone(), &mut dtstamp_map); - if stored_ical != ical { - panic!( - "ICalendar mismatch for {}: expected {}, got {}", - command.test_name, stored_ical, ical - ); - } - }) - .unwrap_or_else(|| { - panic!( - "ICalendar not found for account: {}, name: {}", - account, name - ); - }); - } - Command::Delete => { - let account = command - .parameters - .first() - .expect("Account parameter is required") - .as_str(); - let name = command - .parameters - .get(1) - .expect("Name parameter is required") - .as_str(); - let store = store.get_mut(account).expect("Account not found in store"); - - if let Some(mut ical) = store.remove(name) { - last_itip = Some(itip_cancel(&mut ical, &[account])); - } else { - panic!( - "ICalendar not found for account: {}, name: {}", - account, name - ); - } - } - Command::Expect => { - let last_itip_str = match last_itip - .as_ref() - .expect("No last iTIP message to compare against") - { - Ok(m) => m.to_string(&mut dtstamp_map), - Err(e) => format!("{e:?}"), - }; - assert_eq!( - command.payload.trim(), - last_itip_str.trim(), - "iTIP message mismatch for {} at line {}: expected {}, got {}", - command.test_name, - command.line_num, - command.payload, - last_itip_str - ); - } - Command::Send => { - let mut results = String::new(); - match last_itip { - Some(Ok(message)) => { - for rcpt in &message.to { - let result = match itip_snapshot( - &message.message, - &[rcpt.as_str()], - false, - ) { - Ok(itip_snapshots) => { - match store - .entry(rcpt.to_string()) - .or_default() - .entry(itip_snapshots.uid.to_string()) - { - Entry::Occupied(mut entry) => { - let ical = entry.get_mut(); - let snapshots = itip_snapshot( - ical, - &[rcpt.as_str()], - false, - ) - .expect("Failed to create iTIP snapshot"); - - match itip_process_message( - ical, - snapshots, - &message.message, - itip_snapshots, - message.from.clone(), - ) { - Ok(result) => match result { - MergeResult::Actions(changes) => { - itip_merge_changes(ical, changes); - Ok(None) - } - MergeResult::Message(message) => { - Ok(Some(message)) - } - MergeResult::None => Ok(None), - }, - Err(err) => Err(err), - } - } - Entry::Vacant(entry) => { - let mut message = message.message.clone(); - itip_import_message(&mut message) - .expect("Failed to import iTIP message"); - entry.insert(message); - Ok(None) - } - } - } - Err(err) => Err(err), - }; - - match result { - Ok(Some(itip_message)) => { - results.push_str( - &itip_message.to_string(&mut dtstamp_map), - ); - } - Ok(None) => {} - Err(e) => { - results.push_str(&format!("{e:?}")); - } - } - } - - assert_eq!( - results.trim(), command.payload.trim(), - "iTIP send result mismatch for {} at line {}: expected {}, got {}", - command.test_name, command.line_num, command.payload, results - ); - } - Some(Err(e)) => { - panic!( - "Failed to create iTIP message for {} at line {}: {:?}", - command.test_name, command.line_num, e - ); - } - None => { - panic!( - "No iTIP message to send for {} at line {}", - command.test_name, command.line_num - ); - } - } - last_itip = None; - } - } - } - } - } - - impl ItipMessage { - pub fn to_string(&self, map: &mut AHashMap) -> String { - use std::fmt::Write; - let mut f = String::new(); - let mut to = self.to.iter().map(|t| t.as_str()).collect::>(); - to.sort_unstable(); - let mut changed = self - .changed_properties - .iter() - .map(|p| p.as_str()) - .collect::>(); - changed.sort_unstable(); - writeln!(&mut f, "from: {}", self.from).unwrap(); - writeln!(&mut f, "to: {}", to.join(", ")).unwrap(); - writeln!(&mut f, "changes: {}", changed.join(", ")).unwrap(); - write!(&mut f, "{}", normalize_ical(self.message.clone(), map)).unwrap(); - f - } - } - - fn normalize_ical(mut ical: ICalendar, map: &mut AHashMap) -> String { - let mut comps = ical - .components - .iter() - .enumerate() - .filter(|(comp_id, _)| { - ical.components[0] - .component_ids - .contains(&(*comp_id as u16)) - }) - .collect::>(); - comps.sort_unstable_by_key(|(_, comp)| *comp); - ical.components[0].component_ids = - comps.iter().map(|(comp_id, _)| *comp_id as u16).collect(); - - for comp in &mut ical.components { - for entry in &mut comp.entries { - if let (ICalendarProperty::Dtstamp, Some(ICalendarValue::PartialDateTime(dt))) = - (&entry.name, entry.values.first()) - { - if let Some(index) = map.get(dt) { - entry.values = vec![ICalendarValue::Integer(*index as i64)]; - } else { - let index = map.len(); - map.insert(dt.as_ref().clone(), index); - entry.values = vec![ICalendarValue::Integer(index as i64)]; - } - } - } - } - ical.to_string().replace("\r\n", "\n") - } -} diff --git a/crates/groupware/src/scheduling/organizer.rs b/crates/groupware/src/scheduling/organizer.rs index 2a110bd9..4af6aaf0 100644 --- a/crates/groupware/src/scheduling/organizer.rs +++ b/crates/groupware/src/scheduling/organizer.rs @@ -4,19 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ +use crate::scheduling::{ + InstanceId, ItipError, ItipMessage, ItipSnapshots, + event_cancel::build_cancel_component, + itip::{ItipExportAs, itip_add_tz, itip_build_envelope, itip_export_component}, +}; +use ahash::{AHashMap, AHashSet}; +use calcard::{ common::PartialDateTime, icalendar::{ ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod, - ICalendarParticipationStatus, ICalendarProperty, - }, - scheduling::{ - event_cancel::cancel_component, - itip::{itip_build_envelope, itip_export_component, ItipExportAs}, - InstanceId, ItipError, ItipMessage, ItipSnapshots, + ICalendarParticipationStatus, ICalendarProperty, ICalendarStatus, }, }; -use ahash::AHashSet; +use std::collections::hash_map::Entry; pub(crate) fn organizer_handle_update( old_ical: &ICalendar, @@ -24,12 +25,10 @@ pub(crate) fn organizer_handle_update( old_itip: ItipSnapshots<'_>, new_itip: ItipSnapshots<'_>, increment_sequences: &mut Vec, -) -> Result { - let mut added_instances = Vec::new(); - let mut deleted_instances = Vec::new(); - let mut updated_instances = Vec::new(); - let mut is_full_update = false; +) -> Result, ItipError> { + let mut changed_instances: Vec<(&InstanceId, &str, &ICalendarMethod)> = Vec::new(); let mut increment_sequence = false; + let mut changed_properties = AHashSet::new(); for (instance_id, instance) in &new_itip.components { if let Some(old_instance) = old_itip.components.get(instance_id) { @@ -37,13 +36,10 @@ pub(crate) fn organizer_handle_update( let changed_attendees = instance.attendees != old_instance.attendees; if changed_entries || changed_attendees { - increment_sequence = increment_sequence - || changed_attendees - || instance - .entries - .symmetric_difference(&old_instance.entries) - .any(|entry| { - matches!( + if changed_entries { + for entry in instance.entries.symmetric_difference(&old_instance.entries) { + increment_sequence = increment_sequence + || matches!( entry.name, ICalendarProperty::Dtstart | ICalendarProperty::Dtend @@ -54,166 +50,254 @@ pub(crate) fn organizer_handle_update( | ICalendarProperty::Exdate | ICalendarProperty::Status | ICalendarProperty::Location - ) - }); - - if instance_id == &InstanceId::Main { - is_full_update = true; - break; - } else { - updated_instances.push((instance_id, instance)); + ); + changed_properties.insert(entry.name); + } } + + if changed_attendees { + changed_instances.extend( + old_instance + .external_attendees() + .filter(|attendee| attendee.send_update_messages()) + .map(|attendee| attendee.email.email.as_str()) + .collect::>() + .difference( + &instance + .external_attendees() + .map(|attendee| attendee.email.email.as_str()) + .collect::>(), + ) + .map(|attendee| (instance_id, *attendee, &ICalendarMethod::Cancel)), + ); + changed_properties.insert(&ICalendarProperty::Attendee); + increment_sequence = true; + } + + changed_instances.extend(instance.attendees.iter().filter_map(|attendee| { + if attendee.send_update_messages() { + Some(( + instance_id, + attendee.email.email.as_str(), + &ICalendarMethod::Request, + )) + } else { + None + } + })); } } else if instance_id != &InstanceId::Main { - added_instances.push((instance_id, instance)); + changed_properties.insert(&ICalendarProperty::Exdate); + let method = if matches!(instance.comp.status(), Some(ICalendarStatus::Cancelled)) { + &ICalendarMethod::Cancel + } else { + &ICalendarMethod::Add + }; + + changed_instances.extend(instance.attendees.iter().filter_map(|attendee| { + if attendee.send_invite_messages() { + Some((instance_id, attendee.email.email.as_str(), method)) + } else { + None + } + })); + increment_sequence = true; } else { - return Err(ItipError::ChangeNotAllowed); + return Err(ItipError::CannotModifyInstance); } } - if !is_full_update { - for (instance_id, old_instance) in &old_itip.components { - if !new_itip.components.contains_key(instance_id) { - if instance_id != &InstanceId::Main { - deleted_instances.push((instance_id, old_instance)); - increment_sequence = true; - } else { - return Err(ItipError::ChangeNotAllowed); + for (instance_id, old_instance) in &old_itip.components { + if !new_itip.components.contains_key(instance_id) { + if instance_id != &InstanceId::Main { + changed_instances.extend(old_instance.attendees.iter().filter_map(|attendee| { + if attendee.send_update_messages() { + Some(( + instance_id, + attendee.email.email.as_str(), + &ICalendarMethod::Cancel, + )) + } else { + None + } + })); + changed_properties.insert(&ICalendarProperty::Exdate); + increment_sequence = true; + } else { + return Err(ItipError::CannotModifyInstance); + } + } + } + + if changed_instances.is_empty() { + return Err(ItipError::NothingToSend); + } + + // Remove partial notifications for attendees that receive a full update for the main instance + // or, that will receive both add and remove messages + let mut send_full_update: AHashSet<&str> = AHashSet::new(); + let mut send_partial_update: AHashMap<&str, AHashMap<&ICalendarMethod, Vec<&InstanceId>>> = + AHashMap::new(); + for (instance_id, email, method) in &changed_instances { + if *instance_id == &InstanceId::Main && *method == &ICalendarMethod::Request { + send_full_update.insert(*email); + send_partial_update.remove(email); + } else if !send_full_update.contains(email) { + match send_partial_update.entry(email) { + Entry::Occupied(mut entry) => { + let entry = entry.get_mut(); + let is_empty = entry.is_empty(); + match entry.entry(method) { + Entry::Occupied(mut method_entry) => { + method_entry.get_mut().push(*instance_id); + } + Entry::Vacant(method_entry) if is_empty => { + method_entry.insert(vec![*instance_id]); + } + _ => { + // Switch to full update for this participant + send_full_update.insert(*email); + send_partial_update.remove(email); + } + } + } + Entry::Vacant(entry) => { + entry.insert(AHashMap::from_iter([(*method, vec![*instance_id])])); } } } } - let (method, instances) = match ( - !added_instances.is_empty(), - !deleted_instances.is_empty(), - !updated_instances.is_empty(), - is_full_update, - ) { - (true, false, false, false) => { - // Send ADD message - (ICalendarMethod::Add, added_instances) - } - (false, true, false, false) => { - // Send CANCEL message - (ICalendarMethod::Cancel, deleted_instances) - } - (false, false, true, false) => { - // Send REQUEST message for changed instances - (ICalendarMethod::Request, updated_instances) - } - (_, _, _, true) => { - // Send full REQUEST message - return organizer_request_full( - new_ical, - new_itip, - increment_sequence.then_some(increment_sequences), - false, - ); - } - _ => return Err(ItipError::NothingToSend), - }; - - // Prepare iTIP message - let (ical, itip, is_cancel) = if matches!(method, ICalendarMethod::Cancel) { - (old_ical, &old_itip, true) - } else { - (new_ical, &new_itip, false) - }; - let dt_stamp = PartialDateTime::now(); - let mut message = ICalendar { - components: vec![ICalendarComponent::default(); ical.components.len()], - }; - message.components[0] = itip_build_envelope(method); - - let mut recipients = AHashSet::new(); - let mut copy_components = AHashSet::new(); - let mut increment_sequences = Vec::new(); - - for (instance_id, comp) in instances { - // Prepare component for iTIP - let sequence = if increment_sequence { - comp.sequence.unwrap_or_default() + 1 - } else { - comp.sequence.unwrap_or_default() - }; - let orig_component = &ical.components[comp.comp_id as usize]; - let component = if !is_cancel { - // Add attendees - for attendee in &comp.attendees { - if attendee.send_update_messages() { - recipients.insert(attendee.email.email.as_str()); - } - } - - if increment_sequence { - increment_sequences.push(comp.comp_id); - } - - // Export component with updated sequence and participation status - itip_export_component( - orig_component, - itip.uid, - &dt_stamp, - sequence, - ItipExportAs::Organizer(&ICalendarParticipationStatus::NeedsAction), - ) - } else if let Some(mut cancel_comp) = cancel_component( - ical, - itip, - comp, - sequence, - dt_stamp.clone(), - &mut recipients, + // Prepare full updates + let mut messages = Vec::new(); + let changed_properties = changed_properties.into_iter().cloned().collect::>(); + if !send_full_update.is_empty() { + match organizer_request_full( + new_ical, + &new_itip, + increment_sequence.then_some(increment_sequences), + false, ) { - if let InstanceId::Recurrence(recurrence_id) = instance_id { - cancel_comp - .entries - .push(orig_component.entries[recurrence_id.entry_id as usize].clone()); + Ok(mut message) => { + message.changed_properties = changed_properties.clone(); + messages.push(message); + } + Err(err) => { + if send_partial_update.is_empty() { + return Err(err); + } } - cancel_comp - } else { - continue; // Skip if no component was created - }; - - // Add component to message - message.components[comp.comp_id as usize] = component; - message.components[0].component_ids.push(comp.comp_id); - } - - // Copy timezones and alarms - for (comp_id, comp) in ical.components.iter().enumerate() { - if !is_cancel && matches!(comp.component_type, ICalendarComponentType::VTimezone) { - copy_components.extend(comp.component_ids.iter().copied()); - message.components[0].component_ids.push(comp_id as u16); - } else if !copy_components.contains(&(comp_id as u16)) { - continue; } - message.components[comp_id] = comp.clone(); } - message.components[0].component_ids.sort_unstable(); - if !recipients.is_empty() { - let message = ItipMessage { - method: ICalendarMethod::Request, - from: itip.organizer.email.email.clone(), - to: recipients.into_iter().map(|e| e.to_string()).collect(), - changed_properties: vec![], - message, - }; + // Prepare partial updates + if !send_partial_update.is_empty() { + // Group updates by email and method + let mut updates: AHashMap<(&ICalendarMethod, Vec<&InstanceId>), Vec<&str>> = + AHashMap::new(); + for (email, partial_updates) in send_partial_update { + for (method, mut instances) in partial_updates { + instances.sort_unstable(); + instances.dedup(); + updates.entry((method, instances)).or_default().push(email); + } + } - Ok(message) - } else { - Err(ItipError::NothingToSend) + let dt_stamp = PartialDateTime::now(); + for ((method, instances), emails) in updates { + let (mut ical, mut itip, is_cancel) = if matches!(method, ICalendarMethod::Cancel) { + (old_ical, &old_itip, true) + } else { + (new_ical, &new_itip, false) + }; + + // Prepare iTIP message + let mut message = ICalendar { + components: Vec::with_capacity(instances.len() + 1), + }; + message.components.push(itip_build_envelope(method.clone())); + + let mut increment_sequences = Vec::new(); + + for instance_id in instances { + let comp = match itip.components.get(instance_id) { + Some(comp) => comp, + None => { + // New component added with CANCELLED status + ical = new_ical; + itip = &new_itip; + itip.components.get(instance_id).unwrap() + } + }; + // Prepare component for iTIP + let sequence = if increment_sequence { + comp.sequence.unwrap_or_default() + 1 + } else { + comp.sequence.unwrap_or_default() + }; + let orig_component = comp.comp; + let component = if !is_cancel { + if increment_sequence { + increment_sequences.push(comp.comp_id); + } + + // Export component with updated sequence and participation status + itip_export_component( + orig_component, + itip.uid, + &dt_stamp, + sequence, + 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 + }; + + // Add component to message + let comp_id = message.components.len() as u16; + message.components.push(component); + message.components[0].component_ids.push(comp_id); + } + + // Add timezones + itip_add_tz(&mut message, ical); + + messages.push(ItipMessage { + method: method.clone(), + from: itip.organizer.email.email.clone(), + to: emails.into_iter().map(|e| e.to_string()).collect(), + changed_properties: if method == &ICalendarMethod::Request { + changed_properties.clone() + } else { + vec![] + }, + message, + }); + } } + + Ok(messages) } pub(crate) fn organizer_request_full( ical: &ICalendar, - itip: ItipSnapshots<'_>, + itip: &ItipSnapshots<'_>, mut increment_sequence: Option<&mut Vec>, - include_alarms: bool, + is_first_request: bool, ) -> Result { // Prepare iTIP message let dt_stamp = PartialDateTime::now(); @@ -225,7 +309,7 @@ pub(crate) fn organizer_request_full( let mut recipients = AHashSet::new(); let mut copy_components = AHashSet::new(); - for comp in itip.components.into_values() { + for comp in itip.components.values() { // Prepare component for iTIP let sequence = if let Some(increment_sequence) = &mut increment_sequence { increment_sequence.push(comp.comp_id); @@ -243,7 +327,7 @@ pub(crate) fn organizer_request_full( ); // Add VALARM sub-components - if include_alarms { + if is_first_request { for sub_comp_id in &orig_component.component_ids { if matches!( ical.components[*sub_comp_id as usize].component_type, @@ -260,9 +344,11 @@ pub(crate) fn organizer_request_full( message.components[0].component_ids.push(comp.comp_id); // Add attendees - for attendee in comp.attendees { - if attendee.send_invite_messages() { - recipients.insert(attendee.email.email); + for attendee in &comp.attendees { + if (is_first_request && attendee.send_invite_messages()) + || (!is_first_request && attendee.send_update_messages()) + { + recipients.insert(&attendee.email.email); } } } @@ -282,8 +368,8 @@ pub(crate) fn organizer_request_full( if !recipients.is_empty() { Ok(ItipMessage { method: ICalendarMethod::Request, - from: itip.organizer.email.email, - to: recipients.into_iter().collect(), + from: itip.organizer.email.email.clone(), + to: recipients.into_iter().map(|e| e.to_string()).collect(), changed_properties: vec![], message, }) diff --git a/crates/groupware/src/scheduling/snapshot.rs b/crates/groupware/src/scheduling/snapshot.rs index 89123a53..4296e284 100644 --- a/crates/groupware/src/scheduling/snapshot.rs +++ b/crates/groupware/src/scheduling/snapshot.rs @@ -4,19 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - icalendar::{ - ICalendar, ICalendarParameter, ICalendarProperty, ICalendarScheduleAgentValue, - ICalendarValue, Uri, - }, - scheduling::{ - Attendee, Email, InstanceId, ItipDateTime, ItipEntry, ItipEntryValue, ItipError, - ItipSnapshot, ItipSnapshots, Organizer, RecurrenceId, - }, +use crate::scheduling::{ + Attendee, Email, InstanceId, ItipDateTime, ItipEntry, ItipEntryValue, ItipError, ItipSnapshot, + ItipSnapshots, Organizer, RecurrenceId, }; use ahash::AHashMap; +use calcard::icalendar::{ + ICalendar, ICalendarParameter, ICalendarProperty, ICalendarScheduleAgentValue, ICalendarValue, + Uri, +}; -pub(crate) fn itip_snapshot<'x, 'y>( +pub fn itip_snapshot<'x, 'y>( ical: &'x ICalendar, account_emails: &'y [&'y str], force_add_client_scheduling: bool, @@ -26,6 +24,7 @@ pub(crate) fn itip_snapshot<'x, 'y>( let mut components = AHashMap::new(); let mut expect_object_type = None; let mut has_local_emails = false; + let mut has_scheduling_info = false; let mut tz_resolver = None; for (comp_id, comp) in ical.components.iter().enumerate() { @@ -44,10 +43,16 @@ pub(crate) fn itip_snapshot<'x, 'y>( } _ => {} } + has_scheduling_info = true; let mut sched_comp = ItipSnapshot { comp_id: comp_id as u16, - ..Default::default() + comp, + attendees: Default::default(), + dtstamp: Default::default(), + entries: Default::default(), + sequence: Default::default(), + request_status: Default::default(), }; let mut instance_id = InstanceId::Main; @@ -297,7 +302,7 @@ pub(crate) fn itip_snapshot<'x, 'y>( uid: uid.ok_or(ItipError::MissingUid)?, components, }) - } else if !has_local_emails { + } else if has_scheduling_info { Err(ItipError::NotOrganizerNorAttendee) } else { Err(ItipError::NoSchedulingInfo) diff --git a/crates/migration/Cargo.toml b/crates/migration/Cargo.toml index f827ccbc..090d6359 100644 --- a/crates/migration/Cargo.toml +++ b/crates/migration/Cargo.toml @@ -19,7 +19,7 @@ dav-proto = { path = "../dav-proto" } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-auth = { version = "0.7.1", features = ["rkyv"] } sieve-rs = { version = "0.7", features = ["rkyv"] } -calcard = { version = "0.1.2", features = ["rkyv"] } +calcard = { version = "0.1.3", features = ["rkyv"] } tokio = { version = "1.45", features = ["net", "macros"] } serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index 6cc75cbb..4efa274a 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -18,7 +18,7 @@ smtp-proto = { version = "0.1.6", features = ["rkyv", "serde"] } tokio = { version = "1.45", features = ["rt"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = { version = "0.4" } -calcard = { version = "0.1.2", features = ["rkyv"] } +calcard = { version = "0.1.3", features = ["rkyv"] } chrono = { version = "0.4", features = ["unstable-locales"] } serde = { version = "1.0", features = ["derive"]} serde_json = "1.0" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 66e6e778..38c45036 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -29,7 +29,7 @@ imap = { path = "../crates/imap", features = ["test_mode"] } imap_proto = { path = "../crates/imap-proto" } dav = { path = "../crates/dav", features = ["test_mode"] } dav-proto = { path = "../crates/dav-proto", features = ["test_mode"] } -calcard = { version = "0.1.2", features = ["rkyv"] } +calcard = { version = "0.1.3", features = ["rkyv"] } groupware = { path = "../crates/groupware", features = ["test_mode"] } http = { path = "../crates/http", features = ["test_mode", "enterprise"] } http_proto = { path = "../crates/http-proto" } diff --git a/tests/resources/itip/google_calendar.txt b/tests/resources/itip/google_calendar.txt new file mode 100644 index 00000000..b29385ae --- /dev/null +++ b/tests/resources/itip/google_calendar.txt @@ -0,0 +1,265 @@ +# Send initial request +> put a@gmail.com 1qnf39p0m9h6n1cm9qa9d8mocv@google.com +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:America/Los_Angeles +X-LIC-LOCATION:America/Los_Angeles +BEGIN:DAYLIGHT +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +TZNAME:PDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +TZNAME:PST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=America/Los_Angeles:20250619T060000 +DTEND;TZID=America/Los_Angeles:20250619T064500 +RRULE:FREQ=WEEKLY;WKST=SU;COUNT=2;BYDAY=TH +DTSTAMP:20250616T182403Z +ORGANIZER;CN=John Doe:mailto:a@gmail.com +UID:1qnf39p0m9h6n1cm9qa9d8mocv@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;CN=b@gmail.com;X-NUM-GUESTS=0:mailto:b@gmail.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE + ;CN=John Doe;X-NUM-GUESTS=0:mailto:a@gmail.com +X-MICROSOFT-CDO-OWNERAPPTID:299828133 +CREATED:20250616T182358Z +LAST-MODIFIED:20250616T182358Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Meet me maybe +DESCRIPTION:This is the event description +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:This is an event reminder +TRIGGER:-P0DT0H10M0S +END:VALARM +END:VEVENT +END:VCALENDAR + +> expect +from: a@gmail.com +to: b@gmail.com +changes: +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +DESCRIPTION:This is the event description +LOCATION: +STATUS:CONFIRMED +SUMMARY:Meet me maybe +DTEND;TZID=America/Los_Angeles:20250619T064500 +DTSTART;TZID=America/Los_Angeles:20250619T060000 +TRANSP:OPAQUE +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;CN=b@gmail.com;X-NUM-GUESTS=0:mailto:b@gmail.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE; + CN="John Doe";X-NUM-GUESTS=0:mailto:a@gmail.com +ORGANIZER;CN="John Doe":mailto:a@gmail.com +UID:1qnf39p0m9h6n1cm9qa9d8mocv@google.com +RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=TH;WKST=SU +CREATED:20250616T182358Z +DTSTAMP:0 +LAST-MODIFIED:20250616T182358Z +SEQUENCE:1 +X-MICROSOFT-CDO-OWNERAPPTID:299828133 +BEGIN:VALARM +DESCRIPTION:This is an event reminder +ACTION:DISPLAY +TRIGGER:-PT10M +END:VALARM +END:VEVENT +BEGIN:VTIMEZONE +TZID:America/Los_Angeles +X-LIC-LOCATION:America/Los_Angeles +BEGIN:DAYLIGHT +DTSTART:19700308T020000 +TZNAME:PDT +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:19701101T020000 +TZNAME:PST +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR + +# Send iTIP +> send + +# Update the event, expect no changes +> put a@gmail.com 1qnf39p0m9h6n1cm9qa9d8mocv@google.com +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:America/Los_Angeles +X-LIC-LOCATION:America/Los_Angeles +BEGIN:DAYLIGHT +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +TZNAME:PDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +TZNAME:PST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=America/Los_Angeles:20250619T060000 +DTEND;TZID=America/Los_Angeles:20250619T064500 +RRULE:FREQ=WEEKLY;WKST=SU;COUNT=2;BYDAY=TH +DTSTAMP:20250616T182416Z +ORGANIZER;CN=John Doe:mailto:a@gmail.com +UID:1qnf39p0m9h6n1cm9qa9d8mocv@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;CN=b@gmail.com;X-NUM-GUESTS=0:mailto:b@gmail.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE + ;CN=John Doe;X-NUM-GUESTS=0:mailto:a@gmail.com +X-MICROSOFT-CDO-OWNERAPPTID:299828133 +CREATED:20250616T182358Z +LAST-MODIFIED:20250616T182415Z +LOCATION: +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Meet me maybe +DESCRIPTION:This is the updated event description +TRANSP:OPAQUE +BEGIN:VALARM +ACTION:DISPLAY +DESCRIPTION:This is an event reminder +TRIGGER:-P0DT0H10M0S +END:VALARM +END:VEVENT +END:VCALENDAR + +> expect +from: a@gmail.com +to: b@gmail.com +changes: DESCRIPTION +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +DESCRIPTION:This is the updated event description +LOCATION: +STATUS:CONFIRMED +SUMMARY:Meet me maybe +DTEND;TZID=America/Los_Angeles:20250619T064500 +DTSTART;TZID=America/Los_Angeles:20250619T060000 +TRANSP:OPAQUE +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;CN=b@gmail.com;X-NUM-GUESTS=0:mailto:b@gmail.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE; + CN="John Doe";X-NUM-GUESTS=0:mailto:a@gmail.com +ORGANIZER;CN="John Doe":mailto:a@gmail.com +UID:1qnf39p0m9h6n1cm9qa9d8mocv@google.com +RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=TH;WKST=SU +CREATED:20250616T182358Z +DTSTAMP:0 +LAST-MODIFIED:20250616T182415Z +SEQUENCE:1 +X-MICROSOFT-CDO-OWNERAPPTID:299828133 +END:VEVENT +BEGIN:VTIMEZONE +TZID:America/Los_Angeles +X-LIC-LOCATION:America/Los_Angeles +BEGIN:DAYLIGHT +DTSTART:19700308T020000 +TZNAME:PDT +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:19701101T020000 +TZNAME:PST +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR + +> send + +# Make sure the original alarms are preserved +> get b@gmail.com 1qnf39p0m9h6n1cm9qa9d8mocv@google.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +DESCRIPTION:This is the updated event description +LOCATION: +STATUS:CONFIRMED +SUMMARY:Meet me maybe +DTEND;TZID=America/Los_Angeles:20250619T064500 +DTSTART;TZID=America/Los_Angeles:20250619T060000 +TRANSP:OPAQUE +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;CN=b@gmail.com;X-NUM-GUESTS=0:mailto:b@gmail.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE; + CN="John Doe";X-NUM-GUESTS=0:mailto:a@gmail.com +ORGANIZER;CN="John Doe":mailto:a@gmail.com +UID:1qnf39p0m9h6n1cm9qa9d8mocv@google.com +RRULE:FREQ=WEEKLY;COUNT=2;BYDAY=TH;WKST=SU +CREATED:20250616T182358Z +DTSTAMP:0 +LAST-MODIFIED:20250616T182358Z +SEQUENCE:1 +X-MICROSOFT-CDO-OWNERAPPTID:299828133 +BEGIN:VALARM +DESCRIPTION:This is an event reminder +ACTION:DISPLAY +TRIGGER:-PT10M +END:VALARM +END:VEVENT +BEGIN:VTIMEZONE +TZID:America/Los_Angeles +X-LIC-LOCATION:America/Los_Angeles +BEGIN:DAYLIGHT +DTSTART:19700308T020000 +TZNAME:PDT +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 +END:DAYLIGHT +BEGIN:STANDARD +DTSTART:19701101T020000 +TZNAME:PST +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 +END:STANDARD +END:VTIMEZONE +END:VCALENDAR + + + diff --git a/tests/resources/itip/itip_incoming.txt b/tests/resources/itip/itip_incoming.txt new file mode 100644 index 00000000..46f2d938 --- /dev/null +++ b/tests/resources/itip/itip_incoming.txt @@ -0,0 +1,130 @@ +# iTIP tests + +# Recipient is not organizer or attendee +> itip x@example.com y@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=C:mailto:c@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal:mailto:d@example.com +ATTENDEE;RSVP=FALSE;CUTYPE=ROOM:conf_big@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:mailto:e@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> send +NotOrganizerNorAttendee + +> reset + +# Refreshing an event (1) +> put a@example.com 123456789@example.com +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:123456789@example.com +SEQUENCE:0 +RDATE:19980304T180000Z +RDATE:19980311T180000Z +RDATE:19980318T180000Z +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +SUMMARY:Review Accounts +DTSTART:19980304T180000Z +DTEND:19980304T200000Z +DTSTAMP:19980303T193000Z +LOCATION:Conference Room A +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +# Refreshing an event (2) +> expect +from: a@example.com +to: b@example.com +changes: +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +LOCATION:Conference Room A +STATUS:CONFIRMED +SUMMARY:Review Accounts +DTEND:19980304T200000Z +DTSTART:19980304T180000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:123456789@example.com +RDATE:19980304T180000Z +RDATE:19980311T180000Z +RDATE:19980318T180000Z +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +END:VCALENDAR + +# Refreshing an event (3) +> send + +# Refreshing an event (4) +> itip b@example.com a@example.com +BEGIN:VCALENDAR +METHOD:REFRESH +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:123456789@example.com +SEQUENCE:0 +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +DTSTART:19980304T180000Z +END:VEVENT +END:VCALENDAR + +# Refreshing an event (5) +> send +from: a@example.com +to: b@example.com +changes: +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +LOCATION:Conference Room A +STATUS:CONFIRMED +SUMMARY:Review Accounts +DTEND:19980304T200000Z +DTSTART:19980304T180000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:123456789@example.com +RDATE:19980304T180000Z +RDATE:19980311T180000Z +RDATE:19980318T180000Z +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +END:VCALENDAR + + + diff --git a/tests/resources/itip/put_validation.txt b/tests/resources/itip/put_validation.txt new file mode 100644 index 00000000..c6950f00 --- /dev/null +++ b/tests/resources/itip/put_validation.txt @@ -0,0 +1,322 @@ +# Scheduling invalid actions + +# Event has no scheduling information +> put x@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +NoSchedulingInfo + +> reset + +# X is not the organizer +> put x@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +NotOrganizerNorAttendee + +> reset + +# No attendees +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +NothingToSend + +> reset + + +# Organizer with client scheduling agent +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER;SCHEDULE-AGENT=CLIENT:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +OtherSchedulingAgent + +> reset + +# Single participant with client scheduling agent +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;SCHEDULE-AGENT=CLIENT;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +NothingToSend + +> reset + +# Only send updates to clients with server scheduling agent +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:d@example.com +ATTENDEE;SCHEDULE-AGENT=CLIENT;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +from: a@example.com +to: d@example.com +changes: +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Conference +DTEND:19970701T2100000Z +DTSTART:19970701T200000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B;PARTSTAT=NEEDS-ACTION:mailto:b@exa + mple.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B;PARTSTAT=NEEDS-ACTION:mailto:d@exa + mple.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +END:VCALENDAR + +> reset + +# Multiple object types should be rejected +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +BEGIN:VTODO +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VTODO +END:VCALENDAR + +> expect +MultipleObjectTypes + +> reset + +# Multiple organizers should be rejected +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +ORGANIZER:mailto:d@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=D:mailto:d@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +MultipleOrganizer + +> reset + +# Different UIDs should be rejected +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:other-uid@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +MultipleUid + +> reset + +# Multiple object instances should be rejected +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +RECURRENCE-ID:19970701T200000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +RECURRENCE-ID:19970701T200000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +MultipleObjectInstances + diff --git a/tests/resources/itip/rfc5546_event_recurring.txt b/tests/resources/itip/rfc5546_event_recurring.txt new file mode 100644 index 00000000..2c3a97b9 --- /dev/null +++ b/tests/resources/itip/rfc5546_event_recurring.txt @@ -0,0 +1,833 @@ +# RFC5546 - Group Event Request + +# A sample meeting request is sent from "A" to "B", "C", and "D". +> put a@example.com guid-1@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:guid-1@example.com +SEQUENCE:0 +RRULE:FREQ=MONTHLY;BYMONTHDAY=1;UNTIL=19980901T210000Z +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970601T210000Z +DTEND:19970601T220000Z +LOCATION:Conference Call +DTSTAMP:19970526T083000Z +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com, c@example.com, d@example.com +changes: +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Conference Call +LOCATION:Conference Call +STATUS:CONFIRMED +SUMMARY:IETF Calendaring Working Group Meeting +DTEND:19970601T220000Z +DTSTART:19970601T210000Z +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION: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 +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +END:VCALENDAR + +# Send iTIP request to attendees +> send + +# Change a recurrence instance +> put a@example.com guid-1@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:guid-1@example.com +SEQUENCE:2 +RRULE:FREQ=MONTHLY;BYMONTHDAY=1;UNTIL=19980901T210000Z +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970601T210000Z +DTEND:19970601T220000Z +LOCATION:Conference Call +DTSTAMP:19970526T083000Z +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:guid-1@example.com +RECURRENCE-ID:19970701T210000Z +SEQUENCE:1 +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970703T210000Z +DTEND:19970703T220000Z +LOCATION:Conference Call +DTSTAMP:19970626T093000Z +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com, c@example.com, d@example.com +changes: +BEGIN:VCALENDAR +METHOD:ADD +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Conference Call +LOCATION:Conference Call +STATUS:CONFIRMED +SUMMARY:IETF Calendaring Working Group Meeting +DTEND:19970703T220000Z +DTSTART:19970703T210000Z +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION: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 +DTSTAMP:0 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +# Send iTIP update to attendees +> send + +# Cancel a recurrence instance only for B +> put a@example.com guid-1@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:guid-1@example.com +SEQUENCE:3 +RRULE:FREQ=MONTHLY;BYMONTHDAY=1;UNTIL=19980901T210000Z +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970601T210000Z +DTEND:19970601T220000Z +LOCATION:Conference Call +DTSTAMP:19970526T083000Z +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:guid-1@example.com +RECURRENCE-ID:19970701T210000Z +SEQUENCE:2 +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970703T210000Z +DTEND:19970703T220000Z +LOCATION:Conference Call +DTSTAMP:19970626T093000Z +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:guid-1@example.com +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +RECURRENCE-ID:19970801T210000Z +SEQUENCE:2 +STATUS:CANCELLED +DTSTAMP:19970721T093000Z +END:VEVENT +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com +changes: +BEGIN:VCALENDAR +METHOD:CANCEL +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CANCELLED +ATTENDEE:mailto:b@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID:19970801T210000Z +UID:guid-1@example.com +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +END:VCALENDAR + +# Send iTIP cancellation to B +> send + +# Make sure B has the cancelled event +> get b@example.com guid-1@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Conference Call +LOCATION:Conference Call +STATUS:CONFIRMED +SUMMARY:IETF Calendaring Working Group Meeting +DTEND:19970601T220000Z +DTSTART:19970601T210000Z +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION: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 +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Conference Call +LOCATION:Conference Call +STATUS:CONFIRMED +SUMMARY:IETF Calendaring Working Group Meeting +DTEND:19970703T220000Z +DTSTART:19970703T210000Z +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION: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 +DTSTAMP:0 +SEQUENCE:2 +END:VEVENT +BEGIN:VEVENT +STATUS:CANCELLED +ATTENDEE:mailto:b@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID:19970801T210000Z +UID:guid-1@example.com +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +END:VCALENDAR + +# Change all future instances +> put a@example.com guid-1@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:guid-1@example.com +SEQUENCE:4 +RRULE:FREQ=MONTHLY;BYMONTHDAY=1;UNTIL=19980901T210000Z +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970601T210000Z +DTEND:19970601T220000Z +LOCATION:Conference Call +DTSTAMP:19970526T083000Z +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:guid-1@example.com +RECURRENCE-ID:19970701T210000Z +SEQUENCE:3 +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970703T210000Z +DTEND:19970703T220000Z +LOCATION:Conference Call +DTSTAMP:19970626T093000Z +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:guid-1@example.com +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +RECURRENCE-ID:19970801T210000Z +SEQUENCE:3 +STATUS:CANCELLED +DTSTAMP:19970721T093000Z +END:VEVENT +BEGIN:VEVENT +UID:guid-1@example.com +RECURRENCE-ID;THISANDFUTURE:19970901T210000Z +SEQUENCE:3 +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com +ATTENDEE;RSVP=TRUE:mailto:d@example.com +DESCRIPTION:IETF-C&S Discussion +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970901T210000Z +DTEND:19970901T220000Z +LOCATION:Building 32, Microsoft, Seattle, WA +DTSTAMP:19970526T083000Z +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com, c@example.com, d@example.com +changes: +BEGIN:VCALENDAR +METHOD:ADD +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Discussion +LOCATION:Building 32\, Microsoft\, Seattle\, WA +STATUS:CONFIRMED +SUMMARY:IETF Calendaring Working Group Meeting +DTEND:19970901T220000Z +DTSTART:19970901T210000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:d@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID;THISANDFUTURE:19970901T210000Z +UID:guid-1@example.com +DTSTAMP:0 +SEQUENCE:4 +END:VEVENT +END:VCALENDAR + +# Send iTIP update to attendees +> send + +# Make sure B has the complete event including all updates +> get b@example.com guid-1@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Conference Call +LOCATION:Conference Call +STATUS:CONFIRMED +SUMMARY:IETF Calendaring Working Group Meeting +DTEND:19970601T220000Z +DTSTART:19970601T210000Z +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION: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 +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Conference Call +LOCATION:Conference Call +STATUS:CONFIRMED +SUMMARY:IETF Calendaring Working Group Meeting +DTEND:19970703T220000Z +DTSTART:19970703T210000Z +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;PARTSTAT=NEEDS-ACTION: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 +DTSTAMP:0 +SEQUENCE:2 +END:VEVENT +BEGIN:VEVENT +STATUS:CANCELLED +ATTENDEE:mailto:b@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID:19970801T210000Z +UID:guid-1@example.com +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Discussion +LOCATION:Building 32\, Microsoft\, Seattle\, WA +STATUS:CONFIRMED +SUMMARY:IETF Calendaring Working Group Meeting +DTEND:19970901T220000Z +DTSTART:19970901T210000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:d@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID;THISANDFUTURE:19970901T210000Z +UID:guid-1@example.com +DTSTAMP:0 +SEQUENCE:4 +END:VEVENT +END:VCALENDAR + +# Cancel the recurring event +> put a@example.com guid-1@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:guid-1@example.com +SEQUENCE:4 +RRULE:FREQ=MONTHLY;BYMONTHDAY=1;UNTIL=19980901T210000Z +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970601T210000Z +DTEND:19970601T220000Z +LOCATION:Conference Call +DTSTAMP:19970526T083000Z +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:guid-1@example.com +RECURRENCE-ID:19970701T210000Z +SEQUENCE:3 +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +DESCRIPTION:IETF-C&S Conference Call +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970703T210000Z +DTEND:19970703T220000Z +LOCATION:Conference Call +DTSTAMP:19970626T093000Z +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:guid-1@example.com +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE:mailto:b@example.com +RECURRENCE-ID:19970801T210000Z +SEQUENCE:3 +STATUS:CANCELLED +DTSTAMP:19970721T093000Z +END:VEVENT +BEGIN:VEVENT +UID:guid-1@example.com +RECURRENCE-ID;THISANDFUTURE:19970901T210000Z +SEQUENCE:3 +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com +ATTENDEE;RSVP=TRUE:mailto:d@example.com +DESCRIPTION:IETF-C&S Discussion +CLASS:PUBLIC +SUMMARY:IETF Calendaring Working Group Meeting +DTSTART:19970901T210000Z +DTEND:19970901T220000Z +LOCATION:Building 32, Microsoft, Seattle, WA +DTSTAMP:19970526T083000Z +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +# Cancel a recurring event +> delete a@example.com guid-1@example.com + +> expect +from: a@example.com +to: b@example.com, c@example.com, d@example.com +changes: +BEGIN:VCALENDAR +METHOD:CANCEL +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CANCELLED +ATTENDEE:mailto:a@example.com +ATTENDEE:mailto:b@example.com +ATTENDEE:mailto:c@example.com +ATTENDEE:mailto:d@example.com +ORGANIZER:mailto:a@example.com +UID:guid-1@example.com +DTSTAMP:0 +SEQUENCE:5 +END:VEVENT +END:VCALENDAR + +# Send iTIP cancellation to attendees +> send + +# Make sure all instances in B were deleted +> get b@example.com guid-1@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Conference Call +LOCATION:Conference Call +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 +ORGANIZER:mailto:a@example.com +UID:guid-1@example.com +RRULE:FREQ=MONTHLY;UNTIL=19980901T210000Z;BYMONTHDAY=1 +DTSTAMP:0 +END:VEVENT +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Conference Call +LOCATION:Conference Call +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 +ORGANIZER:mailto:a@example.com +RECURRENCE-ID:19970701T210000Z +UID:guid-1@example.com +DTSTAMP:0 +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 +ORGANIZER:mailto:a@example.com +RECURRENCE-ID:19970801T210000Z +UID:guid-1@example.com +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +BEGIN:VEVENT +CLASS:PUBLIC +DESCRIPTION:IETF-C&S Discussion +LOCATION:Building 32\, Microsoft\, Seattle\, WA +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 +ORGANIZER:mailto:a@example.com +RECURRENCE-ID;THISANDFUTURE:19970901T210000Z +UID:guid-1@example.com +DTSTAMP:0 +SEQUENCE:4 +END:VEVENT +END:VCALENDAR + +# Add a new series of instances to the recurring event +> put a@example.com 123456789@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:123456789@example.com +SEQUENCE:0 +RRULE:WKST=SU;BYDAY=TU;FREQ=WEEKLY +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +SUMMARY:Review Accounts +DTSTART:19980303T210000Z +DTEND:19980303T220000Z +LOCATION:The White Room +DTSTAMP:19980301T093000Z +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:123456789@example.com +SEQUENCE:2 +RECURRENCE-ID;THISANDFUTURE:19970901T210000Z +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com +SUMMARY:Review Accounts +DTSTAMP:19980303T193000Z +LOCATION:The Red Room +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com, c@example.com +changes: +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +LOCATION:The White Room +STATUS:CONFIRMED +SUMMARY:Review Accounts +DTEND:19980303T220000Z +DTSTART:19980303T210000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:123456789@example.com +RRULE:FREQ=WEEKLY;BYDAY=TU;WKST=SU +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +BEGIN:VEVENT +LOCATION:The Red Room +STATUS:CONFIRMED +SUMMARY:Review Accounts +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID;THISANDFUTURE:19970901T210000Z +UID:123456789@example.com +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +END:VCALENDAR + +# Send iTIP request to B and C +> send + +# Add a new series of instances to the recurring event (update) +> put a@example.com 123456789@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +UID:123456789@example.com +SEQUENCE:2 +RRULE:WKST=SU;BYDAY=TU,TH;FREQ=WEEKLY +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +SUMMARY:Review Accounts +DTSTART:19980303T210000Z +DTEND:19980303T220000Z +DTSTAMP:19980303T193000Z +LOCATION:The White Room +STATUS:CONFIRMED +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com +changes: EXDATE, RRULE +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +LOCATION:The White Room +STATUS:CONFIRMED +SUMMARY:Review Accounts +DTEND:19980303T220000Z +DTSTART:19980303T210000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:123456789@example.com +RRULE:FREQ=WEEKLY;BYDAY=TU,TH;WKST=SU +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +END:VCALENDAR +================================ +from: a@example.com +to: c@example.com +changes: +BEGIN:VCALENDAR +METHOD:CANCEL +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CANCELLED +ATTENDEE:mailto:c@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID;THISANDFUTURE:19970901T210000Z +UID:123456789@example.com +DTSTAMP:0 +SEQUENCE:4 +END:VEVENT +END:VCALENDAR + +# Send iTIP request to B and cancellation to C +> send + +# Make sure B has the updated event +> get b@example.com 123456789@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +LOCATION:The White Room +STATUS:CONFIRMED +SUMMARY:Review Accounts +DTEND:19980303T220000Z +DTSTART:19980303T210000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:123456789@example.com +RRULE:FREQ=WEEKLY;BYDAY=TU,TH;WKST=SU +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +END:VCALENDAR + +# Make sure C has the updated event +> get c@example.com 123456789@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +LOCATION:The Red Room +STATUS:CANCELLED +SUMMARY:Review Accounts +ATTENDEE:mailto:c@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID;THISANDFUTURE:19970901T210000Z +UID:123456789@example.com +DTSTAMP:0 +END:VEVENT +BEGIN:VEVENT +LOCATION:The White Room +STATUS:CONFIRMED +SUMMARY:Review Accounts +DTEND:19980303T220000Z +DTSTART:19980303T210000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:123456789@example.com +RRULE:FREQ=WEEKLY;BYDAY=TU;WKST=SU +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +END:VCALENDAR + +# Delete the event from B +> delete b@example.com 123456789@example.com + +> expect +from: b@example.com +to: a@example.com +changes: +BEGIN:VCALENDAR +METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +ATTENDEE;PARTSTAT=DECLINED:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:123456789@example.com +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +END:VCALENDAR + +> send + +# Make sure A has the cancellation from B +> get a@example.com 123456789@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +LOCATION:The White Room +STATUS:CONFIRMED +SUMMARY:Review Accounts +DTEND:19980303T220000Z +DTSTART:19980303T210000Z +ATTENDEE;PARTSTAT=DECLINED:mailto:b@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ORGANIZER:mailto:a@example.com +UID:123456789@example.com +RRULE:FREQ=WEEKLY;BYDAY=TU,TH;WKST=SU +DTSTAMP:1 +SEQUENCE:3 +END:VEVENT +END:VCALENDAR + diff --git a/tests/resources/itip/rfc5546_event_single.txt b/tests/resources/itip/rfc5546_event_single.txt new file mode 100644 index 00000000..34cc673e --- /dev/null +++ b/tests/resources/itip/rfc5546_event_single.txt @@ -0,0 +1,683 @@ +# RFC5546 - Group Event Request + +# A sample meeting request is sent from "A" to "B", "C", and "D". +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B:mailto:b@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=C:mailto:c@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal:mailto:d@example.com +ATTENDEE;RSVP=FALSE;CUTYPE=ROOM:conf_big@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:mailto:e@example.com +DTSTAMP:19970611T190000Z +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:0 +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com, c@example.com, d@example.com +changes: +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Conference +DTEND:19970701T2100000Z +DTSTART:19970701T200000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:mailto:e@example.com +ATTENDEE;RSVP=FALSE;CUTYPE=ROOM:conf_big@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B;PARTSTAT=NEEDS-ACTION:mailto:b@exa + mple.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=C;PARTSTAT=NEEDS-ACTION:mailto:c@exa + mple.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +END:VCALENDAR + +# Send iTIP request to B, C, and D +> send + +# Make sure B receives the request +> get b@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Conference +DTEND:19970701T2100000Z +DTSTART:19970701T200000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:mailto:e@example.com +ATTENDEE;RSVP=FALSE;CUTYPE=ROOM:conf_big@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B;PARTSTAT=NEEDS-ACTION:mailto:b@exa + mple.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=C;PARTSTAT=NEEDS-ACTION:mailto:c@exa + mple.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:1 +END:VEVENT +END:VCALENDAR + +# B accepts the request +> put b@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +BEGIN:VEVENT +DTSTAMP:19970701T200000Z +SEQUENCE:1 +UID:calsrv.example.com-873970198738777@example.com +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B;PARTSTAT=ACCEPTED:mailto:b@exa + mple.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=C;PARTSTAT=NEEDS-ACTION:mailto:c@exa + mple.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;RSVP=FALSE;CUTYPE=ROOM:conf_big@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:mailto:e@example.com +DTSTART:19970701T200000Z +DTEND:19970701T2100000Z +SUMMARY:Conference +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +from: b@example.com +to: a@example.com +changes: +BEGIN:VCALENDAR +METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B;PARTSTAT=ACCEPTED:mailto:b@example + .com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:1 +REQUEST-STATUS:2.0;Success +END:VEVENT +END:VCALENDAR + +# Send iTIP reply to A +> send + +# Make sure A receives the reply +> get a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Conference +DTEND:19970701T2100000Z +DTSTART:19970701T200000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN=A:mailto:a@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:mailto:e@example.com +ATTENDEE;RSVP=FALSE;CUTYPE=ROOM:conf_big@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=B;PARTSTAT=ACCEPTED;SCHEDULE-STATUS= + 2.0:mailto:b@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=C:mailto:c@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal:mailto:d@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:1 +SEQUENCE:1 +END:VEVENT +END:VCALENDAR + +# A moved the event to a new time +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:b@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:c@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal:mailto:d@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE;CUTYPE=ROOM:mailto:conf@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:mailto:e@example.com +DTSTART:19970701T180000Z +DTEND:19970701T190000Z +SUMMARY:Phone Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:1 +DTSTAMP:19970613T190000Z +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com, c@example.com, d@example.com +changes: ATTENDEE, DTEND, DTSTART, SUMMARY +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:mailto:e@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE;CUTYPE=ROOM:mailto:conf@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:c@example. + com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +# Send iTIP request to B, C, and D +> send + +# Make sure C receives the request +> get c@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE:mailto:e@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE;CUTYPE=ROOM:mailto:conf@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:c@example. + com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +# "C" delegates presence at the meeting to "E". +> put c@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +BEGIN:VEVENT +DTSTAMP:19970701T180000Z +UID:calsrv.example.com-873970198738777@example.com +ORGANIZER:mailto:a@example.com +STATUS:CONFIRMED +SEQUENCE:2 +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@example.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com":mailto:e@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE;CUTYPE=ROOM:mailto:conf@example.com +DTSTART:19970701T180000Z +DTEND:19970701T190000Z +SUMMARY:Phone Conference +END:VEVENT +END:VCALENDAR + +> expect +from: c@example.com +to: a@example.com +changes: +BEGIN:VCALENDAR +METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@ex + ample.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com":mailto:e@example.c + om +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:2 +REQUEST-STATUS:2.0;Success +END:VEVENT +END:VCALENDAR +================================ +from: c@example.com +to: e@example.com +changes: +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@ex + ample.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;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=NEEDS-AC + TION:mailto:e@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +> send + +# Make sure E receives the request +> get e@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@ex + ample.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;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=NEEDS-AC + TION:mailto:e@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +# Make sure A receives the request +> get a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +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:mailto:b@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal:mailto:d@example.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com":mailto:e@example.c + om +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:2 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +# Delegate E accepts the request +> put e@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +BEGIN:VEVENT +DTSTAMP:19970701T180000Z +SEQUENCE:2 +UID:calsrv.example.com-873970198738777@example.com +ORGANIZER:mailto:a@example.com +STATUS:CONFIRMED +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@ex + ample.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=ACCEPTED:mailto:e@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE;CUTYPE=ROOM:mailto:conf@example.com +DTSTART:19970701T180000Z +DTEND:19970701T190000Z +SUMMARY:Phone Conference +END:VEVENT +END:VCALENDAR + +> expect +from: e@example.com +to: a@example.com, c@example.com +changes: +BEGIN:VCALENDAR +METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@ex + ample.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=ACCEPTED: + mailto:e@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:2 +REQUEST-STATUS:2.0;Success +END:VEVENT +END:VCALENDAR + +> send + +# Make sure A receives the reply +> get a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +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:mailto:b@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal:mailto:d@example.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=ACCEPTED; + SCHEDULE-STATUS=2.0:mailto:e@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:2 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +# Make sure C receives the reply +> get c@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@ex + ample.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;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=ACCEPTED; + SCHEDULE-STATUS=2.0:mailto:e@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:3 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +# Delegate E declines the request +> put e@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +BEGIN:VEVENT +DTSTAMP:19970701T180000Z +SEQUENCE:2 +UID:calsrv.example.com-873970198738777@example.com +ORGANIZER:mailto:a@example.com +STATUS:CONFIRMED +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@ex + ample.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=DECLINED:mailto:e@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE;CUTYPE=ROOM:mailto:conf@example.com +DTSTART:19970701T180000Z +DTEND:19970701T190000Z +SUMMARY:Phone Conference +END:VEVENT +END:VCALENDAR + +> expect +from: e@example.com +to: a@example.com, c@example.com +changes: +BEGIN:VCALENDAR +METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@ex + ample.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=DECLINED: + mailto:e@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:2 +REQUEST-STATUS:2.0;Success +END:VEVENT +END:VCALENDAR + +# Send iTIP reply to A and C +> send + +# Make sure C receives the reply +> get c@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@example.com":mailto:c@ex + ample.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;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@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:3 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +# Make sure A receives the reply +> get a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +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:mailto:b@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:2 +SEQUENCE:2 +END:VEVENT +END:VCALENDAR + +# Remove B and expect an iTIP cancelation +> put a@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED:mailto:a@example.com +ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@exam + ple.com";SCHEDULE-STATUS=2.0:mailto:c@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;CN=Hal:mailto:d@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;RSVP=FALSE;CUTYPE=ROOM:mailto:conf@example.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=DECLINED; + SCHEDULE-STATUS=2.0:mailto:e@example.com +DTSTART:19970701T180000Z +DTEND:19970701T190000Z +SUMMARY:Phone Conference +UID:calsrv.example.com-873970198738777@example.com +SEQUENCE:2 +DTSTAMP:19970701T180000Z +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR + +> expect +from: a@example.com +to: c@example.com, d@example.com +changes: ATTENDEE +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CONFIRMED +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:e@exam + ple.com":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;PARTSTAT=NEEDS-ACTION:mailto:d@e + xample.com +ATTENDEE;RSVP=TRUE;DELEGATED-FROM="mailto:c@example.com";PARTSTAT=DECLINED: + mailto:e@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +END:VCALENDAR +================================ +from: a@example.com +to: b@example.com +changes: +BEGIN:VCALENDAR +METHOD:CANCEL +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CANCELLED +ATTENDEE:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:3 +END:VEVENT +END:VCALENDAR + +# Send iTIP cancelation +> send + +# Make sure B receives the cancelation +> get b@example.com calsrv.example.com-873970198738777@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VEVENT +STATUS:CANCELLED +SUMMARY:Phone Conference +DTEND:19970701T190000Z +DTSTART:19970701T180000Z +ATTENDEE:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +END:VEVENT +END:VCALENDAR + +# Delete the event from A's calendar and expect an iTIP cancelation +> delete a@example.com calsrv.example.com-873970198738777@example.com + +> expect +from: a@example.com +to: c@example.com, d@example.com +changes: +BEGIN:VCALENDAR +METHOD:CANCEL +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 +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777@example.com +DTSTAMP:0 +SEQUENCE:4 +END:VEVENT +END:VCALENDAR + + diff --git a/tests/resources/itip/rfc5546_todo.txt b/tests/resources/itip/rfc5546_todo.txt new file mode 100644 index 00000000..9d8eea8e --- /dev/null +++ b/tests/resources/itip/rfc5546_todo.txt @@ -0,0 +1,468 @@ +# RFC5546 - Todo Request + +# A sample todo is sent from "A" to "B", "C", and "D". +> put a@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VTODO +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:b@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com +ATTENDEE;RSVP=TRUE:mailto:d@example.com +DTSTART:19970701T170000Z +DUE:19970722T170000Z +PRIORITY:1 +SUMMARY:Create the requirements document +UID:calsrv.example.com-873970198738777-00@example.com +SEQUENCE:0 +DTSTAMP:19970717T200000Z +STATUS:NEEDS-ACTION +END:VTODO +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com, c@example.com, d@example.com +changes: +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VTODO +PRIORITY:1 +STATUS:NEEDS-ACTION +SUMMARY:Create the requirements document +DUE:19970722T170000Z +DTSTART:19970701T170000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=NEEDS-ACTION:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:d@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:0 +SEQUENCE:1 +END:VTODO +END:VCALENDAR + +# Send iTIP request to the attendees +> send + +# Make sure B received the todo +> get b@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VTODO +PRIORITY:1 +STATUS:NEEDS-ACTION +SUMMARY:Create the requirements document +DUE:19970722T170000Z +DTSTART:19970701T170000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=NEEDS-ACTION:mailto:a@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:b@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:c@example.com +ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:d@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:0 +SEQUENCE:1 +END:VTODO +END:VCALENDAR + +# "B" accepts the to-do. +> put b@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +ORGANIZER:mailto:a@example.com +ATTENDEE;PARTSTAT=ACCEPTED:mailto:b@example.com +UID:calsrv.example.com-873970198738777-00@example.com +COMMENT:I'll send you my input by email +SEQUENCE:1 +PRIORITY:1 +STATUS:IN-PROCESS +DTSTART:19970701T170000Z +DUE:19970722T170000Z +DTSTAMP:19970717T203000Z +END:VTODO +END:VCALENDAR + +> expect +from: b@example.com +to: a@example.com +changes: +BEGIN:VCALENDAR +METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VTODO +STATUS:IN-PROCESS +ATTENDEE;PARTSTAT=ACCEPTED:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:0 +SEQUENCE:1 +REQUEST-STATUS:2.0;Success +END:VTODO +END:VCALENDAR + +# Send iTIP reply to the organizer +> send + +# Make sure "A" received the reply +> get a@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +PRIORITY:1 +STATUS:IN-PROCESS +SUMMARY:Create the requirements document +DUE:19970722T170000Z +DTSTART:19970701T170000Z +ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:b@example.com +ATTENDEE;ROLE=CHAIR:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com +ATTENDEE;RSVP=TRUE:mailto:d@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:1 +SEQUENCE:1 +END:VTODO +END:VCALENDAR + +# "B" updates percent completion of the to-do. +> put b@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +ORGANIZER:mailto:a@example.com +PERCENT-COMPLETE:75 +ATTENDEE;PARTSTAT=IN-PROCESS:mailto:b@example.com +UID:calsrv.example.com-873970198738777-00@example.com +COMMENT:I'll send you my input by email +SEQUENCE:1 +PRIORITY:1 +STATUS:IN-PROCESS +DTSTART:19970701T170000Z +DUE:19970722T170000Z +DTSTAMP:19970717T203000Z +END:VTODO +END:VCALENDAR + +> expect +from: b@example.com +to: a@example.com +changes: +BEGIN:VCALENDAR +METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VTODO +PERCENT-COMPLETE:75 +STATUS:IN-PROCESS +ATTENDEE;PARTSTAT=IN-PROCESS:mailto:b@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:0 +SEQUENCE:1 +REQUEST-STATUS:2.0;Success +END:VTODO +END:VCALENDAR + +# Send iTIP reply to the organizer +> send + +# Make sure "A" received the reply +> get a@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +PERCENT-COMPLETE:75 +PRIORITY:1 +STATUS:IN-PROCESS +SUMMARY:Create the requirements document +DUE:19970722T170000Z +DTSTART:19970701T170000Z +ATTENDEE;PARTSTAT=IN-PROCESS;SCHEDULE-STATUS=2.0:mailto:b@example.com +ATTENDEE;ROLE=CHAIR:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com +ATTENDEE;RSVP=TRUE:mailto:d@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:1 +SEQUENCE:1 +END:VTODO +END:VCALENDAR + +# "D" completed the to-do. +> put d@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +ORGANIZER:mailto:a@example.com +ATTENDEE;PARTSTAT=COMPLETED:mailto:d@example.com +UID:calsrv.example.com-873970198738777-00@example.com +COMMENT:I'll send you my input by email +SEQUENCE:1 +PRIORITY:1 +DTSTART:19970701T170000Z +DUE:19970722T170000Z +DTSTAMP:19970717T203000Z +END:VTODO +END:VCALENDAR + +> expect +from: d@example.com +to: a@example.com +changes: +BEGIN:VCALENDAR +METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VTODO +ATTENDEE;PARTSTAT=COMPLETED:mailto:d@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:0 +SEQUENCE:1 +REQUEST-STATUS:2.0;Success +END:VTODO +END:VCALENDAR + +# Send iTIP reply to the organizer +> send + +# Make sure "A" received the reply +> get a@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +PERCENT-COMPLETE:75 +PRIORITY:1 +STATUS:IN-PROCESS +SUMMARY:Create the requirements document +DUE:19970722T170000Z +DTSTART:19970701T170000Z +ATTENDEE;PARTSTAT=COMPLETED;SCHEDULE-STATUS=2.0:mailto:d@example.com +ATTENDEE;PARTSTAT=IN-PROCESS;SCHEDULE-STATUS=2.0:mailto:b@example.com +ATTENDEE;ROLE=CHAIR:mailto:a@example.com +ATTENDEE;RSVP=TRUE:mailto:c@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:1 +SEQUENCE:1 +END:VTODO +END:VCALENDAR + +# Recurring to-do request +> put a@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +ORGANIZER:mailto:a@example.com +ATTENDEE;ROLE=CHAIR:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:b@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:d@example.com +RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR +DTSTART:19980101T100000Z +DUE:19980103T100000Z +SUMMARY:Send Status Reports to Area Managers +UID:calsrv.example.com-873970198738777-00@example.com +SEQUENCE:0 +DTSTAMP:19970717T200000Z +STATUS:NEEDS-ACTION +PRIORITY:1 +END:VTODO +END:VCALENDAR + +> expect +from: a@example.com +to: b@example.com, d@example.com +changes: ATTENDEE, DTSTART, DUE, PERCENT-COMPLETE, RRULE, STATUS, SUMMARY +BEGIN:VCALENDAR +METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VTODO +PRIORITY:1 +STATUS:NEEDS-ACTION +SUMMARY:Send Status Reports to Area Managers +DUE:19980103T100000Z +DTSTART:19980101T100000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=NEEDS-ACTION:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:d@example. + com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR +DTSTAMP:0 +SEQUENCE:1 +END:VTODO +END:VCALENDAR +================================ +from: a@example.com +to: c@example.com +changes: +BEGIN:VCALENDAR +METHOD:CANCEL +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VTODO +STATUS:CANCELLED +ATTENDEE:mailto:c@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:0 +SEQUENCE:2 +END:VTODO +END:VCALENDAR + +> send + +# Make sure "C" received the cancel request +> get c@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VTODO +PRIORITY:1 +STATUS:CANCELLED +SUMMARY:Create the requirements document +DUE:19970722T170000Z +DTSTART:19970701T170000Z +ATTENDEE:mailto:c@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:0 +END:VTODO +END:VCALENDAR + +# Make sure "B" received the updated to-do +> get b@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +COMMENT:I'll send you my input by email +PRIORITY:1 +STATUS:NEEDS-ACTION +SUMMARY:Send Status Reports to Area Managers +DUE:19980103T100000Z +DTSTART:19980101T100000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=NEEDS-ACTION:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:d@example. + com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR +DTSTAMP:0 +SEQUENCE:1 +END:VTODO +END:VCALENDAR + +# Reply to an instance of a recurring to-do +> put b@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +COMMENT:I'll send you my input by email +PRIORITY:1 +STATUS:NEEDS-ACTION +SUMMARY:Send Status Reports to Area Managers +DUE:19980103T100000Z +DTSTART:19980101T100000Z +ATTENDEE;ROLE=CHAIR;PARTSTAT=NEEDS-ACTION:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:b@example. + com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION:mailto:d@example. + com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR +DTSTAMP:19970717T233000Z +SEQUENCE:1 +END:VTODO +BEGIN:VTODO +ORGANIZER:mailto:a@example.com +ATTENDEE;PARTSTAT=IN-PROCESS:mailto:b@example.com +PERCENT-COMPLETE:75 +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:19970717T233000Z +RECURRENCE-ID:19980101T170000Z +SEQUENCE:1 +END:VTODO +END:VCALENDAR + +> expect +from: b@example.com +to: a@example.com +changes: +BEGIN:VCALENDAR +METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 +BEGIN:VTODO +PERCENT-COMPLETE:75 +ATTENDEE;PARTSTAT=IN-PROCESS:mailto:b@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID:19980101T170000Z +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:0 +SEQUENCE:1 +REQUEST-STATUS:2.0;Success +END:VTODO +END:VCALENDAR + +> send + +# Make sure "A" received the reply +> get a@example.com calsrv.example.com-873970198738777-00@example.com +BEGIN:VCALENDAR +PRODID:-//Example/ExampleCalendarClient//EN +VERSION:2.0 +BEGIN:VTODO +PRIORITY:1 +STATUS:NEEDS-ACTION +SUMMARY:Send Status Reports to Area Managers +DUE:19980103T100000Z +DTSTART:19980101T100000Z +ATTENDEE;ROLE=CHAIR:mailto:a@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:b@example.com +ATTENDEE;RSVP=TRUE;CUTYPE=INDIVIDUAL:mailto:d@example.com +ORGANIZER:mailto:a@example.com +UID:calsrv.example.com-873970198738777-00@example.com +RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=1FR +DTSTAMP:1 +SEQUENCE:1 +END:VTODO +BEGIN:VTODO +PERCENT-COMPLETE:75 +ATTENDEE;PARTSTAT=IN-PROCESS:mailto:b@example.com +ORGANIZER:mailto:a@example.com +RECURRENCE-ID:19980101T170000Z +UID:calsrv.example.com-873970198738777-00@example.com +DTSTAMP:0 +SEQUENCE:1 +END:VTODO +END:VCALENDAR + + diff --git a/tests/resources/itip/rfc6638_recurring.txt b/tests/resources/itip/rfc6638_recurring.txt index 2b50d52f..ba3a5e8a 100644 --- a/tests/resources/itip/rfc6638_recurring.txt +++ b/tests/resources/itip/rfc6638_recurring.txt @@ -42,39 +42,39 @@ from: cyrus@example.com to: bernard@example.net changes: BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Stalwart Labs LLC//Stalwart Server//EN METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 BEGIN:VEVENT -DTSTAMP:0 -SEQUENCE:1 -UID:9263504FD3AD -DTSTART;TZID=America/Montreal:20090601T150000 -DTEND;TZID=America/Montreal:20090601T160000 -RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1 -TRANSP:OPAQUE SUMMARY:Review Internet-Draft -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com -ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e - xample.com +DTEND;TZID=America/Montreal:20090601T160000 +DTSTART;TZID=America/Montreal:20090601T150000 +TRANSP:OPAQUE ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP= TRUE;PARTSTAT=NEEDS-ACTION:mailto:bernard@example.net +ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e + xample.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1 +DTSTAMP:0 +SEQUENCE:1 END:VEVENT BEGIN:VTIMEZONE TZID:America/Montreal BEGIN:STANDARD DTSTART:20071104T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T020000 -RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 END:DAYLIGHT END:VTIMEZONE END:VCALENDAR @@ -85,38 +85,38 @@ END:VCALENDAR # Make sure the participant receives the event > get bernard@example.net 9263504FD3AD BEGIN:VCALENDAR -VERSION:2.0 PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 BEGIN:VEVENT -DTSTAMP:0 -SEQUENCE:1 -UID:9263504FD3AD -DTSTART;TZID=America/Montreal:20090601T150000 -DTEND;TZID=America/Montreal:20090601T160000 -RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1 -TRANSP:OPAQUE SUMMARY:Review Internet-Draft -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com -ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e - xample.com +DTEND;TZID=America/Montreal:20090601T160000 +DTSTART;TZID=America/Montreal:20090601T150000 +TRANSP:OPAQUE ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP= TRUE;PARTSTAT=NEEDS-ACTION:mailto:bernard@example.net +ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e + xample.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1 +DTSTAMP:0 +SEQUENCE:1 END:VEVENT BEGIN:VTIMEZONE TZID:America/Montreal BEGIN:STANDARD DTSTART:20071104T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T020000 -RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 END:DAYLIGHT END:VTIMEZONE END:VCALENDAR @@ -176,43 +176,43 @@ from: bernard@example.net to: cyrus@example.com changes: BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Stalwart Labs LLC//Stalwart Server//EN METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 BEGIN:VEVENT -DTSTAMP:0 -SEQUENCE:1 -UID:9263504FD3AD -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE= REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +DTSTAMP:0 +SEQUENCE:1 REQUEST-STATUS:2.0;Success END:VEVENT BEGIN:VEVENT -DTSTAMP:0 -SEQUENCE:1 -UID:9263504FD3AD -RECURRENCE-ID;TZID=America/Montreal:20090602T150000 -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED;ROLE= REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +RECURRENCE-ID;TZID=America/Montreal:20090602T150000 +UID:9263504FD3AD +DTSTAMP:0 +SEQUENCE:1 REQUEST-STATUS:2.0;Success END:VEVENT BEGIN:VTIMEZONE TZID:America/Montreal BEGIN:STANDARD DTSTART:20071104T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T020000 -RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 END:DAYLIGHT END:VTIMEZONE END:VCALENDAR @@ -223,47 +223,47 @@ END:VCALENDAR # Organizer receives the response > get cyrus@example.com 9263504FD3AD BEGIN:VCALENDAR -VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN +VERSION:2.0 BEGIN:VEVENT -UID:9263504FD3AD -SEQUENCE:1 -DTSTAMP:1 -DTSTART;TZID=America/Montreal:20090601T150000 -DTEND;TZID=America/Montreal:20090601T160000 -RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1 -TRANSP:OPAQUE SUMMARY:Review Internet-Draft -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com -ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e - xample.com +DTEND;TZID=America/Montreal:20090601T160000 +DTSTART;TZID=America/Montreal:20090601T150000 +TRANSP:OPAQUE ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP= TRUE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:bernard@example.net +ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e + xample.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1 +DTSTAMP:1 +SEQUENCE:1 END:VEVENT BEGIN:VEVENT -DTSTAMP:0 -SEQUENCE:1 -UID:9263504FD3AD -RECURRENCE-ID;TZID=America/Montreal:20090602T150000 -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED;ROLE= REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +RECURRENCE-ID;TZID=America/Montreal:20090602T150000 +UID:9263504FD3AD +DTSTAMP:0 +SEQUENCE:1 END:VEVENT BEGIN:VTIMEZONE TZID:America/Montreal BEGIN:STANDARD DTSTART:20071104T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T020000 -RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 END:DAYLIGHT END:VTIMEZONE END:VCALENDAR @@ -324,32 +324,32 @@ from: bernard@example.net to: cyrus@example.com changes: BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Stalwart Labs LLC//Stalwart Server//EN METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 BEGIN:VEVENT -ORGANIZER:mailto:cyrus@example.com ATTENDEE;PARTSTAT=DECLINED:mailto:bernard@example.net +ORGANIZER:mailto:cyrus@example.com +RECURRENCE-ID;TZID=America/Montreal:20090603T150000 UID:9263504FD3AD DTSTAMP:0 SEQUENCE:1 -RECURRENCE-ID;TZID=America/Montreal:20090603T150000 END:VEVENT BEGIN:VTIMEZONE TZID:America/Montreal BEGIN:STANDARD DTSTART:20071104T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T020000 -RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 END:DAYLIGHT END:VTIMEZONE END:VCALENDAR @@ -360,55 +360,55 @@ END:VCALENDAR # Organizer receives the response > get cyrus@example.com 9263504FD3AD BEGIN:VCALENDAR -VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN +VERSION:2.0 BEGIN:VEVENT -ORGANIZER:mailto:cyrus@example.com ATTENDEE;PARTSTAT=DECLINED:mailto:bernard@example.net +ORGANIZER:mailto:cyrus@example.com +RECURRENCE-ID;TZID=America/Montreal:20090603T150000 UID:9263504FD3AD DTSTAMP:0 SEQUENCE:1 -RECURRENCE-ID;TZID=America/Montreal:20090603T150000 END:VEVENT BEGIN:VEVENT -UID:9263504FD3AD -SEQUENCE:1 -DTSTAMP:1 -DTSTART;TZID=America/Montreal:20090601T150000 -DTEND;TZID=America/Montreal:20090601T160000 -RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1 -TRANSP:OPAQUE SUMMARY:Review Internet-Draft -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com -ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e - xample.com +DTEND;TZID=America/Montreal:20090601T160000 +DTSTART;TZID=America/Montreal:20090601T150000 +TRANSP:OPAQUE ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP= TRUE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:bernard@example.net +ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e + xample.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +RRULE:FREQ=DAILY;COUNT=5;INTERVAL=1 +DTSTAMP:1 +SEQUENCE:1 END:VEVENT BEGIN:VEVENT -DTSTAMP:0 -SEQUENCE:1 -UID:9263504FD3AD -RECURRENCE-ID;TZID=America/Montreal:20090602T150000 -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED;ROLE= REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +RECURRENCE-ID;TZID=America/Montreal:20090602T150000 +UID:9263504FD3AD +DTSTAMP:0 +SEQUENCE:1 END:VEVENT BEGIN:VTIMEZONE TZID:America/Montreal BEGIN:STANDARD DTSTART:20071104T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST TZOFFSETFROM:-0400 TZOFFSETTO:-0500 +RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 END:STANDARD BEGIN:DAYLIGHT DTSTART:20070311T020000 -RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT TZOFFSETFROM:-0500 TZOFFSETTO:-0400 +RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 END:DAYLIGHT END:VTIMEZONE END:VCALENDAR diff --git a/tests/resources/itip/rfc6638_single.txt b/tests/resources/itip/rfc6638_single.txt index dfd15c0c..c94283ba 100644 --- a/tests/resources/itip/rfc6638_single.txt +++ b/tests/resources/itip/rfc6638_single.txt @@ -26,51 +26,51 @@ from: cyrus@example.com to: bernard@example.net, mike@example.org, wilfredo@example.com changes: BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Stalwart Labs LLC//Stalwart Server//EN METHOD:REQUEST +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 BEGIN:VEVENT -DTSTAMP:0 -SEQUENCE:1 -UID:9263504FD3AD -DTSTART:20090602T160000Z -DTEND:20090602T170000Z -TRANSP:OPAQUE SUMMARY:Lunch -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com -ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e - xample.com -ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; - ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com +DTEND:20090602T170000Z +DTSTART:20090602T160000Z +TRANSP:OPAQUE ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net +ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e + xample.com ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;RSVP=TRUE;PARTSTAT=NEEDS-ACTI ON:mailto:mike@example.org +ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; + ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +DTSTAMP:0 +SEQUENCE:1 END:VEVENT END:VCALENDAR # Make sure the sequence number is updated > get cyrus@example.com 9263504FD3AD BEGIN:VCALENDAR -VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN +VERSION:2.0 BEGIN:VEVENT -UID:9263504FD3AD -SEQUENCE:1 -DTSTAMP:1 -DTSTART:20090602T160000Z -DTEND:20090602T170000Z -TRANSP:OPAQUE SUMMARY:Lunch -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com -ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e - xample.com -ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; - ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com +DTEND:20090602T170000Z +DTSTART:20090602T160000Z +TRANSP:OPAQUE ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net +ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e + xample.com ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;RSVP=TRUE:mailto:mike@example. org +ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; + ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +DTSTAMP:1 +SEQUENCE:1 END:VEVENT END:VCALENDAR @@ -80,25 +80,25 @@ END:VCALENDAR # Make sure the message was received by the attendees > get wilfredo@example.com 9263504FD3AD BEGIN:VCALENDAR -VERSION:2.0 PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 BEGIN:VEVENT -DTSTAMP:0 -SEQUENCE:1 -UID:9263504FD3AD -DTSTART:20090602T160000Z -DTEND:20090602T170000Z -TRANSP:OPAQUE SUMMARY:Lunch -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com -ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e - xample.com -ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; - ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com +DTEND:20090602T170000Z +DTSTART:20090602T160000Z +TRANSP:OPAQUE ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net +ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e + xample.com ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;RSVP=TRUE;PARTSTAT=NEEDS-ACTI ON:mailto:mike@example.org +ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; + ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +DTSTAMP:0 +SEQUENCE:1 END:VEVENT END:VCALENDAR @@ -131,29 +131,29 @@ END:VCALENDAR # Make sure the sequence number is not updated > get wilfredo@example.com 9263504FD3AD BEGIN:VCALENDAR -VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN +VERSION:2.0 BEGIN:VEVENT -UID:9263504FD3AD -SEQUENCE:0 -DTSTAMP:1 -DTSTART:20090602T160000Z -DTEND:20090602T170000Z -TRANSP:OPAQUE SUMMARY:Lunch -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com -ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e - xample.com -ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE= - REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com +DTEND:20090602T170000Z +DTSTART:20090602T160000Z +TRANSP:OPAQUE ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net +ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e + xample.com ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TR UE:mailto:mike@example.org +ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE= + REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +DTSTAMP:1 +SEQUENCE:0 BEGIN:VALARM -TRIGGER:-PT15M -ACTION:DISPLAY DESCRIPTION:Reminder +ACTION:DISPLAY +TRIGGER:-PT15M END:VALARM END:VEVENT END:VCALENDAR @@ -163,16 +163,16 @@ from: wilfredo@example.com to: cyrus@example.com changes: BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Stalwart Labs LLC//Stalwart Server//EN METHOD:REPLY +PRODID:-//Stalwart Labs LLC//Stalwart Server//EN +VERSION:2.0 BEGIN:VEVENT -DTSTAMP:0 -SEQUENCE:0 -UID:9263504FD3AD -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE= REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +DTSTAMP:0 +SEQUENCE:0 REQUEST-STATUS:2.0;Success END:VEVENT END:VCALENDAR @@ -183,25 +183,25 @@ END:VCALENDAR # Make sure the message was received by the organizer > get cyrus@example.com 9263504FD3AD BEGIN:VCALENDAR -VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN +VERSION:2.0 BEGIN:VEVENT -UID:9263504FD3AD -SEQUENCE:1 -DTSTAMP:1 -DTSTART:20090602T160000Z -DTEND:20090602T170000Z -TRANSP:OPAQUE SUMMARY:Lunch -ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com -ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e - xample.com -ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT; - RSVP=TRUE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:wilfredo@example.com +DTEND:20090602T170000Z +DTSTART:20090602T160000Z +TRANSP:OPAQUE ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION; ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net +ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@e + xample.com ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;RSVP=TRUE:mailto:mike@example. org +ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT; + RSVP=TRUE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:wilfredo@example.com +ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com +UID:9263504FD3AD +DTSTAMP:1 +SEQUENCE:1 END:VEVENT END:VCALENDAR diff --git a/tests/src/webdav/cal_itip.rs b/tests/src/webdav/cal_itip.rs new file mode 100644 index 00000000..427280c4 --- /dev/null +++ b/tests/src/webdav/cal_itip.rs @@ -0,0 +1,413 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use ahash::AHashMap; +use calcard::{ + common::PartialDateTime, + icalendar::{ICalendar, ICalendarMethod, ICalendarProperty, ICalendarValue}, +}; +use groupware::scheduling::{ + ItipMessage, + event_cancel::itip_cancel, + event_create::itip_create, + event_update::itip_update, + inbound::{MergeResult, itip_import_message, itip_merge_changes, itip_process_message}, + snapshot::itip_snapshot, +}; +use std::{collections::hash_map::Entry, path::PathBuf}; + +struct Test { + test_name: String, + command: Command, + line_num: usize, + parameters: Vec, + payload: String, +} + +#[derive(Debug, PartialEq, Eq)] +enum Command { + Put, + Get, + Delete, + Expect, + Send, + Reset, + Itip, +} + +pub fn test() { + for entry in std::fs::read_dir( + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("resources") + .join("itip"), + ) + .unwrap() + { + let entry = entry.unwrap(); + let path = entry.path(); + if path.extension().is_none_or(|ext| ext != "txt") { + continue; + } + let file_name = path.file_name().unwrap().to_str().unwrap(); + let rules = std::fs::read_to_string(&path).unwrap(); + let mut last_comment = ""; + let mut last_command = ""; + let mut last_line_num = 0; + let mut payload = String::new(); + let mut commands = Vec::new(); + + for (line_num, line) in rules.lines().enumerate() { + if line.starts_with('#') { + last_comment = line.trim_start_matches('#').trim(); + } else if let Some(command) = line.strip_prefix("> ") { + last_command = command.trim(); + last_line_num = line_num; + } else if !line.is_empty() { + payload.push_str(line); + payload.push('\n'); + } else { + if last_command.is_empty() && payload.is_empty() { + continue; + } + let mut command_and_args = last_command.split_whitespace(); + let command = match command_and_args + .next() + .expect("Command should not be empty") + { + "put" => Command::Put, + "get" => Command::Get, + "expect" => Command::Expect, + "send" => Command::Send, + "delete" => Command::Delete, + "reset" => Command::Reset, + "itip" => Command::Itip, + _ => panic!("Unknown command: {}", last_command), + }; + + commands.push(Test { + command, + test_name: last_comment.to_string(), + line_num: last_line_num, + parameters: command_and_args.map(String::from).collect(), + payload: payload.trim().to_string(), + }); + + last_command = ""; + last_line_num = 0; + payload.clear(); + } + } + + if commands.is_empty() { + panic!("No commands found in file: {}", file_name); + } else if !last_command.is_empty() { + panic!( + "File ended with command '{}' at line {} without payload", + last_command, last_line_num + ); + } + + println!("====== Running test: {} ======", file_name); + + let mut store: AHashMap> = AHashMap::new(); + let mut dtstamp_map: AHashMap = AHashMap::new(); + let mut last_itip = None; + + for command in &commands { + if command.command != Command::Put { + println!("{} (line {})", command.test_name, command.line_num); + } + match command.command { + Command::Put => { + let account = command + .parameters + .first() + .expect("Account parameter is required"); + let name = command + .parameters + .get(1) + .expect("Name parameter is required"); + let mut ical = ICalendar::parse(&command.payload) + .expect("Failed to parse iCalendar payload"); + match store + .entry(account.to_string()) + .or_default() + .entry(name.to_string()) + { + Entry::Occupied(mut entry) => { + last_itip = Some(itip_update(&mut ical, entry.get_mut(), &[account])); + entry.insert(ical); + } + Entry::Vacant(entry) => { + last_itip = Some( + itip_create(&mut ical, &[account]).map(|message| vec![message]), + ); + entry.insert(ical); + } + } + } + Command::Get => { + let account = command + .parameters + .first() + .expect("Account parameter is required") + .as_str(); + let name = command + .parameters + .get(1) + .expect("Name parameter is required") + .as_str(); + let ical = ICalendar::parse(&command.payload) + .expect("Failed to parse iCalendar payload") + .to_string() + .replace("\r\n", "\n"); + store + .get(account) + .and_then(|account_store| account_store.get(name)) + .map(|stored_ical| { + let stored_ical = normalize_ical(stored_ical.clone(), &mut dtstamp_map); + if stored_ical != ical { + panic!( + "ICalendar mismatch for {}: expected {}, got {}", + command.test_name, ical, stored_ical + ); + } + }) + .unwrap_or_else(|| { + panic!( + "ICalendar not found for account: {}, name: {}", + account, name + ); + }); + } + Command::Delete => { + let account = command + .parameters + .first() + .expect("Account parameter is required") + .as_str(); + let name = command + .parameters + .get(1) + .expect("Name parameter is required") + .as_str(); + let store = store.get_mut(account).expect("Account not found in store"); + + if let Some(mut ical) = store.remove(name) { + last_itip = + Some(itip_cancel(&mut ical, &[account]).map(|message| vec![message])); + } else { + panic!( + "ICalendar not found for account: {}, name: {}", + account, name + ); + } + } + Command::Expect => { + let last_itip_str = match last_itip + .as_ref() + .expect("No last iTIP message to compare against") + { + Ok(m) => { + let mut result = String::new(); + for (i, m) in m.iter().enumerate() { + if i > 0 { + result.push_str("================================\n"); + } + result.push_str(&m.to_string(&mut dtstamp_map)); + } + result + } + Err(e) => format!("{e:?}"), + }; + + assert_eq!( + command.payload.trim(), + last_itip_str.trim(), + "iTIP message mismatch for {} at line {}: expected {}, got {}", + command.test_name, + command.line_num, + command.payload, + last_itip_str + ); + } + Command::Send => { + let mut results = String::new(); + match last_itip { + Some(Ok(messages)) => { + for message in messages { + for rcpt in &message.to { + let result = match itip_snapshot( + &message.message, + &[rcpt.as_str()], + false, + ) { + Ok(itip_snapshots) => { + match store + .entry(rcpt.to_string()) + .or_default() + .entry(itip_snapshots.uid.to_string()) + { + Entry::Occupied(mut entry) => { + let ical = entry.get_mut(); + let snapshots = itip_snapshot( + ical, + &[rcpt.as_str()], + false, + ) + .expect("Failed to create iTIP snapshot"); + + match itip_process_message( + ical, + snapshots, + &message.message, + itip_snapshots, + message.from.clone(), + ) { + Ok(result) => match result { + MergeResult::Actions(changes) => { + itip_merge_changes(ical, changes); + Ok(None) + } + MergeResult::Message(message) => { + Ok(Some(message)) + } + MergeResult::None => Ok(None), + }, + Err(err) => Err(err), + } + } + Entry::Vacant(entry) => { + let mut message = message.message.clone(); + itip_import_message(&mut message) + .expect("Failed to import iTIP message"); + entry.insert(message); + Ok(None) + } + } + } + Err(err) => Err(err), + }; + + match result { + Ok(Some(itip_message)) => { + results.push_str( + &itip_message.to_string(&mut dtstamp_map), + ); + } + Ok(None) => {} + Err(e) => { + results.push_str(&format!("{e:?}")); + } + } + } + } + + assert_eq!( + results.trim(), + command.payload.trim(), + "iTIP send result mismatch for {} at line {}: expected {}, got {}", + command.test_name, + command.line_num, + command.payload, + results + ); + } + Some(Err(e)) => { + panic!( + "Failed to create iTIP message for {} at line {}: {:?}", + command.test_name, command.line_num, e + ); + } + None => { + panic!( + "No iTIP message to send for {} at line {}", + command.test_name, command.line_num + ); + } + } + last_itip = None; + } + Command::Itip => { + let mut commands = command.parameters.iter(); + last_itip = Some(Ok(vec![ItipMessage { + method: ICalendarMethod::Request, + from: commands + .next() + .expect("From parameter is required") + .to_string(), + to: commands.map(|s| s.to_string()).collect::>(), + changed_properties: vec![], + message: ICalendar::parse(&command.payload) + .expect("Failed to parse iCalendar payload"), + }])) + } + Command::Reset => { + store.clear(); + dtstamp_map.clear(); + last_itip = None; + } + } + } + } +} + +trait ItipMessageExt { + fn to_string(&self, map: &mut AHashMap) -> String; +} + +impl ItipMessageExt for ItipMessage { + fn to_string(&self, map: &mut AHashMap) -> String { + use std::fmt::Write; + let mut f = String::new(); + let mut to = self.to.iter().map(|t| t.as_str()).collect::>(); + to.sort_unstable(); + let mut changed = self + .changed_properties + .iter() + .map(|p| p.as_str()) + .collect::>(); + changed.sort_unstable(); + writeln!(&mut f, "from: {}", self.from).unwrap(); + writeln!(&mut f, "to: {}", to.join(", ")).unwrap(); + writeln!(&mut f, "changes: {}", changed.join(", ")).unwrap(); + write!(&mut f, "{}", normalize_ical(self.message.clone(), map)).unwrap(); + f + } +} + +fn normalize_ical(mut ical: ICalendar, map: &mut AHashMap) -> String { + let mut comps = ical + .components + .iter() + .enumerate() + .filter(|(comp_id, _)| { + ical.components[0] + .component_ids + .contains(&(*comp_id as u16)) + }) + .collect::>(); + comps.sort_unstable_by_key(|(_, comp)| *comp); + ical.components[0].component_ids = comps.iter().map(|(comp_id, _)| *comp_id as u16).collect(); + + for comp in &mut ical.components { + for entry in &mut comp.entries { + if let (ICalendarProperty::Dtstamp, Some(ICalendarValue::PartialDateTime(dt))) = + (&entry.name, entry.values.first()) + { + if let Some(index) = map.get(dt) { + entry.values = vec![ICalendarValue::Integer(*index as i64)]; + } else { + let index = map.len(); + map.insert(dt.as_ref().clone(), index); + entry.values = vec![ICalendarValue::Integer(index as i64)]; + } + } + } + comp.entries.sort_unstable(); + } + ical.to_string().replace("\r\n", "\n") +} diff --git a/tests/src/webdav/mod.rs b/tests/src/webdav/mod.rs index 0c7c94f5..68b50275 100644 --- a/tests/src/webdav/mod.rs +++ b/tests/src/webdav/mod.rs @@ -48,6 +48,7 @@ use utils::config::Config; pub mod acl; pub mod basic; pub mod cal_alarm; +pub mod cal_itip; pub mod cal_query; pub mod card_query; pub mod copy_move; @@ -71,7 +72,7 @@ pub async fn webdav_tests() { ) .await; - basic::test(&handle).await; + /*basic::test(&handle).await; put_get::test(&handle).await; mkcol::test(&handle).await; copy_move::test(&handle).await; @@ -83,7 +84,8 @@ pub async fn webdav_tests() { acl::test(&handle).await; card_query::test(&handle).await; cal_query::test(&handle).await; - cal_alarm::test(&handle).await; + cal_alarm::test(&handle).await;*/ + cal_itip::test(); // Print elapsed time let elapsed = start_time.elapsed();