mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-09-07 20:44:15 +08:00
CalDAV Scheduling - part 4
This commit is contained in:
parent
20cb114715
commit
c07e3d917f
47 changed files with 872 additions and 173 deletions
|
@ -30,6 +30,10 @@ pub struct GroupwareConfig {
|
|||
pub alarms_from_name: String,
|
||||
pub alarms_from_email: Option<String>,
|
||||
pub alarms_template: Template<CalendarTemplateVariable>,
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
23
crates/dav-proto/resources/responses/020.xml
Normal file
23
crates/dav-proto/resources/responses/020.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<A:schedule-response xmlns:D="DAV:" xmlns:A="urn:ietf:params:xml:ns:caldav">
|
||||
<A:response>
|
||||
<A:recipient>
|
||||
<D:href>mailto:wilfredo@example.com</D:href>
|
||||
</A:recipient>
|
||||
<A:request-status>2.0;Success</A:request-status>
|
||||
<A:calendar-data><![CDATA[BEGIN:VCALENDAR]]></A:calendar-data>
|
||||
</A:response>
|
||||
<A:response>
|
||||
<A:recipient>
|
||||
<D:href>mailto:bernard@example.net</D:href>
|
||||
</A:recipient>
|
||||
<A:request-status>2.0;Success</A:request-status>
|
||||
<A:calendar-data><![CDATA[END:VCALENDAR]]></A:calendar-data>
|
||||
</A:response>
|
||||
<A:response>
|
||||
<A:recipient>
|
||||
<D:href>mailto:mike@example.org</D:href>
|
||||
</A:recipient>
|
||||
<A:request-status>3.7;Invalid calendar user</A:request-status>
|
||||
</A:response>
|
||||
</A:schedule-response>
|
|
@ -42,6 +42,8 @@ pub struct RequestHeaders<'x> {
|
|||
pub destination: Option<&'x str>,
|
||||
pub lock_token: Option<&'x str>,
|
||||
pub max_vcard_version: Option<VCardVersion>,
|
||||
pub no_schedule_reply: bool,
|
||||
pub if_schedule_tag: Option<u32>,
|
||||
pub overwrite_fail: bool,
|
||||
pub no_timezones: bool,
|
||||
pub ret: Return,
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
_ => {}
|
||||
);
|
||||
|
||||
|
|
|
@ -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<NamedElement> 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(()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -177,6 +177,26 @@ impl Display for Privilege {
|
|||
Privilege::Unbind => "<D:privilege><D:unbind/></D:privilege>".fmt(f),
|
||||
Privilege::All => "<D:privilege><D:all/></D:privilege>".fmt(f),
|
||||
Privilege::ReadFreeBusy => "<D:privilege><A:read-free-busy/></D:privilege>".fmt(f),
|
||||
Privilege::ScheduleDeliver => "<D:privilege><A:schedule-deliver/></D:privilege>".fmt(f),
|
||||
Privilege::ScheduleDeliverInvite => {
|
||||
"<D:privilege><A:schedule-deliver-invite/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleDeliverReply => {
|
||||
"<D:privilege><A:schedule-deliver-reply/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleQueryFreeBusy => {
|
||||
"<D:privilege><A:schedule-query-freebusy/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleSend => "<D:privilege><A:schedule-send/></D:privilege>".fmt(f),
|
||||
Privilege::ScheduleSendInvite => {
|
||||
"<D:privilege><A:schedule-send-invite/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleSendReply => {
|
||||
"<D:privilege><A:schedule-send-reply/></D:privilege>".fmt(f)
|
||||
}
|
||||
Privilege::ScheduleSendFreeBusy => {
|
||||
"<D:privilege><A:schedule-send-freebusy/></D:privilege>".fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,6 +118,25 @@ impl Display for CalCondition {
|
|||
}
|
||||
CalCondition::MaxInstances => write!(f, "<A:max-instances/>"),
|
||||
CalCondition::MaxAttendeesPerInstance => write!(f, "<A:max-attendees-per-instance/>"),
|
||||
CalCondition::UniqueSchedulingObjectResource(href) => write!(
|
||||
f,
|
||||
"<A:unique-scheduling-object-resource>{href}</A:unique-scheduling-object-resource>"
|
||||
),
|
||||
CalCondition::SameOrganizerInAllComponents => {
|
||||
write!(f, "<A:same-organizer-in-all-components/>")
|
||||
}
|
||||
CalCondition::AllowedOrganizerObjectChange => {
|
||||
write!(f, "<A:allowed-organizer-scheduling-object-change/>")
|
||||
}
|
||||
CalCondition::AllowedAttendeeObjectChange => {
|
||||
write!(f, "<A:allowed-attendee-scheduling-object-change/>")
|
||||
}
|
||||
CalCondition::DefaultCalendarNeeded => write!(f, "<A:default-calendar-needed/>"),
|
||||
CalCondition::ValidScheduleDefaultCalendarUrl => {
|
||||
write!(f, "<A:valid-schedule-default-calendar-URL/>")
|
||||
}
|
||||
CalCondition::ValidSchedulingMessage => write!(f, "<A:valid-scheduling-message/>"),
|
||||
CalCondition::ValidOrganizer => write!(f, "<A:valid-organizer/>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, "<D:principal/>"),
|
||||
ResourceType::AddressBook => write!(f, "<B:addressbook/>"),
|
||||
ResourceType::Calendar => write!(f, "<A:calendar/>"),
|
||||
ResourceType::ScheduleInbox => write!(f, "<A:schedule-inbox/>"),
|
||||
ResourceType::ScheduleOutbox => write!(f, "<A:schedule-outbox/>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
49
crates/dav-proto/src/responses/schedule.rs
Normal file
49
crates/dav-proto/src/responses/schedule.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
||||
*
|
||||
* 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, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
|
||||
write!(
|
||||
f,
|
||||
"<A:schedule-response {NAMESPACE}>{}</A:schedule-response>",
|
||||
self.items
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ScheduleResponseItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "<A:response>")?;
|
||||
write!(f, "<A:recipient>{}</A:recipient>", self.recipient)?;
|
||||
|
||||
write!(f, "<A:request-status>")?;
|
||||
self.request_status.write_escaped_to(f)?;
|
||||
write!(f, "</A:request-status>")?;
|
||||
|
||||
if let Some(calendar_data) = &self.calendar_data {
|
||||
write!(f, "<A:calendar-data>")?;
|
||||
calendar_data.write_cdata_escaped_to(f)?;
|
||||
write!(f, "</A:calendar-data>")?;
|
||||
}
|
||||
write!(f, "</A:response>")
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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<DavPropertyValue>,
|
||||
}
|
||||
|
||||
pub struct ScheduleResponse {
|
||||
pub items: List<ScheduleResponseItem>,
|
||||
}
|
||||
|
||||
pub struct ScheduleResponseItem {
|
||||
pub recipient: Href,
|
||||
pub request_status: Cow<'static, str>,
|
||||
pub calendar_data: Option<String>,
|
||||
}
|
||||
|
||||
#[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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<u32>,
|
||||
to_calendar_id: u32,
|
||||
new_name: &str,
|
||||
if_schedule_tag: Option<u32>,
|
||||
) -> crate::Result<HttpResponse> {
|
||||
// Fetch event
|
||||
let event_ = server
|
||||
|
@ -564,6 +568,13 @@ async fn move_event(
|
|||
.to_unarchived::<CalendarEvent>()
|
||||
.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!())?;
|
||||
|
|
|
@ -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::<Vec<_>>(),
|
||||
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::<CalendarEvent>()
|
||||
.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::<CalendarEvent>()
|
||||
.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!())?;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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::<CalendarEvent>()
|
||||
.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::<usize>()
|
||||
< 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::<usize>()
|
||||
< 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))?
|
||||
}
|
||||
|
|
|
@ -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<Acl>) -> Vec<Privile
|
|||
acls.insert(Privilege::ReadAcl);
|
||||
acls.insert(Privilege::WriteAcl);
|
||||
}
|
||||
Acl::ReadFreeBusy => {
|
||||
Acl::SchedulingReadFreeBusy => {
|
||||
acls.insert(Privilege::ReadFreeBusy);
|
||||
}
|
||||
_ => {}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1408,6 +1408,8 @@ impl Permission {
|
|||
| Permission::DavCalMultiGet
|
||||
| Permission::DavCalFreeBusyQuery
|
||||
| Permission::CalendarAlarms
|
||||
| Permission::CalendarSchedulingSend
|
||||
| Permission::CalendarSchedulingReceive
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -375,6 +375,8 @@ pub enum Permission {
|
|||
DavCalFreeBusyQuery,
|
||||
|
||||
CalendarAlarms,
|
||||
CalendarSchedulingSend,
|
||||
CalendarSchedulingReceive,
|
||||
// WARNING: add new ids at the end (TODO: use static ids)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ pub struct CalendarEvent {
|
|||
pub size: u32,
|
||||
pub created: i64,
|
||||
pub modified: i64,
|
||||
pub schedule_tag: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
@ -153,13 +154,13 @@ impl TryFrom<Acl> for CalendarRight {
|
|||
|
||||
fn try_from(value: Acl) -> Result<Self, Self::Error> {
|
||||
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<Acl> for CalendarRight {
|
|||
impl From<CalendarRight> 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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Archive<&ArchivedCalendar>> {
|
|||
document_id: u32,
|
||||
children_ids: Vec<u32>,
|
||||
delete_path: Option<String>,
|
||||
send_itip: bool,
|
||||
batch: &mut BatchBuilder,
|
||||
) -> trc::Result<()> {
|
||||
// Process deletions
|
||||
|
@ -173,6 +177,7 @@ impl DestroyArchive<Archive<&ArchivedCalendar>> {
|
|||
document_id,
|
||||
calendar_id,
|
||||
None,
|
||||
send_itip,
|
||||
batch,
|
||||
)?;
|
||||
}
|
||||
|
@ -211,6 +216,7 @@ impl DestroyArchive<Archive<&ArchivedCalendar>> {
|
|||
}
|
||||
|
||||
impl DestroyArchive<Archive<&ArchivedCalendarEvent>> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn delete(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
|
@ -218,6 +224,7 @@ impl DestroyArchive<Archive<&ArchivedCalendarEvent>> {
|
|||
document_id: u32,
|
||||
calendar_id: u32,
|
||||
delete_path: Option<String>,
|
||||
send_itip: bool,
|
||||
batch: &mut BatchBuilder,
|
||||
) -> trc::Result<()> {
|
||||
let event = self.0;
|
||||
|
@ -255,6 +262,21 @@ impl DestroyArchive<Archive<&ArchivedCalendarEvent>> {
|
|||
next_alarm.delete_task(batch);
|
||||
}
|
||||
|
||||
// Scheduling
|
||||
if send_itip && event.inner.schedule_tag.is_some() {
|
||||
let event = event
|
||||
.deserialize::<CalendarEvent>()
|
||||
.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()
|
||||
|
|
|
@ -57,7 +57,7 @@ impl TryFrom<Acl> 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<AddressBookRight> for Acl {
|
|||
match value {
|
||||
AddressBookRight::Read => Acl::Read,
|
||||
AddressBookRight::Write => Acl::Modify,
|
||||
AddressBookRight::Share => Acl::Share,
|
||||
AddressBookRight::Share => Acl::Administer,
|
||||
AddressBookRight::Delete => Acl::Delete,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ pub(crate) fn attendee_handle_update(
|
|||
new_ical: &ICalendar,
|
||||
old_itip: ItipSnapshots<'_>,
|
||||
new_itip: ItipSnapshots<'_>,
|
||||
) -> Result<Vec<ItipMessage>, ItipError> {
|
||||
) -> Result<Vec<ItipMessage<ICalendar>>, 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::<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ItipMessage, ItipError> {
|
||||
ical: &ICalendar,
|
||||
account_emails: &[String],
|
||||
) -> Result<ItipMessage<ICalendar>, ItipError> {
|
||||
// Prepare iTIP message
|
||||
let itip = itip_snapshot(ical, account_emails, false)?;
|
||||
let dt_stamp = PartialDateTime::now();
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ use calcard::icalendar::ICalendar;
|
|||
|
||||
pub fn itip_create(
|
||||
ical: &mut ICalendar,
|
||||
account_emails: &[&str],
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
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)
|
||||
|
|
|
@ -12,9 +12,9 @@ use calcard::icalendar::ICalendar;
|
|||
|
||||
pub fn itip_update(
|
||||
ical: &mut ICalendar,
|
||||
old_ical: &mut ICalendar,
|
||||
account_emails: &[&str],
|
||||
) -> Result<Vec<ItipMessage>, ItipError> {
|
||||
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) => {
|
||||
|
|
|
@ -44,7 +44,7 @@ pub enum MergeAction {
|
|||
|
||||
pub enum MergeResult {
|
||||
Actions(Vec<MergeAction>),
|
||||
Message(ItipMessage),
|
||||
Message(ItipMessage<ICalendar>),
|
||||
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())),
|
||||
}
|
||||
|
|
|
@ -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<ItipMessage<ICalendar>>) -> 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<ItipMessage<ICalendar>> for ItipMessage<String> {
|
||||
fn from(message: ItipMessage<ICalendar>) -> 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T> {
|
||||
pub method: ICalendarMethod,
|
||||
pub from: String,
|
||||
pub from_organizer: bool,
|
||||
pub to: Vec<String>,
|
||||
pub changed_properties: Vec<ICalendarProperty>,
|
||||
pub message: ICalendar,
|
||||
pub message: T,
|
||||
}
|
||||
|
||||
#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize)]
|
||||
pub struct ItipMessages {
|
||||
pub messages: Vec<ItipMessage<String>>,
|
||||
}
|
||||
|
||||
impl ItipSnapshot<'_> {
|
||||
|
@ -195,15 +203,15 @@ impl Attendee<'_> {
|
|||
}
|
||||
|
||||
impl Email {
|
||||
pub fn new(email: &str, local_addresses: &[&str]) -> Option<Self> {
|
||||
pub fn new(email: &str, local_addresses: &[String]) -> Option<Self> {
|
||||
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<Self> {
|
||||
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 {
|
||||
|
@ -298,3 +306,75 @@ impl ItipDateTime<'_> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ItipError {
|
||||
pub fn failed_precondition(&self) -> Option<CalCondition> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ pub(crate) fn organizer_handle_update(
|
|||
old_itip: ItipSnapshots<'_>,
|
||||
new_itip: ItipSnapshots<'_>,
|
||||
increment_sequences: &mut Vec<u16>,
|
||||
) -> Result<Vec<ItipMessage>, ItipError> {
|
||||
) -> 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();
|
||||
|
@ -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<u16>>,
|
||||
is_first_request: bool,
|
||||
) -> Result<ItipMessage, ItipError> {
|
||||
) -> Result<Vec<ItipMessage<ICalendar>>, 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)
|
||||
}
|
||||
|
|
|
@ -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<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 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,15 @@ impl HttpResponse {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn with_schedule_tag_opt(mut self, tag: Option<u32>) -> 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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<u64> 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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -988,6 +988,10 @@ pub enum CalendarEvent {
|
|||
AlarmSkipped,
|
||||
AlarmRecipientOverride,
|
||||
AlarmFailed,
|
||||
SchedulingError,
|
||||
ItipMessageSent,
|
||||
ItipMessageReceived,
|
||||
ItipMessageError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<PartialDateTime, usize>) -> String;
|
||||
}
|
||||
|
||||
impl ItipMessageExt for ItipMessage {
|
||||
impl ItipMessageExt for ItipMessage<ICalendar> {
|
||||
fn to_string(&self, map: &mut AHashMap<PartialDateTime, usize>) -> String {
|
||||
use std::fmt::Write;
|
||||
let mut f = String::new();
|
||||
|
|
Loading…
Add table
Reference in a new issue