CalDAV Scheduling - part 3

This commit is contained in:
mdecimus
2025-06-17 15:04:01 +02:00
parent 55e62a6830
commit 20cb114715
28 changed files with 4091 additions and 1069 deletions

5
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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"] }

View File

@@ -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"

View File

@@ -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]

View File

@@ -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 {

View File

@@ -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},
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<ItipMessage, ItipError> {
) -> Result<Vec<ItipMessage>, 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::<AHashSet<_>>();
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),

View File

@@ -4,20 +4,21 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
common::PartialDateTime,
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod, ICalendarProperty,
ICalendarValue,
},
scheduling::{
use crate::scheduling::{
InstanceId, ItipError, ItipMessage, ItipSnapshots,
attendee::attendee_decline,
itip::{itip_add_tz, itip_build_envelope, itip_finalize},
snapshot::itip_snapshot,
Email, InstanceId, ItipError, ItipMessage, ItipSnapshot, ItipSnapshots,
},
};
use ahash::AHashSet;
use calcard::{
common::PartialDateTime,
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod, ICalendarProperty,
ICalendarStatus, ICalendarValue,
},
};
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<ICalendarComponent> {
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<T, I>(
component_type: ICalendarComponentType,
itip: &ItipSnapshots<'_>,
sequence: i64,
dt_stamp: PartialDateTime,
cancel_guests: AHashSet<&Email>,
) -> ICalendarComponent {
cancel_guests: T,
) -> ICalendarComponent
where
T: Iterator<Item = I>,
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);

View File

@@ -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);
})
}

View File

@@ -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<ItipMessage, ItipError> {
) -> Result<Vec<ItipMessage>, 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),

View File

@@ -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::{
use calcard::icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod,
ICalendarParameter, ICalendarParameterName, ICalendarProperty, ICalendarStatus,
ICalendarValue, Uri,
},
scheduling::{
organizer::organizer_request_full, InstanceId, ItipError, ItipMessage, ItipSnapshots,
},
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::<Vec<_>>(),
));
}
}
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::<AHashSet<_>>();
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
let entries = itip_component
.entries
.iter()
.filter(|entry| changed_entries.contains(&entry.name))
.cloned()
.collect(),
});
.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<MergeAction>) {
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<MergeAction>,
) -> 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::<Vec<_>>(),
));
}
}
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<MergeAction>) {
let mut remove_component_ids = Vec::new();
for action in changes {
match action {

View File

@@ -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,
}
}

View File

@@ -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<InstanceId, ItipSnapshot<'x>>,
}
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct ItipSnapshot<'x> {
pub comp_id: u16,
pub comp: &'x ICalendarComponent,
pub attendees: AHashSet<Attendee<'x>>,
pub dtstamp: Option<&'x PartialDateTime>,
pub entries: AHashSet<ItipEntry<'x>>,
@@ -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<Item = &'x Attendee<'x>> + '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<Item = &Attendee<'_>> + '_ {
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<String>,
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<String, AHashMap<String, ICalendar>> = AHashMap::new();
let mut dtstamp_map: AHashMap<PartialDateTime, usize> = 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<PartialDateTime, usize>) -> String {
use std::fmt::Write;
let mut f = String::new();
let mut to = self.to.iter().map(|t| t.as_str()).collect::<Vec<_>>();
to.sort_unstable();
let mut changed = self
.changed_properties
.iter()
.map(|p| p.as_str())
.collect::<Vec<_>>();
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<PartialDateTime, usize>) -> String {
let mut comps = ical
.components
.iter()
.enumerate()
.filter(|(comp_id, _)| {
ical.components[0]
.component_ids
.contains(&(*comp_id as u16))
})
.collect::<Vec<_>>();
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")
}
}

View File

