mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-03-17 14:34:03 +00:00
CalDAV Scheduling - part 1
This commit is contained in:
265
crates/groupware/src/scheduling/attendee.rs
Normal file
265
crates/groupware/src/scheduling/attendee.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* 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_build_envelope, itip_export_component},
|
||||
Email, InstanceId, ItipEntryValue, ItipError, ItipMessage, ItipSnapshot, ItipSnapshots,
|
||||
SchedulingInfo,
|
||||
},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
|
||||
pub(crate) fn attendee_handle_update(
|
||||
old_ical: &ICalendar,
|
||||
new_ical: &ICalendar,
|
||||
old_itip: ItipSnapshots<'_>,
|
||||
new_itip: ItipSnapshots<'_>,
|
||||
info: &mut SchedulingInfo,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
let dt_stamp = PartialDateTime::now();
|
||||
let mut message = ICalendar {
|
||||
components: Vec::with_capacity(2),
|
||||
};
|
||||
message
|
||||
.components
|
||||
.push(itip_build_envelope(ICalendarMethod::Reply));
|
||||
|
||||
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) {
|
||||
match (new_entry.name, &new_entry.value) {
|
||||
(ICalendarProperty::Exdate, ItipEntryValue::DateTime(udt))
|
||||
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,
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
(
|
||||
ICalendarProperty::Exdate
|
||||
| ICalendarProperty::Summary
|
||||
| ICalendarProperty::Description,
|
||||
_,
|
||||
) => {}
|
||||
_ => {
|
||||
// Adding these properties is not allowed
|
||||
return Err(ItipError::ChangeNotAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send participation status update
|
||||
if attendee.is_server_scheduling
|
||||
&& ((attendee.part_stat != old_attendee.part_stat)
|
||||
|| attendee.force_send.is_some())
|
||||
{
|
||||
// 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],
|
||||
new_itip.uid,
|
||||
&dt_stamp,
|
||||
info.sequence,
|
||||
None,
|
||||
));
|
||||
mail_from = Some(&attendee.email.email);
|
||||
}
|
||||
|
||||
// Check removed fields
|
||||
for removed_entry in instance.entries.difference(&old_instance.entries) {
|
||||
if !matches!(
|
||||
removed_entry.name,
|
||||
ICalendarProperty::Exdate
|
||||
| ICalendarProperty::Summary
|
||||
| ICalendarProperty::Description
|
||||
) {
|
||||
// Removing these properties is not allowed
|
||||
return Err(ItipError::ChangeNotAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Change in local attendee email is not allowed
|
||||
return Err(ItipError::ChangeNotAllowed);
|
||||
}
|
||||
}
|
||||
} else if let Some(local_attendee) = instance
|
||||
.local_attendee()
|
||||
.filter(|_| instance_id != &InstanceId::Main)
|
||||
{
|
||||
// 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],
|
||||
new_itip.uid,
|
||||
&dt_stamp,
|
||||
info.sequence,
|
||||
None,
|
||||
));
|
||||
mail_from = Some(&local_attendee.email.email);
|
||||
} else {
|
||||
return Err(ItipError::ChangeNotAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
for (instance_id, old_instance) in &old_itip.components {
|
||||
if !new_itip.components.contains_key(instance_id) {
|
||||
if instance_id != &InstanceId::Main && 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,
|
||||
&dt_stamp,
|
||||
info.sequence,
|
||||
&mut email_rcpt,
|
||||
) {
|
||||
// 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 {
|
||||
// Removing instances is not allowed
|
||||
return Err(ItipError::ChangeNotAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(from) = mail_from {
|
||||
email_rcpt.insert(&new_itip.organizer.email.email);
|
||||
|
||||
Ok(ItipMessage {
|
||||
method: ICalendarMethod::Reply,
|
||||
from: from.to_string(),
|
||||
to: email_rcpt.into_iter().map(|e| e.to_string()).collect(),
|
||||
changed_properties: vec![],
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
Err(ItipError::NothingToSend)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attendee_decline<'x>(
|
||||
ical: &'x ICalendar,
|
||||
instance_id: &'x InstanceId<'x>,
|
||||
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];
|
||||
let mut cancel_comp = ICalendarComponent {
|
||||
component_type: component.component_type.clone(),
|
||||
entries: Vec::with_capacity(5),
|
||||
component_ids: vec![],
|
||||
};
|
||||
|
||||
let mut local_attendee = None;
|
||||
let mut delegated_from = None;
|
||||
|
||||
for attendee in &comp.attendees {
|
||||
if attendee.email.is_local {
|
||||
if attendee.is_server_scheduling
|
||||
&& attendee.rsvp.is_none_or(|rsvp| rsvp)
|
||||
&& (attendee.force_send.is_some()
|
||||
|| !matches!(
|
||||
attendee.part_stat,
|
||||
Some(
|
||||
ICalendarParticipationStatus::Declined
|
||||
| ICalendarParticipationStatus::Delegated
|
||||
)
|
||||
))
|
||||
{
|
||||
local_attendee = Some(attendee);
|
||||
}
|
||||
} else if attendee.delegated_to.iter().any(|d| d.is_local) {
|
||||
cancel_comp
|
||||
.entries
|
||||
.push(component.entries[attendee.entry_id as usize].clone());
|
||||
delegated_from = Some(&attendee.email.email);
|
||||
}
|
||||
}
|
||||
|
||||
local_attendee.map(|local_attendee| {
|
||||
cancel_comp.add_property(
|
||||
ICalendarProperty::Organizer,
|
||||
ICalendarValue::Text(itip.organizer.email.to_string()),
|
||||
);
|
||||
cancel_comp.add_property_with_params(
|
||||
ICalendarProperty::Attendee,
|
||||
[ICalendarParameter::Partstat(
|
||||
ICalendarParticipationStatus::Declined,
|
||||
)],
|
||||
ICalendarValue::Text(local_attendee.email.to_string()),
|
||||
);
|
||||
cancel_comp.add_uid(itip.uid);
|
||||
cancel_comp.add_dtstamp(dt_stamp.clone());
|
||||
cancel_comp.add_sequence(sequence);
|
||||
|
||||
if let InstanceId::Recurrence(recurrence_id) = instance_id {
|
||||
cancel_comp
|
||||
.entries
|
||||
.push(component.entries[recurrence_id.entry_id as usize].clone());
|
||||
}
|
||||
if let Some(delegated_from) = delegated_from {
|
||||
email_rcpt.insert(delegated_from);
|
||||
}
|
||||
|
||||
(cancel_comp, &local_attendee.email)
|
||||
})
|
||||
}
|
||||
181
crates/groupware/src/scheduling/event_cancel.rs
Normal file
181
crates/groupware/src/scheduling/event_cancel.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
common::PartialDateTime,
|
||||
icalendar::{
|
||||
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod, ICalendarProperty,
|
||||
ICalendarValue,
|
||||
},
|
||||
scheduling::{
|
||||
attendee::attendee_decline, itip::itip_build_envelope, snapshot::itip_snapshot, Email,
|
||||
ItipError, ItipMessage, ItipSnapshot, ItipSnapshots, SchedulingInfo,
|
||||
},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
|
||||
pub fn itip_cancel(
|
||||
ical: &ICalendar,
|
||||
account_emails: &[&str],
|
||||
info: &mut SchedulingInfo,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
// Prepare iTIP message
|
||||
let itip = itip_snapshot(ical, account_emails, false)?;
|
||||
let dt_stamp = PartialDateTime::now();
|
||||
let mut message = ICalendar {
|
||||
components: Vec::with_capacity(2),
|
||||
};
|
||||
|
||||
if itip.organizer.email.is_local {
|
||||
// Send cancel message
|
||||
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() {
|
||||
component_type = &ical.components[comp.comp_id as usize].component_type;
|
||||
for attendee in &comp.attendees {
|
||||
if attendee.send_scheduling_messages() {
|
||||
recipients.insert(attendee.email.email.clone());
|
||||
}
|
||||
cancel_guests.insert(&attendee.email);
|
||||
}
|
||||
}
|
||||
|
||||
if !recipients.is_empty() && component_type != &ICalendarComponentType::VFreebusy {
|
||||
info.sequence = sequence;
|
||||
|
||||
message.components.push(build_cancel_component(
|
||||
component_type.clone(),
|
||||
&itip,
|
||||
sequence,
|
||||
dt_stamp,
|
||||
cancel_guests,
|
||||
));
|
||||
|
||||
Ok(ItipMessage {
|
||||
method: ICalendarMethod::Cancel,
|
||||
from: itip.organizer.email.email,
|
||||
to: recipients.into_iter().collect(),
|
||||
changed_properties: vec![],
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
Err(ItipError::NothingToSend)
|
||||
}
|
||||
} else {
|
||||
// Send decline message
|
||||
message
|
||||
.components
|
||||
.push(itip_build_envelope(ICalendarMethod::Reply));
|
||||
|
||||
// Decline attendance for all instances that have local attendees
|
||||
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,
|
||||
) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(from) = mail_from {
|
||||
email_rcpt.insert(&itip.organizer.email.email);
|
||||
|
||||
Ok(ItipMessage {
|
||||
method: ICalendarMethod::Reply,
|
||||
from: from.to_string(),
|
||||
to: email_rcpt.into_iter().map(|e| e.to_string()).collect(),
|
||||
changed_properties: vec![],
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
Err(ItipError::NothingToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_component<'x>(
|
||||
ical: &'x ICalendar,
|
||||
itip: &'x ItipSnapshots<'x>,
|
||||
comp: &'x ItipSnapshot<'x>,
|
||||
sequence: u32,
|
||||
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_scheduling_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(
|
||||
component_type: ICalendarComponentType,
|
||||
itip: &ItipSnapshots<'_>,
|
||||
sequence: u32,
|
||||
dt_stamp: PartialDateTime,
|
||||
cancel_guests: AHashSet<&Email>,
|
||||
) -> ICalendarComponent {
|
||||
let mut cancel_comp = ICalendarComponent {
|
||||
component_type,
|
||||
entries: Vec::with_capacity(cancel_guests.len() + 5),
|
||||
component_ids: vec![],
|
||||
};
|
||||
cancel_comp.add_property(
|
||||
ICalendarProperty::Method,
|
||||
ICalendarValue::Method(ICalendarMethod::Cancel),
|
||||
);
|
||||
cancel_comp.add_dtstamp(dt_stamp);
|
||||
cancel_comp.add_sequence(sequence);
|
||||
cancel_comp.add_uid(itip.uid);
|
||||
cancel_comp.add_property(
|
||||
ICalendarProperty::Organizer,
|
||||
ICalendarValue::Text(itip.organizer.email.to_string()),
|
||||
);
|
||||
|
||||
for email in cancel_guests {
|
||||
cancel_comp.add_property(
|
||||
ICalendarProperty::Attendee,
|
||||
ICalendarValue::Text(email.to_string()),
|
||||
);
|
||||
}
|
||||
cancel_comp
|
||||
}
|
||||
31
crates/groupware/src/scheduling/event_create.rs
Normal file
31
crates/groupware/src/scheduling/event_create.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
icalendar::ICalendar,
|
||||
scheduling::{
|
||||
organizer::organizer_request_full, snapshot::itip_snapshot, ItipError, ItipMessage,
|
||||
SchedulingInfo,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn itip_create(
|
||||
ical: &ICalendar,
|
||||
account_emails: &[&str],
|
||||
info: &mut SchedulingInfo,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
let itip = itip_snapshot(ical, account_emails, false)?;
|
||||
if !itip.organizer.is_server_scheduling {
|
||||
Err(ItipError::OtherSchedulingAgent)
|
||||
} 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;
|
||||
})
|
||||
}
|
||||
}
|
||||
51
crates/groupware/src/scheduling/event_update.rs
Normal file
51
crates/groupware/src/scheduling/event_update.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
icalendar::ICalendar,
|
||||
scheduling::{
|
||||
attendee::attendee_handle_update, event_cancel::itip_cancel,
|
||||
organizer::organizer_handle_update, snapshot::itip_snapshot, ItipError, ItipMessage,
|
||||
SchedulingInfo,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn itip_update(
|
||||
ical: &ICalendar,
|
||||
old_ical: &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) => {
|
||||
if old_itip.organizer.email != new_itip.organizer.email {
|
||||
// RFC 6638 does not support replacing the organizer
|
||||
Err(ItipError::ChangeNotAllowed)
|
||||
} else if old_itip.organizer.email.is_local {
|
||||
organizer_handle_update(old_ical, ical, old_itip, new_itip, info)
|
||||
} else {
|
||||
attendee_handle_update(old_ical, ical, old_itip, new_itip, info)
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
match &err {
|
||||
ItipError::NoSchedulingInfo
|
||||
| ItipError::NotOrganizer
|
||||
| ItipError::NotOrganizerNorAttendee
|
||||
| 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)
|
||||
} else {
|
||||
Err(ItipError::ChangeNotAllowed)
|
||||
}
|
||||
}
|
||||
_ => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/groupware/src/scheduling/inbound.rs
Normal file
5
crates/groupware/src/scheduling/inbound.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
128
crates/groupware/src/scheduling/itip.rs
Normal file
128
crates/groupware/src/scheduling/itip.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
common::PartialDateTime,
|
||||
icalendar::{
|
||||
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod,
|
||||
ICalendarParameter, ICalendarParticipationStatus, ICalendarProperty, ICalendarValue,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) fn itip_build_envelope(method: ICalendarMethod) -> ICalendarComponent {
|
||||
let todo = "fix prodid";
|
||||
ICalendarComponent {
|
||||
component_type: ICalendarComponentType::VCalendar,
|
||||
entries: vec![
|
||||
ICalendarEntry {
|
||||
name: ICalendarProperty::Version,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Text("2.0".to_string())],
|
||||
},
|
||||
ICalendarEntry {
|
||||
name: ICalendarProperty::Prodid,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Text("Stalwart".to_string())],
|
||||
},
|
||||
ICalendarEntry {
|
||||
name: ICalendarProperty::Method,
|
||||
params: vec![],
|
||||
values: vec![ICalendarValue::Method(method)],
|
||||
},
|
||||
],
|
||||
component_ids: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn itip_export_component(
|
||||
component: &ICalendarComponent,
|
||||
uid: &str,
|
||||
dt_stamp: &PartialDateTime,
|
||||
sequence: u32,
|
||||
export_as_organizer: Option<&ICalendarParticipationStatus>,
|
||||
) -> ICalendarComponent {
|
||||
let mut comp = ICalendarComponent {
|
||||
component_type: component.component_type.clone(),
|
||||
entries: Vec::with_capacity(component.entries.len() + 1),
|
||||
component_ids: Default::default(),
|
||||
};
|
||||
|
||||
comp.add_dtstamp(dt_stamp.clone());
|
||||
comp.add_sequence(sequence);
|
||||
comp.add_uid(uid);
|
||||
|
||||
for entry in &component.entries {
|
||||
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;
|
||||
|
||||
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 {
|
||||
new_entry
|
||||
.params
|
||||
.push(ICalendarParameter::Partstat(partstat.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
comp.entries.push(new_entry);
|
||||
}
|
||||
ICalendarProperty::RequestStatus
|
||||
| ICalendarProperty::Dtstamp
|
||||
| ICalendarProperty::Sequence
|
||||
| ICalendarProperty::Uid => {}
|
||||
_ => {
|
||||
if export_as_organizer.is_some()
|
||||
|| matches!(entry.name, ICalendarProperty::RecurrenceId)
|
||||
{
|
||||
comp.entries.push(entry.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
comp
|
||||
}
|
||||
|
||||
pub fn itip_remove_info(ical: &mut ICalendar) {
|
||||
let todo = "call after insert or update";
|
||||
for comp in ical.components.iter_mut() {
|
||||
if comp.component_type.is_scheduling_object() {
|
||||
// Remove scheduling info from non-updated components
|
||||
for entry in comp.entries.iter_mut() {
|
||||
if matches!(
|
||||
entry.name,
|
||||
ICalendarProperty::Organizer | ICalendarProperty::Attendee
|
||||
) {
|
||||
entry.params.retain(|param| {
|
||||
!matches!(
|
||||
param,
|
||||
ICalendarParameter::Rsvp(true)
|
||||
| ICalendarParameter::ScheduleForceSend(_)
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
254
crates/groupware/src/scheduling/mod.rs
Normal file
254
crates/groupware/src/scheduling/mod.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
common::PartialDateTime,
|
||||
icalendar::{
|
||||
ICalendar, ICalendarComponentType, ICalendarDuration, ICalendarMethod,
|
||||
ICalendarParticipationRole, ICalendarParticipationStatus, ICalendarPeriod,
|
||||
ICalendarProperty, ICalendarRecurrenceRule, ICalendarScheduleForceSendValue,
|
||||
ICalendarStatus, ICalendarUserTypes, Uri,
|
||||
},
|
||||
};
|
||||
use ahash::{AHashMap, AHashSet};
|
||||
use std::{fmt::Display, hash::Hash};
|
||||
|
||||
pub mod attendee;
|
||||
pub mod event_cancel;
|
||||
pub mod event_create;
|
||||
pub mod event_update;
|
||||
pub mod inbound;
|
||||
pub mod itip;
|
||||
pub mod organizer;
|
||||
pub mod snapshot;
|
||||
|
||||
pub struct ItipSnapshots<'x> {
|
||||
pub organizer: Organizer<'x>,
|
||||
pub uid: &'x str,
|
||||
pub sequence: Option<i64>,
|
||||
pub components: AHashMap<InstanceId<'x>, ItipSnapshot<'x>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ItipSnapshot<'x> {
|
||||
pub comp_id: u16,
|
||||
pub attendees: AHashSet<Attendee<'x>>,
|
||||
pub dtstamp: Option<&'x PartialDateTime>,
|
||||
pub entries: AHashSet<ItipEntry<'x>>,
|
||||
pub request_status: Vec<&'x str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ItipEntry<'x> {
|
||||
pub name: &'x ICalendarProperty,
|
||||
pub value: ItipEntryValue<'x>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ItipEntryValue<'x> {
|
||||
DateTime(UnresolvedDateTime<'x>),
|
||||
Period(&'x ICalendarPeriod),
|
||||
Duration(&'x ICalendarDuration),
|
||||
Status(&'x ICalendarStatus),
|
||||
RRule(&'x ICalendarRecurrenceRule),
|
||||
Text(&'x str),
|
||||
Integer(i64),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct UnresolvedDateTime<'x> {
|
||||
pub date: &'x PartialDateTime,
|
||||
pub tz_id: Option<&'x str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum InstanceId<'x> {
|
||||
Main,
|
||||
Recurrence(RecurrenceId<'x>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RecurrenceId<'x> {
|
||||
pub entry_id: u16,
|
||||
pub date: UnresolvedDateTime<'x>,
|
||||
pub this_and_future: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Attendee<'x> {
|
||||
pub entry_id: u16,
|
||||
pub email: Email,
|
||||
pub part_stat: Option<&'x ICalendarParticipationStatus>,
|
||||
pub delegated_from: Vec<Email>,
|
||||
pub delegated_to: Vec<Email>,
|
||||
pub role: Option<&'x ICalendarParticipationRole>,
|
||||
pub cu_type: Option<&'x ICalendarUserTypes>,
|
||||
pub sent_by: Option<Email>,
|
||||
pub rsvp: Option<bool>,
|
||||
pub is_server_scheduling: bool,
|
||||
pub force_send: Option<&'x ICalendarScheduleForceSendValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Organizer<'x> {
|
||||
pub entry_id: u16,
|
||||
pub email: Email,
|
||||
pub is_server_scheduling: bool,
|
||||
pub force_send: Option<&'x ICalendarScheduleForceSendValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Email {
|
||||
pub email: String,
|
||||
pub is_local: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SchedulingInfo {
|
||||
pub sequence: u32,
|
||||
pub tag: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ItipError {
|
||||
NoSchedulingInfo,
|
||||
OtherSchedulingAgent,
|
||||
NotOrganizer,
|
||||
NotOrganizerNorAttendee,
|
||||
NothingToSend,
|
||||
MissingUid,
|
||||
MultipleUid,
|
||||
MultipleSequence,
|
||||
MultipleOrganizer,
|
||||
MultipleObjectTypes,
|
||||
MultipleObjectInstances,
|
||||
ChangeNotAllowed,
|
||||
}
|
||||
|
||||
pub struct ItipMessage {
|
||||
pub method: ICalendarMethod,
|
||||
pub from: String,
|
||||
pub to: Vec<String>,
|
||||
pub changed_properties: Vec<ICalendarProperty>,
|
||||
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
|
||||
.iter()
|
||||
.any(|attendee| attendee.email.is_local)
|
||||
}
|
||||
|
||||
pub fn local_attendee(&self) -> Option<&Attendee<'_>> {
|
||||
self.attendees
|
||||
.iter()
|
||||
.find(|attendee| attendee.email.is_local)
|
||||
}
|
||||
}
|
||||
|
||||
impl Attendee<'_> {
|
||||
pub fn send_scheduling_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
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Email {
|
||||
pub fn new(email: &str, local_addresses: &[&str]) -> Option<Self> {
|
||||
email.contains('@').then(|| {
|
||||
let email = email.trim().trim_start_matches("mailto:").to_lowercase();
|
||||
let is_local = local_addresses.contains(&email.as_str());
|
||||
Email { email, is_local }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_uri(uri: &Uri, local_addresses: &[&str]) -> Option<Self> {
|
||||
if let Uri::Location(uri) = uri {
|
||||
Email::new(uri.as_str(), local_addresses)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Attendee<'_> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.email == other.email
|
||||
&& self.part_stat == other.part_stat
|
||||
&& self.delegated_from == other.delegated_from
|
||||
&& self.delegated_to == other.delegated_to
|
||||
&& self.role == other.role
|
||||
&& self.cu_type == other.cu_type
|
||||
&& self.sent_by == other.sent_by
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Attendee<'_> {}
|
||||
|
||||
impl Hash for Attendee<'_> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.email.hash(state);
|
||||
self.part_stat.hash(state);
|
||||
self.delegated_from.hash(state);
|
||||
self.delegated_to.hash(state);
|
||||
self.role.hash(state);
|
||||
self.cu_type.hash(state);
|
||||
self.sent_by.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Email {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "mailto:{}", self.email)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Email {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.email.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Email {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.email == other.email
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Email {}
|
||||
|
||||
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 Hash for RecurrenceId<'_> {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.date.hash(state);
|
||||
self.this_and_future.hash(state);
|
||||
}
|
||||
}
|
||||
278
crates/groupware/src/scheduling/organizer.rs
Normal file
278
crates/groupware/src/scheduling/organizer.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
common::PartialDateTime,
|
||||
icalendar::{
|
||||
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod,
|
||||
ICalendarParticipationStatus, ICalendarProperty,
|
||||
},
|
||||
scheduling::{
|
||||
event_cancel::cancel_component,
|
||||
itip::{itip_build_envelope, itip_export_component},
|
||||
InstanceId, ItipError, ItipMessage, ItipSnapshots, SchedulingInfo,
|
||||
},
|
||||
};
|
||||
use ahash::AHashSet;
|
||||
|
||||
pub(crate) fn organizer_handle_update(
|
||||
old_ical: &ICalendar,
|
||||
new_ical: &ICalendar,
|
||||
old_itip: ItipSnapshots<'_>,
|
||||
new_itip: ItipSnapshots<'_>,
|
||||
info: &mut SchedulingInfo,
|
||||
) -> 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;
|
||||
let mut increment_sequence = false;
|
||||
|
||||
for (instance_id, instance) in &new_itip.components {
|
||||
if let Some(old_instance) = old_itip.components.get(instance_id) {
|
||||
let changed_entries = instance.entries != old_instance.entries;
|
||||
let changed_attendees = instance.attendees != old_instance.attendees;
|
||||
|
||||
if changed_entries || changed_attendees {
|
||||
increment_sequence = increment_sequence
|
||||
|| changed_attendees
|
||||
|| instance
|
||||
.entries
|
||||
.symmetric_difference(&old_instance.entries)
|
||||
.any(|entry| {
|
||||
!matches!(
|
||||
entry.name,
|
||||
ICalendarProperty::Summary
|
||||
| ICalendarProperty::Description
|
||||
| ICalendarProperty::Priority
|
||||
)
|
||||
});
|
||||
|
||||
if instance_id == &InstanceId::Main {
|
||||
is_full_update = true;
|
||||
break;
|
||||
} else {
|
||||
updated_instances.push((instance_id, instance));
|
||||
}
|
||||
}
|
||||
} else if instance_id != &InstanceId::Main {
|
||||
added_instances.push((instance_id, instance));
|
||||
increment_sequence = true;
|
||||
} else {
|
||||
return Err(ItipError::ChangeNotAllowed);
|
||||
}
|
||||
}
|
||||
|
||||
if !is_full_update {
|
||||
for (instance_id, old_instance) in &old_itip.components {
|
||||
if !new_itip.components.contains_key(instance_id) {
|
||||
if instance_id != &InstanceId::Main {
|
||||
deleted_instances.push((instance_id, old_instance));
|
||||
increment_sequence = true;
|
||||
} else {
|
||||
return Err(ItipError::ChangeNotAllowed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sequence = if increment_sequence {
|
||||
info.sequence + 1
|
||||
} else {
|
||||
info.sequence
|
||||
};
|
||||
|
||||
let (method, instances) = match (
|
||||
!added_instances.is_empty(),
|
||||
!deleted_instances.is_empty(),
|
||||
!updated_instances.is_empty(),
|
||||
is_full_update,
|
||||
) {
|
||||
(true, false, false, false) => {
|
||||
// Send ADD message
|
||||
(ICalendarMethod::Add, added_instances)
|
||||
}
|
||||
(false, true, false, false) => {
|
||||
// Send CANCEL message
|
||||
(ICalendarMethod::Cancel, deleted_instances)
|
||||
}
|
||||
(false, false, true, false) => {
|
||||
// Send REQUEST message for changed instances
|
||||
(ICalendarMethod::Request, updated_instances)
|
||||
}
|
||||
(_, _, _, true) => {
|
||||
// Send full REQUEST message
|
||||
return organizer_request_full(new_ical, new_itip, sequence, false).inspect(|_| {
|
||||
info.sequence = sequence;
|
||||
});
|
||||
}
|
||||
_ => return Err(ItipError::NothingToSend),
|
||||
};
|
||||
|
||||
// Prepare iTIP message
|
||||
let (ical, itip, is_cancel) = if matches!(method, ICalendarMethod::Cancel) {
|
||||
(old_ical, &old_itip, true)
|
||||
} else {
|
||||
(new_ical, &new_itip, false)
|
||||
};
|
||||
let dt_stamp = PartialDateTime::now();
|
||||
let mut message = ICalendar {
|
||||
components: vec![ICalendarComponent::default(); ical.components.len()],
|
||||
};
|
||||
message.components[0] = itip_build_envelope(method);
|
||||
|
||||
let mut recipients = AHashSet::new();
|
||||
let mut copy_components = AHashSet::new();
|
||||
let mut scheduling_component_ids = Vec::with_capacity(ical.components.len());
|
||||
|
||||
for (instance_id, comp) in instances {
|
||||
// Prepare component for iTIP
|
||||
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() {
|
||||
recipients.insert(attendee.email.email.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Export component with updated sequence and participation status
|
||||
itip_export_component(
|
||||
orig_component,
|
||||
itip.uid,
|
||||
&dt_stamp,
|
||||
sequence,
|
||||
Some(&ICalendarParticipationStatus::NeedsAction),
|
||||
)
|
||||
} else if let Some(mut cancel_comp) = cancel_component(
|
||||
ical,
|
||||
itip,
|
||||
comp,
|
||||
sequence,
|
||||
dt_stamp.clone(),
|
||||
&mut recipients,
|
||||
) {
|
||||
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
|
||||
scheduling_component_ids.push(comp.comp_id);
|
||||
message.components[comp.comp_id as usize] = component;
|
||||
message.components[0].component_ids.push(comp.comp_id);
|
||||
}
|
||||
|
||||
// Copy timezones and alarms
|
||||
for (comp_id, comp) in ical.components.iter().enumerate() {
|
||||
if !is_cancel && matches!(comp.component_type, ICalendarComponentType::VTimezone) {
|
||||
copy_components.extend(comp.component_ids.iter().copied());
|
||||
} 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);
|
||||
}
|
||||
|
||||
if !recipients.is_empty() {
|
||||
if increment_sequence {
|
||||
info.sequence = sequence;
|
||||
}
|
||||
|
||||
Ok(ItipMessage {
|
||||
method: ICalendarMethod::Request,
|
||||
from: itip.organizer.email.email.clone(),
|
||||
to: recipients.into_iter().map(|e| e.to_string()).collect(),
|
||||
changed_properties: vec![],
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
Err(ItipError::NothingToSend)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn organizer_request_full(
|
||||
ical: &ICalendar,
|
||||
itip: ItipSnapshots<'_>,
|
||||
sequence: u32,
|
||||
include_alarms: bool,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
// Prepare iTIP message
|
||||
let dt_stamp = PartialDateTime::now();
|
||||
let mut message = ICalendar {
|
||||
components: vec![ICalendarComponent::default(); ical.components.len()],
|
||||
};
|
||||
message.components[0] = itip_build_envelope(ICalendarMethod::Request);
|
||||
|
||||
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 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),
|
||||
);
|
||||
scheduling_component_ids.push(comp.comp_id);
|
||||
|
||||
// Add VALARM sub-components
|
||||
if include_alarms {
|
||||
for sub_comp_id in &orig_component.component_ids {
|
||||
if matches!(
|
||||
ical.components[*sub_comp_id as usize].component_type,
|
||||
ICalendarComponentType::VAlarm
|
||||
) {
|
||||
copy_components.insert(*sub_comp_id);
|
||||
component.component_ids.push(*sub_comp_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add component to message
|
||||
message.components[comp.comp_id as usize] = component;
|
||||
message.components[0].component_ids.push(comp.comp_id);
|
||||
|
||||
// Add attendees
|
||||
for attendee in comp.attendees {
|
||||
if attendee.send_scheduling_messages() {
|
||||
recipients.insert(attendee.email.email);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy timezones and alarms
|
||||
for (comp_id, comp) in ical.components.iter().enumerate() {
|
||||
if matches!(comp.component_type, ICalendarComponentType::VTimezone) {
|
||||
copy_components.extend(comp.component_ids.iter().copied());
|
||||
} 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);
|
||||
}
|
||||
|
||||
if !recipients.is_empty() {
|
||||
Ok(ItipMessage {
|
||||
method: ICalendarMethod::Request,
|
||||
from: itip.organizer.email.email,
|
||||
to: recipients.into_iter().collect(),
|
||||
changed_properties: vec![],
|
||||
message,
|
||||
})
|
||||
} else {
|
||||
Err(ItipError::NothingToSend)
|
||||
}
|
||||
}
|
||||
293
crates/groupware/src/scheduling/snapshot.rs
Normal file
293
crates/groupware/src/scheduling/snapshot.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
icalendar::{
|
||||
ICalendar, ICalendarParameter, ICalendarProperty, ICalendarScheduleAgentValue,
|
||||
ICalendarValue, Uri,
|
||||
},
|
||||
scheduling::{
|
||||
Attendee, Email, InstanceId, ItipEntry, ItipEntryValue, ItipError, ItipSnapshot,
|
||||
ItipSnapshots, Organizer, RecurrenceId, UnresolvedDateTime,
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
|
||||
pub(crate) fn itip_snapshot<'x>(
|
||||
ical: &'x ICalendar,
|
||||
account_emails: &'x [&'x 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;
|
||||
|
||||
for (comp_id, comp) in ical.components.iter().enumerate() {
|
||||
if comp.component_type.is_scheduling_object()
|
||||
&& comp
|
||||
.entries
|
||||
.iter()
|
||||
.any(|e| matches!(e.name, ICalendarProperty::Organizer))
|
||||
{
|
||||
match expect_object_type {
|
||||
Some(expected) if expected != &comp.component_type => {
|
||||
return Err(ItipError::MultipleObjectTypes);
|
||||
}
|
||||
None => {
|
||||
expect_object_type = Some(&comp.component_type);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut sched_comp = ItipSnapshot {
|
||||
comp_id: comp_id as u16,
|
||||
..Default::default()
|
||||
};
|
||||
let mut instance_id = InstanceId::Main;
|
||||
|
||||
for (entry_id, entry) in comp.entries.iter().enumerate() {
|
||||
match &entry.name {
|
||||
ICalendarProperty::Organizer => {
|
||||
if let Some(email) = entry
|
||||
.values
|
||||
.first()
|
||||
.and_then(|v| v.as_text())
|
||||
.and_then(|v| Email::new(v, account_emails))
|
||||
{
|
||||
let mut part = Organizer {
|
||||
entry_id: entry_id as u16,
|
||||
email,
|
||||
is_server_scheduling: true,
|
||||
force_send: None,
|
||||
};
|
||||
has_local_emails |= part.email.is_local;
|
||||
|
||||
for param in &entry.params {
|
||||
match param {
|
||||
ICalendarParameter::ScheduleAgent(agent) => {
|
||||
part.is_server_scheduling =
|
||||
agent == &ICalendarScheduleAgentValue::Server;
|
||||
}
|
||||
ICalendarParameter::ScheduleForceSend(force_send) => {
|
||||
part.force_send = Some(force_send);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !part.is_server_scheduling && !force_add_client_scheduling {
|
||||
return Err(ItipError::OtherSchedulingAgent);
|
||||
}
|
||||
|
||||
match organizer {
|
||||
Some(existing_organizer)
|
||||
if existing_organizer.email.email != part.email.email =>
|
||||
{
|
||||
return Err(ItipError::MultipleOrganizer);
|
||||
}
|
||||
None => {
|
||||
organizer = Some(part);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
ICalendarProperty::Attendee => {
|
||||
if let Some(email) = entry
|
||||
.values
|
||||
.first()
|
||||
.and_then(|v| v.as_text())
|
||||
.and_then(|v| Email::new(v, account_emails))
|
||||
{
|
||||
let mut part = Attendee {
|
||||
entry_id: entry_id as u16,
|
||||
email,
|
||||
rsvp: None,
|
||||
is_server_scheduling: true,
|
||||
force_send: None,
|
||||
part_stat: None,
|
||||
delegated_from: vec![],
|
||||
delegated_to: vec![],
|
||||
cu_type: None,
|
||||
role: None,
|
||||
sent_by: None,
|
||||
};
|
||||
|
||||
for param in &entry.params {
|
||||
match param {
|
||||
ICalendarParameter::ScheduleAgent(agent) => {
|
||||
part.is_server_scheduling =
|
||||
agent == &ICalendarScheduleAgentValue::Server;
|
||||
}
|
||||
ICalendarParameter::Rsvp(rsvp) => {
|
||||
part.rsvp = Some(*rsvp);
|
||||
}
|
||||
ICalendarParameter::ScheduleForceSend(force_send) => {
|
||||
part.force_send = Some(force_send);
|
||||
}
|
||||
ICalendarParameter::Partstat(value) => {
|
||||
part.part_stat = Some(value);
|
||||
}
|
||||
ICalendarParameter::Cutype(value) => {
|
||||
part.cu_type = Some(value);
|
||||
}
|
||||
ICalendarParameter::DelegatedFrom(value) => {
|
||||
part.delegated_from = value
|
||||
.iter()
|
||||
.filter_map(|uri| Email::from_uri(uri, account_emails))
|
||||
.collect();
|
||||
}
|
||||
ICalendarParameter::DelegatedTo(value) => {
|
||||
part.delegated_to = value
|
||||
.iter()
|
||||
.filter_map(|uri| Email::from_uri(uri, account_emails))
|
||||
.collect();
|
||||
}
|
||||
ICalendarParameter::Role(value) => {
|
||||
part.role = Some(value);
|
||||
}
|
||||
ICalendarParameter::SentBy(value) => {
|
||||
part.sent_by = Email::from_uri(value, account_emails);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
has_local_emails |= part.email.is_local
|
||||
&& (force_add_client_scheduling || part.is_server_scheduling);
|
||||
|
||||
sched_comp.attendees.insert(part);
|
||||
}
|
||||
}
|
||||
ICalendarProperty::Uid => {
|
||||
if let Some(uid_) = entry
|
||||
.values
|
||||
.first()
|
||||
.and_then(|v| v.as_text())
|
||||
.map(|v| v.trim())
|
||||
.filter(|v| !v.is_empty())
|
||||
{
|
||||
match uid {
|
||||
Some(existing_uid) if existing_uid != uid_ => {
|
||||
return Err(ItipError::MultipleUid);
|
||||
}
|
||||
None => {
|
||||
uid = Some(uid_);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
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_);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
};
|
||||
for param in &entry.params {
|
||||
match param {
|
||||
ICalendarParameter::Tzid(id) => {
|
||||
recurrence_id.date.tz_id = Some(id.as_str());
|
||||
}
|
||||
ICalendarParameter::Range => {
|
||||
recurrence_id.this_and_future = true;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
instance_id = InstanceId::Recurrence(recurrence_id);
|
||||
}
|
||||
}
|
||||
ICalendarProperty::RequestStatus => {
|
||||
if let Some(value) = entry.values.first().and_then(|v| v.as_text()) {
|
||||
sched_comp.request_status.push(value);
|
||||
}
|
||||
}
|
||||
ICalendarProperty::Dtstamp => {
|
||||
sched_comp.dtstamp =
|
||||
entry.values.first().and_then(|v| v.as_partial_date_time());
|
||||
}
|
||||
ICalendarProperty::Dtstart
|
||||
| ICalendarProperty::Dtend
|
||||
| ICalendarProperty::Duration
|
||||
| ICalendarProperty::Due
|
||||
| ICalendarProperty::Rrule
|
||||
| ICalendarProperty::Rdate
|
||||
| ICalendarProperty::Exdate
|
||||
| ICalendarProperty::Status
|
||||
| ICalendarProperty::Location
|
||||
| ICalendarProperty::Summary
|
||||
| ICalendarProperty::Description
|
||||
| ICalendarProperty::Priority => {
|
||||
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,
|
||||
tz_id,
|
||||
})
|
||||
}
|
||||
ICalendarValue::Duration(v) => ItipEntryValue::Duration(v),
|
||||
ICalendarValue::RecurrenceRule(v) => ItipEntryValue::RRule(v),
|
||||
ICalendarValue::Period(v) => ItipEntryValue::Period(v),
|
||||
ICalendarValue::Integer(v) => ItipEntryValue::Integer(*v),
|
||||
ICalendarValue::Text(v) => ItipEntryValue::Text(v.as_str()),
|
||||
ICalendarValue::Status(v) => ItipEntryValue::Status(v),
|
||||
_ => continue,
|
||||
};
|
||||
sched_comp.entries.insert(ItipEntry {
|
||||
name: &entry.name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if components.insert(instance_id, sched_comp).is_some() {
|
||||
return Err(ItipError::MultipleObjectInstances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !components.is_empty() && has_local_emails {
|
||||
Ok(ItipSnapshots {
|
||||
organizer: organizer.ok_or(ItipError::NoSchedulingInfo)?,
|
||||
uid: uid.ok_or(ItipError::MissingUid)?,
|
||||
sequence,
|
||||
components,
|
||||
})
|
||||
} else if !has_local_emails {
|
||||
Err(ItipError::NotOrganizerNorAttendee)
|
||||
} else {
|
||||
Err(ItipError::NoSchedulingInfo)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user