diff --git a/crates/common/src/config/groupware.rs b/crates/common/src/config/groupware.rs index d2ab4f2c..013bc884 100644 --- a/crates/common/src/config/groupware.rs +++ b/crates/common/src/config/groupware.rs @@ -30,6 +30,10 @@ pub struct GroupwareConfig { pub alarms_from_name: String, pub alarms_from_email: Option, pub alarms_template: Template, + pub itip_enabled: bool, + pub itip_auto_add: bool, + pub itip_inbound_max_ical_size: usize, + pub itip_outbound_max_recipients: usize, // Addressbook settings pub max_vcard_size: usize, @@ -123,6 +127,18 @@ impl GroupwareConfig { "/../../resources/email-templates/calendar-alarm.html.min" ))) .expect("Failed to parse calendar template"), + itip_enabled: config + .property("calendar.scheduling.enabled") + .unwrap_or(true), + itip_auto_add: config + .property("calendar.scheduling.inbound.auto-add") + .unwrap_or(false), + itip_inbound_max_ical_size: config + .property("calendar.scheduling.inbound.max-size") + .unwrap_or(512 * 1024), + itip_outbound_max_recipients: config + .property("calendar.scheduling.outbound.max-recipients") + .unwrap_or(100), } } } diff --git a/crates/dav-proto/resources/responses/020.xml b/crates/dav-proto/resources/responses/020.xml new file mode 100644 index 00000000..89eb7c2b --- /dev/null +++ b/crates/dav-proto/resources/responses/020.xml @@ -0,0 +1,23 @@ + + + + +mailto:wilfredo@example.com + +2.0;Success + + + + +mailto:bernard@example.net + +2.0;Success + + + + +mailto:mike@example.org + +3.7;Invalid calendar user + + diff --git a/crates/dav-proto/src/lib.rs b/crates/dav-proto/src/lib.rs index 14da60a2..27883ecc 100644 --- a/crates/dav-proto/src/lib.rs +++ b/crates/dav-proto/src/lib.rs @@ -42,6 +42,8 @@ pub struct RequestHeaders<'x> { pub destination: Option<&'x str>, pub lock_token: Option<&'x str>, pub max_vcard_version: Option, + pub no_schedule_reply: bool, + pub if_schedule_tag: Option, pub overwrite_fail: bool, pub no_timezones: bool, pub ret: Return, diff --git a/crates/dav-proto/src/parser/header.rs b/crates/dav-proto/src/parser/header.rs index aeeb35e1..4f2ba1bb 100644 --- a/crates/dav-proto/src/parser/header.rs +++ b/crates/dav-proto/src/parser/header.rs @@ -4,9 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use calcard::vcard::VCardVersion; - use crate::{Condition, Depth, If, RequestHeaders, ResourceState, Return, Timeout}; +use calcard::vcard::VCardVersion; impl<'x> RequestHeaders<'x> { pub fn new(uri: &'x str) -> Self { @@ -101,6 +100,14 @@ impl<'x> RequestHeaders<'x> { } return true; }, + "If-Schedule-Tag-Match" => { + self.if_schedule_tag = value.trim().trim_matches('"').parse().ok(); + return true; + }, + "Schedule-Reply" => { + self.no_schedule_reply = value == "F"; + return true; + }, _ => {} ); diff --git a/crates/dav-proto/src/parser/property.rs b/crates/dav-proto/src/parser/property.rs index 8d4528be..cc6717ed 100644 --- a/crates/dav-proto/src/parser/property.rs +++ b/crates/dav-proto/src/parser/property.rs @@ -559,9 +559,30 @@ impl DavProperty { (Namespace::CalDav, Element::MaxAttendeesPerInstance) => { Some(DavProperty::CalDav(CalDavProperty::MaxAttendeesPerInstance)) } + (Namespace::CalDav, Element::ScheduleDefaultCalendarUrl) => Some(DavProperty::CalDav( + CalDavProperty::ScheduleDefaultCalendarURL, + )), + (Namespace::CalDav, Element::ScheduleTag) => { + Some(DavProperty::CalDav(CalDavProperty::ScheduleTag)) + } + (Namespace::CalDav, Element::ScheduleCalendarTransp) => { + Some(DavProperty::CalDav(CalDavProperty::ScheduleCalendarTransp)) + } (Namespace::CalDav, Element::CalendarHomeSet) => { Some(DavProperty::Principal(PrincipalProperty::CalendarHomeSet)) } + (Namespace::CalDav, Element::CalendarUserAddressSet) => Some(DavProperty::Principal( + PrincipalProperty::CalendarUserAddressSet, + )), + (Namespace::CalDav, Element::CalendarUserType) => { + Some(DavProperty::Principal(PrincipalProperty::CalendarUserType)) + } + (Namespace::CalDav, Element::ScheduleInboxUrl) => { + Some(DavProperty::Principal(PrincipalProperty::ScheduleInboxURL)) + } + (Namespace::CalDav, Element::ScheduleOutboxUrl) => { + Some(DavProperty::Principal(PrincipalProperty::ScheduleOutboxURL)) + } (Namespace::CalDav, Element::CalendarData) => Some(DavProperty::CalDav( CalDavProperty::CalendarData(Default::default()), )), @@ -588,6 +609,8 @@ impl TryFrom for ResourceType { (Namespace::Dav, Element::Principal) => Ok(ResourceType::Principal), (Namespace::CardDav, Element::Addressbook) => Ok(ResourceType::AddressBook), (Namespace::CalDav, Element::Calendar) => Ok(ResourceType::Calendar), + (Namespace::CalDav, Element::ScheduleInbox) => Ok(ResourceType::ScheduleInbox), + (Namespace::CalDav, Element::ScheduleOutbox) => Ok(ResourceType::ScheduleOutbox), _ => Err(()), } } diff --git a/crates/dav-proto/src/requests/acl.rs b/crates/dav-proto/src/requests/acl.rs index 550bcd4c..76390c51 100644 --- a/crates/dav-proto/src/requests/acl.rs +++ b/crates/dav-proto/src/requests/acl.rs @@ -252,6 +252,22 @@ impl Privilege { (Namespace::Dav, Element::Unbind) => Some(Privilege::Unbind), (Namespace::Dav, Element::All) => Some(Privilege::All), (Namespace::CalDav, Element::ReadFreeBusy) => Some(Privilege::ReadFreeBusy), + (Namespace::CalDav, Element::ScheduleDeliver) => Some(Privilege::ScheduleDeliver), + (Namespace::CalDav, Element::ScheduleDeliverInvite) => { + Some(Privilege::ScheduleDeliverInvite) + } + (Namespace::CalDav, Element::ScheduleDeliverReply) => { + Some(Privilege::ScheduleDeliverReply) + } + (Namespace::CalDav, Element::ScheduleQueryFreebusy) => { + Some(Privilege::ScheduleQueryFreeBusy) + } + (Namespace::CalDav, Element::ScheduleSend) => Some(Privilege::ScheduleSend), + (Namespace::CalDav, Element::ScheduleSendInvite) => Some(Privilege::ScheduleSendInvite), + (Namespace::CalDav, Element::ScheduleSendReply) => Some(Privilege::ScheduleSendReply), + (Namespace::CalDav, Element::ScheduleSendFreebusy) => { + Some(Privilege::ScheduleSendFreeBusy) + } _ => None, } } diff --git a/crates/dav-proto/src/responses/acl.rs b/crates/dav-proto/src/responses/acl.rs index 3211ff90..1a5b7823 100644 --- a/crates/dav-proto/src/responses/acl.rs +++ b/crates/dav-proto/src/responses/acl.rs @@ -177,6 +177,26 @@ impl Display for Privilege { Privilege::Unbind => "".fmt(f), Privilege::All => "".fmt(f), Privilege::ReadFreeBusy => "".fmt(f), + Privilege::ScheduleDeliver => "".fmt(f), + Privilege::ScheduleDeliverInvite => { + "".fmt(f) + } + Privilege::ScheduleDeliverReply => { + "".fmt(f) + } + Privilege::ScheduleQueryFreeBusy => { + "".fmt(f) + } + Privilege::ScheduleSend => "".fmt(f), + Privilege::ScheduleSendInvite => { + "".fmt(f) + } + Privilege::ScheduleSendReply => { + "".fmt(f) + } + Privilege::ScheduleSendFreeBusy => { + "".fmt(f) + } } } } diff --git a/crates/dav-proto/src/responses/error.rs b/crates/dav-proto/src/responses/error.rs index 95a62847..08d64203 100644 --- a/crates/dav-proto/src/responses/error.rs +++ b/crates/dav-proto/src/responses/error.rs @@ -118,6 +118,25 @@ impl Display for CalCondition { } CalCondition::MaxInstances => write!(f, ""), CalCondition::MaxAttendeesPerInstance => write!(f, ""), + CalCondition::UniqueSchedulingObjectResource(href) => write!( + f, + "{href}" + ), + CalCondition::SameOrganizerInAllComponents => { + write!(f, "") + } + CalCondition::AllowedOrganizerObjectChange => { + write!(f, "") + } + CalCondition::AllowedAttendeeObjectChange => { + write!(f, "") + } + CalCondition::DefaultCalendarNeeded => write!(f, ""), + CalCondition::ValidScheduleDefaultCalendarUrl => { + write!(f, "") + } + CalCondition::ValidSchedulingMessage => write!(f, ""), + CalCondition::ValidOrganizer => write!(f, ""), } } } diff --git a/crates/dav-proto/src/responses/mod.rs b/crates/dav-proto/src/responses/mod.rs index f98cf523..55d4689d 100644 --- a/crates/dav-proto/src/responses/mod.rs +++ b/crates/dav-proto/src/responses/mod.rs @@ -11,8 +11,7 @@ pub mod mkcol; pub mod multistatus; pub mod property; pub mod propstat; - -use std::fmt::{Display, Write}; +pub mod schedule; use crate::schema::{ property::{Comp, ResourceType, SupportedCollation}, @@ -20,6 +19,7 @@ use crate::schema::{ response::{Href, List, Location, ResponseDescription, Status, SyncToken}, Namespaces, }; +use std::fmt::{Display, Write}; trait XmlEscape { fn write_escaped_to(&self, out: &mut impl Write) -> std::fmt::Result; @@ -151,6 +151,8 @@ impl Display for ResourceType { ResourceType::Principal => write!(f, ""), ResourceType::AddressBook => write!(f, ""), ResourceType::Calendar => write!(f, ""), + ResourceType::ScheduleInbox => write!(f, ""), + ResourceType::ScheduleOutbox => write!(f, ""), } } } @@ -215,7 +217,7 @@ mod tests { Ace, AclRestrictions, BaseCondition, ErrorResponse, GrantDeny, Href, List, MkColResponse, MultiStatus, Principal, PrincipalSearchProperty, PrincipalSearchPropertySet, PropResponse, PropStat, RequiredPrincipal, Resource, - Response, SupportedPrivilege, + Response, ScheduleResponse, ScheduleResponseItem, SupportedPrivilege, }, Namespace, }, @@ -371,7 +373,7 @@ mod tests { DavPropertyValue::new(WebDavProperty::GetETag, "\"fffff-abcd2\""), DavPropertyValue::new( CalDavProperty::CalendarData(Default::default()), - ICalendar::parse( + DavValue::CData( r#"BEGIN:VCALENDAR VERSION:2.0 BEGIN:VEVENT @@ -382,9 +384,9 @@ SUMMARY:Event #2 bis bis UID:00959BC664CA650E933C892C@example.com END:VEVENT END:VCALENDAR -"#, - ) - .unwrap(), +"# + .to_string(), + ), ), ])], ), @@ -394,7 +396,7 @@ END:VCALENDAR DavPropertyValue::new(WebDavProperty::GetETag, "\"fffff-abcd3\""), DavPropertyValue::new( CalDavProperty::CalendarData(Default::default()), - ICalendar::parse( + DavValue::CData( r#"BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Example Corp.//CalDAV Client//EN @@ -405,9 +407,9 @@ SUMMARY:Event #3 UID:DC6C50A017428C5216A2F1CD@example.com END:VEVENT END:VCALENDAR -"#, - ) - .unwrap(), +"# + .to_string(), + ), ), ])], ), @@ -429,7 +431,7 @@ END:VCALENDAR DavPropertyValue::new(WebDavProperty::GetETag, "\"23ba4d-ff11fb\""), DavPropertyValue::new( CardDavProperty::AddressData(Default::default()), - VCard::parse( + DavValue::CData( r#"BEGIN:VCARD VERSION:3.0 NICKNAME:me @@ -437,9 +439,9 @@ UID:34222-232@example.com FN:Cyrus Daboo EMAIL:daboo@example.com END:VCARD -"#, - ) - .unwrap(), +"# + .to_string(), + ), ), ])], )]) @@ -640,6 +642,27 @@ END:VCARD ])], )]) .to_string(), + // 020.xml + ScheduleResponse { + items: List(vec![ + ScheduleResponseItem { + recipient: Href("mailto:wilfredo@example.com".to_string()), + request_status: "2.0;Success".into(), + calendar_data: Some("BEGIN:VCALENDAR".to_string()), + }, + ScheduleResponseItem { + recipient: Href("mailto:bernard@example.net".to_string()), + request_status: "2.0;Success".into(), + calendar_data: Some("END:VCALENDAR".to_string()), + }, + ScheduleResponseItem { + recipient: Href("mailto:mike@example.org".to_string()), + request_status: "3.7;Invalid calendar user".into(), + calendar_data: None, + }, + ]), + } + .to_string(), ] .into_iter() .enumerate() diff --git a/crates/dav-proto/src/responses/property.rs b/crates/dav-proto/src/responses/property.rs index 64bbb76e..37d5bb48 100644 --- a/crates/dav-proto/src/responses/property.rs +++ b/crates/dav-proto/src/responses/property.rs @@ -194,6 +194,9 @@ impl DavProperty { CalDavProperty::CalendarData(_) => "A:calendar-data", CalDavProperty::TimezoneServiceSet => "A:timezone-service-set", CalDavProperty::TimezoneId => "A:calendar-timezone-id", + CalDavProperty::ScheduleDefaultCalendarURL => "A:schedule-default-calendar-URL", + CalDavProperty::ScheduleTag => "A:schedule-tag", + CalDavProperty::ScheduleCalendarTransp => "A:schedule-calendar-transp", }, DavProperty::Principal(prop) => match prop { PrincipalProperty::AlternateURISet => "D:alternate-URI-set", @@ -203,6 +206,10 @@ impl DavProperty { PrincipalProperty::CalendarHomeSet => "A:calendar-home-set", PrincipalProperty::AddressbookHomeSet => "B:addressbook-home-set", PrincipalProperty::PrincipalAddress => "B:principal-address", + PrincipalProperty::CalendarUserAddressSet => "A:calendar-user-address-set", + PrincipalProperty::CalendarUserType => "A:calendar-user-type", + PrincipalProperty::ScheduleInboxURL => "A:schedule-inbox-URL", + PrincipalProperty::ScheduleOutboxURL => "A:schedule-outbox-URL", }, DavProperty::DeadProperty(dead) => { return (dead.name.as_str(), dead.attrs.as_deref()) @@ -217,9 +224,14 @@ impl DavProperty { DavProperty::WebDav(WebDavProperty::GetCTag) => Namespace::CalendarServer, DavProperty::CardDav(_) | DavProperty::Principal(PrincipalProperty::AddressbookHomeSet) => Namespace::CardDav, - DavProperty::CalDav(_) | DavProperty::Principal(PrincipalProperty::CalendarHomeSet) => { - Namespace::CalDav - } + DavProperty::CalDav(_) + | DavProperty::Principal( + PrincipalProperty::CalendarHomeSet + | PrincipalProperty::CalendarUserAddressSet + | PrincipalProperty::CalendarUserType + | PrincipalProperty::ScheduleInboxURL + | PrincipalProperty::ScheduleOutboxURL, + ) => Namespace::CalDav, _ => Namespace::Dav, } } diff --git a/crates/dav-proto/src/responses/schedule.rs b/crates/dav-proto/src/responses/schedule.rs new file mode 100644 index 00000000..ac504b58 --- /dev/null +++ b/crates/dav-proto/src/responses/schedule.rs @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + responses::{XmlCdataEscape, XmlEscape}, + schema::{ + response::{ScheduleResponse, ScheduleResponseItem}, + Namespaces, + }, +}; +use std::fmt::Display; + +const NAMESPACE: Namespaces = Namespaces { + cal: true, + card: false, + cs: false, +}; + +impl Display for ScheduleResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + write!( + f, + "{}", + self.items + ) + } +} + +impl Display for ScheduleResponseItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "")?; + write!(f, "{}", self.recipient)?; + + write!(f, "")?; + self.request_status.write_escaped_to(f)?; + write!(f, "")?; + + if let Some(calendar_data) = &self.calendar_data { + write!(f, "")?; + calendar_data.write_cdata_escaped_to(f)?; + write!(f, "")?; + } + write!(f, "") + } +} diff --git a/crates/dav-proto/src/schema/property.rs b/crates/dav-proto/src/schema/property.rs index 17ef759b..8b63bca7 100644 --- a/crates/dav-proto/src/schema/property.rs +++ b/crates/dav-proto/src/schema/property.rs @@ -98,6 +98,9 @@ pub enum CalDavProperty { CalendarData(CalendarData), TimezoneServiceSet, TimezoneId, + ScheduleDefaultCalendarURL, + ScheduleTag, + ScheduleCalendarTransp, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -111,6 +114,10 @@ pub enum PrincipalProperty { CalendarHomeSet, AddressbookHomeSet, PrincipalAddress, + CalendarUserAddressSet, + CalendarUserType, + ScheduleInboxURL, + ScheduleOutboxURL, } #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -205,6 +212,8 @@ pub enum ResourceType { Principal, AddressBook, Calendar, + ScheduleInbox, + ScheduleOutbox, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -263,6 +272,14 @@ pub enum Privilege { Unbind, All, ReadFreeBusy, + ScheduleDeliver, + ScheduleDeliverInvite, + ScheduleDeliverReply, + ScheduleQueryFreeBusy, + ScheduleSend, + ScheduleSendInvite, + ScheduleSendReply, + ScheduleSendFreeBusy, } impl Privilege { diff --git a/crates/dav-proto/src/schema/response.rs b/crates/dav-proto/src/schema/response.rs index aab22a34..e44520d7 100644 --- a/crates/dav-proto/src/schema/response.rs +++ b/crates/dav-proto/src/schema/response.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::fmt::Display; +use std::{borrow::Cow, fmt::Display}; use calcard::{ icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty}, @@ -94,6 +94,16 @@ pub struct PropResponse { pub properties: List, } +pub struct ScheduleResponse { + pub items: List, +} + +pub struct ScheduleResponseItem { + pub recipient: Href, + pub request_status: Cow<'static, str>, + pub calendar_data: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(test, derive(serde::Serialize, serde::Deserialize))] pub struct SupportedPrivilege { @@ -237,6 +247,14 @@ pub enum CalCondition { MaxResourceSize(u32), MaxInstances, MaxAttendeesPerInstance, + UniqueSchedulingObjectResource(Href), + SameOrganizerInAllComponents, + AllowedOrganizerObjectChange, + AllowedAttendeeObjectChange, + DefaultCalendarNeeded, + ValidScheduleDefaultCalendarUrl, + ValidSchedulingMessage, + ValidOrganizer, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -338,6 +356,14 @@ impl CalCondition { CalCondition::MaxResourceSize(_) => "MaxResourceSize", CalCondition::MaxInstances => "MaxInstances", CalCondition::MaxAttendeesPerInstance => "MaxAttendeesPerInstance", + CalCondition::UniqueSchedulingObjectResource(_) => "UniqueSchedulingObjectResource", + CalCondition::SameOrganizerInAllComponents => "SameOrganizerInAllComponents", + CalCondition::AllowedOrganizerObjectChange => "AllowedOrganizerObjectChange", + CalCondition::AllowedAttendeeObjectChange => "AllowedAttendeeObjectChange", + CalCondition::DefaultCalendarNeeded => "DefaultCalendarNeeded", + CalCondition::ValidScheduleDefaultCalendarUrl => "ValidScheduleDefaultCalendarUrl", + CalCondition::ValidSchedulingMessage => "ValidSchedulingMessage", + CalCondition::ValidOrganizer => "ValidOrganizer", } } } diff --git a/crates/dav/src/calendar/copy_move.rs b/crates/dav/src/calendar/copy_move.rs index e74208ac..a550537b 100644 --- a/crates/dav/src/calendar/copy_move.rs +++ b/crates/dav/src/calendar/copy_move.rs @@ -243,6 +243,7 @@ impl CalendarCopyMoveRequestHandler for Server { to_resource.document_id().into(), to_calendar_id, new_name, + headers.if_schedule_tag, ) .await } else { @@ -310,6 +311,7 @@ impl CalendarCopyMoveRequestHandler for Server { None, to_calendar_id, new_name, + headers.if_schedule_tag, ) .await } else { @@ -523,6 +525,7 @@ async fn copy_event( to_document_id, to_calendar_id, None, + false, &mut batch, ) .caused_by(trc::location!())?; @@ -553,6 +556,7 @@ async fn move_event( to_document_id: Option, to_calendar_id: u32, new_name: &str, + if_schedule_tag: Option, ) -> crate::Result { // Fetch event let event_ = server @@ -564,6 +568,13 @@ async fn move_event( .to_unarchived::() .caused_by(trc::location!())?; + // Validate headers + if if_schedule_tag.is_some() + && event.inner.schedule_tag.as_ref().map(|t| t.to_native()) != if_schedule_tag + { + return Err(DavError::Code(StatusCode::PRECONDITION_FAILED)); + } + // Validate UID if from_account_id != to_account_id || from_calendar_id != to_calendar_id @@ -634,6 +645,7 @@ async fn move_event( from_document_id, from_calendar_id, from_resource_path.into(), + false, &mut batch, ) .caused_by(trc::location!())?; @@ -672,6 +684,7 @@ async fn move_event( to_document_id, to_calendar_id, None, + false, &mut batch, ) .caused_by(trc::location!())?; @@ -810,6 +823,7 @@ async fn copy_container( to_document_id, to_children_ids, None, + false, &mut batch, ) .await @@ -893,6 +907,7 @@ async fn copy_container( from_child_document_id, from_document_id, None, + false, &mut batch, ) .caused_by(trc::location!())?; diff --git a/crates/dav/src/calendar/delete.rs b/crates/dav/src/calendar/delete.rs index 82bd0e61..1a28bbb4 100644 --- a/crates/dav/src/calendar/delete.rs +++ b/crates/dav/src/calendar/delete.rs @@ -14,6 +14,7 @@ use crate::{ }; use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; use dav_proto::RequestHeaders; +use directory::Permission; use groupware::{ DestroyArchive, cache::GroupwareCache, @@ -62,6 +63,10 @@ impl CalendarDeleteRequestHandler for Server { .by_path(delete_path) .ok_or(DavError::Code(StatusCode::NOT_FOUND))?; let document_id = delete_resource.document_id(); + let send_itip = self.core.groupware.itip_enabled + && !headers.no_schedule_reply + && !access_token.emails.is_empty() + && access_token.has_permission(Permission::CalendarSchedulingSend); // Fetch entry let mut batch = BatchBuilder::new(); @@ -117,6 +122,7 @@ impl CalendarDeleteRequestHandler for Server { .map(|r| r.document_id()) .collect::>(), resources.format_resource(delete_resource).into(), + send_itip, &mut batch, ) .await @@ -153,21 +159,29 @@ impl CalendarDeleteRequestHandler for Server { ) .await?; + // Validate schedule tag + let event = event_ + .to_unarchived::() + .caused_by(trc::location!())?; + if headers.if_schedule_tag.is_some() + && event.inner.schedule_tag.as_ref().map(|t| t.to_native()) + != headers.if_schedule_tag + { + return Err(DavError::Code(StatusCode::PRECONDITION_FAILED)); + } + // Delete event - DestroyArchive( - event_ - .to_unarchived::() - .caused_by(trc::location!())?, - ) - .delete( - access_token, - account_id, - document_id, - calendar_id, - resources.format_resource(delete_resource).into(), - &mut batch, - ) - .caused_by(trc::location!())?; + DestroyArchive(event) + .delete( + access_token, + account_id, + document_id, + calendar_id, + resources.format_resource(delete_resource).into(), + send_itip, + &mut batch, + ) + .caused_by(trc::location!())?; } self.commit_batch(batch).await.caused_by(trc::location!())?; diff --git a/crates/dav/src/calendar/freebusy.rs b/crates/dav/src/calendar/freebusy.rs index f9997bcf..d2c1a721 100644 --- a/crates/dav/src/calendar/freebusy.rs +++ b/crates/dav/src/calendar/freebusy.rs @@ -77,7 +77,11 @@ impl CalendarFreebusyRequestHandler for Server { // Obtain shared ids let shared_ids = if !access_token.is_member(account_id) { resources - .shared_containers(access_token, [Acl::ReadItems, Acl::ReadFreeBusy], false) + .shared_containers( + access_token, + [Acl::ReadItems, Acl::SchedulingReadFreeBusy], + false, + ) .into() } else { None diff --git a/crates/dav/src/calendar/get.rs b/crates/dav/src/calendar/get.rs index 84779d87..2dd1bac6 100644 --- a/crates/dav/src/calendar/get.rs +++ b/crates/dav/src/calendar/get.rs @@ -88,6 +88,7 @@ impl CalendarGetRequestHandler for Server { // Validate headers let etag = event_.etag(); + let schedule_tag = event.schedule_tag.as_ref().map(|tag| tag.to_native()); self.validate_headers( access_token, headers, @@ -107,6 +108,7 @@ impl CalendarGetRequestHandler for Server { let response = HttpResponse::new(StatusCode::OK) .with_content_type("text/calendar; charset=utf-8") .with_etag(etag) + .with_schedule_tag_opt(schedule_tag) .with_last_modified(Rfc1123DateTime::new(i64::from(event.modified)).to_string()); let ical = event.data.event.to_string(); diff --git a/crates/dav/src/calendar/update.rs b/crates/dav/src/calendar/update.rs index 7e67f76f..f8a7d9a5 100644 --- a/crates/dav/src/calendar/update.rs +++ b/crates/dav/src/calendar/update.rs @@ -4,31 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::collections::HashSet; - -use calcard::{ - Entry, Parser, - common::timezone::Tz, - icalendar::{ICalendar, ICalendarComponentType}, -}; -use common::{DavName, Server, auth::AccessToken}; -use dav_proto::{ - RequestHeaders, Return, - schema::{property::Rfc1123DateTime, response::CalCondition}, -}; -use groupware::{ - cache::GroupwareCache, - calendar::{CalendarEvent, CalendarEventData}, -}; -use http_proto::HttpResponse; -use hyper::StatusCode; -use jmap_proto::types::{ - acl::Acl, - collection::{Collection, SyncCollection}, -}; -use store::write::{BatchBuilder, now}; -use trc::AddContext; - +use super::assert_is_unique_uid; use crate::{ DavError, DavErrorCondition, DavMethod, common::{ @@ -39,8 +15,31 @@ use crate::{ file::DavFileResource, fix_percent_encoding, }; - -use super::assert_is_unique_uid; +use calcard::{ + Entry, Parser, + common::timezone::Tz, + icalendar::{ICalendar, ICalendarComponentType}, +}; +use common::{DavName, Server, auth::AccessToken}; +use dav_proto::{ + RequestHeaders, Return, + schema::{property::Rfc1123DateTime, response::CalCondition}, +}; +use directory::Permission; +use groupware::{ + cache::GroupwareCache, + calendar::{CalendarEvent, CalendarEventData}, + scheduling::{ItipMessages, event_create::itip_create, event_update::itip_update}, +}; +use http_proto::HttpResponse; +use hyper::StatusCode; +use jmap_proto::types::{ + acl::Acl, + collection::{Collection, SyncCollection}, +}; +use std::collections::HashSet; +use store::write::{BatchBuilder, now}; +use trc::AddContext; pub(crate) trait CalendarUpdateRequestHandler: Sync + Send { fn handle_calendar_update_request( @@ -176,6 +175,14 @@ impl CalendarUpdateRequestHandler for Server { ))); } + // Validate schedule tag + if headers.if_schedule_tag.is_some() + && event.inner.schedule_tag.as_ref().map(|t| t.to_native()) + != headers.if_schedule_tag + { + return Err(DavError::Code(StatusCode::PRECONDITION_FAILED)); + } + // Obtain previous alarm let prev_email_alarm = event.inner.data.next_alarm(now() as i64, Tz::Floating); @@ -184,6 +191,7 @@ impl CalendarUpdateRequestHandler for Server { let mut new_event = event .deserialize::() .caused_by(trc::location!())?; + let old_ical = new_event.data.event; new_event.size = bytes.len() as u32; new_event.data = CalendarEventData::new( ical, @@ -191,10 +199,59 @@ impl CalendarUpdateRequestHandler for Server { self.core.groupware.max_ical_instances, &mut next_email_alarm, ); - let has_alarms = next_email_alarm.is_some(); + + // Scheduling + let mut itip_messages = None; + if self.core.groupware.itip_enabled + && !access_token.emails.is_empty() + && access_token.has_permission(Permission::CalendarSchedulingSend) + { + let result = if let Some(schedule_tag) = &mut new_event.schedule_tag { + *schedule_tag += 1; + itip_update( + &mut new_event.data.event, + &old_ical, + access_token.emails.as_slice(), + ) + } else { + itip_create(&mut new_event.data.event, access_token.emails.as_slice()) + }; + + match result { + Ok(messages) => { + if messages.iter().map(|r| r.to.len()).sum::() + < self.core.groupware.itip_outbound_max_recipients + { + itip_messages = Some(ItipMessages::new(messages)); + } else { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + CalCondition::MaxAttendeesPerInstance, + ))); + } + } + Err(err) => { + if let Some(failed_precondition) = err.failed_precondition() { + trc::event!( + Calendar(trc::CalendarEvent::SchedulingError), + AccountId = account_id, + DocumentId = document_id, + Details = err.to_string(), + ); + + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + failed_precondition, + ))); + } + } + } + } + let nudge_queue = next_email_alarm.is_some() || itip_messages.is_some(); // Prepare write batch let mut batch = BatchBuilder::new(); + let schedule_tag = new_event.schedule_tag; let etag = new_event .update(access_token, event, account_id, document_id, &mut batch) .caused_by(trc::location!())? @@ -207,12 +264,19 @@ impl CalendarUpdateRequestHandler for Server { next_alarm.write_task(&mut batch); } } + if let Some(itip_messages) = itip_messages { + itip_messages + .queue(&mut batch) + .caused_by(trc::location!())?; + } self.commit_batch(batch).await.caused_by(trc::location!())?; - if has_alarms { + if nudge_queue { self.notify_task_queue(); } - Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag)) + Ok(HttpResponse::new(StatusCode::NO_CONTENT) + .with_etag_opt(etag) + .with_schedule_tag_opt(schedule_tag)) } else if let Some((Some(parent), name)) = resources.map_parent(resource_name.as_ref()) { if !parent.is_container() { return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)); @@ -266,7 +330,7 @@ impl CalendarUpdateRequestHandler for Server { // Build event let mut next_email_alarm = None; - let event = CalendarEvent { + let mut event = CalendarEvent { names: vec![DavName { name: name.to_string(), parent_id: parent.document_id(), @@ -280,7 +344,44 @@ impl CalendarUpdateRequestHandler for Server { size: bytes.len() as u32, ..Default::default() }; - let has_alarms = next_email_alarm.is_some(); + + // Scheduling + let mut itip_messages = None; + if self.core.groupware.itip_enabled + && !access_token.emails.is_empty() + && access_token.has_permission(Permission::CalendarSchedulingSend) + { + match itip_create(&mut event.data.event, access_token.emails.as_slice()) { + Ok(messages) => { + if messages.iter().map(|r| r.to.len()).sum::() + < self.core.groupware.itip_outbound_max_recipients + { + event.schedule_tag = Some(1); + itip_messages = Some(ItipMessages::new(messages)); + } else { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + CalCondition::MaxAttendeesPerInstance, + ))); + } + } + Err(err) => { + if let Some(failed_precondition) = err.failed_precondition() { + trc::event!( + Calendar(trc::CalendarEvent::SchedulingError), + AccountId = account_id, + Details = err.to_string(), + ); + + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::PRECONDITION_FAILED, + failed_precondition, + ))); + } + } + } + } + let nudge_queue = next_email_alarm.is_some() || itip_messages.is_some(); // Prepare write batch let mut batch = BatchBuilder::new(); @@ -289,6 +390,7 @@ impl CalendarUpdateRequestHandler for Server { .assign_document_ids(account_id, Collection::CalendarEvent, 1) .await .caused_by(trc::location!())?; + let schedule_tag = event.schedule_tag; let etag = event .insert( access_token, @@ -299,14 +401,20 @@ impl CalendarUpdateRequestHandler for Server { ) .caused_by(trc::location!())? .etag(); - + if let Some(itip_messages) = itip_messages { + itip_messages + .queue(&mut batch) + .caused_by(trc::location!())?; + } self.commit_batch(batch).await.caused_by(trc::location!())?; - if has_alarms { + if nudge_queue { self.notify_task_queue(); } - Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag)) + Ok(HttpResponse::new(StatusCode::CREATED) + .with_etag_opt(etag) + .with_schedule_tag_opt(schedule_tag)) } else { Err(DavError::Code(StatusCode::CONFLICT))? } diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index 12ead11d..192662cc 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -339,9 +339,43 @@ impl DavAclHandler for Server { Privilege::WriteAcl => { acls.insert(Acl::Administer); } - Privilege::ReadFreeBusy => { + Privilege::ReadFreeBusy + | Privilege::ScheduleQueryFreeBusy + | Privilege::ScheduleSendFreeBusy => { if collection == Collection::Calendar { - acls.insert(Acl::ReadFreeBusy); + acls.insert(Acl::SchedulingReadFreeBusy); + } else { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::NotSupportedPrivilege, + ))); + } + } + Privilege::ScheduleDeliver | Privilege::ScheduleSend => { + if collection == Collection::Calendar { + acls.insert(Acl::SchedulingReadFreeBusy); + acls.insert(Acl::SchedulingInvite); + acls.insert(Acl::SchedulingReply); + } else { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::NotSupportedPrivilege, + ))); + } + } + Privilege::ScheduleDeliverInvite | Privilege::ScheduleSendInvite => { + if collection == Collection::Calendar { + acls.insert(Acl::SchedulingInvite); + } else { + return Err(DavError::Condition(DavErrorCondition::new( + StatusCode::FORBIDDEN, + BaseCondition::NotSupportedPrivilege, + ))); + } + } + Privilege::ScheduleDeliverReply | Privilege::ScheduleSendReply => { + if collection == Collection::Calendar { + acls.insert(Acl::SchedulingReply); } else { return Err(DavError::Condition(DavErrorCondition::new( StatusCode::FORBIDDEN, @@ -524,7 +558,7 @@ pub(crate) fn current_user_privilege_set(acl_bitmap: Bitmap) -> Vec { + Acl::SchedulingReadFreeBusy => { acls.insert(Privilege::ReadFreeBusy); } _ => {} diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index d7a4307b..5ca4403c 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -17,10 +17,10 @@ use dav_proto::schema::{ response::{Href, MultiStatus, PropStat, Response}, }; use directory::{QueryBy, backend::internal::manage::ManageDirectory}; +use groupware::RFC_3986; use groupware::cache::GroupwareCache; use hyper::StatusCode; use jmap_proto::types::collection::Collection; -use groupware::RFC_3986; use trc::AddContext; use crate::{ @@ -287,6 +287,12 @@ impl PrincipalPropFind for Server { fields_not_found.push(DavPropertyValue::empty(property.clone())); response.set_namespace(Namespace::CardDav); } + PrincipalProperty::CalendarUserAddressSet => { + let todo = "implement"; + } + PrincipalProperty::CalendarUserType => todo!(), + PrincipalProperty::ScheduleInboxURL => todo!(), + PrincipalProperty::ScheduleOutboxURL => todo!(), }, _ => { response.set_namespace(property.namespace()); diff --git a/crates/directory/src/core/mod.rs b/crates/directory/src/core/mod.rs index 94546fa5..c85d4dec 100644 --- a/crates/directory/src/core/mod.rs +++ b/crates/directory/src/core/mod.rs @@ -247,6 +247,10 @@ impl Permission { Permission::DavCalMultiGet => "Retrieve multiple calendar entries in a single request", Permission::DavCalFreeBusyQuery => "Query free/busy time information for scheduling", Permission::CalendarAlarms => "Receive calendar alarms via e-mail", + Permission::CalendarSchedulingSend => "Send calendar scheduling requests via e-mail", + Permission::CalendarSchedulingReceive => { + "Receive calendar scheduling requests via e-mail" + } } } } diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index 45a162e8..556abd09 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -1408,6 +1408,8 @@ impl Permission { | Permission::DavCalMultiGet | Permission::DavCalFreeBusyQuery | Permission::CalendarAlarms + | Permission::CalendarSchedulingSend + | Permission::CalendarSchedulingReceive ) } diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 6bfa242a..41703101 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -375,6 +375,8 @@ pub enum Permission { DavCalFreeBusyQuery, CalendarAlarms, + CalendarSchedulingSend, + CalendarSchedulingReceive, // WARNING: add new ids at the end (TODO: use static ids) } diff --git a/crates/groupware/src/calendar/alarm.rs b/crates/groupware/src/calendar/alarm.rs index 57058f8a..3852f016 100644 --- a/crates/groupware/src/calendar/alarm.rs +++ b/crates/groupware/src/calendar/alarm.rs @@ -211,11 +211,20 @@ impl ExpandAlarm for ICalendarComponent { }; } ICalendarProperty::Action => { - is_email_alert = entry - .values - .first() - .and_then(|v| v.as_text()) - .is_some_and(|v| v.eq_ignore_ascii_case("email")); + is_email_alert = is_email_alert + || entry + .values + .first() + .and_then(|v| v.as_text()) + .is_some_and(|v| v.eq_ignore_ascii_case("email")); + } + ICalendarProperty::Summary | ICalendarProperty::Description => { + is_email_alert = is_email_alert + || entry + .values + .first() + .and_then(|v| v.as_text()) + .is_some_and(|v| v.contains("@email")); } _ => {} } diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index fad570cb..71d9c2ac 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -76,6 +76,7 @@ pub struct CalendarEvent { pub size: u32, pub created: i64, pub modified: i64, + pub schedule_tag: Option, } #[derive( @@ -153,13 +154,13 @@ impl TryFrom for CalendarRight { fn try_from(value: Acl) -> Result { match value { - Acl::ReadFreeBusy => Ok(CalendarRight::ReadFreeBusy), + Acl::SchedulingReadFreeBusy => Ok(CalendarRight::ReadFreeBusy), Acl::ReadItems => Ok(CalendarRight::ReadItems), Acl::Modify => Ok(CalendarRight::WriteAll), Acl::ModifyItemsOwn => Ok(CalendarRight::WriteOwn), Acl::ModifyPrivateProperties => Ok(CalendarRight::UpdatePrivate), - Acl::RSVP => Ok(CalendarRight::RSVP), - Acl::Share => Ok(CalendarRight::Share), + Acl::SchedulingReply => Ok(CalendarRight::RSVP), + Acl::Administer => Ok(CalendarRight::Share), Acl::Delete => Ok(CalendarRight::Delete), _ => Err(value), } @@ -169,13 +170,13 @@ impl TryFrom for CalendarRight { impl From for Acl { fn from(value: CalendarRight) -> Self { match value { - CalendarRight::ReadFreeBusy => Acl::ReadFreeBusy, + CalendarRight::ReadFreeBusy => Acl::SchedulingReadFreeBusy, CalendarRight::ReadItems => Acl::ReadItems, CalendarRight::WriteAll => Acl::Modify, CalendarRight::WriteOwn => Acl::ModifyItemsOwn, CalendarRight::UpdatePrivate => Acl::ModifyPrivateProperties, - CalendarRight::RSVP => Acl::RSVP, - CalendarRight::Share => Acl::Share, + CalendarRight::RSVP => Acl::SchedulingReply, + CalendarRight::Share => Acl::Administer, CalendarRight::Delete => Acl::Delete, } } diff --git a/crates/groupware/src/calendar/storage.rs b/crates/groupware/src/calendar/storage.rs index 7e3aff03..e6f98e07 100644 --- a/crates/groupware/src/calendar/storage.rs +++ b/crates/groupware/src/calendar/storage.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{DavResourceName, DestroyArchive, RFC_3986}; +use crate::{ + DavResourceName, DestroyArchive, RFC_3986, + scheduling::{ItipMessages, event_cancel::itip_cancel}, +}; use calcard::common::timezone::Tz; use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; use jmap_proto::types::collection::{Collection, VanishedCollection}; @@ -153,6 +156,7 @@ impl DestroyArchive> { document_id: u32, children_ids: Vec, delete_path: Option, + send_itip: bool, batch: &mut BatchBuilder, ) -> trc::Result<()> { // Process deletions @@ -173,6 +177,7 @@ impl DestroyArchive> { document_id, calendar_id, None, + send_itip, batch, )?; } @@ -211,6 +216,7 @@ impl DestroyArchive> { } impl DestroyArchive> { + #[allow(clippy::too_many_arguments)] pub fn delete( self, access_token: &AccessToken, @@ -218,6 +224,7 @@ impl DestroyArchive> { document_id: u32, calendar_id: u32, delete_path: Option, + send_itip: bool, batch: &mut BatchBuilder, ) -> trc::Result<()> { let event = self.0; @@ -255,6 +262,21 @@ impl DestroyArchive> { next_alarm.delete_task(batch); } + // Scheduling + if send_itip && event.inner.schedule_tag.is_some() { + let event = event + .deserialize::() + .caused_by(trc::location!())?; + + if let Ok(messages) = + itip_cancel(&event.data.event, access_token.emails.as_slice()) + { + ItipMessages::new(vec![messages]) + .queue(batch) + .caused_by(trc::location!())?; + } + } + batch .custom( ObjectIndexBuilder::<_, ()>::new() diff --git a/crates/groupware/src/contact/mod.rs b/crates/groupware/src/contact/mod.rs index 0eb8da94..8642dd8a 100644 --- a/crates/groupware/src/contact/mod.rs +++ b/crates/groupware/src/contact/mod.rs @@ -57,7 +57,7 @@ impl TryFrom for AddressBookRight { match value { Acl::Read => Ok(AddressBookRight::Read), Acl::Modify => Ok(AddressBookRight::Write), - Acl::Share => Ok(AddressBookRight::Share), + Acl::Administer => Ok(AddressBookRight::Share), Acl::Delete => Ok(AddressBookRight::Delete), _ => Err(value), } @@ -69,7 +69,7 @@ impl From for Acl { match value { AddressBookRight::Read => Acl::Read, AddressBookRight::Write => Acl::Modify, - AddressBookRight::Share => Acl::Share, + AddressBookRight::Share => Acl::Administer, AddressBookRight::Delete => Acl::Delete, } } diff --git a/crates/groupware/src/scheduling/attendee.rs b/crates/groupware/src/scheduling/attendee.rs index 75af26e2..f1e20151 100644 --- a/crates/groupware/src/scheduling/attendee.rs +++ b/crates/groupware/src/scheduling/attendee.rs @@ -25,7 +25,7 @@ pub(crate) fn attendee_handle_update( new_ical: &ICalendar, old_itip: ItipSnapshots<'_>, new_itip: ItipSnapshots<'_>, -) -> Result, ItipError> { +) -> Result>, ItipError> { let dt_stamp = PartialDateTime::now(); let mut message = ICalendar { components: Vec::with_capacity(2), @@ -224,6 +224,7 @@ pub(crate) fn attendee_handle_update( let mut responses = vec![ItipMessage { method: ICalendarMethod::Reply, from: from.to_string(), + from_organizer: false, to: email_rcpt.into_iter().map(|e| e.to_string()).collect(), changed_properties: vec![], message, @@ -232,11 +233,17 @@ pub(crate) fn attendee_handle_update( // 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(); - if let Ok(mut message) = organizer_request_full(new_ical, &new_itip, None, true) { - message.from = from; - message.to = new_delegates; - responses.push(message); + let new_delegates = new_delegates + .into_iter() + .map(|e| e.to_string()) + .collect::>(); + 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); + } } } diff --git a/crates/groupware/src/scheduling/event_cancel.rs b/crates/groupware/src/scheduling/event_cancel.rs index 735face4..f7c65ea4 100644 --- a/crates/groupware/src/scheduling/event_cancel.rs +++ b/crates/groupware/src/scheduling/event_cancel.rs @@ -7,7 +7,7 @@ use crate::scheduling::{ InstanceId, ItipError, ItipMessage, ItipSnapshots, attendee::attendee_decline, - itip::{itip_add_tz, itip_build_envelope, itip_finalize}, + itip::{itip_add_tz, itip_build_envelope}, snapshot::itip_snapshot, }; use ahash::AHashSet; @@ -21,9 +21,9 @@ use calcard::{ use std::fmt::Display; pub fn itip_cancel( - ical: &mut ICalendar, - account_emails: &[&str], -) -> Result { + ical: &ICalendar, + account_emails: &[String], +) -> Result, ItipError> { // Prepare iTIP message let itip = itip_snapshot(ical, account_emails, false)?; let dt_stamp = PartialDateTime::now(); @@ -41,7 +41,6 @@ pub fn itip_cancel( let mut recipients = AHashSet::new(); let mut cancel_guests = AHashSet::new(); let mut component_type = &ICalendarComponentType::VEvent; - let mut increment_sequences = Vec::new(); let mut sequence = 0; for (instance_id, comp) in &itip.components { component_type = &comp.comp.component_type; @@ -53,7 +52,6 @@ pub fn itip_cancel( } // Increment sequence if needed - increment_sequences.push(comp.comp_id); if instance_id == &InstanceId::Main { sequence = comp.sequence.unwrap_or_default() + 1; } @@ -67,17 +65,15 @@ pub fn itip_cancel( dt_stamp, cancel_guests.iter(), )); - let message = ItipMessage { + + Ok(ItipMessage { method: ICalendarMethod::Cancel, from: itip.organizer.email.email, + from_organizer: true, to: recipients.into_iter().collect(), changed_properties: vec![], message, - }; - - itip_finalize(ical, &increment_sequences); - - Ok(message) + }) } else { Err(ItipError::NothingToSend) } @@ -107,17 +103,15 @@ pub fn itip_cancel( itip_add_tz(&mut message, ical); email_rcpt.insert(&itip.organizer.email.email); - let message = ItipMessage { + + Ok(ItipMessage { method: ICalendarMethod::Reply, from: from.to_string(), + from_organizer: false, to: email_rcpt.into_iter().map(|e| e.to_string()).collect(), changed_properties: vec![], message, - }; - - itip_finalize(ical, &[]); - - Ok(message) + }) } else { Err(ItipError::NothingToSend) } diff --git a/crates/groupware/src/scheduling/event_create.rs b/crates/groupware/src/scheduling/event_create.rs index f6682fac..6dd0b57b 100644 --- a/crates/groupware/src/scheduling/event_create.rs +++ b/crates/groupware/src/scheduling/event_create.rs @@ -12,8 +12,8 @@ use calcard::icalendar::ICalendar; pub fn itip_create( ical: &mut ICalendar, - account_emails: &[&str], -) -> Result { + account_emails: &[String], +) -> Result>, ItipError> { let itip = itip_snapshot(ical, account_emails, false)?; if !itip.organizer.is_server_scheduling { Err(ItipError::OtherSchedulingAgent) diff --git a/crates/groupware/src/scheduling/event_update.rs b/crates/groupware/src/scheduling/event_update.rs index c1d8fc8a..cfa636d5 100644 --- a/crates/groupware/src/scheduling/event_update.rs +++ b/crates/groupware/src/scheduling/event_update.rs @@ -12,9 +12,9 @@ use calcard::icalendar::ICalendar; pub fn itip_update( ical: &mut ICalendar, - old_ical: &mut ICalendar, - account_emails: &[&str], -) -> Result, ItipError> { + old_ical: &ICalendar, + account_emails: &[String], +) -> Result>, ItipError> { let old_itip = itip_snapshot(old_ical, account_emails, false)?; match itip_snapshot(ical, account_emails, false) { Ok(new_itip) => { diff --git a/crates/groupware/src/scheduling/inbound.rs b/crates/groupware/src/scheduling/inbound.rs index 19d0fb8d..c5e496b3 100644 --- a/crates/groupware/src/scheduling/inbound.rs +++ b/crates/groupware/src/scheduling/inbound.rs @@ -44,7 +44,7 @@ pub enum MergeAction { pub enum MergeResult { Actions(Vec), - Message(ItipMessage), + Message(ItipMessage), None, } @@ -72,10 +72,18 @@ pub fn itip_process_message( handle_reply(&snapshots, &itip_snapshots, &sender, &mut merge_actions)?; } ICalendarMethod::Refresh => { - return organizer_request_full(ical, &snapshots, None, false).map(|mut message| { - message.to = vec![sender]; - MergeResult::Message(message) - }); + 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())), } diff --git a/crates/groupware/src/scheduling/itip.rs b/crates/groupware/src/scheduling/itip.rs index 47c1366a..87efde35 100644 --- a/crates/groupware/src/scheduling/itip.rs +++ b/crates/groupware/src/scheduling/itip.rs @@ -12,6 +12,13 @@ use calcard::{ }, }; use common::PROD_ID; +use store::{ + Serialize, + write::{Archiver, BatchBuilder, TaskQueueClass, ValueClass, now}, +}; +use trc::AddContext; + +use crate::scheduling::{ItipMessage, ItipMessages}; pub(crate) fn itip_build_envelope(method: ICalendarMethod) -> ICalendarComponent { ICalendarComponent { @@ -244,3 +251,46 @@ pub(crate) fn can_attendee_modify_property( _ => false, } } + +impl ItipMessages { + pub fn new(messages: Vec>) -> Self { + ItipMessages { + messages: messages.into_iter().map(|m| m.into()).collect(), + } + } + + pub fn queue(self, batch: &mut BatchBuilder) -> trc::Result<()> { + let due = now(); + batch.set( + ValueClass::TaskQueue(TaskQueueClass::SendItip { + due, + is_payload: false, + }), + vec![], + ); + batch.set( + ValueClass::TaskQueue(TaskQueueClass::SendItip { + due, + is_payload: true, + }), + Archiver::new(self) + .serialize() + .caused_by(trc::location!())?, + ); + + Ok(()) + } +} + +impl From> for ItipMessage { + fn from(message: ItipMessage) -> Self { + ItipMessage { + method: message.method, + from: message.from, + from_organizer: message.from_organizer, + to: message.to, + changed_properties: message.changed_properties, + message: message.message.to_string(), + } + } +} diff --git a/crates/groupware/src/scheduling/mod.rs b/crates/groupware/src/scheduling/mod.rs index 16068231..6abfc253 100644 --- a/crates/groupware/src/scheduling/mod.rs +++ b/crates/groupware/src/scheduling/mod.rs @@ -8,12 +8,13 @@ use ahash::{AHashMap, AHashSet}; use calcard::{ common::PartialDateTime, icalendar::{ - ICalendar, ICalendarComponent, ICalendarDuration, ICalendarEntry, ICalendarMethod, - ICalendarParameter, ICalendarParticipationRole, ICalendarParticipationStatus, - ICalendarPeriod, ICalendarProperty, ICalendarRecurrenceRule, - ICalendarScheduleForceSendValue, ICalendarStatus, ICalendarUserTypes, ICalendarValue, Uri, + ICalendarComponent, ICalendarDuration, ICalendarEntry, ICalendarMethod, ICalendarParameter, + ICalendarParticipationRole, ICalendarParticipationStatus, ICalendarPeriod, + ICalendarProperty, ICalendarRecurrenceRule, ICalendarScheduleForceSendValue, + ICalendarStatus, ICalendarUserTypes, ICalendarValue, Uri, }, }; +use dav_proto::schema::response::CalCondition; use std::{fmt::Display, hash::Hash}; pub mod attendee; @@ -134,12 +135,19 @@ pub enum ItipError { UnsupportedMethod(ICalendarMethod), } -pub struct ItipMessage { +#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] +pub struct ItipMessage { pub method: ICalendarMethod, pub from: String, + pub from_organizer: bool, pub to: Vec, pub changed_properties: Vec, - pub message: ICalendar, + pub message: T, +} + +#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)] +pub struct ItipMessages { + pub messages: Vec>, } impl ItipSnapshot<'_> { @@ -195,15 +203,15 @@ impl Attendee<'_> { } impl Email { - pub fn new(email: &str, local_addresses: &[&str]) -> Option { + pub fn new(email: &str, local_addresses: &[String]) -> Option { email.contains('@').then(|| { let email = email.trim().trim_start_matches("mailto:").to_lowercase(); - let is_local = local_addresses.contains(&email.as_str()); + let is_local = local_addresses.contains(&email); Email { email, is_local } }) } - pub fn from_uri(uri: &Uri, local_addresses: &[&str]) -> Option { + pub fn from_uri(uri: &Uri, local_addresses: &[String]) -> Option { if let Uri::Location(uri) = uri { Email::new(uri.as_str(), local_addresses) } else { @@ -298,3 +306,75 @@ impl ItipDateTime<'_> { } } } + +impl ItipError { + pub fn failed_precondition(&self) -> Option { + match self { + ItipError::NoSchedulingInfo + | ItipError::OtherSchedulingAgent + | ItipError::NotOrganizer + | ItipError::NotOrganizerNorAttendee + | ItipError::NothingToSend => None, + ItipError::MultipleOrganizer => Some(CalCondition::SameOrganizerInAllComponents), + ItipError::SenderIsOrganizer + | ItipError::SenderIsNotParticipant(_) + | ItipError::OrganizerMismatch => Some(CalCondition::ValidOrganizer), + ItipError::CannotModifyProperty(_) + | ItipError::CannotModifyInstance + | ItipError::CannotModifyAddress => Some(CalCondition::AllowedAttendeeObjectChange), + ItipError::MissingUid + | ItipError::MultipleUid + | ItipError::MultipleObjectTypes + | ItipError::MultipleObjectInstances + | ItipError::MissingMethod + | ItipError::InvalidComponentType + | ItipError::OutOfSequence + | ItipError::UnknownParticipant(_) + | ItipError::UnsupportedMethod(_) => Some(CalCondition::ValidSchedulingMessage), + } + } +} + +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::SenderIsOrganizer => write!(f, "Sender is the organizer of the event"), + ItipError::SenderIsNotParticipant(participant) => { + write!(f, "Sender {participant:?} is not a participant") + } + ItipError::UnknownParticipant(participant) => { + write!(f, "Unknown participant: {}", participant) + } + ItipError::UnsupportedMethod(method) => { + write!(f, "Unsupported method: {}", method.as_str()) + } + } + } +} diff --git a/crates/groupware/src/scheduling/organizer.rs b/crates/groupware/src/scheduling/organizer.rs index 4af6aaf0..45f7f4d4 100644 --- a/crates/groupware/src/scheduling/organizer.rs +++ b/crates/groupware/src/scheduling/organizer.rs @@ -25,7 +25,7 @@ pub(crate) fn organizer_handle_update( old_itip: ItipSnapshots<'_>, new_itip: ItipSnapshots<'_>, increment_sequences: &mut Vec, -) -> Result, ItipError> { +) -> Result>, ItipError> { let mut changed_instances: Vec<(&InstanceId, &str, &ICalendarMethod)> = Vec::new(); let mut increment_sequence = false; let mut changed_properties = AHashSet::new(); @@ -179,9 +179,11 @@ pub(crate) fn organizer_handle_update( increment_sequence.then_some(increment_sequences), false, ) { - Ok(mut message) => { - message.changed_properties = changed_properties.clone(); - messages.push(message); + Ok(messages_) => { + for mut message in messages_ { + message.changed_properties = changed_properties.clone(); + messages.push(message); + } } Err(err) => { if send_partial_update.is_empty() { @@ -279,6 +281,7 @@ pub(crate) fn organizer_handle_update( messages.push(ItipMessage { method: method.clone(), from: itip.organizer.email.email.clone(), + from_organizer: true, to: emails.into_iter().map(|e| e.to_string()).collect(), changed_properties: if method == &ICalendarMethod::Request { changed_properties.clone() @@ -298,7 +301,7 @@ pub(crate) fn organizer_request_full( itip: &ItipSnapshots<'_>, mut increment_sequence: Option<&mut Vec>, is_first_request: bool, -) -> Result { +) -> Result>, ItipError> { // Prepare iTIP message let dt_stamp = PartialDateTime::now(); let mut message = ICalendar { @@ -310,6 +313,11 @@ pub(crate) fn organizer_request_full( 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); @@ -366,13 +374,14 @@ pub(crate) fn organizer_request_full( message.components[0].component_ids.sort_unstable(); if !recipients.is_empty() { - Ok(ItipMessage { + Ok(vec![ItipMessage { method: ICalendarMethod::Request, from: itip.organizer.email.email.clone(), + from_organizer: true, to: recipients.into_iter().map(|e| e.to_string()).collect(), changed_properties: vec![], message, - }) + }]) } else { Err(ItipError::NothingToSend) } diff --git a/crates/groupware/src/scheduling/snapshot.rs b/crates/groupware/src/scheduling/snapshot.rs index 4296e284..47188ab4 100644 --- a/crates/groupware/src/scheduling/snapshot.rs +++ b/crates/groupware/src/scheduling/snapshot.rs @@ -16,24 +16,28 @@ use calcard::icalendar::{ pub fn itip_snapshot<'x, 'y>( ical: &'x ICalendar, - account_emails: &'y [&'y str], + account_emails: &'y [String], force_add_client_scheduling: bool, ) -> Result, 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> = 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 has_scheduling_info = false; let mut tz_resolver = None; for (comp_id, comp) in ical.components.iter().enumerate() { - if comp.component_type.is_scheduling_object() - && comp - .entries - .iter() - .any(|e| matches!(e.name, ICalendarProperty::Organizer)) - { + if comp.component_type.is_scheduling_object() { match expect_object_type { Some(expected) if expected != &comp.component_type => { return Err(ItipError::MultipleObjectTypes); @@ -43,7 +47,6 @@ pub fn itip_snapshot<'x, 'y>( } _ => {} } - has_scheduling_info = true; let mut sched_comp = ItipSnapshot { comp_id: comp_id as u16, @@ -296,16 +299,14 @@ pub fn itip_snapshot<'x, 'y>( } } - if !components.is_empty() && has_local_emails { + if has_local_emails { Ok(ItipSnapshots { organizer: organizer.ok_or(ItipError::NoSchedulingInfo)?, uid: uid.ok_or(ItipError::MissingUid)?, components, }) - } else if has_scheduling_info { - Err(ItipError::NotOrganizerNorAttendee) } else { - Err(ItipError::NoSchedulingInfo) + Err(ItipError::NotOrganizerNorAttendee) } } diff --git a/crates/http-proto/src/response.rs b/crates/http-proto/src/response.rs index c49c199f..44322f03 100644 --- a/crates/http-proto/src/response.rs +++ b/crates/http-proto/src/response.rs @@ -60,6 +60,15 @@ impl HttpResponse { } } + pub fn with_schedule_tag_opt(mut self, tag: Option) -> Self { + if let Some(tag) = tag { + self.builder = self.builder.header("Schedule-Tag", format!("\"{tag}\"")); + self + } else { + self + } + } + pub fn with_last_modified(mut self, last_modified: String) -> Self { self.builder = self.builder.header(header::LAST_MODIFIED, last_modified); self diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index 711c6769..c7407bce 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -235,7 +235,7 @@ impl ParseHttp for Server { "DAV", concat!( "1, 2, 3, access-control, extended-mkcol, calendar-access, ", - "calendar-no-timezone, addressbook" + "calendar-auto-schedule, calendar-no-timezone, addressbook" ), ) .with_header( diff --git a/crates/jmap-proto/src/types/acl.rs b/crates/jmap-proto/src/types/acl.rs index 12ee219d..960ac977 100644 --- a/crates/jmap-proto/src/types/acl.rs +++ b/crates/jmap-proto/src/types/acl.rs @@ -36,11 +36,11 @@ pub enum Acl { CreateChild = 7, Administer = 8, Submit = 9, - ReadFreeBusy = 10, - ModifyItemsOwn = 11, - ModifyPrivateProperties = 12, - RSVP = 13, - Share = 14, + SchedulingReadFreeBusy = 10, + SchedulingInvite = 11, + SchedulingReply = 12, + ModifyItemsOwn = 13, + ModifyPrivateProperties = 14, None = 15, } @@ -90,12 +90,12 @@ impl Acl { Acl::CreateChild => "createChild", Acl::Administer => "administer", Acl::Submit => "submit", - Acl::ReadFreeBusy => "readFreeBusy", Acl::ModifyItemsOwn => "modifyItemsOwn", Acl::ModifyPrivateProperties => "modifyPrivateProperties", - Acl::RSVP => "rsvp", - Acl::Share => "share", Acl::None => "", + Acl::SchedulingReadFreeBusy => "schedulingReadFreeBusy", + Acl::SchedulingInvite => "schedulingInvite", + Acl::SchedulingReply => "schedulingReply", } } } @@ -144,6 +144,11 @@ impl From for Acl { 7 => Acl::CreateChild, 8 => Acl::Administer, 9 => Acl::Submit, + 10 => Acl::SchedulingReadFreeBusy, + 11 => Acl::SchedulingInvite, + 12 => Acl::SchedulingReply, + 13 => Acl::ModifyItemsOwn, + 14 => Acl::ModifyPrivateProperties, _ => Acl::None, } } diff --git a/crates/migration/src/calendar.rs b/crates/migration/src/calendar.rs index c369d9f3..70eed83b 100644 --- a/crates/migration/src/calendar.rs +++ b/crates/migration/src/calendar.rs @@ -109,6 +109,7 @@ pub(crate) async fn migrate_calendar_events(server: &Server) -> trc::Result<()> size: event.size, created: event.created, modified: event.modified, + schedule_tag: None, }; let mut batch = BatchBuilder::new(); batch diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index e2b92268..cfafd5ce 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -308,6 +308,22 @@ impl ValueClass { .write(document_id) .write(*event_id) .write(*alarm_id), + TaskQueueClass::SendItip { due, is_payload } => { + if !*is_payload { + serializer + .write(*due) + .write(account_id) + .write(4u8) + .write(document_id) + } else { + serializer + .write(u64::MAX) + .write(account_id) + .write(5u8) + .write(document_id) + .write(*due) + } + } }, ValueClass::Blob(op) => match op { BlobOp::Reserve { hash, until } => serializer @@ -581,6 +597,13 @@ impl ValueClass { (BLOB_HASH_LEN + U64_LEN * 2) + 1 } TaskQueueClass::SendAlarm { .. } => U64_LEN + (U32_LEN * 3) + 1, + TaskQueueClass::SendItip { is_payload, .. } => { + if *is_payload { + (U64_LEN * 2) + (U32_LEN * 2) + 1 + } else { + U64_LEN + (U32_LEN * 2) + 1 + } + } }, ValueClass::Queue(q) => match q { QueueClass::Message(_) => U64_LEN, diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index 542fac97..62cbfd79 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -209,6 +209,10 @@ pub enum TaskQueueClass { event_id: u16, alarm_id: u16, }, + SendItip { + due: u64, + is_payload: bool, + }, } #[derive(Debug, PartialEq, Clone, Eq, Hash)] diff --git a/crates/trc/src/event/description.rs b/crates/trc/src/event/description.rs index 1dbc39fb..83dcee00 100644 --- a/crates/trc/src/event/description.rs +++ b/crates/trc/src/event/description.rs @@ -1895,6 +1895,10 @@ impl CalendarEvent { CalendarEvent::AlarmSkipped => "Calendar alarm skipped", CalendarEvent::AlarmRecipientOverride => "Calendar alarm recipient overriden", CalendarEvent::AlarmFailed => "Calendar alarm could not be sent", + CalendarEvent::SchedulingError => "Calendar scheduling error", + CalendarEvent::ItipMessageSent => "Calendar iTIP message sent", + CalendarEvent::ItipMessageReceived => "Calendar iTIP message received", + CalendarEvent::ItipMessageError => "Incoming calendar iTIP message error", } } @@ -1907,6 +1911,14 @@ impl CalendarEvent { CalendarEvent::AlarmSkipped => "A calendar alarm was skipped", CalendarEvent::AlarmRecipientOverride => "A calendar alarm recipient was overridden", CalendarEvent::AlarmFailed => "A calendar alarm could not be sent to the recipient", + CalendarEvent::SchedulingError => { + "An error occurred processing the calendar scheduling request" + } + CalendarEvent::ItipMessageSent => "A calendar iTIP message has been sent", + CalendarEvent::ItipMessageReceived => "A calendar iTIP/iMIP message has been received", + CalendarEvent::ItipMessageError => { + "An error occurred while processing an incoming iTIP/iMIP message" + } } } } diff --git a/crates/trc/src/event/level.rs b/crates/trc/src/event/level.rs index 6786da2a..03ce0276 100644 --- a/crates/trc/src/event/level.rs +++ b/crates/trc/src/event/level.rs @@ -537,11 +537,15 @@ impl EventType { }, EventType::WebDav(_) => Level::Debug, EventType::Calendar(event) => match event { - CalendarEvent::AlarmSent => Level::Info, + CalendarEvent::ItipMessageSent + | CalendarEvent::ItipMessageReceived + | CalendarEvent::AlarmSent => Level::Info, CalendarEvent::AlarmFailed => Level::Warn, CalendarEvent::RuleExpansionError | CalendarEvent::AlarmSkipped - | CalendarEvent::AlarmRecipientOverride => Level::Debug, + | CalendarEvent::AlarmRecipientOverride + | CalendarEvent::SchedulingError + | CalendarEvent::ItipMessageError => Level::Debug, }, } } diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index a21f22bf..aea3bf03 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -988,6 +988,10 @@ pub enum CalendarEvent { AlarmSkipped, AlarmRecipientOverride, AlarmFailed, + SchedulingError, + ItipMessageSent, + ItipMessageReceived, + ItipMessageError, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/trc/src/serializers/binary.rs b/crates/trc/src/serializers/binary.rs index 368f7d51..40cf1c9e 100644 --- a/crates/trc/src/serializers/binary.rs +++ b/crates/trc/src/serializers/binary.rs @@ -892,6 +892,10 @@ impl EventType { EventType::Calendar(CalendarEvent::AlarmSkipped) => 580, EventType::Calendar(CalendarEvent::AlarmRecipientOverride) => 581, EventType::Calendar(CalendarEvent::AlarmFailed) => 582, + EventType::Calendar(CalendarEvent::SchedulingError) => 583, + EventType::Calendar(CalendarEvent::ItipMessageSent) => 584, + EventType::Calendar(CalendarEvent::ItipMessageReceived) => 585, + EventType::Calendar(CalendarEvent::ItipMessageError) => 586, } } @@ -1520,6 +1524,10 @@ impl EventType { 580 => Some(EventType::Calendar(CalendarEvent::AlarmSkipped)), 581 => Some(EventType::Calendar(CalendarEvent::AlarmRecipientOverride)), 582 => Some(EventType::Calendar(CalendarEvent::AlarmFailed)), + 583 => Some(EventType::Calendar(CalendarEvent::SchedulingError)), + 584 => Some(EventType::Calendar(CalendarEvent::ItipMessageSent)), + 585 => Some(EventType::Calendar(CalendarEvent::ItipMessageReceived)), + 586 => Some(EventType::Calendar(CalendarEvent::ItipMessageError)), _ => None, } } diff --git a/tests/src/webdav/cal_itip.rs b/tests/src/webdav/cal_itip.rs index 427280c4..a3114e6f 100644 --- a/tests/src/webdav/cal_itip.rs +++ b/tests/src/webdav/cal_itip.rs @@ -138,13 +138,15 @@ pub fn test() { .entry(name.to_string()) { Entry::Occupied(mut entry) => { - last_itip = Some(itip_update(&mut ical, entry.get_mut(), &[account])); + last_itip = Some(itip_update( + &mut ical, + entry.get_mut(), + &[account.to_string()], + )); entry.insert(ical); } Entry::Vacant(entry) => { - last_itip = Some( - itip_create(&mut ical, &[account]).map(|message| vec![message]), - ); + last_itip = Some(itip_create(&mut ical, &[account.to_string()])); entry.insert(ical); } } @@ -196,9 +198,10 @@ pub fn test() { .as_str(); let store = store.get_mut(account).expect("Account not found in store"); - if let Some(mut ical) = store.remove(name) { - last_itip = - Some(itip_cancel(&mut ical, &[account]).map(|message| vec![message])); + if let Some(ical) = store.remove(name) { + last_itip = Some( + itip_cancel(&ical, &[account.to_string()]).map(|message| vec![message]), + ); } else { panic!( "ICalendar not found for account: {}, name: {}", @@ -242,7 +245,7 @@ pub fn test() { for rcpt in &message.to { let result = match itip_snapshot( &message.message, - &[rcpt.as_str()], + &[rcpt.to_string()], false, ) { Ok(itip_snapshots) => { @@ -255,7 +258,7 @@ pub fn test() { let ical = entry.get_mut(); let snapshots = itip_snapshot( ical, - &[rcpt.as_str()], + &[rcpt.to_string()], false, ) .expect("Failed to create iTIP snapshot"); @@ -335,6 +338,7 @@ pub fn test() { let mut commands = command.parameters.iter(); last_itip = Some(Ok(vec![ItipMessage { method: ICalendarMethod::Request, + from_organizer: false, from: commands .next() .expect("From parameter is required") @@ -359,7 +363,7 @@ trait ItipMessageExt { fn to_string(&self, map: &mut AHashMap) -> String; } -impl ItipMessageExt for ItipMessage { +impl ItipMessageExt for ItipMessage { fn to_string(&self, map: &mut AHashMap) -> String { use std::fmt::Write; let mut f = String::new();