@@ -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<u16>,
) -> Result<ItipMessage, ItipError> {
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<Vec<ItipMessage>, 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 {
if changed_entries {
for entry in instance.entries.symmetric_difference(&old_instance.entries) {
increment_sequence = increment_sequence
|| changed_attendees
|| instance
.entries
.symmetric_difference(&old_instance.entries)
.any(|entry| {
matches!(
|| matches!(
entry.name,
ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
@@ -54,99 +50,194 @@ 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::<AHashSet<_>>()
.difference(
&instance
.external_attendees()
.map(|attendee| attendee.email.email.as_str())
.collect::<AHashSet<_>>(),
)
.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));
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::ChangeNotAllowed);
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(
// Prepare full updates
let mut messages = Vec::new();
let changed_properties = changed_properties.into_iter().cloned().collect::<Vec<_>>();
if !send_full_update.is_empty() {
match organizer_request_full(
new_ical,
new_itip,
&new_itip,
increment_sequence.then_some(increment_sequences),
false,
);
) {
Ok(mut message) => {
message.changed_properties = changed_properties.clone();
messages.push(message);
}
Err(err) => {
if send_partial_update.is_empty() {
return Err(err);
}
}
}
}
_ => return Err(ItipError::NothingToSend),
};
// Prepare iTIP message
let (ical, itip, is_cancel) = if matches!(method, ICalendarMethod::Cancel) {
// 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);
}
}
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)
};
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();
// 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, comp) in instances {
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 = &ical.components[comp.comp_id as usize];
let orig_component = comp.comp;
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);
}
@@ -159,61 +250,54 @@ pub(crate) fn organizer_handle_update(
sequence,
ItipExportAs::Organizer(&ICalendarParticipationStatus::NeedsAction),
)
} else if let Some(mut cancel_comp) = cancel_component(
ical,
} else {
let mut cancel_comp = build_cancel_component(
orig_component.component_type.clone(),
itip,
comp,
sequence,
dt_stamp.clone(),
&mut recipients,
) {
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
} 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);
let comp_id = message.components.len() as u16;
message.components.push(component);
message.components[0].component_ids.push(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();
// Add timezones
itip_add_tz(&mut message, ical);
if !recipients.is_empty() {
let message = ItipMessage {
method: ICalendarMethod::Request,
messages.push(ItipMessage {
method: method.clone(),
from: itip.organizer.email.email.clone(),
to: recipients.into_iter().map(|e| e.to_string()).collect(),
changed_properties: vec![],
message,
};
Ok(message)
to: emails.into_iter().map(|e| e.to_string()).collect(),
changed_properties: if method == &ICalendarMethod::Request {
changed_properties.clone()
} else {
Err(ItipError::NothingToSend)
vec![]
},
message,
});
}
}
Ok(messages)
}
pub(crate) fn organizer_request_full(
ical: &ICalendar,
itip: ItipSnapshots<'_>,
itip: &ItipSnapshots<'_>,
mut increment_sequence: Option<&mut Vec<u16>>,
include_alarms: bool,
is_first_request: bool,
) -> Result<ItipMessage, ItipError> {
// 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,
})

View File

@@ -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)

View File

@@ -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"

View File

@@ -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"

View File

@@ -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" }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,413 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* 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<String>,
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<String, AHashMap<String, ICalendar>> = AHashMap::new();
let mut dtstamp_map: AHashMap<PartialDateTime, usize> = 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::<Vec<_>>(),
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<PartialDateTime, usize>) -> String;
}
impl ItipMessageExt for ItipMessage {
fn to_string(&self, map: &mut AHashMap<PartialDateTime, usize>) -> String {
use std::fmt::Write;
let mut f = String::new();
let mut to = self.to.iter().map(|t| t.as_str()).collect::<Vec<_>>();
to.sort_unstable();
let mut changed = self
.changed_properties
.iter()
.map(|p| p.as_str())
.collect::<Vec<_>>();
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<PartialDateTime, usize>) -> String {
let mut comps = ical
.components
.iter()
.enumerate()
.filter(|(comp_id, _)| {
ical.components[0]
.component_ids
.contains(&(*comp_id as u16))
})
.collect::<Vec<_>>();
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")
}

View File

@@ -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();