CalDAV Scheduling - part 4

This commit is contained in:
mdecimus
2025-06-18 18:21:46 +02:00
parent 20cb114715
commit c07e3d917f
47 changed files with 872 additions and 173 deletions

View 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>

View File

@@ -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,

View File

@@ -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;
},
_ => {}
);

View File

@@ -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(()),
}
}

View File

@@ -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,
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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/>"),
}
}
}

View File

@@ -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()

View File

@@ -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,
}
}

View 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>")
}
}

View File

@@ -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 {

View File

@@ -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",
}
}
}