mail-server/crates/groupware/src/scheduling/inbound.rs
2025-06-18 18:21:46 +02:00

625 lines
28 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, ItipSnapshots, organizer::organizer_request_full,
};
use ahash::AHashSet;
use calcard::icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod,
ICalendarParameter, ICalendarParameterName, ICalendarProperty, ICalendarStatus, ICalendarValue,
Uri,
};
#[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<ICalendar>),
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 => {
handle_reply(&snapshots, &itip_snapshots, &sender, &mut merge_actions)?;
}
ICalendarMethod::Refresh => {
return organizer_request_full(ical, &snapshots, None, false).and_then(
|messages| {
messages
.into_iter()
.next()
.map(|mut message| {
message.to = vec![sender];
MergeResult::Message(message)
})
.ok_or(ItipError::NothingToSend)
},
);
}
_ => return Err(ItipError::UnsupportedMethod(method.clone())),
}
} else {
// Handle organizer and attendees 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() {
let 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,
});
merge_actions.push(MergeAction::AddEntries {
component_id: snapshot.comp_id,
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 => {
let mut cancel_all_instances = false;
for (instance_id, itip_snapshot) in &itip_snapshots.components {
if let Some(snapshot) = snapshots.components.get(instance_id) {
if itip_snapshot.sequence.unwrap_or_default()
>= snapshot.sequence.unwrap_or_default()
{
// Cancel instance
let itip_component = itip_snapshot.comp;
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(),
});
cancel_all_instances =
cancel_all_instances || instance_id == &InstanceId::Main;
} else {
return Err(ItipError::OutOfSequence);
}
} else {
let itip_component = itip_snapshot.comp;
merge_actions.push(MergeAction::AddComponent {
component: ICalendarComponent {
component_type: itip_component.component_type.clone(),
entries: itip_component
.entries
.iter()
.filter(|entry| {
!matches!(
entry.name,
ICalendarProperty::Status | ICalendarProperty::Other(_)
)
})
.cloned()
.chain([ICalendarEntry {
name: ICalendarProperty::Status,
params: vec![],
values: vec![ICalendarValue::Status(
ICalendarStatus::Cancelled,
)],
}])
.collect(),
component_ids: vec![],
},
});
}
}
if cancel_all_instances {
// Remove all instances
let itip_main = itip_snapshots.components.get(&InstanceId::Main).unwrap();
let itip_component = itip_main.comp;
for (instance_id, snapshot) in &snapshots.components {
if !itip_snapshots.components.contains_key(instance_id) {
merge_actions.push(MergeAction::RemoveEntries {
component_id: snapshot.comp_id,
entries: [
ICalendarProperty::Organizer,
ICalendarProperty::Attendee,
ICalendarProperty::Status,
]
.into_iter()
.collect(),
});
merge_actions.push(MergeAction::AddEntries {
component_id: snapshot.comp_id,
entries: itip_component
.entries
.iter()
.filter(|entry| {
matches!(
entry.name,
ICalendarProperty::Organizer
| ICalendarProperty::Attendee
)
})
.cloned()
.chain([ICalendarEntry {
name: ICalendarProperty::Status,
params: vec![],
values: vec![ICalendarValue::Status(
ICalendarStatus::Cancelled,
)],
}])
.collect(),
});
}
}
}
}
ICalendarMethod::Reply
if itip_snapshots.components.values().any(|snapshot| {
snapshot.external_attendees().any(|a| {
a.email.email == sender && a.delegated_from.iter().any(|a| a.is_local)
})
}) =>
{
handle_reply(&snapshots, &itip_snapshots, &sender, &mut merge_actions)?;
}
_ => return Err(ItipError::UnsupportedMethod(method.clone())),
}
}
if !merge_actions.is_empty() {
Ok(MergeResult::Actions(merge_actions))
} else {
Ok(MergeResult::None)
}
}
pub fn itip_import_message(ical: &mut ICalendar) -> Result<(), ItipError> {
let todo = "use before insert";
let todo = "sender must not be organizer";
let mut expect_object_type = None;
for comp in ical.components.iter_mut() {
if comp.component_type.is_scheduling_object() {
match expect_object_type {
Some(expected) if expected != &comp.component_type => {
return Err(ItipError::MultipleObjectTypes);
}
None => {
expect_object_type = Some(&comp.component_type);
}
_ => {}
}
} else if comp.component_type == ICalendarComponentType::VCalendar {
comp.entries
.retain(|entry| !matches!(entry.name, ICalendarProperty::Method));
}
}
Ok(())
}
fn handle_reply(
snapshots: &ItipSnapshots<'_>,
itip_snapshots: &ItipSnapshots<'_>,
sender: &str,
merge_actions: &mut Vec<MergeAction>,
) -> Result<(), ItipError> {
for (instance_id, itip_snapshot) in &itip_snapshots.components {
if let Some(snapshot) = snapshots.components.get(instance_id) {
if let (Some(attendee), Some(updated_attendee)) = (
snapshot.attendee_by_email(sender),
itip_snapshot.attendee_by_email(sender),
) {
let itip_component = itip_snapshot.comp;
let changed_part_stat = attendee.part_stat != updated_attendee.part_stat;
let changed_rsvp = attendee.rsvp != updated_attendee.rsvp;
let changed_delegated_to = attendee.delegated_to != updated_attendee.delegated_to;
let has_request_status = !itip_snapshot.request_status.is_empty();
if changed_part_stat || changed_rsvp || changed_delegated_to || has_request_status {
// Update participant status
let mut add_parameters = Vec::new();
let mut remove_parameters = Vec::new();
if changed_part_stat {
remove_parameters.push(ICalendarParameterName::Partstat);
if let Some(part_stat) = updated_attendee.part_stat {
add_parameters.push(ICalendarParameter::Partstat(part_stat.clone()));
}
}
if changed_rsvp {
remove_parameters.push(ICalendarParameterName::Rsvp);
if let Some(rsvp) = updated_attendee.rsvp {
add_parameters.push(ICalendarParameter::Rsvp(rsvp));
}
}
if changed_delegated_to {
remove_parameters.push(ICalendarParameterName::DelegatedTo);
if !updated_attendee.delegated_to.is_empty() {
add_parameters.push(ICalendarParameter::DelegatedTo(
updated_attendee
.delegated_to
.iter()
.map(|email| Uri::Location(email.to_string()))
.collect::<Vec<_>>(),
));
}
}
if has_request_status {
remove_parameters.push(ICalendarParameterName::ScheduleStatus);
add_parameters.push(ICalendarParameter::ScheduleStatus(
itip_snapshot.request_status.join(","),
));
}
merge_actions.push(MergeAction::RemoveParameters {
component_id: snapshot.comp_id,
entry_id: attendee.entry_id,
parameters: remove_parameters,
});
merge_actions.push(MergeAction::AddParameters {
component_id: snapshot.comp_id,
entry_id: attendee.entry_id,
parameters: add_parameters,
});
// Add unknown delegated attendees
for delegated_to in &updated_attendee.delegated_to {
if let Some(itip_delegated) =
itip_snapshot.attendee_by_email(&delegated_to.email)
{
if let Some(delegated) = snapshot.attendee_by_email(&delegated_to.email)
{
if delegated != itip_delegated {
merge_actions.push(MergeAction::RemoveParameters {
component_id: snapshot.comp_id,
entry_id: delegated.entry_id,
parameters: vec![
ICalendarParameterName::DelegatedTo,
ICalendarParameterName::DelegatedFrom,
ICalendarParameterName::Partstat,
ICalendarParameterName::Rsvp,
ICalendarParameterName::ScheduleStatus,
ICalendarParameterName::Role,
],
});
merge_actions.push(MergeAction::AddParameters {
component_id: snapshot.comp_id,
entry_id: delegated.entry_id,
parameters: itip_component.entries
[itip_delegated.entry_id as usize]
.params
.iter()
.filter(|param| {
matches!(
param,
ICalendarParameter::DelegatedTo(_)
| ICalendarParameter::DelegatedFrom(_)
| ICalendarParameter::Partstat(_)
| ICalendarParameter::Rsvp(_)
| ICalendarParameter::ScheduleStatus(_)
| ICalendarParameter::Role(_)
)
})
.cloned()
.collect(),
});
}
} else {
merge_actions.push(MergeAction::AddEntries {
component_id: snapshot.comp_id,
entries: vec![
itip_component.entries[itip_delegated.entry_id as usize]
.clone(),
],
});
}
}
}
}
// Add changed properties for VTODO
if snapshot.comp.component_type == ICalendarComponentType::VTodo {
let mut remove_entries = AHashSet::new();
let mut add_entries = Vec::new();
for entry in itip_component.entries.iter() {
if matches!(
entry.name,
ICalendarProperty::PercentComplete
| ICalendarProperty::Status
| ICalendarProperty::Completed
) {
remove_entries.insert(entry.name.clone());
add_entries.push(entry.clone());
}
}
if !add_entries.is_empty() {
merge_actions.push(MergeAction::RemoveEntries {
component_id: snapshot.comp_id,
entries: remove_entries,
});
merge_actions.push(MergeAction::AddEntries {
component_id: snapshot.comp_id,
entries: add_entries,
});
}
}
} else {
return Err(ItipError::SenderIsNotParticipant(sender.to_string()));
}
} else if itip_snapshot.attendee_by_email(sender).is_some() {
// Add component
let itip_component = itip_snapshot.comp;
let is_todo = itip_component.component_type == ICalendarComponentType::VTodo;
merge_actions.push(MergeAction::AddComponent {
component: ICalendarComponent {
component_type: itip_component.component_type.clone(),
entries: itip_component
.entries
.iter()
.filter(|entry| {
matches!(
entry.name,
ICalendarProperty::Organizer
| ICalendarProperty::Attendee
| ICalendarProperty::Uid
| ICalendarProperty::Dtstamp
| ICalendarProperty::Sequence
| ICalendarProperty::RecurrenceId
) || (is_todo
&& matches!(
entry.name,
ICalendarProperty::PercentComplete
| ICalendarProperty::Status
| ICalendarProperty::Completed
))
})
.cloned()
.collect(),
component_ids: vec![],
},
});
} else {
return Err(ItipError::SenderIsNotParticipant(sender.to_string()));
}
}
Ok(())
}
pub fn itip_merge_changes(ical: &mut ICalendar, changes: Vec<MergeAction>) {
let mut remove_component_ids = Vec::new();
for action in changes {
match action {
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)
}