CalDAV Scheduling - part 3
This commit is contained in:
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
265
tests/resources/itip/google_calendar.txt
Normal file
265
tests/resources/itip/google_calendar.txt
Normal 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
|
||||
|
||||
|
||||
|
||||
130
tests/resources/itip/itip_incoming.txt
Normal file
130
tests/resources/itip/itip_incoming.txt
Normal 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
|
||||
|
||||
|
||||
|
||||
322
tests/resources/itip/put_validation.txt
Normal file
322
tests/resources/itip/put_validation.txt
Normal 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
|
||||
|
||||
833
tests/resources/itip/rfc5546_event_recurring.txt
Normal file
833
tests/resources/itip/rfc5546_event_recurring.txt
Normal 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
|
||||
|
||||
683
tests/resources/itip/rfc5546_event_single.txt
Normal file
683
tests/resources/itip/rfc5546_event_single.txt
Normal 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
|
||||
|
||||
|
||||
468
tests/resources/itip/rfc5546_todo.txt
Normal file
468
tests/resources/itip/rfc5546_todo.txt
Normal 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
413
tests/src/webdav/cal_itip.rs
Normal file
413
tests/src/webdav/cal_itip.rs
Normal 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")
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user