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.
450 lines
14 KiB
Rust
450 lines
14 KiB
Rust
/*
|
|
* 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")
|
|
}
|
|
}
|
|
}
|
|
}
|