Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+377
View File
@@ -0,0 +1,377 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
Email, InstanceId, ItipEntries, ItipEntryValue, ItipError, ItipMessage, ItipSnapshot,
ItipSnapshots, ItipSummary,
itip::{
ItipExportAs, can_attendee_modify_property, itip_add_tz, itip_build_envelope,
itip_export_component,
},
organizer::organizer_request_full,
};
use ahash::{AHashMap, AHashSet};
use calcard::{
common::PartialDateTime,
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod, ICalendarParameter,
ICalendarParticipationStatus, ICalendarProperty, ICalendarValue,
},
};
pub(crate) fn attendee_handle_update(
new_ical: &ICalendar,
old_itip: ItipSnapshots<'_>,
new_itip: ItipSnapshots<'_>,
) -> Result<Vec<ItipMessage<ICalendar>>, 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 new_delegates = AHashSet::new();
let mut part_stat = &ICalendarParticipationStatus::NeedsAction;
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(local_attendee), Some(old_local_attendee))
if local_attendee.email == old_local_attendee.email =>
{
// Distinguish a genuine add/remove of a restricted property from a value-only drift
// caused by a client re-encoding the same property
let old_name_counts = count_entry_names(&old_instance.entries);
let new_name_counts = count_entry_names(&instance.entries);
// Check added fields
let mut send_update = false;
for new_entry in instance.entries.difference(&old_instance.entries) {
match (new_entry.name, &new_entry.value) {
(ICalendarProperty::Exdate, ItipEntryValue::DateTime(date))
if instance_id == &InstanceId::Main =>
{
if let Some((mut cancel_comp, attendee_email)) = attendee_decline(
instance_id,
&old_itip,
old_instance,
&dt_stamp,
&mut email_rcpt,
false,
) {
// Add EXDATE as RECURRENCE-ID
cancel_comp
.entries
.push(date.to_entry(ICalendarProperty::RecurrenceId));
part_stat = &ICalendarParticipationStatus::Declined;
// Add cancel component
let comp_id = message.components.len() as u32;
message.components[0].component_ids.push(comp_id);
message.components.push(cancel_comp);
mail_from = Some(&attendee_email.email);
}
}
_ => {
// Changing these properties is not allowed
if !can_attendee_modify_property(
&instance.comp.component_type,
new_entry.name,
) {
if name_count(&new_name_counts, new_entry.name)
> name_count(&old_name_counts, new_entry.name)
{
return Err(ItipError::CannotModifyProperty(
new_entry.name.clone(),
));
}
} else {
send_update = send_update
|| (instance.comp.component_type
== ICalendarComponentType::VTodo
&& matches!(
new_entry.name,
ICalendarProperty::Status
| ICalendarProperty::PercentComplete
| ICalendarProperty::Completed
));
}
}
}
}
// Send participation status update
if local_attendee.is_server_scheduling
&& ((local_attendee.part_stat != old_local_attendee.part_stat)
|| local_attendee.force_send.is_some()
|| send_update)
{
// Build the attendee list
if let Some(new_partstat) = local_attendee.part_stat {
part_stat = new_partstat;
}
let mut attendee_entry_uids = vec![local_attendee.entry_id];
let old_delegates = old_instance
.external_attendees()
.filter(|a| a.is_delegated_from(old_local_attendee))
.map(|a| a.email.email.as_str())
.collect::<AHashSet<_>>();
for external_attendee in instance.external_attendees() {
if external_attendee.is_delegated_from(local_attendee) {
if external_attendee.send_invite_messages()
&& !old_delegates
.contains(&external_attendee.email.email.as_str())
{
new_delegates.insert(external_attendee.email.email.as_str());
}
} else if external_attendee.is_delegated_to(local_attendee) {
if external_attendee.send_update_messages() {
email_rcpt.insert(external_attendee.email.email.as_str());
}
} else {
continue;
}
attendee_entry_uids.push(external_attendee.entry_id);
}
let comp_id = message.components.len() as u32;
message.components[0].component_ids.push(comp_id);
message.components.push(itip_export_component(
instance.comp,
new_itip.uid,
&dt_stamp,
instance.sequence.unwrap_or_default(),
ItipExportAs::Attendee(attendee_entry_uids),
));
mail_from = Some(&local_attendee.email.email);
}
// Check removed fields
for removed_entry in old_instance.entries.difference(&instance.entries) {
if !can_attendee_modify_property(
&instance.comp.component_type,
removed_entry.name,
) && name_count(&old_name_counts, removed_entry.name)
> name_count(&new_name_counts, removed_entry.name)
{
// Removing these properties is not allowed
return Err(ItipError::CannotModifyProperty(
removed_entry.name.clone(),
));
}
}
}
_ => {
// Change in local attendee email is not allowed
return Err(ItipError::CannotModifyAddress);
}
}
} else if let Some(local_attendee) = instance
.local_attendee()
.filter(|_| instance_id != &InstanceId::Main)
{
let mut attendee_entry_uids = vec![local_attendee.entry_id];
for external_attendee in instance.external_attendees() {
if external_attendee.is_delegated_from(local_attendee) {
if external_attendee.send_invite_messages() {
new_delegates.insert(external_attendee.email.email.as_str());
}
} else if external_attendee.is_delegated_to(local_attendee) {
if external_attendee.send_update_messages() {
email_rcpt.insert(external_attendee.email.email.as_str());
}
} else {
continue;
}
attendee_entry_uids.push(external_attendee.entry_id);
}
// A new instance has been added
let comp_id = message.components.len() as u32;
message.components[0].component_ids.push(comp_id);
message.components.push(itip_export_component(
instance.comp,
new_itip.uid,
&dt_stamp,
instance.sequence.unwrap_or_default(),
ItipExportAs::Attendee(attendee_entry_uids),
));
mail_from = Some(&local_attendee.email.email);
} else {
return Err(ItipError::CannotModifyInstance);
}
}
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(
instance_id,
&old_itip,
old_instance,
&dt_stamp,
&mut email_rcpt,
false,
) {
// Add cancel component
let comp_id = message.components.len() as u32;
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::CannotModifyInstance);
}
}
}
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);
let mut responses = vec![ItipMessage {
from: from.to_string(),
from_organizer: false,
to: email_rcpt.into_iter().map(|e| e.to_string()).collect(),
summary: ItipSummary::Rsvp {
part_stat: part_stat.clone(),
current: new_itip
.main_instance_or_default()
.build_summary(Some(&new_itip.organizer), &[]),
},
message,
}];
// Invite new delegates
if !new_delegates.is_empty() {
let from = from.to_string();
let new_delegates = new_delegates
.into_iter()
.map(|e| e.to_string())
.collect::<Vec<_>>();
if let Ok(messages_) = organizer_request_full(new_ical, &new_itip, None, true) {
for mut message in messages_ {
message.from = from.clone();
message.to = new_delegates.clone();
message.from_organizer = false;
responses.push(message);
}
}
}
Ok(responses)
} else {
Err(ItipError::NothingToSend)
}
}
pub(crate) fn attendee_decline<'x>(
instance_id: &'x InstanceId,
itip: &'x ItipSnapshots<'x>,
comp: &'x ItipSnapshot<'x>,
dt_stamp: &'x PartialDateTime,
email_rcpt: &mut AHashSet<&'x str>,
skip_needs_action: bool,
) -> Option<(ICalendarComponent, &'x Email)> {
let component = comp.comp;
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)
&& match attendee.part_stat {
Some(
ICalendarParticipationStatus::Declined
| ICalendarParticipationStatus::Delegated,
) => attendee.force_send.is_some(),
Some(ICalendarParticipationStatus::NeedsAction) => !skip_needs_action,
_ => true,
}
{
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(comp.sequence.unwrap_or_default());
cancel_comp.entries.extend(
component
.entries
.iter()
.filter(|e| {
matches!(
e.name,
ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Duration
| ICalendarProperty::Due
| ICalendarProperty::Description
| ICalendarProperty::Summary
)
})
.cloned(),
);
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)
})
}
fn count_entry_names<'x>(entries: &'x ItipEntries<'x>) -> AHashMap<&'x ICalendarProperty, usize> {
let mut counts = AHashMap::with_capacity(entries.len());
for entry in entries {
*counts.entry(entry.name).or_insert(0) += 1;
}
counts
}
#[inline]
fn name_count(counts: &AHashMap<&ICalendarProperty, usize>, name: &ICalendarProperty) -> usize {
counts.get(name).copied().unwrap_or(0)
}
@@ -0,0 +1,183 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* 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],
is_deletion: bool,
) -> 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,
is_deletion,
) {
// Add cancel component
let comp_id = message.components.len() as u32;
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
}
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
ItipError, ItipMessage, itip::itip_finalize, organizer::organizer_request_full,
snapshot::itip_snapshot,
};
use calcard::icalendar::ICalendar;
pub fn itip_create(
ical: &mut ICalendar,
account_emails: &[String],
) -> Result<Vec<ItipMessage<ICalendar>>, 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 {
organizer_request_full(ical, &itip, None, true).inspect(|_| {
itip_finalize(ical, &[]);
})
}
}
@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
ItipError, ItipMessage, attendee::attendee_handle_update, event_cancel::itip_cancel,
itip::itip_finalize, organizer::organizer_handle_update, snapshot::itip_snapshot,
};
use calcard::icalendar::ICalendar;
pub fn itip_update(
ical: &mut ICalendar,
old_ical: &ICalendar,
account_emails: &[String],
) -> Result<Vec<ItipMessage<ICalendar>>, 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::OrganizerMismatch)
} else if old_itip.organizer.email.is_local {
organizer_handle_update(old_ical, ical, old_itip, new_itip, &mut sequences)
} else {
attendee_handle_update(ical, old_itip, new_itip)
}
.inspect(|_| {
itip_finalize(ical, &sequences);
})
}
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, false).map(|message| vec![message])
} else {
Err(ItipError::CannotModifyAddress)
}
}
_ => Err(err),
}
}
}
}
+594
View File
@@ -0,0 +1,594 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::ItipValue;
use calcard::{
common::timezone::Tz,
icalendar::{ICalendarDay, ICalendarFrequency, ICalendarRecurrenceRule, ICalendarWeekday},
};
use chrono::{DateTime, NaiveDate, TimeZone, Weekday};
use common::i18n::{self, Locale, PluralForms};
use icu_datetime::{DateTimeFormatter, fieldsets};
use icu_locale_core::{Locale as IcuLocale, locale};
use icu_plurals::{PluralCategory, PluralRuleType, PluralRules, PluralRulesOptions};
use std::fmt::Write;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DateStyle {
Short,
Long,
}
pub struct TextFormatter {
pub locale: &'static Locale,
date_short: DateTimeFormatter<fieldsets::YMDT>,
date_long: DateTimeFormatter<fieldsets::YMDT>,
weekday_short: DateTimeFormatter<fieldsets::E>,
weekday: DateTimeFormatter<fieldsets::E>,
month: DateTimeFormatter<fieldsets::M>,
cardinal: PluralRules,
ordinal: PluralRules,
}
impl TextFormatter {
pub fn new(language: &str) -> trc::Result<Self> {
let locale = i18n::locale_or_default(language);
let icu_locale = IcuLocale::try_from_str(locale.name).unwrap_or(locale!("en-US"));
let failed = |detail: &'static str| {
move |err: icu_datetime::DateTimeFormatterLoadError| {
trc::EventType::Calendar(trc::CalendarEvent::ItipMessageError)
.into_err()
.caused_by(trc::location!())
.details(detail)
.ctx(trc::Key::Reason, err.to_string())
}
};
let plural_prefs = (&icu_locale).into();
let datetime_prefs = (&icu_locale).into();
let plural_rules = |options| {
PluralRules::try_new(plural_prefs, options).map_err(|err| {
trc::EventType::Calendar(trc::CalendarEvent::ItipMessageError)
.into_err()
.caused_by(trc::location!())
.details("Failed to load plural rules")
.ctx(trc::Key::Reason, err.to_string())
})
};
Ok(Self {
locale,
date_short: DateTimeFormatter::try_new(
datetime_prefs,
fieldsets::YMD::medium().with_time_hm(),
)
.map_err(failed("Failed to load short date formatter"))?,
date_long: DateTimeFormatter::try_new(
datetime_prefs,
fieldsets::YMD::long().with_time_hm(),
)
.map_err(failed("Failed to load long date formatter"))?,
weekday_short: DateTimeFormatter::try_new(datetime_prefs, fieldsets::E::short())
.map_err(failed("Failed to load short weekday formatter"))?,
weekday: DateTimeFormatter::try_new(datetime_prefs, fieldsets::E::long())
.map_err(failed("Failed to load weekday formatter"))?,
month: DateTimeFormatter::try_new(datetime_prefs, fieldsets::M::long())
.map_err(failed("Failed to load month formatter"))?,
cardinal: plural_rules(PluralRulesOptions::default())?,
ordinal: plural_rules(
PluralRulesOptions::default().with_type(PluralRuleType::Ordinal),
)?,
})
}
pub fn field(&self, out: &mut String, value: &ItipValue, style: DateStyle) {
match value {
ItipValue::Text(text) => out.push_str(text),
ItipValue::Time(time) => {
let tz = Tz::from_id(time.tz_id).unwrap_or(Tz::UTC);
let (weekday, date) = match style {
DateStyle::Short => (&self.weekday_short, &self.date_short),
DateStyle::Long => (&self.weekday, &self.date_long),
};
let local = tz
.from_utc_datetime(
&DateTime::from_timestamp(time.start, 0)
.unwrap_or_default()
.naive_local(),
)
.naive_local();
let _ = write!(
out,
"{}, {}",
weekday.format(&local.date()),
date.format(&local)
);
if let Some(name) = tz.name().filter(|name| !name.is_empty()) {
let _ = write!(out, " ({name})");
}
}
ItipValue::Rrule(rrule) => self.recurrence(out, rrule),
ItipValue::Participants(_) => {}
}
}
pub fn field_to_string(&self, value: &ItipValue, style: DateStyle) -> String {
let mut out = String::with_capacity(32);
self.field(&mut out, value, style);
out
}
pub fn recurrence(&self, out: &mut String, rule: &ICalendarRecurrenceRule) {
let start = out.len();
self.write_frequency(out, &rule.freq, rule.interval.unwrap_or(1));
if !rule.byday.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_on, |out| {
self.write_list(out, rule.byday.len(), |out, index| {
self.write_day(out, &rule.byday[index])
})
});
}
if !rule.byhour.is_empty() || !rule.byminute.is_empty() {
let hours = rule.byhour.len().max(1);
let minutes = rule.byminute.len().max(1);
self.write_clause(out, start, self.locale.calendar_rrule_at, |out| {
self.write_list(out, hours * minutes, |out, index| {
let hour = rule.byhour.get(index / minutes).copied().unwrap_or(0);
let minute = rule.byminute.get(index % minutes).copied().unwrap_or(0);
let _ = write!(out, "{hour:02}:{minute:02}");
})
});
}
if !rule.bymonthday.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_on_the, |out| {
self.write_list(out, rule.bymonthday.len(), |out, index| {
self.write_signed_ordinal(out, rule.bymonthday[index] as i32)
})
});
}
if !rule.bymonth.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_in, |out| {
self.write_list(out, rule.bymonth.len(), |out, index| {
self.write_month(out, rule.bymonth[index].month())
})
});
}
if !rule.byyearday.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_on, |out| {
self.write_list(out, rule.byyearday.len(), |out, index| {
self.write_counted(
out,
rule.byyearday[index] as i32,
self.locale.calendar_rrule_year_day,
)
})
});
}
if !rule.byweekno.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_in, |out| {
self.write_list(out, rule.byweekno.len(), |out, index| {
self.write_counted(
out,
rule.byweekno[index] as i32,
self.locale.calendar_rrule_week_no,
)
})
});
}
if !rule.bysetpos.is_empty() {
self.write_clause(out, start, self.locale.calendar_rrule_setpos, |out| {
self.write_list(out, rule.bysetpos.len(), |out, index| {
self.write_signed_ordinal(out, rule.bysetpos[index])
})
});
}
if let Some(count) = rule.count.as_ref() {
if out.len() > start {
out.push_str(", ");
}
let form = self
.cardinal
.category_for(*count)
.plural_form(&self.locale.calendar_rrule_count);
write_number(out, form, "$n", *count as u64);
}
}
fn write_clause(
&self,
out: &mut String,
start: usize,
template: &str,
write_items: impl FnOnce(&mut String),
) {
let restore = out.len();
if out.len() > start {
out.push(' ');
}
let (before, after) = template.split_once("$list").unwrap_or((template, ""));
out.push_str(before);
let items_start = out.len();
write_items(out);
if out.len() == items_start {
out.truncate(restore);
} else {
out.push_str(after);
}
}
fn write_list(
&self,
out: &mut String,
len: usize,
mut write_item: impl FnMut(&mut String, usize),
) {
match len {
0 => {}
1 => write_item(out, 0),
_ => {
let conjunction = self.locale.calendar_rrule_and;
let (prefix, rest) = conjunction.split_once("$a").unwrap_or(("", conjunction));
let (separator, suffix) = rest.split_once("$b").unwrap_or((rest, ""));
out.push_str(prefix);
for index in 0..len - 1 {
if index > 0 {
out.push_str(", ");
}
write_item(out, index);
}
out.push_str(separator);
write_item(out, len - 1);
out.push_str(suffix);
}
}
}
fn write_frequency(&self, out: &mut String, freq: &ICalendarFrequency, interval: u16) {
let entry = match freq {
ICalendarFrequency::Secondly => self.locale.calendar_rrule_secondly,
ICalendarFrequency::Minutely => self.locale.calendar_rrule_minutely,
ICalendarFrequency::Hourly => self.locale.calendar_rrule_hourly,
ICalendarFrequency::Daily => self.locale.calendar_rrule_daily,
ICalendarFrequency::Weekly => self.locale.calendar_rrule_weekly,
ICalendarFrequency::Monthly => self.locale.calendar_rrule_monthly,
ICalendarFrequency::Yearly => self.locale.calendar_rrule_yearly,
};
let form = self
.cardinal
.category_for(interval as u32)
.plural_form(&entry);
write_number(out, form, "$n", interval as u64);
}
fn write_ordinal(&self, out: &mut String, n: u32) {
let form = self
.ordinal
.category_for(n)
.plural_form(&self.locale.calendar_rrule_ordinal);
write_number(out, form, "$n", n as u64);
}
fn write_signed_ordinal(&self, out: &mut String, value: i32) {
if value < 0 {
let (before, after) = self
.locale
.calendar_rrule_from_end
.split_once("$ordinal")
.unwrap_or((self.locale.calendar_rrule_from_end, ""));
out.push_str(before);
self.write_ordinal(out, value.unsigned_abs());
out.push_str(after);
} else {
self.write_ordinal(out, value.unsigned_abs());
}
}
fn write_counted(&self, out: &mut String, value: i32, template: &str) {
let count = value.unsigned_abs() as u64;
if value < 0 {
let (before, after) = self
.locale
.calendar_rrule_from_end
.split_once("$ordinal")
.unwrap_or((self.locale.calendar_rrule_from_end, ""));
out.push_str(before);
write_number(out, template, "$n", count);
out.push_str(after);
} else {
write_number(out, template, "$n", count);
}
}
fn write_weekday(&self, out: &mut String, weekday: ICalendarWeekday) {
let weekday = match weekday {
ICalendarWeekday::Monday => Weekday::Mon,
ICalendarWeekday::Tuesday => Weekday::Tue,
ICalendarWeekday::Wednesday => Weekday::Wed,
ICalendarWeekday::Thursday => Weekday::Thu,
ICalendarWeekday::Friday => Weekday::Fri,
ICalendarWeekday::Saturday => Weekday::Sat,
ICalendarWeekday::Sunday => Weekday::Sun,
};
if let Some(date) = NaiveDate::from_isoywd_opt(2024, 1, weekday) {
let _ = write!(out, "{}", self.weekday.format(&date));
}
}
fn write_month(&self, out: &mut String, month: u8) {
if let Some(date) = NaiveDate::from_ymd_opt(2024, month.clamp(1, 12) as u32, 1) {
let _ = write!(out, "{}", self.month.format(&date));
}
}
fn write_day(&self, out: &mut String, day: &ICalendarDay) {
let Some(occurrence) = day.ordwk.filter(|occurrence| *occurrence != 0) else {
self.write_weekday(out, day.weekday);
return;
};
let (wrap_before, wrap_after) = if occurrence < 0 {
let from_end = self.locale.calendar_rrule_from_end;
from_end.split_once("$ordinal").unwrap_or((from_end, ""))
} else {
("", "")
};
out.push_str(wrap_before);
let mut rest = self.locale.calendar_rrule_nth_weekday;
loop {
let Some((at, placeholder)) = ["$ordinal", "$weekday"]
.into_iter()
.filter_map(|placeholder| rest.find(placeholder).map(|at| (at, placeholder)))
.min()
else {
out.push_str(rest);
break;
};
out.push_str(&rest[..at]);
if placeholder == "$ordinal" {
self.write_ordinal(out, occurrence.unsigned_abs() as u32);
} else {
self.write_weekday(out, day.weekday);
}
rest = &rest[at + placeholder.len()..];
}
out.push_str(wrap_after);
}
}
trait PluralCategoryExt {
fn plural_form(&self, forms: &PluralForms) -> &'static str;
}
impl PluralCategoryExt for PluralCategory {
fn plural_form(&self, forms: &PluralForms) -> &'static str {
match self {
PluralCategory::Zero => forms.zero,
PluralCategory::One => forms.one,
PluralCategory::Two => forms.two,
PluralCategory::Few => forms.few,
PluralCategory::Many => forms.many,
PluralCategory::Other => forms.other,
}
}
}
fn write_number(out: &mut String, template: &str, placeholder: &str, value: u64) {
let mut rest = template;
while let Some((before, after)) = rest.split_once(placeholder) {
out.push_str(before);
let _ = write!(out, "{value}");
rest = after;
}
out.push_str(rest);
}
pub fn hyperlink(value: &str) -> Option<&str> {
let (scheme, _) = value.split_once(':')?;
["https", "http", "tel", "sip", "sips", "xmpp"]
.iter()
.any(|candidate| scheme.eq_ignore_ascii_case(candidate))
.then_some(value)
}
#[cfg(test)]
mod tests {
use super::{PluralCategoryExt, PluralForms, TextFormatter, i18n};
use calcard::icalendar::{
ICalendarDay, ICalendarFrequency, ICalendarRecurrenceRule, ICalendarWeekday,
};
use icu_plurals::PluralCategory;
fn rule(freq: ICalendarFrequency, interval: Option<u16>) -> ICalendarRecurrenceRule {
ICalendarRecurrenceRule {
freq,
interval,
..Default::default()
}
}
fn format(language: &str, rule: &ICalendarRecurrenceRule) -> String {
let mut out = String::new();
TextFormatter::new(language)
.expect("formatter")
.recurrence(&mut out, rule);
out
}
#[test]
fn every_shipped_locale_loads_icu_data() {
for locale in i18n::ALL_LOCALES {
let formatter = TextFormatter::new(locale.name)
.unwrap_or_else(|err| panic!("{}: {err:?}", locale.name));
assert_eq!(formatter.locale.name, locale.name);
let mut out = String::new();
formatter.recurrence(
&mut out,
&ICalendarRecurrenceRule {
freq: ICalendarFrequency::Monthly,
interval: Some(2),
count: Some(5),
bymonthday: vec![3, -1],
byday: vec![ICalendarDay {
ordwk: Some(2),
weekday: ICalendarWeekday::Monday,
}],
..Default::default()
},
);
assert!(!out.is_empty(), "{} produced no text", locale.name);
}
}
#[test]
fn plural_form_selects_category_and_falls_back_to_other() {
let forms = PluralForms {
zero: "many",
one: "single",
two: "many",
few: "a few",
many: "many",
other: "many",
};
assert_eq!(PluralCategory::One.plural_form(&forms), "single");
assert_eq!(PluralCategory::Few.plural_form(&forms), "a few");
assert_eq!(PluralCategory::Many.plural_form(&forms), "many");
assert_eq!(PluralCategory::Other.plural_form(&forms), "many");
// Categories a locale omits are filled from "other" at build time
let polish = i18n::locale("pl-PL").expect("locale must exist");
assert_eq!(polish.calendar_rrule_secondly.many, "Co $n sekund");
assert_eq!(
polish.calendar_rrule_secondly.zero,
polish.calendar_rrule_secondly.other
);
}
#[test]
fn frequency_uses_cardinal_plural_rules() {
assert_eq!(
format("en", &rule(ICalendarFrequency::Weekly, None)),
"Every week"
);
assert_eq!(
format("en", &rule(ICalendarFrequency::Weekly, Some(2))),
"Every 2 weeks"
);
// Polish distinguishes one / few / many, unlike English
assert_eq!(
format("pl", &rule(ICalendarFrequency::Weekly, Some(1))),
"Co tydzień"
);
assert_eq!(
format("pl", &rule(ICalendarFrequency::Weekly, Some(2))),
"Co 2 tygodnie"
);
assert_eq!(
format("pl", &rule(ICalendarFrequency::Weekly, Some(5))),
"Co 5 tygodni"
);
}
#[test]
fn weekday_names_are_localized_without_translation_keys() {
let mut byday = rule(ICalendarFrequency::Weekly, None);
byday.byday = vec![ICalendarDay {
weekday: ICalendarWeekday::Monday,
ordwk: None,
}];
assert_eq!(format("en", &byday), "Every week on Monday");
assert_eq!(format("es", &byday), "Cada semana los lunes");
}
#[test]
fn ordinal_weekday_uses_ordinal_plural_rules() {
let mut byday = rule(ICalendarFrequency::Monthly, None);
byday.byday = vec![ICalendarDay {
weekday: ICalendarWeekday::Tuesday,
ordwk: Some(2),
}];
assert_eq!(format("en", &byday), "Every month on the 2nd Tuesday");
byday.byday = vec![ICalendarDay {
weekday: ICalendarWeekday::Tuesday,
ordwk: Some(3),
}];
assert_eq!(format("en", &byday), "Every month on the 3rd Tuesday");
byday.byday = vec![ICalendarDay {
weekday: ICalendarWeekday::Tuesday,
ordwk: Some(-1),
}];
assert_eq!(
format("en", &byday),
"Every month on the 1st Tuesday from the end"
);
}
#[test]
fn count_is_pluralized_and_appended() {
let mut counted = rule(ICalendarFrequency::Daily, None);
counted.count = Some(1);
assert_eq!(format("en", &counted), "Every day, 1 time");
counted.count = Some(5);
assert_eq!(format("en", &counted), "Every day, 5 times");
}
#[test]
fn multiple_days_are_joined_with_the_localized_conjunction() {
let mut byday = rule(ICalendarFrequency::Weekly, None);
byday.byday = vec![
ICalendarDay {
weekday: ICalendarWeekday::Monday,
ordwk: None,
},
ICalendarDay {
weekday: ICalendarWeekday::Wednesday,
ordwk: None,
},
ICalendarDay {
weekday: ICalendarWeekday::Friday,
ordwk: None,
},
];
assert_eq!(
format("en", &byday),
"Every week on Monday, Wednesday and Friday"
);
}
#[test]
fn unknown_language_falls_back_to_english_rules() {
assert_eq!(
format("zz", &rule(ICalendarFrequency::Weekly, Some(3))),
"Every 3 weeks"
);
}
}
+624
View File
@@ -0,0 +1,624 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* 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::OrganizerIsLocalAddress);
}
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 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.extend(updated_attendee.delegated_to.iter().map(
|email| {
ICalendarParameter::delegated_to(Uri::Location(
email.to_string(),
))
},
));
}
}
// RFC 6638 3.2.5: the status defaults to 2.0 when the reply carries none
remove_parameters.push(ICalendarParameterName::ScheduleStatus);
add_parameters.push(ICalendarParameter::schedule_status(
if has_request_status {
itip_snapshot.request_status.join(",")
} else {
"2.0".to_string()
},
));
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.name,
ICalendarParameterName::DelegatedTo
| ICalendarParameterName::DelegatedFrom
| ICalendarParameterName::Partstat
| ICalendarParameterName::Rsvp
| ICalendarParameterName::ScheduleStatus
| ICalendarParameterName::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<u32> = 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.contains(&param.name));
}
MergeAction::AddComponent { component } => {
let comp_id = ical.components.len() as u32;
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 as u32);
}
}
}
if !remove_component_ids.is_empty() {
ical.remove_component_ids(&remove_component_ids);
}
}
pub fn itip_method(ical: &ICalendar) -> Result<&ICalendarMethod, ItipError> {
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)
}
+480
View File
@@ -0,0 +1,480 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{Email, ItipMessage, ItipMessages, ItipSummary};
use calcard::{
common::{IanaString, PartialDateTime},
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarEntry, ICalendarMethod,
ICalendarParameter, ICalendarParameterName, ICalendarParameterValue,
ICalendarParticipationStatus, ICalendarProperty, ICalendarScheduleAgentValue,
ICalendarValue,
},
};
use common::PROD_ID;
use registry::schema::structs::{
Task, TaskCalendarItipContents, TaskCalendarItipMessage, TaskStatus,
};
use store::write::BatchBuilder;
pub(crate) fn itip_build_envelope(method: ICalendarMethod) -> ICalendarComponent {
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(PROD_ID.to_string())],
},
ICalendarEntry {
name: ICalendarProperty::Method,
params: vec![],
values: vec![ICalendarValue::Method(method)],
},
],
component_ids: Default::default(),
}
}
pub fn itip_assign_organizer(ical: &mut ICalendar, organizer_address: &str) -> bool {
let mut assigned = false;
for component in &mut ical.components {
if !component.component_type.is_scheduling_object() {
continue;
}
let mut has_organizer = false;
let mut has_attendee = false;
for entry in &component.entries {
match entry.name {
ICalendarProperty::Organizer => has_organizer = true,
ICalendarProperty::Attendee => has_attendee = true,
_ => {}
}
}
if has_attendee && !has_organizer {
component.entries.push(ICalendarEntry {
name: ICalendarProperty::Organizer,
params: vec![],
values: vec![ICalendarValue::Text(format!("mailto:{organizer_address}"))],
});
assigned = true;
}
}
assigned
}
pub(crate) const ITIP_STATUS_INVALID_USER: &str = "3.7";
fn itip_is_server_scheduling(entry: &ICalendarEntry) -> bool {
!entry.params.iter().any(|param| {
matches!(
(&param.name, &param.value),
(
ICalendarParameterName::ScheduleAgent,
ICalendarParameterValue::ScheduleAgent(
ICalendarScheduleAgentValue::Client | ICalendarScheduleAgentValue::None
)
)
)
})
}
fn itip_unreachable_entries(
ical: &ICalendar,
account_emails: &[String],
) -> Option<Vec<(usize, usize)>> {
let (comp_id, entry_id, entry) = ical
.components
.iter()
.enumerate()
.filter(|(_, comp)| comp.component_type.is_scheduling_object())
.find_map(|(comp_id, comp)| {
comp.entries
.iter()
.enumerate()
.find(|(_, entry)| entry.name == ICalendarProperty::Organizer)
.map(|(entry_id, entry)| (comp_id, entry_id, entry))
})?;
if !itip_is_server_scheduling(entry) {
return None;
}
match Email::new(entry.values.first()?.as_text()?, account_emails) {
Some(email) if !email.is_local => return None,
Some(_) => {}
None => return Some(vec![(comp_id, entry_id)]),
}
let mut unreachable = Vec::new();
for (comp_id, comp) in ical.components.iter().enumerate() {
if !comp.component_type.is_scheduling_object() {
continue;
}
for (entry_id, entry) in comp.entries.iter().enumerate() {
if entry.name != ICalendarProperty::Attendee || !itip_is_server_scheduling(entry) {
continue;
}
let rsvp = !entry.params.iter().any(|param| {
matches!(
(&param.name, &param.value),
(
ICalendarParameterName::Rsvp,
ICalendarParameterValue::Bool(false)
)
)
});
if rsvp
&& entry
.values
.first()
.and_then(|value| value.as_text())
.is_some_and(|value| Email::new(value, account_emails).is_none())
{
unreachable.push((comp_id, entry_id));
}
}
}
Some(unreachable)
}
pub fn itip_unreachable_recipient<'x>(
ical: &'x ICalendar,
account_emails: &[String],
) -> Option<&'x str> {
itip_unreachable_entries(ical, account_emails)?
.first()
.and_then(|(comp_id, entry_id)| {
ical.components[*comp_id].entries[*entry_id]
.values
.first()
.and_then(|value| value.as_text())
})
}
pub fn itip_set_unreachable_status(ical: &mut ICalendar, account_emails: &[String]) {
let Some(unreachable) = itip_unreachable_entries(ical, account_emails) else {
return;
};
for (comp_id, comp) in ical.components.iter_mut().enumerate() {
if !comp.component_type.is_scheduling_object() {
continue;
}
for (entry_id, entry) in comp.entries.iter_mut().enumerate() {
if !matches!(
entry.name,
ICalendarProperty::Organizer | ICalendarProperty::Attendee
) {
continue;
}
entry.params.retain(|param| {
param.name != ICalendarParameterName::ScheduleStatus
|| param.value.as_text() != Some(ITIP_STATUS_INVALID_USER)
});
if unreachable.contains(&(comp_id, entry_id)) {
entry.params.push(ICalendarParameter::schedule_status(
ITIP_STATUS_INVALID_USER.to_string(),
));
}
}
}
}
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: i64,
export_as: ItipExportAs<'_>,
) -> ICalendarComponent {
let is_todo = component.component_type == ICalendarComponentType::VTodo;
let mut comp = ICalendarComponent {
component_type: component.component_type.clone(),
entries: Vec::with_capacity(component.entries.len() + 1),
component_ids: Default::default(),
};
comp.add_dtstamp(dt_stamp.clone());
comp.add_sequence(sequence);
comp.add_uid(uid);
for (entry_id, entry) in component.entries.iter().enumerate() {
match (&entry.name, &export_as) {
(
ICalendarProperty::Organizer | ICalendarProperty::Attendee,
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;
let mut rsvp = true;
for entry in &entry.params {
match &entry.name {
ICalendarParameterName::ScheduleStatus
| ICalendarParameterName::ScheduleAgent
| ICalendarParameterName::ScheduleForceSend => {}
_ => {
match &entry.name {
ICalendarParameterName::Rsvp => {
rsvp = !matches!(
entry.value,
ICalendarParameterValue::Bool(false)
);
}
ICalendarParameterName::Partstat => {
has_partstat = true;
}
_ => {}
}
new_entry.params.push(entry.clone())
}
}
}
if !has_partstat && rsvp && entry.name == ICalendarProperty::Attendee {
new_entry
.params
.push(ICalendarParameter::partstat((*partstat).clone()));
}
comp.entries.push(new_entry);
}
(
ICalendarProperty::Organizer | ICalendarProperty::Attendee,
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.name,
ICalendarParameterName::ScheduleStatus
| ICalendarParameterName::ScheduleAgent
| ICalendarParameterName::ScheduleForceSend
)
})
.cloned()
.collect(),
values: entry.values.clone(),
});
}
(
ICalendarProperty::RequestStatus
| ICalendarProperty::Dtstamp
| ICalendarProperty::Sequence
| ICalendarProperty::Uid,
_,
) => {}
(_, ItipExportAs::Organizer(_))
| (
ICalendarProperty::RecurrenceId
| ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Duration
| ICalendarProperty::Due
| ICalendarProperty::Description
| ICalendarProperty::Summary,
_,
) => {
comp.entries.push(entry.clone());
}
(
ICalendarProperty::Status
| ICalendarProperty::PercentComplete
| ICalendarProperty::Completed,
_,
) if is_todo => {
comp.entries.push(entry.clone());
}
_ => {}
}
}
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(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
for entry in comp.entries.iter_mut() {
if matches!(
entry.name,
ICalendarProperty::Organizer | ICalendarProperty::Attendee
) {
entry.params.retain(|param| {
!matches!(param.name, ICalendarParameterName::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(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.name, ICalendarParameterName::Tzid))
})
}) && !has_timezones
{
message.copy_timezones(ical);
}
message.add_missing_timezones();
}
#[inline]
pub(crate) fn can_attendee_modify_property(
component_type: &ICalendarComponentType,
property: &ICalendarProperty,
) -> bool {
match component_type {
ICalendarComponentType::VEvent | ICalendarComponentType::VJournal => {
matches!(
property,
ICalendarProperty::Exdate
| ICalendarProperty::Summary
| ICalendarProperty::Description
| ICalendarProperty::Comment
)
}
ICalendarComponentType::VTodo => matches!(
property,
ICalendarProperty::Exdate
| ICalendarProperty::Summary
| ICalendarProperty::Description
| ICalendarProperty::Status
| ICalendarProperty::PercentComplete
| ICalendarProperty::Completed
| ICalendarProperty::Comment
),
_ => false,
}
}
impl ItipMessages {
pub fn new(messages: Vec<ItipMessage<ICalendar>>) -> Self {
ItipMessages {
messages: messages
.into_iter()
.map(|m| TaskCalendarItipContents {
from: m.from,
i_calendar_data: m.message.to_string(),
is_from_organizer: m.from_organizer,
summary: serde_json::to_string(&m.summary).unwrap_or_default(),
to: m.to.into(),
})
.collect(),
}
}
pub fn queue(self, batch: &mut BatchBuilder) -> trc::Result<()> {
batch.schedule_task(Task::CalendarItipMessage(TaskCalendarItipMessage {
account_id: batch.last_account_id().unwrap().into(),
document_id: batch.last_document_id().unwrap().into(),
messages: self.messages.into(),
status: TaskStatus::now(),
}));
Ok(())
}
}
impl From<ItipMessage<ICalendar>> for ItipMessage<String> {
fn from(message: ItipMessage<ICalendar>) -> Self {
ItipMessage {
from: message.from,
from_organizer: message.from_organizer,
to: message.to,
summary: message.summary,
message: message.message.to_string(),
}
}
}
impl ItipSummary {
pub fn method(&self) -> &str {
match self {
ItipSummary::Invite(_) => ICalendarMethod::Request.as_str(),
ItipSummary::Update { method, .. } => method.as_str(),
ItipSummary::Cancel(_) => ICalendarMethod::Cancel.as_str(),
ItipSummary::Rsvp { .. } => ICalendarMethod::Reply.as_str(),
}
}
}
+449
View File
@@ -0,0 +1,449 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{decode_mailto_address, extract_addr_spec};
use ahash::{AHashMap, AHashSet};
use calcard::{
common::{IanaString, PartialDateTime},
icalendar::{
ICalendar, ICalendarComponent, ICalendarDuration, ICalendarEntry, ICalendarMethod,
ICalendarParameter, ICalendarParticipationRole, ICalendarParticipationStatus,
ICalendarPeriod, ICalendarProperty, ICalendarRecurrenceRule,
ICalendarScheduleForceSendValue, ICalendarStatus, ICalendarUserTypes, ICalendarValue, Uri,
},
};
use indexmap::IndexSet;
use registry::schema::structs::TaskCalendarItipContents;
use std::{fmt::Display, hash::Hash};
use utils::sanitize_email;
pub mod attendee;
pub mod event_cancel;
pub mod event_create;
pub mod event_update;
pub mod format;
pub mod inbound;
pub mod itip;
pub mod organizer;
pub mod snapshot;
#[derive(Debug)]
pub struct ItipSnapshots<'x> {
pub organizer: Organizer<'x>,
pub uid: &'x str,
pub components: AHashMap<InstanceId, ItipSnapshot<'x>>,
}
#[derive(Debug)]
pub struct ItipSnapshot<'x> {
pub comp_id: u16,
pub comp: &'x ICalendarComponent,
pub attendees: AHashSet<Attendee<'x>>,
pub dtstamp: Option<&'x PartialDateTime>,
pub entries: ItipEntries<'x>,
pub sequence: Option<i64>,
pub request_status: Vec<&'x str>,
}
pub type ItipEntries<'x> = IndexSet<ItipEntry<'x>, ahash::RandomState>;
#[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(ItipDateTime<'x>),
Period(&'x ICalendarPeriod),
Duration(&'x ICalendarDuration),
Status(&'x ICalendarStatus),
RRule(&'x ICalendarRecurrenceRule),
Text(&'x str),
Integer(i64),
}
#[derive(Debug)]
pub struct ItipDateTime<'x> {
pub date: &'x PartialDateTime,
pub tz_id: Option<&'x str>,
pub tz_code: u16,
pub timestamp: i64,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InstanceId {
Main,
Recurrence(RecurrenceId),
}
#[derive(Debug, PartialOrd, Ord)]
pub struct RecurrenceId {
pub entry_id: u16,
pub date: i64,
pub this_and_future: bool,
}
#[derive(Debug)]
pub struct Attendee<'x> {
pub entry_id: u16,
pub email: Email,
pub name: Option<&'x str>,
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 name: Option<&'x str>,
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 enum ItipError {
NoSchedulingInfo,
OtherSchedulingAgent,
NotOrganizer,
NotOrganizerNorAttendee,
NothingToSend,
MissingUid,
MultipleUid,
MultipleOrganizer,
MultipleObjectTypes,
MultipleObjectInstances,
CannotModifyProperty(ICalendarProperty),
CannotModifyInstance,
CannotModifyAddress,
OrganizerMismatch,
MissingMethod,
InvalidComponentType,
OutOfSequence,
OrganizerIsLocalAddress,
SenderIsNotOrganizerNorAttendee,
SenderIsNotParticipant(String),
UnknownParticipant(String),
UnsupportedMethod(ICalendarMethod),
ICalendarParseError,
EventNotFound,
EventTooLarge,
QuotaExceeded,
NoDefaultCalendar,
AutoAddDisabled,
}
#[derive(Debug)]
pub struct ItipMessage<T> {
pub from: String,
pub from_organizer: bool,
pub to: Vec<String>,
pub summary: ItipSummary,
pub message: T,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum ItipSummary {
Invite(Vec<ItipField>),
Update {
method: ICalendarMethod,
current: Vec<ItipField>,
previous: Vec<ItipField>,
},
Cancel(Vec<ItipField>),
Rsvp {
part_stat: ICalendarParticipationStatus,
current: Vec<ItipField>,
},
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ItipField {
pub name: ICalendarProperty,
pub value: ItipValue,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", content = "value")]
pub enum ItipValue {
Text(String),
Time(ItipTime),
Rrule(Box<ICalendarRecurrenceRule>),
Participants(Vec<ItipParticipant>),
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ItipTime {
pub start: i64,
pub tz_id: u16,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ItipParticipant {
pub email: String,
pub name: Option<String>,
pub is_organizer: bool,
}
pub struct ItipMessages {
pub messages: Vec<TaskCalendarItipContents>,
}
impl Attendee<'_> {
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
}))
}
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))
}
pub fn is_delegated_from(&self, attendee: &Attendee<'_>) -> bool {
self.delegated_from
.iter()
.any(|d| d.email == attendee.email.email)
}
pub fn is_delegated_to(&self, attendee: &Attendee<'_>) -> bool {
self.delegated_to
.iter()
.any(|d| d.email == attendee.email.email)
}
}
impl Email {
pub fn new(email: &str, local_addresses: &[String]) -> Option<Self> {
let decoded = decode_mailto_address(email.trim());
let email = sanitize_email(decoded.as_ref())
.or_else(|| extract_addr_spec(decoded.as_ref()).and_then(sanitize_email))?;
let is_local = local_addresses.contains(&email);
Some(Email { email, is_local })
}
pub fn from_uri(uri: &Uri, local_addresses: &[String]) -> Option<Self> {
if let Uri::Location(uri) = uri {
Email::new(uri.as_str(), local_addresses)
} else {
None
}
}
}
pub fn ical_size(ical: &ICalendar) -> usize {
struct SizeWriter(usize);
impl std::fmt::Write for SizeWriter {
fn write_str(&mut self, text: &str) -> std::fmt::Result {
self.0 += text.len();
Ok(())
}
}
let mut writer = SizeWriter(0);
let _ = ical.write_to(&mut writer);
writer.0
}
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);
}
}
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()))],
}
}
}
impl ItipError {
pub fn is_jmap_error(&self) -> bool {
matches!(
self,
ItipError::MultipleOrganizer
| ItipError::OrganizerIsLocalAddress
| ItipError::SenderIsNotParticipant(_)
| ItipError::OrganizerMismatch
| ItipError::CannotModifyProperty(_)
| ItipError::CannotModifyInstance
| ItipError::CannotModifyAddress
//| ItipError::MissingUid
| ItipError::MultipleUid
| ItipError::MultipleObjectTypes
| ItipError::MultipleObjectInstances
| ItipError::MissingMethod
| ItipError::InvalidComponentType
| ItipError::OutOfSequence
| ItipError::UnknownParticipant(_)
| ItipError::UnsupportedMethod(_)
)
}
}
impl Display for ItipError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ItipError::NoSchedulingInfo => write!(f, "No scheduling information found"),
ItipError::OtherSchedulingAgent => write!(f, "Other scheduling agent"),
ItipError::NotOrganizer => write!(f, "Not the organizer of the event"),
ItipError::NotOrganizerNorAttendee => write!(f, "Not an organizer or attendee"),
ItipError::NothingToSend => write!(f, "No iTIP messages to send"),
ItipError::MissingUid => write!(f, "Missing UID in iCalendar object"),
ItipError::MultipleUid => write!(f, "Multiple UIDs found in iCalendar object"),
ItipError::MultipleOrganizer => {
write!(f, "Multiple organizers found in iCalendar object")
}
ItipError::MultipleObjectTypes => {
write!(f, "Multiple object types found in iCalendar object")
}
ItipError::MultipleObjectInstances => {
write!(f, "Multiple object instances found in iCalendar object")
}
ItipError::CannotModifyProperty(prop) => {
write!(f, "Cannot modify property {}", prop.as_str())
}
ItipError::CannotModifyInstance => write!(f, "Cannot modify instance of the event"),
ItipError::CannotModifyAddress => write!(f, "Cannot modify address of the event"),
ItipError::OrganizerMismatch => write!(f, "Organizer mismatch in iCalendar object"),
ItipError::MissingMethod => write!(f, "Missing method in the iTIP message"),
ItipError::InvalidComponentType => {
write!(f, "Invalid component type in iCalendar object")
}
ItipError::OutOfSequence => write!(f, "Old sequence number found"),
ItipError::OrganizerIsLocalAddress => {
write!(
f,
"Organizer matches one of the recipient's account addresses"
)
}
ItipError::SenderIsNotParticipant(participant) => {
write!(f, "Sender {participant:?} is not a participant")
}
ItipError::SenderIsNotOrganizerNorAttendee => {
write!(f, "Sender is neither organizer nor attendee")
}
ItipError::UnknownParticipant(participant) => {
write!(f, "Unknown participant: {}", participant)
}
ItipError::UnsupportedMethod(method) => {
write!(f, "Unsupported method: {}", method.as_str())
}
ItipError::ICalendarParseError => write!(f, "Failed to parse iCalendar object"),
ItipError::EventNotFound => write!(f, "Event found in index but not in database"),
ItipError::EventTooLarge => write!(
f,
"Applying the iTIP message would exceed the maximum event size"
),
ItipError::QuotaExceeded => write!(f, "Quota exceeded"),
ItipError::NoDefaultCalendar => write!(f, "No default calendar found for the account"),
ItipError::AutoAddDisabled => {
write!(f, "Auto-adding events is disabled for this account")
}
}
}
}
@@ -0,0 +1,403 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
InstanceId, ItipError, ItipMessage, ItipSnapshots, ItipSummary,
event_cancel::build_cancel_component,
itip::{ItipExportAs, itip_add_tz, itip_build_envelope, itip_export_component},
};
use ahash::{AHashMap, AHashSet};
use calcard::{
common::PartialDateTime,
icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarMethod,
ICalendarParticipationStatus, ICalendarProperty, ICalendarStatus,
},
};
use std::collections::hash_map::Entry;
pub(crate) fn organizer_handle_update(
old_ical: &ICalendar,
new_ical: &ICalendar,
old_itip: ItipSnapshots<'_>,
new_itip: ItipSnapshots<'_>,
increment_sequences: &mut Vec<u16>,
) -> Result<Vec<ItipMessage<ICalendar>>, ItipError> {
let mut changed_instances: Vec<(&InstanceId, &str, &ICalendarMethod)> = Vec::new();
let mut increment_sequence = false;
let mut changed_properties = AHashSet::new();
for (instance_id, instance) in &new_itip.components {
if let Some(old_instance) = old_itip.components.get(instance_id) {
let changed_entries = instance.entries != old_instance.entries;
let changed_attendees = instance.attendees != old_instance.attendees;
if changed_entries || changed_attendees {
if changed_entries {
for entry in instance.entries.symmetric_difference(&old_instance.entries) {
increment_sequence = increment_sequence
|| matches!(
entry.name,
ICalendarProperty::Dtstart
| ICalendarProperty::Dtend
| ICalendarProperty::Duration
| ICalendarProperty::Due
| ICalendarProperty::Rrule
| ICalendarProperty::Rdate
| ICalendarProperty::Exdate
| ICalendarProperty::Status
| ICalendarProperty::Location
);
changed_properties.insert(entry.name);
}
}
if changed_attendees {
changed_instances.extend(
old_instance
.external_attendees()
.filter(|attendee| attendee.send_update_messages())
.map(|attendee| attendee.email.email.as_str())
.collect::<AHashSet<_>>()
.difference(
&instance
.external_attendees()
.map(|attendee| attendee.email.email.as_str())
.collect::<AHashSet<_>>(),
)
.map(|attendee| (instance_id, *attendee, &ICalendarMethod::Cancel)),
);
changed_properties.insert(&ICalendarProperty::Attendee);
increment_sequence = true;
}
changed_instances.extend(instance.attendees.iter().filter_map(|attendee| {
if attendee.send_update_messages() {
Some((
instance_id,
attendee.email.email.as_str(),
&ICalendarMethod::Request,
))
} else {
None
}
}));
}
} else if instance_id != &InstanceId::Main {
changed_properties.insert(&ICalendarProperty::Exdate);
let method = if matches!(instance.comp.status(), Some(ICalendarStatus::Cancelled)) {
&ICalendarMethod::Cancel
} else {
&ICalendarMethod::Request
};
changed_instances.extend(instance.attendees.iter().filter_map(|attendee| {
if attendee.send_invite_messages() {
Some((instance_id, attendee.email.email.as_str(), method))
} else {
None
}
}));
increment_sequence = true;
} else {
return Err(ItipError::CannotModifyInstance);
}
}
for (instance_id, old_instance) in &old_itip.components {
if !new_itip.components.contains_key(instance_id) {
if instance_id != &InstanceId::Main {
changed_instances.extend(old_instance.attendees.iter().filter_map(|attendee| {
if attendee.send_update_messages() {
Some((
instance_id,
attendee.email.email.as_str(),
&ICalendarMethod::Cancel,
))
} else {
None
}
}));
changed_properties.insert(&ICalendarProperty::Exdate);
increment_sequence = true;
} else {
return Err(ItipError::CannotModifyInstance);
}
}
}
if changed_instances.is_empty() {
return Err(ItipError::NothingToSend);
}
// Remove partial notifications for attendees that receive a full update for the main instance
// or, that will receive both add and remove messages
let mut send_full_update: AHashSet<&str> = AHashSet::new();
let mut send_partial_update: AHashMap<&str, AHashMap<&ICalendarMethod, Vec<&InstanceId>>> =
AHashMap::new();
for (instance_id, email, method) in &changed_instances {
if *instance_id == &InstanceId::Main && *method == &ICalendarMethod::Request {
send_full_update.insert(*email);
send_partial_update.remove(email);
} else if !send_full_update.contains(email) {
match send_partial_update.entry(email) {
Entry::Occupied(mut entry) => {
let entry = entry.get_mut();
let is_empty = entry.is_empty();
match entry.entry(method) {
Entry::Occupied(mut method_entry) => {
method_entry.get_mut().push(*instance_id);
}
Entry::Vacant(method_entry) if is_empty => {
method_entry.insert(vec![*instance_id]);
}
_ => {
// Switch to full update for this participant
send_full_update.insert(*email);
send_partial_update.remove(email);
}
}
}
Entry::Vacant(entry) => {
entry.insert(AHashMap::from_iter([(*method, vec![*instance_id])]));
}
}
}
}
// Build summary of changed properties
let new_summary = new_itip
.main_instance_or_default()
.build_summary(Some(&new_itip.organizer), &[]);
let old_summary = old_itip
.main_instance_or_default()
.build_summary(Some(&old_itip.organizer), &new_summary);
// Prepare full updates
let mut messages = Vec::new();
if !send_full_update.is_empty() {
match organizer_request_full(
new_ical,
&new_itip,
increment_sequence.then_some(increment_sequences),
false,
) {
Ok(messages_) => {
for mut message in messages_ {
message.summary = ItipSummary::Update {
method: ICalendarMethod::Request,
current: new_summary.clone(),
previous: old_summary.clone(),
};
messages.push(message);
}
}
Err(err) => {
if send_partial_update.is_empty() {
return Err(err);
}
}
}
}
// Prepare partial updates
if !send_partial_update.is_empty() {
// Group updates by email and method
let mut updates: AHashMap<(&ICalendarMethod, Vec<&InstanceId>), Vec<&str>> =
AHashMap::new();
for (email, partial_updates) in send_partial_update {
for (method, mut instances) in partial_updates {
instances.sort_unstable();
instances.dedup();
updates.entry((method, instances)).or_default().push(email);
}
}
let dt_stamp = PartialDateTime::now();
for ((method, instances), emails) in updates {
let (mut ical, mut itip, is_cancel) = if matches!(method, ICalendarMethod::Cancel) {
(old_ical, &old_itip, true)
} else {
(new_ical, &new_itip, false)
};
// Prepare iTIP message
let mut message = ICalendar {
components: Vec::with_capacity(instances.len() + 1),
};
message.components.push(itip_build_envelope(method.clone()));
let mut increment_sequences = Vec::new();
for instance_id in instances {
let comp = match itip.components.get(instance_id) {
Some(comp) => comp,
None => {
// New component added with CANCELLED status
ical = new_ical;
itip = &new_itip;
itip.components.get(instance_id).unwrap()
}
};
// Prepare component for iTIP
let sequence = if increment_sequence {
comp.sequence.unwrap_or_default() + 1
} else {
comp.sequence.unwrap_or_default()
};
let orig_component = comp.comp;
let component = if !is_cancel {
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,
ItipExportAs::Organizer(&ICalendarParticipationStatus::NeedsAction),
)
} else {
build_cancel_component(orig_component, sequence, dt_stamp.clone(), &emails)
};
// Add component to message
let comp_id = message.components.len() as u32;
message.components.push(component);
message.components[0].component_ids.push(comp_id);
}
// Add timezones
itip_add_tz(&mut message, ical);
messages.push(ItipMessage {
from: itip.organizer.email.email.clone(),
from_organizer: true,
to: emails.into_iter().map(|e| e.to_string()).collect(),
summary: if method == &ICalendarMethod::Cancel {
ItipSummary::Cancel(
new_summary
.iter()
.chain(old_summary.iter())
.map(|summary| (&summary.name, summary))
.collect::<AHashMap<_, _>>()
.into_values()
.cloned()
.collect(),
)
} else {
ItipSummary::Update {
method: method.clone(),
current: new_summary.clone(),
previous: old_summary.clone(),
}
},
message,
});
}
}
Ok(messages)
}
pub(crate) fn organizer_request_full(
ical: &ICalendar,
itip: &ItipSnapshots<'_>,
mut increment_sequence: Option<&mut Vec<u16>>,
is_first_request: bool,
) -> Result<Vec<ItipMessage<ICalendar>>, 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();
for comp in itip.components.values() {
// Skip private components
if comp.attendees.is_empty() {
continue;
}
// 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,
ItipExportAs::Organizer(&ICalendarParticipationStatus::NeedsAction),
);
// Add VALARM sub-components
if is_first_request {
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 as u32);
// Add attendees
for attendee in &comp.attendees {
if (is_first_request && attendee.send_invite_messages())
|| (!is_first_request && attendee.send_update_messages())
{
recipients.insert(&attendee.email.email);
}
}
}
// 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());
message.components[0].component_ids.push(comp_id as u32);
} else if !copy_components.contains(&(comp_id as u32)) {
continue;
}
message.components[comp_id] = comp.clone();
}
message.components[0].component_ids.sort_unstable();
message.add_missing_timezones();
if !recipients.is_empty() {
Ok(vec![ItipMessage {
from: itip.organizer.email.email.clone(),
from_organizer: true,
to: recipients.into_iter().map(|e| e.to_string()).collect(),
summary: ItipSummary::Invite(
itip.main_instance_or_default()
.build_summary(Some(&itip.organizer), &[]),
),
message,
}])
} else {
Err(ItipError::NothingToSend)
}
}
+489
View File
@@ -0,0 +1,489 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::scheduling::{
Attendee, Email, InstanceId, ItipDateTime, ItipEntry, ItipEntryValue, ItipError, ItipField,
ItipParticipant, ItipSnapshot, ItipSnapshots, ItipTime, ItipValue, Organizer, RecurrenceId,
};
use ahash::AHashMap;
use calcard::icalendar::{
ICalendar, ICalendarParameterName, ICalendarParameterValue, ICalendarProperty,
ICalendarScheduleAgentValue, ICalendarValue, Uri,
};
pub fn itip_snapshot<'x, 'y>(
ical: &'x ICalendar,
account_emails: &'y [String],
force_add_client_scheduling: bool,
) -> Result<ItipSnapshots<'x>, ItipError> {
if !ical.components.iter().any(|comp| {
comp.component_type.is_scheduling_object()
&& comp
.entries
.iter()
.any(|e| matches!(e.name, ICalendarProperty::Organizer))
}) {
return Err(ItipError::NoSchedulingInfo);
}
let mut organizer: Option<Organizer<'x>> = None;
let mut uid: Option<&'x str> = 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() {
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,
comp,
attendees: Default::default(),
dtstamp: Default::default(),
entries: Default::default(),
sequence: Default::default(),
request_status: 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,
name: None,
force_send: None,
};
has_local_emails |= part.email.is_local;
for param in &entry.params {
match (&param.name, &param.value) {
(
ICalendarParameterName::ScheduleAgent,
ICalendarParameterValue::ScheduleAgent(
ICalendarScheduleAgentValue::Client
| ICalendarScheduleAgentValue::None,
),
) => {
part.is_server_scheduling = false;
}
(
ICalendarParameterName::ScheduleForceSend,
ICalendarParameterValue::ScheduleForceSend(force_send),
) => {
part.force_send = Some(force_send);
}
(
ICalendarParameterName::Cn,
ICalendarParameterValue::Text(name),
) => {
part.name = Some(name.as_str());
}
_ => {}
}
}
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,
name: None,
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.name, &param.value) {
(
ICalendarParameterName::ScheduleAgent,
ICalendarParameterValue::ScheduleAgent(agent),
) => {
part.is_server_scheduling =
agent == &ICalendarScheduleAgentValue::Server;
}
(
ICalendarParameterName::Rsvp,
ICalendarParameterValue::Bool(rsvp),
) => {
part.rsvp = Some(*rsvp);
}
(
ICalendarParameterName::ScheduleForceSend,
ICalendarParameterValue::ScheduleForceSend(force_send),
) => {
part.force_send = Some(force_send);
}
(
ICalendarParameterName::Partstat,
ICalendarParameterValue::Partstat(value),
) => {
part.part_stat = Some(value);
}
(
ICalendarParameterName::Cutype,
ICalendarParameterValue::Cutype(value),
) => {
part.cu_type = Some(value);
}
(
ICalendarParameterName::DelegatedFrom,
ICalendarParameterValue::Uri(uri),
) => {
if let Some(uri) = Email::from_uri(uri, account_emails) {
part.delegated_from.push(uri);
}
}
(
ICalendarParameterName::DelegatedTo,
ICalendarParameterValue::Uri(uri),
) => {
if let Some(uri) = Email::from_uri(uri, account_emails) {
part.delegated_to.push(uri);
}
}
(
ICalendarParameterName::Role,
ICalendarParameterValue::Role(value),
) => {
part.role = Some(value);
}
(
ICalendarParameterName::SentBy,
ICalendarParameterValue::Uri(value),
) => {
part.sent_by = Email::from_uri(value, account_emails);
}
(
ICalendarParameterName::Cn,
ICalendarParameterValue::Text(name),
) => {
part.name = Some(name.as_str());
}
_ => {}
}
}
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()) {
sched_comp.sequence = Some(sequence);
}
}
ICalendarProperty::RecurrenceId => {
if let Some(date) =
entry.values.first().and_then(|v| v.as_partial_date_time())
{
let mut this_and_future = false;
let mut tz_id = None;
for param in &entry.params {
match (&param.name, &param.value) {
(
ICalendarParameterName::Tzid,
ICalendarParameterValue::Text(id),
) => {
tz_id = Some(id.as_str());
}
(ICalendarParameterName::Range, _) => {
this_and_future = true;
}
_ => (),
}
}
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_or_default(tz_id),
)
.map(|dt| dt.timestamp())
.unwrap_or_else(|| date.to_timestamp().unwrap_or_default()),
this_and_future,
});
}
}
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::Conference
| ICalendarProperty::Summary
| ICalendarProperty::Description
| 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(date) => {
let tz = tz_resolver
.get_or_insert_with(|| ical.build_tz_resolver())
.resolve_or_default(tz_id);
ItipEntryValue::DateTime(ItipDateTime {
date: date.as_ref(),
tz_id,
tz_code: tz.as_id(),
timestamp: date
.to_date_time_with_tz(tz)
.map(|dt| dt.timestamp())
.unwrap_or_else(|| {
date.to_timestamp().unwrap_or_default()
}),
})
}
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 has_local_emails {
Ok(ItipSnapshots {
organizer: organizer.ok_or(ItipError::NoSchedulingInfo)?,
uid: uid.ok_or(ItipError::MissingUid)?,
components,
})
} else {
Err(ItipError::NotOrganizerNorAttendee)
}
}
impl ItipSnapshots<'_> {
pub fn sender_is_organizer_or_attendee(&self, email: &str) -> bool {
self.organizer.email.email == email
|| self.components.values().any(|snapshot| {
snapshot
.attendees
.iter()
.any(|attendee| attendee.email.email == email)
})
}
pub fn main_instance(&self) -> Option<&ItipSnapshot<'_>> {
self.components.get(&InstanceId::Main)
}
pub fn main_instance_or_default(&self) -> &ItipSnapshot<'_> {
self.main_instance()
.unwrap_or_else(|| self.components.values().next().unwrap())
}
}
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)
}
pub fn external_attendees(&self) -> impl Iterator<Item = &Attendee<'_>> + '_ {
self.attendees.iter().filter(|item| !item.email.is_local)
}
pub fn attendee_by_email(&self, email: &str) -> Option<&Attendee<'_>> {
self.attendees
.iter()
.find(|attendee| attendee.email.email == email)
}
pub fn build_summary(
&self,
include_guests: Option<&Organizer<'_>>,
skip_fields: &[ItipField],
) -> Vec<ItipField> {
let mut fields = Vec::with_capacity(5);
for entry in &self.entries {
if matches!(
entry.name,
ICalendarProperty::Summary
| ICalendarProperty::Description
| ICalendarProperty::Dtstart
| ICalendarProperty::Location
| ICalendarProperty::Conference
| ICalendarProperty::Rrule
) {
let value = match &entry.value {
ItipEntryValue::DateTime(dt) => ItipValue::Time(ItipTime {
start: dt.timestamp,
tz_id: dt.tz_code,
}),
ItipEntryValue::RRule(rule) => ItipValue::Rrule(Box::new((*rule).clone())),
ItipEntryValue::Text(value) => ItipValue::Text(value.to_string()),
_ => continue,
};
let field = ItipField {
name: entry.name.clone(),
value,
};
if !skip_fields.contains(&field) {
fields.push(field);
}
}
}
if let Some(organizer) = include_guests {
let mut attendees = Vec::with_capacity(self.attendees.len());
for attendee in &self.attendees {
if attendee.email.email != organizer.email.email {
attendees.push(ItipParticipant {
email: attendee.email.email.to_string(),
name: attendee.name.map(|n| n.to_string()),
is_organizer: false,
});
}
}
attendees.push(ItipParticipant {
email: organizer.email.email.to_string(),
name: organizer.name.map(|n| n.to_string()),
is_organizer: true,
});
attendees.sort_by(|a, b| {
if a.is_organizer && !b.is_organizer {
std::cmp::Ordering::Less
} else if !a.is_organizer && b.is_organizer {
std::cmp::Ordering::Greater
} else if let (Some(a_name), Some(b_name)) = (a.name.as_deref(), b.name.as_deref())
{
match a_name.cmp(b_name) {
std::cmp::Ordering::Equal => a.email.cmp(&b.email),
ord => ord,
}
} else {
a.email.cmp(&b.email)
}
});
let field = ItipField {
name: ICalendarProperty::Attendee,
value: ItipValue::Participants(attendees),
};
if !skip_fields.contains(&field) {
fields.push(field);
}
}
fields
}
}