Files
Stalwart/crates/groupware/src/scheduling/event_cancel.rs

178 lines
6.0 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
InstanceId, ItipError, ItipMessage, ItipSummary,
attendee::attendee_decline,
itip::{itip_add_tz, itip_build_envelope},
snapshot::itip_snapshot,
};
use ahash::AHashSet;
use calcard::{
common::PartialDateTime,
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod,
ICalendarParticipationStatus, ICalendarProperty, ICalendarStatus, ICalendarValue,
},
};
pub fn itip_cancel(
ical: &ICalendar,
account_emails: &[String],
) -> Result<ItipMessage<ICalendar>, 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);
// Fetch guest emails
let mut recipients = AHashSet::new();
let mut cancel_guests = AHashSet::new();
let mut component_type = &ICalendarComponentType::VEvent;
let mut sequence = 0;
for (instance_id, comp) in &itip.components {
component_type = &comp.comp.component_type;
for attendee in &comp.attendees {
if attendee.send_update_messages() {
recipients.insert(attendee.email.email.clone());
}
cancel_guests.insert(&attendee.email);
}
// Increment sequence if needed
if instance_id == &InstanceId::Main {
sequence = comp.sequence.unwrap_or_default() + 1;
}
}
if !recipients.is_empty() && component_type != &ICalendarComponentType::VFreebusy {
let instance = itip.main_instance_or_default();
message.components.push(build_cancel_component(
instance.comp,
sequence,
dt_stamp,
&[],
));
// Add timezones
itip_add_tz(&mut message, ical);
Ok(ItipMessage {
to: recipients.into_iter().collect(),
summary: ItipSummary::Cancel(instance.build_summary(None, &[])),
from: itip.organizer.email.email,
from_organizer: true,
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(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);
message.components.push(cancel_comp);
mail_from = Some(&attendee_email.email);
}
}
if let Some(from) = mail_from {
// Add timezone information if needed
itip_add_tz(&mut message, ical);
email_rcpt.insert(&itip.organizer.email.email);
Ok(ItipMessage {
from: from.to_string(),
from_organizer: false,
to: email_rcpt.into_iter().map(|e| e.to_string()).collect(),
summary: ItipSummary::Rsvp {
part_stat: ICalendarParticipationStatus::Declined,
current: itip.main_instance_or_default().build_summary(None, &[]),
},
message,
})
} else {
Err(ItipError::NothingToSend)
}
}
}
pub(crate) fn build_cancel_component(
component: &ICalendarComponent,
sequence: i64,
dt_stamp: PartialDateTime,
attendees: &[&str],
) -> ICalendarComponent {
let mut cancel_comp = ICalendarComponent {
component_type: component.component_type.clone(),
entries: Vec::with_capacity(7),
component_ids: vec![],
};
cancel_comp.add_property(
ICalendarProperty::Status,
ICalendarValue::Status(ICalendarStatus::Cancelled),
);
cancel_comp.add_dtstamp(dt_stamp);
cancel_comp.add_sequence(sequence);
cancel_comp.entries.extend(
component
.entries
.iter()
.filter(|e| match e.name {
ICalendarProperty::Organizer
| ICalendarProperty::Uid
| ICalendarProperty::Summary
| ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Duration
| ICalendarProperty::Due
| ICalendarProperty::RecurrenceId
| ICalendarProperty::Created
| ICalendarProperty::LastModified
| ICalendarProperty::Description
| ICalendarProperty::Location => true,
ICalendarProperty::Attendee => {
attendees.is_empty()
|| e.values
.first()
.and_then(|v| v.as_text())
.is_some_and(|email| {
attendees.iter().any(|attendee| {
email
.strip_suffix(attendee)
.is_some_and(|v| v.ends_with(':') || v.is_empty())
})
})
}
_ => false,
})
.cloned(),
);
cancel_comp
}