CalDAV Scheduling - part 2
This commit is contained in:
@@ -11,9 +11,8 @@ use crate::{
|
||||
ICalendarParticipationStatus, ICalendarProperty, ICalendarValue,
|
||||
},
|
||||
scheduling::{
|
||||
itip::{itip_build_envelope, itip_export_component},
|
||||
itip::{itip_add_tz, itip_build_envelope, itip_export_component, ItipExportAs},
|
||||
Email, InstanceId, ItipEntryValue, ItipError, ItipMessage, ItipSnapshot, ItipSnapshots,
|
||||
SchedulingInfo,
|
||||
},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
@@ -23,7 +22,6 @@ pub(crate) fn attendee_handle_update(
|
||||
new_ical: &ICalendar,
|
||||
old_itip: ItipSnapshots<'_>,
|
||||
new_itip: ItipSnapshots<'_>,
|
||||
info: &mut SchedulingInfo,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
let dt_stamp = PartialDateTime::now();
|
||||
let mut message = ICalendar {
|
||||
@@ -35,52 +33,35 @@ pub(crate) fn attendee_handle_update(
|
||||
|
||||
let mut mail_from = None;
|
||||
let mut email_rcpt = AHashSet::new();
|
||||
let mut tz_resolver = None;
|
||||
|
||||
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 => {
|
||||
// Check added fields
|
||||
for new_entry in old_instance.entries.difference(&instance.entries) {
|
||||
for new_entry in instance.entries.difference(&old_instance.entries) {
|
||||
match (new_entry.name, &new_entry.value) {
|
||||
(ICalendarProperty::Exdate, ItipEntryValue::DateTime(udt))
|
||||
(ICalendarProperty::Exdate, ItipEntryValue::DateTime(date))
|
||||
if instance_id == &InstanceId::Main =>
|
||||
{
|
||||
if let Some(date) = udt.date.to_date_time_with_tz(
|
||||
tz_resolver
|
||||
.get_or_insert_with(|| old_ical.build_tz_resolver())
|
||||
.resolve(udt.tz_id),
|
||||
if let Some((mut cancel_comp, attendee_email)) = attendee_decline(
|
||||
old_ical,
|
||||
instance_id,
|
||||
&old_itip,
|
||||
old_instance,
|
||||
&dt_stamp,
|
||||
&mut email_rcpt,
|
||||
) {
|
||||
if let Some((mut cancel_comp, attendee_email)) =
|
||||
attendee_decline(
|
||||
old_ical,
|
||||
instance_id,
|
||||
&old_itip,
|
||||
old_instance,
|
||||
&dt_stamp,
|
||||
info.sequence,
|
||||
&mut email_rcpt,
|
||||
)
|
||||
{
|
||||
// Add EXDATE as RECURRENCE-ID
|
||||
cancel_comp.add_property(
|
||||
ICalendarProperty::RecurrenceId,
|
||||
ICalendarValue::PartialDateTime(Box::new(
|
||||
PartialDateTime::from_utc_timestamp(
|
||||
date.timestamp(),
|
||||
),
|
||||
)),
|
||||
);
|
||||
// Add EXDATE as RECURRENCE-ID
|
||||
cancel_comp
|
||||
.entries
|
||||
.push(date.to_entry(ICalendarProperty::RecurrenceId));
|
||||
|
||||
// Add cancel component
|
||||
let comp_id = message.components.len() as u16;
|
||||
message.components[0].component_ids.push(comp_id);
|
||||
message.components.push(cancel_comp);
|
||||
mail_from = Some(&attendee_email.email);
|
||||
}
|
||||
} else {
|
||||
return Err(ItipError::ChangeNotAllowed);
|
||||
// Add cancel component
|
||||
let comp_id = message.components.len() as u16;
|
||||
message.components[0].component_ids.push(comp_id);
|
||||
message.components.push(cancel_comp);
|
||||
mail_from = Some(&attendee_email.email);
|
||||
}
|
||||
}
|
||||
(
|
||||
@@ -108,14 +89,20 @@ pub(crate) fn attendee_handle_update(
|
||||
&new_ical.components[instance.comp_id as usize],
|
||||
new_itip.uid,
|
||||
&dt_stamp,
|
||||
info.sequence,
|
||||
None,
|
||||
instance.sequence.unwrap_or_default(),
|
||||
ItipExportAs::Attendee(
|
||||
instance
|
||||
.attendee_delegates(attendee)
|
||||
.chain([attendee])
|
||||
.map(|a| a.entry_id)
|
||||
.collect(),
|
||||
),
|
||||
));
|
||||
mail_from = Some(&attendee.email.email);
|
||||
}
|
||||
|
||||
// Check removed fields
|
||||
for removed_entry in instance.entries.difference(&old_instance.entries) {
|
||||
for removed_entry in old_instance.entries.difference(&instance.entries) {
|
||||
if !matches!(
|
||||
removed_entry.name,
|
||||
ICalendarProperty::Exdate
|
||||
@@ -143,8 +130,14 @@ pub(crate) fn attendee_handle_update(
|
||||
&new_ical.components[instance.comp_id as usize],
|
||||
new_itip.uid,
|
||||
&dt_stamp,
|
||||
info.sequence,
|
||||
None,
|
||||
instance.sequence.unwrap_or_default(),
|
||||
ItipExportAs::Attendee(
|
||||
instance
|
||||
.attendee_delegates(local_attendee)
|
||||
.chain([local_attendee])
|
||||
.map(|a| a.entry_id)
|
||||
.collect(),
|
||||
),
|
||||
));
|
||||
mail_from = Some(&local_attendee.email.email);
|
||||
} else {
|
||||
@@ -162,7 +155,6 @@ pub(crate) fn attendee_handle_update(
|
||||
&old_itip,
|
||||
old_instance,
|
||||
&dt_stamp,
|
||||
info.sequence,
|
||||
&mut email_rcpt,
|
||||
) {
|
||||
// Add cancel component
|
||||
@@ -181,6 +173,9 @@ pub(crate) fn attendee_handle_update(
|
||||
if let Some(from) = mail_from {
|
||||
email_rcpt.insert(&new_itip.organizer.email.email);
|
||||
|
||||
// Add timezones if needed
|
||||
itip_add_tz(&mut message, new_ical);
|
||||
|
||||
Ok(ItipMessage {
|
||||
method: ICalendarMethod::Reply,
|
||||
from: from.to_string(),
|
||||
@@ -195,11 +190,10 @@ pub(crate) fn attendee_handle_update(
|
||||
|
||||
pub(crate) fn attendee_decline<'x>(
|
||||
ical: &'x ICalendar,
|
||||
instance_id: &'x InstanceId<'x>,
|
||||
instance_id: &'x InstanceId,
|
||||
itip: &'x ItipSnapshots<'x>,
|
||||
comp: &'x ItipSnapshot<'x>,
|
||||
dt_stamp: &'x PartialDateTime,
|
||||
sequence: u32,
|
||||
email_rcpt: &mut AHashSet<&'x str>,
|
||||
) -> Option<(ICalendarComponent, &'x Email)> {
|
||||
let component = &ical.components[comp.comp_id as usize];
|
||||
@@ -249,7 +243,7 @@ pub(crate) fn attendee_decline<'x>(
|
||||
);
|
||||
cancel_comp.add_uid(itip.uid);
|
||||
cancel_comp.add_dtstamp(dt_stamp.clone());
|
||||
cancel_comp.add_sequence(sequence);
|
||||
cancel_comp.add_sequence(comp.sequence.unwrap_or_default());
|
||||
|
||||
if let InstanceId::Recurrence(recurrence_id) = instance_id {
|
||||
cancel_comp
|
||||
|
||||
@@ -11,16 +11,17 @@ use crate::{
|
||||
ICalendarValue,
|
||||
},
|
||||
scheduling::{
|
||||
attendee::attendee_decline, itip::itip_build_envelope, snapshot::itip_snapshot, Email,
|
||||
ItipError, ItipMessage, ItipSnapshot, ItipSnapshots, SchedulingInfo,
|
||||
attendee::attendee_decline,
|
||||
itip::{itip_add_tz, itip_build_envelope, itip_finalize},
|
||||
snapshot::itip_snapshot,
|
||||
Email, InstanceId, ItipError, ItipMessage, ItipSnapshot, ItipSnapshots,
|
||||
},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
|
||||
pub fn itip_cancel(
|
||||
ical: &ICalendar,
|
||||
ical: &mut ICalendar,
|
||||
account_emails: &[&str],
|
||||
info: &mut SchedulingInfo,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
// Prepare iTIP message
|
||||
let itip = itip_snapshot(ical, account_emails, false)?;
|
||||
@@ -34,25 +35,30 @@ pub fn itip_cancel(
|
||||
let mut comp = itip_build_envelope(ICalendarMethod::Cancel);
|
||||
comp.component_ids.push(1);
|
||||
message.components.push(comp);
|
||||
let sequence = info.sequence + 1;
|
||||
|
||||
// Fetch guest emails
|
||||
let mut recipients = AHashSet::new();
|
||||
let mut cancel_guests = AHashSet::new();
|
||||
let mut component_type = &ICalendarComponentType::VEvent;
|
||||
for comp in itip.components.values() {
|
||||
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;
|
||||
for attendee in &comp.attendees {
|
||||
if attendee.send_scheduling_messages() {
|
||||
if attendee.send_update_messages() {
|
||||
recipients.insert(attendee.email.email.clone());
|
||||
}
|
||||
cancel_guests.insert(&attendee.email);
|
||||
}
|
||||
|
||||
// Increment sequence if needed
|
||||
increment_sequences.push(comp.comp_id);
|
||||
if instance_id == &InstanceId::Main {
|
||||
sequence = comp.sequence.unwrap_or_default() + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if !recipients.is_empty() && component_type != &ICalendarComponentType::VFreebusy {
|
||||
info.sequence = sequence;
|
||||
|
||||
message.components.push(build_cancel_component(
|
||||
component_type.clone(),
|
||||
&itip,
|
||||
@@ -60,14 +66,17 @@ pub fn itip_cancel(
|
||||
dt_stamp,
|
||||
cancel_guests,
|
||||
));
|
||||
|
||||
Ok(ItipMessage {
|
||||
let message = ItipMessage {
|
||||
method: ICalendarMethod::Cancel,
|
||||
from: itip.organizer.email.email,
|
||||
to: recipients.into_iter().collect(),
|
||||
changed_properties: vec![],
|
||||
message,
|
||||
})
|
||||
};
|
||||
|
||||
itip_finalize(ical, &increment_sequences);
|
||||
|
||||
Ok(message)
|
||||
} else {
|
||||
Err(ItipError::NothingToSend)
|
||||
}
|
||||
@@ -81,15 +90,9 @@ pub fn itip_cancel(
|
||||
let mut mail_from = None;
|
||||
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,
|
||||
info.sequence,
|
||||
&mut email_rcpt,
|
||||
) {
|
||||
if let Some((cancel_comp, attendee_email)) =
|
||||
attendee_decline(ical, instance_id, &itip, comp, &dt_stamp, &mut email_rcpt)
|
||||
{
|
||||
// Add cancel component
|
||||
let comp_id = message.components.len() as u16;
|
||||
message.components[0].component_ids.push(comp_id);
|
||||
@@ -99,15 +102,21 @@ pub fn itip_cancel(
|
||||
}
|
||||
|
||||
if let Some(from) = mail_from {
|
||||
email_rcpt.insert(&itip.organizer.email.email);
|
||||
// Add timezone information if needed
|
||||
itip_add_tz(&mut message, ical);
|
||||
|
||||
Ok(ItipMessage {
|
||||
email_rcpt.insert(&itip.organizer.email.email);
|
||||
let message = ItipMessage {
|
||||
method: ICalendarMethod::Reply,
|
||||
from: from.to_string(),
|
||||
to: email_rcpt.into_iter().map(|e| e.to_string()).collect(),
|
||||
changed_properties: vec![],
|
||||
message,
|
||||
})
|
||||
};
|
||||
|
||||
itip_finalize(ical, &[]);
|
||||
|
||||
Ok(message)
|
||||
} else {
|
||||
Err(ItipError::NothingToSend)
|
||||
}
|
||||
@@ -118,7 +127,7 @@ pub(crate) fn cancel_component<'x>(
|
||||
ical: &'x ICalendar,
|
||||
itip: &'x ItipSnapshots<'x>,
|
||||
comp: &'x ItipSnapshot<'x>,
|
||||
sequence: u32,
|
||||
sequence: i64,
|
||||
dt_stamp: PartialDateTime,
|
||||
recipients: &mut AHashSet<&'x str>,
|
||||
) -> Option<ICalendarComponent> {
|
||||
@@ -127,7 +136,7 @@ pub(crate) fn cancel_component<'x>(
|
||||
let mut has_recipients = false;
|
||||
|
||||
for attendee in &comp.attendees {
|
||||
if attendee.send_scheduling_messages() {
|
||||
if attendee.send_update_messages() {
|
||||
recipients.insert(attendee.email.email.as_str());
|
||||
has_recipients = true;
|
||||
}
|
||||
@@ -150,7 +159,7 @@ pub(crate) fn cancel_component<'x>(
|
||||
fn build_cancel_component(
|
||||
component_type: ICalendarComponentType,
|
||||
itip: &ItipSnapshots<'_>,
|
||||
sequence: u32,
|
||||
sequence: i64,
|
||||
dt_stamp: PartialDateTime,
|
||||
cancel_guests: AHashSet<&Email>,
|
||||
) -> ICalendarComponent {
|
||||
|
||||
@@ -7,15 +7,14 @@
|
||||
use crate::{
|
||||
icalendar::ICalendar,
|
||||
scheduling::{
|
||||
organizer::organizer_request_full, snapshot::itip_snapshot, ItipError, ItipMessage,
|
||||
SchedulingInfo,
|
||||
itip::itip_finalize, organizer::organizer_request_full, snapshot::itip_snapshot, ItipError,
|
||||
ItipMessage,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn itip_create(
|
||||
ical: &ICalendar,
|
||||
ical: &mut ICalendar,
|
||||
account_emails: &[&str],
|
||||
info: &mut SchedulingInfo,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
let itip = itip_snapshot(ical, account_emails, false)?;
|
||||
if !itip.organizer.is_server_scheduling {
|
||||
@@ -23,9 +22,9 @@ pub fn itip_create(
|
||||
} else if !itip.organizer.email.is_local {
|
||||
Err(ItipError::NotOrganizer)
|
||||
} else {
|
||||
let sequence = std::cmp::max(itip.sequence.unwrap_or_default() as u32, info.sequence) + 1;
|
||||
organizer_request_full(ical, itip, sequence, true).inspect(|_| {
|
||||
info.sequence = sequence;
|
||||
let mut sequences = Vec::new();
|
||||
organizer_request_full(ical, itip, Some(&mut sequences), true).inspect(|_| {
|
||||
itip_finalize(ical, &sequences);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,29 +7,31 @@
|
||||
use crate::{
|
||||
icalendar::ICalendar,
|
||||
scheduling::{
|
||||
attendee::attendee_handle_update, event_cancel::itip_cancel,
|
||||
attendee::attendee_handle_update, event_cancel::itip_cancel, itip::itip_finalize,
|
||||
organizer::organizer_handle_update, snapshot::itip_snapshot, ItipError, ItipMessage,
|
||||
SchedulingInfo,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn itip_update(
|
||||
ical: &ICalendar,
|
||||
old_ical: &ICalendar,
|
||||
ical: &mut ICalendar,
|
||||
old_ical: &mut ICalendar,
|
||||
account_emails: &[&str],
|
||||
info: &mut SchedulingInfo,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
let old_itip = itip_snapshot(old_ical, account_emails, false)?;
|
||||
match itip_snapshot(ical, account_emails, false) {
|
||||
Ok(new_itip) => {
|
||||
let mut sequences = Vec::new();
|
||||
if old_itip.organizer.email != new_itip.organizer.email {
|
||||
// RFC 6638 does not support replacing the organizer
|
||||
Err(ItipError::ChangeNotAllowed)
|
||||
Err(ItipError::OrganizerMismatch)
|
||||
} else if old_itip.organizer.email.is_local {
|
||||
organizer_handle_update(old_ical, ical, old_itip, new_itip, info)
|
||||
organizer_handle_update(old_ical, ical, old_itip, new_itip, &mut sequences)
|
||||
} else {
|
||||
attendee_handle_update(old_ical, ical, old_itip, new_itip, info)
|
||||
attendee_handle_update(old_ical, ical, old_itip, new_itip)
|
||||
}
|
||||
.inspect(|_| {
|
||||
itip_finalize(ical, &sequences);
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
match &err {
|
||||
@@ -39,7 +41,7 @@ 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, info)
|
||||
itip_cancel(old_ical, account_emails)
|
||||
} else {
|
||||
Err(ItipError::ChangeNotAllowed)
|
||||
}
|
||||
|
||||
@@ -3,3 +3,480 @@
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use ahash::AHashSet;
|
||||
|
||||
use crate::{
|
||||
icalendar::{
|
||||
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod,
|
||||
ICalendarParameter, ICalendarParameterName, ICalendarProperty, ICalendarStatus,
|
||||
ICalendarValue, Uri,
|
||||
},
|
||||
scheduling::{
|
||||
organizer::organizer_request_full, InstanceId, ItipError, ItipMessage, ItipSnapshots,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MergeAction {
|
||||
AddEntries {
|
||||
component_id: u16,
|
||||
entries: Vec<ICalendarEntry>,
|
||||
},
|
||||
RemoveEntries {
|
||||
component_id: u16,
|
||||
entries: AHashSet<ICalendarProperty>,
|
||||
},
|
||||
AddParameters {
|
||||
component_id: u16,
|
||||
entry_id: u16,
|
||||
parameters: Vec<ICalendarParameter>,
|
||||
},
|
||||
RemoveParameters {
|
||||
component_id: u16,
|
||||
entry_id: u16,
|
||||
parameters: Vec<ICalendarParameterName>,
|
||||
},
|
||||
AddComponent {
|
||||
component: ICalendarComponent,
|
||||
},
|
||||
RemoveComponent {
|
||||
component_id: u16,
|
||||
},
|
||||
}
|
||||
|
||||
pub enum MergeResult {
|
||||
Actions(Vec<MergeAction>),
|
||||
Message(ItipMessage),
|
||||
None,
|
||||
}
|
||||
|
||||
pub fn itip_process_message(
|
||||
ical: &ICalendar,
|
||||
snapshots: ItipSnapshots<'_>,
|
||||
itip: &ICalendar,
|
||||
itip_snapshots: ItipSnapshots<'_>,
|
||||
sender: String,
|
||||
) -> Result<MergeResult, ItipError> {
|
||||
if snapshots.organizer.email != itip_snapshots.organizer.email {
|
||||
return Err(ItipError::OrganizerMismatch);
|
||||
}
|
||||
|
||||
let method = itip_method(itip)?;
|
||||
let mut merge_actions = Vec::new();
|
||||
|
||||
if snapshots.organizer.email.is_local {
|
||||
// Handle attendee updates
|
||||
if snapshots.organizer.email.email == sender {
|
||||
return Err(ItipError::SenderIsOrganizer);
|
||||
}
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
ICalendarMethod::Refresh => {
|
||||
return organizer_request_full(ical, snapshots, None, false).map(|mut message| {
|
||||
message.to = vec![sender];
|
||||
MergeResult::Message(message)
|
||||
});
|
||||
}
|
||||
_ => return Err(ItipError::UnsupportedMethod(method.clone())),
|
||||
}
|
||||
} else {
|
||||
// Handle organizer updates
|
||||
match method {
|
||||
ICalendarMethod::Request => {
|
||||
let mut is_full_update = false;
|
||||
for (instance_id, itip_snapshot) in &itip_snapshots.components {
|
||||
is_full_update = is_full_update || instance_id == &InstanceId::Main;
|
||||
let itip_component = &itip.components[itip_snapshot.comp_id as usize];
|
||||
|
||||
if let Some(snapshot) = snapshots.components.get(instance_id) {
|
||||
// Merge instances
|
||||
if itip_snapshot.sequence.unwrap_or_default()
|
||||
>= snapshot.sequence.unwrap_or_default()
|
||||
{
|
||||
let mut changed_entries = itip_snapshot
|
||||
.entries
|
||||
.symmetric_difference(&snapshot.entries)
|
||||
.map(|entry| entry.name.clone())
|
||||
.collect::<AHashSet<_>>();
|
||||
if itip_snapshot.attendees != snapshot.attendees {
|
||||
changed_entries.insert(ICalendarProperty::Attendee);
|
||||
}
|
||||
if itip_snapshot.dtstamp.is_some()
|
||||
&& itip_snapshot.dtstamp != snapshot.dtstamp
|
||||
{
|
||||
changed_entries.insert(ICalendarProperty::Dtstamp);
|
||||
}
|
||||
changed_entries.insert(ICalendarProperty::Sequence);
|
||||
|
||||
if !changed_entries.is_empty() {
|
||||
merge_actions.push(MergeAction::AddEntries {
|
||||
component_id: snapshot.comp_id,
|
||||
entries: itip_component
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| changed_entries.contains(&entry.name))
|
||||
.cloned()
|
||||
.collect(),
|
||||
});
|
||||
merge_actions.push(MergeAction::RemoveEntries {
|
||||
component_id: snapshot.comp_id,
|
||||
entries: changed_entries,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return Err(ItipError::OutOfSequence);
|
||||
}
|
||||
} else {
|
||||
// Add instance
|
||||
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::Other(_))
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
component_ids: vec![],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if is_full_update {
|
||||
for (instance_id, snapshot) in &snapshots.components {
|
||||
if !itip_snapshots.components.contains_key(instance_id) {
|
||||
// Remove instance
|
||||
merge_actions.push(MergeAction::RemoveComponent {
|
||||
component_id: snapshot.comp_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ICalendarMethod::Add => {
|
||||
for (instance_id, itip_snapshot) in &itip_snapshots.components {
|
||||
if !snapshots.components.contains_key(instance_id) {
|
||||
let itip_component = &itip.components[itip_snapshot.comp_id as usize];
|
||||
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::Other(_))
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
component_ids: vec![],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ICalendarMethod::Cancel => {
|
||||
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];
|
||||
merge_actions.push(MergeAction::RemoveEntries {
|
||||
component_id: snapshot.comp_id,
|
||||
entries: [
|
||||
ICalendarProperty::Organizer,
|
||||
ICalendarProperty::Attendee,
|
||||
ICalendarProperty::Status,
|
||||
ICalendarProperty::Sequence,
|
||||
]
|
||||
.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(),
|
||||
});
|
||||
} else {
|
||||
return Err(ItipError::OutOfSequence);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err(ItipError::UnsupportedMethod(method.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
if !merge_actions.is_empty() {
|
||||
Ok(MergeResult::Actions(merge_actions))
|
||||
} else {
|
||||
Ok(MergeResult::None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn itip_merge_changes(ical: &mut ICalendar, changes: Vec<MergeAction>) {
|
||||
let c = println!("Merging changes: {:?}", changes);
|
||||
|
||||
let mut remove_component_ids = Vec::new();
|
||||
for action in changes {
|
||||
match action {
|
||||
MergeAction::AddEntries {
|
||||
component_id,
|
||||
entries,
|
||||
} => {
|
||||
let component = &mut ical.components[component_id as usize];
|
||||
component.entries.extend(entries);
|
||||
}
|
||||
MergeAction::RemoveEntries {
|
||||
component_id,
|
||||
entries,
|
||||
} => {
|
||||
let component = &mut ical.components[component_id as usize];
|
||||
component
|
||||
.entries
|
||||
.retain(|entry| !entries.contains(&entry.name));
|
||||
}
|
||||
MergeAction::AddParameters {
|
||||
component_id,
|
||||
entry_id,
|
||||
parameters,
|
||||
} => {
|
||||
ical.components[component_id as usize].entries[entry_id as usize]
|
||||
.params
|
||||
.extend(parameters);
|
||||
}
|
||||
MergeAction::RemoveParameters {
|
||||
component_id,
|
||||
entry_id,
|
||||
parameters,
|
||||
} => {
|
||||
ical.components[component_id as usize].entries[entry_id as usize]
|
||||
.params
|
||||
.retain(|param| !parameters.iter().any(|p| param.matches_name(p)));
|
||||
}
|
||||
MergeAction::AddComponent { component } => {
|
||||
let comp_id = ical.components.len() as u16;
|
||||
if let Some(root) = ical
|
||||
.components
|
||||
.get_mut(0)
|
||||
.filter(|c| c.component_type == ICalendarComponentType::VCalendar)
|
||||
{
|
||||
root.component_ids.push(comp_id);
|
||||
ical.components.push(component);
|
||||
}
|
||||
}
|
||||
MergeAction::RemoveComponent { component_id } => {
|
||||
remove_component_ids.push(component_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !remove_component_ids.is_empty() {
|
||||
ical.remove_component_ids(&remove_component_ids);
|
||||
}
|
||||
}
|
||||
|
||||
fn itip_method(ical: &ICalendar) -> Result<&ICalendarMethod, ItipError> {
|
||||
let todo = "validate max size of components before saving + max itip message size";
|
||||
let todo2 = "make sure root is vcalendar and all components are of the same type";
|
||||
ical.components
|
||||
.first()
|
||||
.and_then(|comp| {
|
||||
comp.entries.iter().find_map(|entry| {
|
||||
if entry.name == ICalendarProperty::Method {
|
||||
entry.values.first().and_then(|value| {
|
||||
if let ICalendarValue::Method(method) = value {
|
||||
Some(method)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.ok_or(ItipError::MissingMethod)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::{
|
||||
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod,
|
||||
ICalendarParameter, ICalendarParticipationStatus, ICalendarProperty, ICalendarValue,
|
||||
},
|
||||
scheduling::ItipError,
|
||||
};
|
||||
|
||||
pub(crate) fn itip_build_envelope(method: ICalendarMethod) -> ICalendarComponent {
|
||||
@@ -25,7 +26,9 @@ pub(crate) fn itip_build_envelope(method: ICalendarMethod) -> ICalendarComponent
|
||||
ICalendarEntry {
|
||||
name: ICalendarProperty::Prodid,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Text("Stalwart".to_string())],
|
||||
values: vec![ICalendarValue::Text(
|
||||
"-//Stalwart Labs LLC//Stalwart Server//EN".to_string(),
|
||||
)],
|
||||
},
|
||||
ICalendarEntry {
|
||||
name: ICalendarProperty::Method,
|
||||
@@ -37,12 +40,17 @@ pub(crate) fn itip_build_envelope(method: ICalendarMethod) -> ICalendarComponent
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ItipExportAs<'x> {
|
||||
Organizer(&'x ICalendarParticipationStatus),
|
||||
Attendee(Vec<u16>),
|
||||
}
|
||||
|
||||
pub(crate) fn itip_export_component(
|
||||
component: &ICalendarComponent,
|
||||
uid: &str,
|
||||
dt_stamp: &PartialDateTime,
|
||||
sequence: u32,
|
||||
export_as_organizer: Option<&ICalendarParticipationStatus>,
|
||||
sequence: i64,
|
||||
export_as: ItipExportAs<'_>,
|
||||
) -> ICalendarComponent {
|
||||
let mut comp = ICalendarComponent {
|
||||
component_type: component.component_type.clone(),
|
||||
@@ -54,45 +62,68 @@ pub(crate) fn itip_export_component(
|
||||
comp.add_sequence(sequence);
|
||||
comp.add_uid(uid);
|
||||
|
||||
for entry in &component.entries {
|
||||
for (entry_id, entry) in component.entries.iter().enumerate() {
|
||||
match &entry.name {
|
||||
ICalendarProperty::Organizer | ICalendarProperty::Attendee => {
|
||||
let mut new_entry = ICalendarEntry {
|
||||
name: entry.name.clone(),
|
||||
params: Vec::with_capacity(entry.params.len()),
|
||||
values: entry.values.clone(),
|
||||
};
|
||||
let mut has_partstat = false;
|
||||
ICalendarProperty::Organizer | ICalendarProperty::Attendee => match &export_as {
|
||||
ItipExportAs::Organizer(partstat) => {
|
||||
let mut new_entry = ICalendarEntry {
|
||||
name: entry.name.clone(),
|
||||
params: Vec::with_capacity(entry.params.len()),
|
||||
values: entry.values.clone(),
|
||||
};
|
||||
let mut has_partstat = false;
|
||||
|
||||
for entry in &entry.params {
|
||||
match entry {
|
||||
ICalendarParameter::ScheduleStatus(_)
|
||||
| ICalendarParameter::ScheduleAgent(_)
|
||||
| ICalendarParameter::ScheduleForceSend(_) => {}
|
||||
_ => {
|
||||
has_partstat =
|
||||
has_partstat || matches!(entry, ICalendarParameter::Partstat(_));
|
||||
new_entry.params.push(entry.clone())
|
||||
for entry in &entry.params {
|
||||
match entry {
|
||||
ICalendarParameter::ScheduleStatus(_)
|
||||
| ICalendarParameter::ScheduleAgent(_)
|
||||
| ICalendarParameter::ScheduleForceSend(_) => {}
|
||||
_ => {
|
||||
has_partstat = has_partstat
|
||||
|| matches!(entry, ICalendarParameter::Partstat(_));
|
||||
new_entry.params.push(entry.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_partstat && entry.name == ICalendarProperty::Attendee {
|
||||
if let Some(partstat) = export_as_organizer {
|
||||
if !has_partstat && entry.name == ICalendarProperty::Attendee {
|
||||
new_entry
|
||||
.params
|
||||
.push(ICalendarParameter::Partstat(partstat.clone()));
|
||||
.push(ICalendarParameter::Partstat((*partstat).clone()));
|
||||
}
|
||||
}
|
||||
|
||||
comp.entries.push(new_entry);
|
||||
}
|
||||
comp.entries.push(new_entry);
|
||||
}
|
||||
ItipExportAs::Attendee(attendee_entry_ids)
|
||||
if attendee_entry_ids.contains(&(entry_id as u16))
|
||||
|| entry.name == ICalendarProperty::Organizer =>
|
||||
{
|
||||
comp.entries.push(ICalendarEntry {
|
||||
name: entry.name.clone(),
|
||||
params: entry
|
||||
.params
|
||||
.iter()
|
||||
.filter(|param| {
|
||||
!matches!(
|
||||
param,
|
||||
ICalendarParameter::ScheduleStatus(_)
|
||||
| ICalendarParameter::ScheduleAgent(_)
|
||||
| ICalendarParameter::ScheduleForceSend(_)
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
values: entry.values.clone(),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ICalendarProperty::RequestStatus
|
||||
| ICalendarProperty::Dtstamp
|
||||
| ICalendarProperty::Sequence
|
||||
| ICalendarProperty::Uid => {}
|
||||
_ => {
|
||||
if export_as_organizer.is_some()
|
||||
if matches!(export_as, ItipExportAs::Organizer(_))
|
||||
|| matches!(entry.name, ICalendarProperty::RecurrenceId)
|
||||
{
|
||||
comp.entries.push(entry.clone());
|
||||
@@ -101,11 +132,21 @@ pub(crate) fn itip_export_component(
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(export_as, ItipExportAs::Attendee(_)) {
|
||||
comp.entries.push(ICalendarEntry {
|
||||
name: ICalendarProperty::RequestStatus,
|
||||
params: vec![],
|
||||
values: vec![
|
||||
ICalendarValue::Text("2.0".to_string()),
|
||||
ICalendarValue::Text("Success".to_string()),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
comp
|
||||
}
|
||||
|
||||
pub fn itip_remove_info(ical: &mut ICalendar) {
|
||||
let todo = "call after insert or update";
|
||||
pub(crate) fn itip_finalize(ical: &mut ICalendar, scheduling_object_ids: &[u16]) {
|
||||
for comp in ical.components.iter_mut() {
|
||||
if comp.component_type.is_scheduling_object() {
|
||||
// Remove scheduling info from non-updated components
|
||||
@@ -114,15 +155,71 @@ pub fn itip_remove_info(ical: &mut ICalendar) {
|
||||
entry.name,
|
||||
ICalendarProperty::Organizer | ICalendarProperty::Attendee
|
||||
) {
|
||||
entry.params.retain(|param| {
|
||||
!matches!(
|
||||
param,
|
||||
ICalendarParameter::Rsvp(true)
|
||||
| ICalendarParameter::ScheduleForceSend(_)
|
||||
)
|
||||
});
|
||||
entry
|
||||
.params
|
||||
.retain(|param| !matches!(param, ICalendarParameter::ScheduleForceSend(_)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for comp_id in scheduling_object_ids {
|
||||
let comp = &mut ical.components[*comp_id as usize];
|
||||
let mut found_sequence = false;
|
||||
for entry in &mut comp.entries {
|
||||
if entry.name == ICalendarProperty::Sequence {
|
||||
if let Some(ICalendarValue::Integer(seq)) = entry.values.first_mut() {
|
||||
*seq += 1;
|
||||
} else {
|
||||
entry.values = vec![ICalendarValue::Integer(1)];
|
||||
}
|
||||
found_sequence = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found_sequence {
|
||||
comp.add_sequence(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if message.components.iter().any(|c| {
|
||||
has_timezones = has_timezones || c.component_type == ICalendarComponentType::VTimezone;
|
||||
|
||||
!has_timezones
|
||||
&& c.entries.iter().any(|e| {
|
||||
e.params
|
||||
.iter()
|
||||
.any(|p| matches!(p, ICalendarParameter::Tzid(_)))
|
||||
})
|
||||
}) && !has_timezones
|
||||
{
|
||||
message.copy_timezones(ical);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
use crate::{
|
||||
common::PartialDateTime,
|
||||
icalendar::{
|
||||
ICalendar, ICalendarComponentType, ICalendarDuration, ICalendarMethod,
|
||||
ICalendarParticipationRole, ICalendarParticipationStatus, ICalendarPeriod,
|
||||
ICalendarProperty, ICalendarRecurrenceRule, ICalendarScheduleForceSendValue,
|
||||
ICalendarStatus, ICalendarUserTypes, Uri,
|
||||
ICalendar, ICalendarComponentType, ICalendarDuration, ICalendarEntry, ICalendarMethod,
|
||||
ICalendarParameter, ICalendarParticipationRole, ICalendarParticipationStatus,
|
||||
ICalendarPeriod, ICalendarProperty, ICalendarRecurrenceRule,
|
||||
ICalendarScheduleForceSendValue, ICalendarStatus, ICalendarUserTypes, ICalendarValue, Uri,
|
||||
},
|
||||
};
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
@@ -25,11 +25,11 @@ pub mod itip;
|
||||
pub mod organizer;
|
||||
pub mod snapshot;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ItipSnapshots<'x> {
|
||||
pub organizer: Organizer<'x>,
|
||||
pub uid: &'x str,
|
||||
pub sequence: Option<i64>,
|
||||
pub components: AHashMap<InstanceId<'x>, ItipSnapshot<'x>>,
|
||||
pub components: AHashMap<InstanceId, ItipSnapshot<'x>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -38,6 +38,7 @@ pub struct ItipSnapshot<'x> {
|
||||
pub attendees: AHashSet<Attendee<'x>>,
|
||||
pub dtstamp: Option<&'x PartialDateTime>,
|
||||
pub entries: AHashSet<ItipEntry<'x>>,
|
||||
pub sequence: Option<i64>,
|
||||
pub request_status: Vec<&'x str>,
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ pub struct ItipEntry<'x> {
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ItipEntryValue<'x> {
|
||||
DateTime(UnresolvedDateTime<'x>),
|
||||
DateTime(ItipDateTime<'x>),
|
||||
Period(&'x ICalendarPeriod),
|
||||
Duration(&'x ICalendarDuration),
|
||||
Status(&'x ICalendarStatus),
|
||||
@@ -58,22 +59,23 @@ pub enum ItipEntryValue<'x> {
|
||||
Integer(i64),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct UnresolvedDateTime<'x> {
|
||||
#[derive(Debug)]
|
||||
pub struct ItipDateTime<'x> {
|
||||
pub date: &'x PartialDateTime,
|
||||
pub tz_id: Option<&'x str>,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum InstanceId<'x> {
|
||||
pub enum InstanceId {
|
||||
Main,
|
||||
Recurrence(RecurrenceId<'x>),
|
||||
Recurrence(RecurrenceId),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RecurrenceId<'x> {
|
||||
pub struct RecurrenceId {
|
||||
pub entry_id: u16,
|
||||
pub date: UnresolvedDateTime<'x>,
|
||||
pub date: i64,
|
||||
pub this_and_future: bool,
|
||||
}
|
||||
|
||||
@@ -106,12 +108,6 @@ pub struct Email {
|
||||
pub is_local: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SchedulingInfo {
|
||||
pub sequence: u32,
|
||||
pub tag: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ItipError {
|
||||
NoSchedulingInfo,
|
||||
@@ -121,11 +117,18 @@ pub enum ItipError {
|
||||
NothingToSend,
|
||||
MissingUid,
|
||||
MultipleUid,
|
||||
MultipleSequence,
|
||||
MultipleOrganizer,
|
||||
MultipleObjectTypes,
|
||||
MultipleObjectInstances,
|
||||
ChangeNotAllowed,
|
||||
OrganizerMismatch,
|
||||
MissingMethod,
|
||||
InvalidComponentType,
|
||||
OutOfSequence,
|
||||
SenderIsOrganizer,
|
||||
SenderIsNotParticipant(String),
|
||||
UnknownParticipant(String),
|
||||
UnsupportedMethod(ICalendarMethod),
|
||||
}
|
||||
|
||||
pub struct ItipMessage {
|
||||
@@ -160,18 +163,41 @@ impl ItipSnapshot<'_> {
|
||||
.iter()
|
||||
.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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Attendee<'_> {
|
||||
pub fn send_scheduling_messages(&self) -> bool {
|
||||
pub fn send_invite_messages(&self) -> bool {
|
||||
!self.email.is_local
|
||||
&& self.is_server_scheduling
|
||||
&& self.rsvp.is_none_or(|rsvp| rsvp)
|
||||
&& (self.force_send.is_some()
|
||||
|| self.part_stat.is_none_or(|part_stat| {
|
||||
part_stat != &ICalendarParticipationStatus::NeedsAction
|
||||
part_stat == &ICalendarParticipationStatus::NeedsAction
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn send_update_messages(&self) -> bool {
|
||||
!self.email.is_local
|
||||
&& self.is_server_scheduling
|
||||
&& self.rsvp.is_none_or(|rsvp| rsvp)
|
||||
&& (self.force_send.is_some()
|
||||
|| self
|
||||
.part_stat
|
||||
.is_none_or(|part_stat| part_stat != &ICalendarParticipationStatus::Declined))
|
||||
}
|
||||
}
|
||||
|
||||
impl Email {
|
||||
@@ -238,17 +264,406 @@ impl PartialEq for Email {
|
||||
|
||||
impl Eq for Email {}
|
||||
|
||||
impl PartialEq for RecurrenceId<'_> {
|
||||
impl PartialEq for RecurrenceId {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.date == other.date && self.this_and_future == other.this_and_future
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for RecurrenceId<'_> {}
|
||||
impl Eq for RecurrenceId {}
|
||||
|
||||
impl Hash for RecurrenceId<'_> {
|
||||
impl Hash for RecurrenceId {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.date.hash(state);
|
||||
self.this_and_future.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ItipDateTime<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.timestamp == other.timestamp
|
||||
}
|
||||
}
|
||||
impl Eq for ItipDateTime<'_> {}
|
||||
|
||||
impl Hash for ItipDateTime<'_> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.timestamp.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl ItipDateTime<'_> {
|
||||
pub fn to_entry(&self, name: ICalendarProperty) -> ICalendarEntry {
|
||||
ICalendarEntry {
|
||||
name,
|
||||
params: self
|
||||
.tz_id
|
||||
.map(|tz_id| vec![ICalendarParameter::Tzid(tz_id.to_string())])
|
||||
.unwrap_or_default(),
|
||||
values: vec![ICalendarValue::PartialDateTime(Box::new(self.date.clone()))],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ use crate::{
|
||||
},
|
||||
scheduling::{
|
||||
event_cancel::cancel_component,
|
||||
itip::{itip_build_envelope, itip_export_component},
|
||||
InstanceId, ItipError, ItipMessage, ItipSnapshots, SchedulingInfo,
|
||||
itip::{itip_build_envelope, itip_export_component, ItipExportAs},
|
||||
InstanceId, ItipError, ItipMessage, ItipSnapshots,
|
||||
},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
@@ -23,7 +23,7 @@ pub(crate) fn organizer_handle_update(
|
||||
new_ical: &ICalendar,
|
||||
old_itip: ItipSnapshots<'_>,
|
||||
new_itip: ItipSnapshots<'_>,
|
||||
info: &mut SchedulingInfo,
|
||||
increment_sequences: &mut Vec<u16>,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
let mut added_instances = Vec::new();
|
||||
let mut deleted_instances = Vec::new();
|
||||
@@ -43,11 +43,17 @@ pub(crate) fn organizer_handle_update(
|
||||
.entries
|
||||
.symmetric_difference(&old_instance.entries)
|
||||
.any(|entry| {
|
||||
!matches!(
|
||||
matches!(
|
||||
entry.name,
|
||||
ICalendarProperty::Summary
|
||||
| ICalendarProperty::Description
|
||||
| ICalendarProperty::Priority
|
||||
ICalendarProperty::Dtstart
|
||||
| ICalendarProperty::Dtend
|
||||
| ICalendarProperty::Duration
|
||||
| ICalendarProperty::Due
|
||||
| ICalendarProperty::Rrule
|
||||
| ICalendarProperty::Rdate
|
||||
| ICalendarProperty::Exdate
|
||||
| ICalendarProperty::Status
|
||||
| ICalendarProperty::Location
|
||||
)
|
||||
});
|
||||
|
||||
@@ -79,12 +85,6 @@ pub(crate) fn organizer_handle_update(
|
||||
}
|
||||
}
|
||||
|
||||
let sequence = if increment_sequence {
|
||||
info.sequence + 1
|
||||
} else {
|
||||
info.sequence
|
||||
};
|
||||
|
||||
let (method, instances) = match (
|
||||
!added_instances.is_empty(),
|
||||
!deleted_instances.is_empty(),
|
||||
@@ -105,9 +105,12 @@ pub(crate) fn organizer_handle_update(
|
||||
}
|
||||
(_, _, _, true) => {
|
||||
// Send full REQUEST message
|
||||
return organizer_request_full(new_ical, new_itip, sequence, false).inspect(|_| {
|
||||
info.sequence = sequence;
|
||||
});
|
||||
return organizer_request_full(
|
||||
new_ical,
|
||||
new_itip,
|
||||
increment_sequence.then_some(increment_sequences),
|
||||
false,
|
||||
);
|
||||
}
|
||||
_ => return Err(ItipError::NothingToSend),
|
||||
};
|
||||
@@ -126,26 +129,35 @@ pub(crate) fn organizer_handle_update(
|
||||
|
||||
let mut recipients = AHashSet::new();
|
||||
let mut copy_components = AHashSet::new();
|
||||
let mut scheduling_component_ids = Vec::with_capacity(ical.components.len());
|
||||
let mut increment_sequences = Vec::new();
|
||||
|
||||
for (instance_id, comp) in instances {
|
||||
// Prepare component for iTIP
|
||||
let sequence = if increment_sequence {
|
||||
comp.sequence.unwrap_or_default() + 1
|
||||
} else {
|
||||
comp.sequence.unwrap_or_default()
|
||||
};
|
||||
let orig_component = &ical.components[comp.comp_id as usize];
|
||||
let component = if !is_cancel {
|
||||
// Add attendees
|
||||
for attendee in &comp.attendees {
|
||||
if attendee.send_scheduling_messages() {
|
||||
if attendee.send_update_messages() {
|
||||
recipients.insert(attendee.email.email.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
if increment_sequence {
|
||||
increment_sequences.push(comp.comp_id);
|
||||
}
|
||||
|
||||
// Export component with updated sequence and participation status
|
||||
itip_export_component(
|
||||
orig_component,
|
||||
itip.uid,
|
||||
&dt_stamp,
|
||||
sequence,
|
||||
Some(&ICalendarParticipationStatus::NeedsAction),
|
||||
ItipExportAs::Organizer(&ICalendarParticipationStatus::NeedsAction),
|
||||
)
|
||||
} else if let Some(mut cancel_comp) = cancel_component(
|
||||
ical,
|
||||
@@ -166,7 +178,6 @@ pub(crate) fn organizer_handle_update(
|
||||
};
|
||||
|
||||
// Add component to message
|
||||
scheduling_component_ids.push(comp.comp_id);
|
||||
message.components[comp.comp_id as usize] = component;
|
||||
message.components[0].component_ids.push(comp.comp_id);
|
||||
}
|
||||
@@ -175,25 +186,24 @@ pub(crate) fn organizer_handle_update(
|
||||
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.push(comp.clone());
|
||||
message.components[0].component_ids.push(comp_id as u16);
|
||||
message.components[comp_id] = comp.clone();
|
||||
}
|
||||
message.components[0].component_ids.sort_unstable();
|
||||
|
||||
if !recipients.is_empty() {
|
||||
if increment_sequence {
|
||||
info.sequence = sequence;
|
||||
}
|
||||
|
||||
Ok(ItipMessage {
|
||||
let message = ItipMessage {
|
||||
method: ICalendarMethod::Request,
|
||||
from: itip.organizer.email.email.clone(),
|
||||
to: recipients.into_iter().map(|e| e.to_string()).collect(),
|
||||
changed_properties: vec![],
|
||||
message,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(message)
|
||||
} else {
|
||||
Err(ItipError::NothingToSend)
|
||||
}
|
||||
@@ -202,7 +212,7 @@ pub(crate) fn organizer_handle_update(
|
||||
pub(crate) fn organizer_request_full(
|
||||
ical: &ICalendar,
|
||||
itip: ItipSnapshots<'_>,
|
||||
sequence: u32,
|
||||
mut increment_sequence: Option<&mut Vec<u16>>,
|
||||
include_alarms: bool,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
// Prepare iTIP message
|
||||
@@ -214,19 +224,23 @@ pub(crate) fn organizer_request_full(
|
||||
|
||||
let mut recipients = AHashSet::new();
|
||||
let mut copy_components = AHashSet::new();
|
||||
let mut scheduling_component_ids = Vec::with_capacity(itip.components.len());
|
||||
|
||||
for comp in itip.components.into_values() {
|
||||
// Prepare component for iTIP
|
||||
let sequence = if let Some(increment_sequence) = &mut increment_sequence {
|
||||
increment_sequence.push(comp.comp_id);
|
||||
comp.sequence.unwrap_or_default() + 1
|
||||
} else {
|
||||
comp.sequence.unwrap_or_default()
|
||||
};
|
||||
let orig_component = &ical.components[comp.comp_id as usize];
|
||||
let mut component = itip_export_component(
|
||||
orig_component,
|
||||
itip.uid,
|
||||
&dt_stamp,
|
||||
sequence,
|
||||
Some(&ICalendarParticipationStatus::NeedsAction),
|
||||
ItipExportAs::Organizer(&ICalendarParticipationStatus::NeedsAction),
|
||||
);
|
||||
scheduling_component_ids.push(comp.comp_id);
|
||||
|
||||
// Add VALARM sub-components
|
||||
if include_alarms {
|
||||
@@ -247,7 +261,7 @@ pub(crate) fn organizer_request_full(
|
||||
|
||||
// Add attendees
|
||||
for attendee in comp.attendees {
|
||||
if attendee.send_scheduling_messages() {
|
||||
if attendee.send_invite_messages() {
|
||||
recipients.insert(attendee.email.email);
|
||||
}
|
||||
}
|
||||
@@ -257,12 +271,13 @@ pub(crate) fn organizer_request_full(
|
||||
for (comp_id, comp) in ical.components.iter().enumerate() {
|
||||
if 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.push(comp.clone());
|
||||
message.components[0].component_ids.push(comp_id as u16);
|
||||
message.components[comp_id] = comp.clone();
|
||||
}
|
||||
message.components[0].component_ids.sort_unstable();
|
||||
|
||||
if !recipients.is_empty() {
|
||||
Ok(ItipMessage {
|
||||
|
||||
@@ -10,23 +10,23 @@ use crate::{
|
||||
ICalendarValue, Uri,
|
||||
},
|
||||
scheduling::{
|
||||
Attendee, Email, InstanceId, ItipEntry, ItipEntryValue, ItipError, ItipSnapshot,
|
||||
ItipSnapshots, Organizer, RecurrenceId, UnresolvedDateTime,
|
||||
Attendee, Email, InstanceId, ItipDateTime, ItipEntry, ItipEntryValue, ItipError,
|
||||
ItipSnapshot, ItipSnapshots, Organizer, RecurrenceId,
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
|
||||
pub(crate) fn itip_snapshot<'x>(
|
||||
pub(crate) fn itip_snapshot<'x, 'y>(
|
||||
ical: &'x ICalendar,
|
||||
account_emails: &'x [&'x str],
|
||||
account_emails: &'y [&'y str],
|
||||
force_add_client_scheduling: bool,
|
||||
) -> Result<ItipSnapshots<'x>, ItipError> {
|
||||
let mut organizer: Option<Organizer<'x>> = None;
|
||||
let mut uid: Option<&'x str> = None;
|
||||
let mut sequence: Option<i64> = None;
|
||||
let mut components = AHashMap::new();
|
||||
let mut expect_object_type = None;
|
||||
let mut has_local_emails = false;
|
||||
let mut tz_resolver = None;
|
||||
|
||||
for (comp_id, comp) in ical.components.iter().enumerate() {
|
||||
if comp.component_type.is_scheduling_object()
|
||||
@@ -185,40 +185,41 @@ pub(crate) fn itip_snapshot<'x>(
|
||||
}
|
||||
}
|
||||
ICalendarProperty::Sequence => {
|
||||
if let Some(sequence_) = entry.values.first().and_then(|v| v.as_integer()) {
|
||||
match sequence {
|
||||
Some(existing_sequence) if existing_sequence != sequence_ => {
|
||||
return Err(ItipError::MultipleSequence);
|
||||
}
|
||||
None => {
|
||||
sequence = Some(sequence_);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(sequence) = entry.values.first().and_then(|v| v.as_integer()) {
|
||||
sched_comp.sequence = Some(sequence);
|
||||
}
|
||||
}
|
||||
ICalendarProperty::RecurrenceId => {
|
||||
if let Some(date) =
|
||||
entry.values.first().and_then(|v| v.as_partial_date_time())
|
||||
{
|
||||
let mut recurrence_id = RecurrenceId {
|
||||
entry_id: entry_id as u16,
|
||||
date: UnresolvedDateTime { date, tz_id: None },
|
||||
this_and_future: false,
|
||||
};
|
||||
let mut this_and_future = false;
|
||||
let mut tz_id = None;
|
||||
|
||||
for param in &entry.params {
|
||||
match param {
|
||||
ICalendarParameter::Tzid(id) => {
|
||||
recurrence_id.date.tz_id = Some(id.as_str());
|
||||
tz_id = Some(id.as_str());
|
||||
}
|
||||
ICalendarParameter::Range => {
|
||||
recurrence_id.this_and_future = true;
|
||||
this_and_future = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
instance_id = InstanceId::Recurrence(recurrence_id);
|
||||
instance_id = InstanceId::Recurrence(RecurrenceId {
|
||||
entry_id: entry_id as u16,
|
||||
date: date
|
||||
.to_date_time_with_tz(
|
||||
tz_resolver
|
||||
.get_or_insert_with(|| ical.build_tz_resolver())
|
||||
.resolve(tz_id),
|
||||
)
|
||||
.map(|dt| dt.timestamp())
|
||||
.unwrap_or_else(|| date.to_timestamp().unwrap_or_default()),
|
||||
this_and_future,
|
||||
});
|
||||
}
|
||||
}
|
||||
ICalendarProperty::RequestStatus => {
|
||||
@@ -241,17 +242,29 @@ pub(crate) fn itip_snapshot<'x>(
|
||||
| ICalendarProperty::Location
|
||||
| ICalendarProperty::Summary
|
||||
| ICalendarProperty::Description
|
||||
| ICalendarProperty::Priority => {
|
||||
| ICalendarProperty::Priority
|
||||
| ICalendarProperty::PercentComplete
|
||||
| ICalendarProperty::Completed => {
|
||||
let tz_id = entry.tz_id();
|
||||
for value in &entry.values {
|
||||
let value = match value {
|
||||
ICalendarValue::Uri(Uri::Location(v)) => {
|
||||
ItipEntryValue::Text(v.as_str())
|
||||
}
|
||||
ICalendarValue::PartialDateTime(partial_date_time) => {
|
||||
ItipEntryValue::DateTime(UnresolvedDateTime {
|
||||
date: partial_date_time,
|
||||
ICalendarValue::PartialDateTime(date) => {
|
||||
ItipEntryValue::DateTime(ItipDateTime {
|
||||
date: date.as_ref(),
|
||||
tz_id,
|
||||
timestamp: date
|
||||
.to_date_time_with_tz(
|
||||
tz_resolver
|
||||
.get_or_insert_with(|| ical.build_tz_resolver())
|
||||
.resolve(tz_id),
|
||||
)
|
||||
.map(|dt| dt.timestamp())
|
||||
.unwrap_or_else(|| {
|
||||
date.to_timestamp().unwrap_or_default()
|
||||
}),
|
||||
})
|
||||
}
|
||||
ICalendarValue::Duration(v) => ItipEntryValue::Duration(v),
|
||||
@@ -282,7 +295,6 @@ pub(crate) fn itip_snapshot<'x>(
|
||||
Ok(ItipSnapshots {
|
||||
organizer: organizer.ok_or(ItipError::NoSchedulingInfo)?,
|
||||
uid: uid.ok_or(ItipError::MissingUid)?,
|
||||
sequence,
|
||||
components,
|
||||
})
|
||||
} else if !has_local_emails {
|
||||
@@ -291,3 +303,11 @@ pub(crate) fn itip_snapshot<'x>(
|
||||
Err(ItipError::NoSchedulingInfo)
|
||||
}
|
||||
}
|
||||
|
||||
impl ItipSnapshot<'_> {
|
||||
pub fn attendee_by_email(&self, email: &str) -> Option<&Attendee<'_>> {
|
||||
self.attendees
|
||||
.iter()
|
||||
.find(|attendee| attendee.email.email == email)
|
||||
}
|
||||
}
|
||||
|
||||
418
tests/resources/itip/rfc6638_recurring.txt
Normal file
418
tests/resources/itip/rfc6638_recurring.txt
Normal file
@@ -0,0 +1,418 @@
|
||||
# RFC6638 - Recurring Event Scheduling
|
||||
|
||||
# Organizer invites participant to a recurring event
|
||||
> put cyrus@example.com 9263504FD3AD
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Montreal
|
||||
BEGIN:STANDARD
|
||||
DTSTART:20071104T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
TZNAME:EST
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:9263504FD3AD
|
||||
SEQUENCE:0
|
||||
DTSTAMP:20090602T185254Z
|
||||
DTSTART;TZID=America/Montreal:20090601T150000
|
||||
DTEND;TZID=America/Montreal:20090601T160000
|
||||
RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5
|
||||
TRANSP:OPAQUE
|
||||
SUMMARY:Review Internet-Draft
|
||||
ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
> expect
|
||||
from: cyrus@example.com
|
||||
to: bernard@example.net
|
||||
changes:
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Stalwart Labs LLC//Stalwart Server//EN
|
||||
METHOD:REQUEST
|
||||
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
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP=
|
||||
TRUE;PARTSTAT=NEEDS-ACTION:mailto:bernard@example.net
|
||||
END:VEVENT
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Montreal
|
||||
BEGIN:STANDARD
|
||||
DTSTART:20071104T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
|
||||
TZNAME:EST
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
||||
|
||||
# Send iTIP message to participant
|
||||
> send
|
||||
|
||||
# Make sure the participant receives the event
|
||||
> get bernard@example.net 9263504FD3AD
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Stalwart Labs LLC//Stalwart Server//EN
|
||||
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
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP=
|
||||
TRUE;PARTSTAT=NEEDS-ACTION:mailto:bernard@example.net
|
||||
END:VEVENT
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Montreal
|
||||
BEGIN:STANDARD
|
||||
DTSTART:20071104T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
|
||||
TZNAME:EST
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
||||
|
||||
# Participant declines an instance of the recurring event
|
||||
> put bernard@example.net 9263504FD3AD
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Montreal
|
||||
BEGIN:STANDARD
|
||||
DTSTART:20071104T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
TZNAME:EST
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:9263504FD3AD
|
||||
SEQUENCE:1
|
||||
DTSTAMP:20090602T185254Z
|
||||
DTSTART;TZID=America/Montreal:20090601T150000
|
||||
DTEND;TZID=America/Montreal:20090601T160000
|
||||
RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5
|
||||
TRANSP:OPAQUE
|
||||
SUMMARY:Review Internet-Draft
|
||||
ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:9263504FD3AD
|
||||
SEQUENCE:1
|
||||
DTSTAMP:20090603T183823Z
|
||||
RECURRENCE-ID;TZID=America/Montreal:20090602T150000
|
||||
DTSTART;TZID=America/Montreal:20090602T150000
|
||||
DTEND;TZID=America/Montreal:20090602T160000
|
||||
TRANSP:TRANSPARENT
|
||||
SUMMARY:Review Internet-Draft
|
||||
ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
> expect
|
||||
from: bernard@example.net
|
||||
to: cyrus@example.com
|
||||
changes:
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Stalwart Labs LLC//Stalwart Server//EN
|
||||
METHOD:REPLY
|
||||
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
|
||||
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
|
||||
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
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
||||
|
||||
# Send iTIP message to organizer
|
||||
> send
|
||||
|
||||
# Organizer receives the response
|
||||
> get cyrus@example.com 9263504FD3AD
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
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
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP=
|
||||
TRUE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:bernard@example.net
|
||||
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
|
||||
END:VEVENT
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Montreal
|
||||
BEGIN:STANDARD
|
||||
DTSTART:20071104T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
|
||||
TZNAME:EST
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
||||
|
||||
# Participant removes an instance of the recurring event
|
||||
> put bernard@example.net 9263504FD3AD
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Montreal
|
||||
BEGIN:STANDARD
|
||||
DTSTART:20071104T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
|
||||
TZNAME:EST
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:9263504FD3AD
|
||||
SEQUENCE:1
|
||||
DTSTAMP:20090602T185254Z
|
||||
DTSTART;TZID=America/Montreal:20090601T150000
|
||||
DTEND;TZID=America/Montreal:20090601T160000
|
||||
RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5
|
||||
EXDATE;TZID=America/Montreal:20090603T150000
|
||||
TRANSP:OPAQUE
|
||||
SUMMARY:Review Internet-Draft
|
||||
ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:9263504FD3AD
|
||||
SEQUENCE:1
|
||||
DTSTAMP:20090603T183823Z
|
||||
RECURRENCE-ID;TZID=America/Montreal:20090602T150000
|
||||
DTSTART;TZID=America/Montreal:20090602T150000
|
||||
DTEND;TZID=America/Montreal:20090602T160000
|
||||
TRANSP:TRANSPARENT
|
||||
SUMMARY:Review Internet-Draft
|
||||
ORGANIZER;CN="Cyrus Daboo":mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Cyrus Daboo";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED:mailto:cyrus@example.com
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=DECLINED;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
> expect
|
||||
from: bernard@example.net
|
||||
to: cyrus@example.com
|
||||
changes:
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Stalwart Labs LLC//Stalwart Server//EN
|
||||
METHOD:REPLY
|
||||
BEGIN:VEVENT
|
||||
ORGANIZER:mailto:cyrus@example.com
|
||||
ATTENDEE;PARTSTAT=DECLINED:mailto:bernard@example.net
|
||||
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
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
||||
|
||||
# Send iTIP message to organizer
|
||||
> send
|
||||
|
||||
# Organizer receives the response
|
||||
> get cyrus@example.com 9263504FD3AD
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
BEGIN:VEVENT
|
||||
ORGANIZER:mailto:cyrus@example.com
|
||||
ATTENDEE;PARTSTAT=DECLINED:mailto:bernard@example.net
|
||||
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
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;RSVP=
|
||||
TRUE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:bernard@example.net
|
||||
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
|
||||
END:VEVENT
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:America/Montreal
|
||||
BEGIN:STANDARD
|
||||
DTSTART:20071104T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
|
||||
TZNAME:EST
|
||||
TZOFFSETFROM:-0400
|
||||
TZOFFSETTO:-0500
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:20070311T020000
|
||||
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
|
||||
TZNAME:EDT
|
||||
TZOFFSETFROM:-0500
|
||||
TZOFFSETTO:-0400
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
END:VCALENDAR
|
||||
|
||||
|
||||
|
||||
|
||||
208
tests/resources/itip/rfc6638_single.txt
Normal file
208
tests/resources/itip/rfc6638_single.txt
Normal file
@@ -0,0 +1,208 @@
|
||||
# RFC6638 - Simple Event Scheduling
|
||||
|
||||
# Organizer Inviting Multiple Attendees
|
||||
> put cyrus@example.com 9263504FD3AD
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
BEGIN:VEVENT
|
||||
UID:9263504FD3AD
|
||||
SEQUENCE:0
|
||||
DTSTAMP:20090602T185254Z
|
||||
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@example.com
|
||||
ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;RSVP=TRUE:mailto:mike@example.org
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
> expect
|
||||
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
|
||||
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
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;
|
||||
ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;RSVP=TRUE;PARTSTAT=NEEDS-ACTI
|
||||
ON:mailto:mike@example.org
|
||||
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
|
||||
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
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;
|
||||
ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;RSVP=TRUE:mailto:mike@example.
|
||||
org
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
# Send iTIP message to attendees
|
||||
> send
|
||||
|
||||
# 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
|
||||
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
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;
|
||||
ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;RSVP=TRUE;PARTSTAT=NEEDS-ACTI
|
||||
ON:mailto:mike@example.org
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
# Wilfredo accepts the invitation
|
||||
> put wilfredo@example.com 9263504FD3AD
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Example Corp.//CalDAV Client//EN
|
||||
BEGIN:VEVENT
|
||||
UID:9263504FD3AD
|
||||
SEQUENCE:0
|
||||
DTSTAMP:20090602T185254Z
|
||||
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@example.com
|
||||
ATTENDEE;CN="Wilfredo Sanchez Vega";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:wilfredo@example.com
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:mike@example.org
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT15M
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
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
|
||||
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
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;
|
||||
ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;RSVP=TR
|
||||
UE:mailto:mike@example.org
|
||||
BEGIN:VALARM
|
||||
TRIGGER:-PT15M
|
||||
ACTION:DISPLAY
|
||||
DESCRIPTION:Reminder
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
> expect
|
||||
from: wilfredo@example.com
|
||||
to: cyrus@example.com
|
||||
changes:
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//Stalwart Labs LLC//Stalwart Server//EN
|
||||
METHOD:REPLY
|
||||
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
|
||||
REQUEST-STATUS:2.0;Success
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
# Send ITIP message to organizer
|
||||
> send
|
||||
|
||||
# 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
|
||||
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
|
||||
ATTENDEE;CN="Bernard Desruisseaux";CUTYPE=INDIVIDUAL;PARTSTAT=NEEDS-ACTION;
|
||||
ROLE=REQ-PARTICIPANT;RSVP=TRUE:mailto:bernard@example.net
|
||||
ATTENDEE;CN="Mike Douglass";CUTYPE=INDIVIDUAL;RSVP=TRUE:mailto:mike@example.
|
||||
org
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
|
||||
|
||||
Reference in New Issue
Block a user