CalDAV: support for supported-calendar-component-set (closes #1893)

This commit is contained in:
mdecimus 2025-10-21 10:18:03 +02:00
parent f6f5d18d68
commit 107561297d
13 changed files with 655 additions and 456 deletions

799
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -137,6 +137,9 @@ impl Display for CalCondition {
}
CalCondition::ValidSchedulingMessage => write!(f, "<A:valid-scheduling-message/>"),
CalCondition::ValidOrganizer => write!(f, "<A:valid-organizer/>"),
CalCondition::SupportedCalendarComponent => {
write!(f, "<A:supported-calendar-component/>")
}
}
}
}

View file

@ -141,7 +141,7 @@ impl Display for SyncToken {
impl Display for Comp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<A:comp name=\"{}\">", self.0.as_str())
write!(f, "<A:comp name=\"{}\"/>", self.0.as_str())
}
}

View file

@ -18,6 +18,7 @@ use crate::{
Namespace, Namespaces,
},
};
use calcard::icalendar::ICalendarComponentType;
use mail_parser::{
parsers::fields::date::{DOW, MONTH},
DateTime,
@ -119,32 +120,32 @@ impl Display for DavValue {
)
)
}
DavValue::SupportedCalendarComponentSet => {
write!(
f,
concat!(
"<A:comp name=\"VEVENT\"/>",
"<A:comp name=\"VTODO\"/>",
"<A:comp name=\"VJOURNAL\"/>",
"<A:comp name=\"VFREEBUSY\"/>",
"<A:comp name=\"VTIMEZONE\"/>",
"<A:comp name=\"VALARM\"/>",
"<A:comp name=\"STANDARD\"/>",
"<A:comp name=\"DAYLIGHT\"/>",
"<A:comp name=\"VAVAILABILITY\"/>",
"<A:comp name=\"AVAILABLE\"/>",
"<A:comp name=\"PARTICIPANT\"/>",
"<A:comp name=\"VLOCATION\"/>",
"<A:comp name=\"VRESOURCE\"/>",
)
)
}
DavValue::Response(v) => v.fmt(f),
DavValue::VCard(_) | DavValue::ICalendar(_) | DavValue::Null => Ok(()),
}
}
}
impl DavValue {
pub fn all_calendar_components() -> Self {
DavValue::Components(List(vec![
Comp(ICalendarComponentType::VEvent),
Comp(ICalendarComponentType::VTodo),
Comp(ICalendarComponentType::VJournal),
Comp(ICalendarComponentType::VFreebusy),
Comp(ICalendarComponentType::VTimezone),
Comp(ICalendarComponentType::VAlarm),
Comp(ICalendarComponentType::Standard),
Comp(ICalendarComponentType::Daylight),
Comp(ICalendarComponentType::VAvailability),
Comp(ICalendarComponentType::Available),
Comp(ICalendarComponentType::Participant),
Comp(ICalendarComponentType::VLocation),
Comp(ICalendarComponentType::VResource),
]))
}
}
impl DavProperty {
fn tag_name(&self) -> (&str, Option<&str>) {
(

View file

@ -169,7 +169,6 @@ pub enum DavValue {
DeadProperty(DeadProperty),
SupportedAddressData,
SupportedCalendarData,
SupportedCalendarComponentSet,
Null,
}

View file

@ -244,6 +244,7 @@ pub enum CalCondition {
Vec<Filter<Vec<ICalendarComponentType>, ICalendarProperty, ICalendarParameterName>>,
),
SupportedCollation(String),
SupportedCalendarComponent,
MinDateTime,
MaxDateTime,
MaxResourceSize(u32),
@ -366,6 +367,7 @@ impl CalCondition {
CalCondition::ValidScheduleDefaultCalendarUrl => "ValidScheduleDefaultCalendarUrl",
CalCondition::ValidSchedulingMessage => "ValidSchedulingMessage",
CalCondition::ValidOrganizer => "ValidOrganizer",
CalCondition::SupportedCalendarComponent => "SupportedCalendarComponent",
}
}
}

View file

@ -25,7 +25,7 @@ use dav_proto::{
};
use groupware::{
cache::GroupwareCache,
calendar::{Calendar, CalendarEvent, Timezone},
calendar::{Calendar, CalendarEvent, SupportedComponent, Timezone},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
@ -36,6 +36,7 @@ use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
use utils::map::bitmap::Bitmap;
pub(crate) trait CalendarPropPatchRequestHandler: Sync + Send {
fn handle_calendar_proppatch_request(
@ -340,6 +341,39 @@ impl CalendarPropPatchRequestHandler for Server {
items.insert_ok(property.property);
}
}
(
DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet),
DavValue::Components(components),
) => {
if !is_update {
calendar.supported_components = Bitmap::<SupportedComponent>::from_iter(
components
.0
.into_iter()
.map(|v| SupportedComponent::from(v.0)),
)
.into_inner();
if calendar.supported_components != 0 {
items.insert_ok(property.property);
} else {
items.insert_precondition_failed_with_description(
property.property,
StatusCode::PRECONDITION_FAILED,
CalCondition::SupportedCalendarComponent,
"At least one supported component must be specified",
);
has_errors = true;
}
} else {
items.insert_precondition_failed_with_description(
property.property,
StatusCode::PRECONDITION_FAILED,
CalCondition::SupportedCalendarComponent,
"Property cannot be modified",
);
has_errors = true;
}
}
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
if self.core.groupware.dead_property_size.is_some() =>
{
@ -362,7 +396,7 @@ impl CalendarPropPatchRequestHandler for Server {
has_errors = true;
}
}
(_, DavValue::Null | DavValue::Components(_)) => {
(_, DavValue::Null) => {
items.insert_ok(property.property);
}
_ => {

View file

@ -27,7 +27,7 @@ use crate::{
propfind::{PrincipalPropFind, build_home_set},
},
};
use calcard::common::timezone::Tz;
use calcard::{common::timezone::Tz, icalendar::ICalendarComponentType};
use common::{DavResourcePath, DavResources, Server, auth::AccessToken};
use dav_proto::{
Depth, RequestHeaders,
@ -36,9 +36,9 @@ use dav_proto::{
schema::{
Collation, Namespace,
property::{
ActiveLock, CalDavProperty, CardDavProperty, DavProperty, DavValue, PrincipalProperty,
Privilege, ReportSet, ResourceType, Rfc1123DateTime, SupportedCollation, SupportedLock,
WebDavProperty,
ActiveLock, CalDavProperty, CardDavProperty, Comp, DavProperty, DavValue,
PrincipalProperty, Privilege, ReportSet, ResourceType, Rfc1123DateTime,
SupportedCollation, SupportedLock, WebDavProperty,
},
request::{DavDeadProperty, DavPropertyValue, PropFind},
response::{
@ -48,7 +48,7 @@ use dav_proto::{
},
};
use directory::{Permission, Type, backend::internal::manage::ManageDirectory};
use groupware::calendar::SCHEDULE_INBOX_ID;
use groupware::calendar::{SCHEDULE_INBOX_ID, SupportedComponent};
use groupware::{
DavCalendarResource, DavResourceName, cache::GroupwareCache, calendar::ArchivedTimezone,
};
@ -67,6 +67,7 @@ use types::{
collection::{Collection, SyncCollection},
dead_property::DeadProperty,
};
use utils::map::bitmap::Bitmap;
pub(crate) trait PropFindRequestHandler: Sync + Send {
fn handle_propfind_request(
@ -898,11 +899,23 @@ impl PropFindRequestHandler for Server {
}
(
CalDavProperty::SupportedCalendarComponentSet,
ArchivedResource::Calendar(_),
ArchivedResource::Calendar(calendar),
) => {
let supported_components =
calendar.inner.supported_components.to_native();
fields.push(DavPropertyValue::new(
property.clone(),
DavValue::SupportedCalendarComponentSet,
if supported_components != 0 {
DavValue::Components(List(
Bitmap::<SupportedComponent>::from(supported_components)
.into_iter()
.map(ICalendarComponentType::from)
.map(Comp)
.collect(),
))
} else {
DavValue::all_calendar_components()
},
));
}
(CalDavProperty::SupportedCalendarData, ArchivedResource::Calendar(_)) => {

View file

@ -19,7 +19,7 @@ tokio = { version = "1.47", features = ["net"] }
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] }
rustls-pki-types = { version = "1" }
ldap3 = { version = "0.12", default-features = false, features = ["tls-rustls"] }
ldap3 = { version = "0.12", default-features = false, features = ["tls-rustls-ring"] }
deadpool = { version = "0.10", features = ["managed", "rt_tokio_1"] }
async-trait = "0.1.68"
ahash = { version = "0.8" }

View file

@ -11,9 +11,12 @@ pub mod index;
pub mod itip;
pub mod storage;
use calcard::icalendar::{ICalendar, ICalendarComponent, ICalendarDuration, ICalendarEntry};
use calcard::icalendar::{
ICalendar, ICalendarComponent, ICalendarComponentType, ICalendarDuration, ICalendarEntry,
};
use common::{DavName, auth::AccessToken};
use types::{acl::AclGrant, dead_property::DeadProperty};
use utils::map::bitmap::BitmapItem;
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
@ -22,11 +25,32 @@ pub struct Calendar {
pub name: String,
pub preferences: Vec<CalendarPreferences>,
pub acls: Vec<AclGrant>,
pub supported_components: u64,
pub dead_properties: DeadProperty,
pub created: i64,
pub modified: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SupportedComponent {
VCalendar, // [RFC5545, Section 3.4]
VEvent, // [RFC5545, Section 3.6.1]
VTodo, // [RFC5545, Section 3.6.2]
VJournal, // [RFC5545, Section 3.6.3]
VFreebusy, // [RFC5545, Section 3.6.4]
VTimezone, // [RFC5545, Section 3.6.5]
VAlarm, // [RFC5545, Section 3.6.6]
Standard, // [RFC5545, Section 3.6.5]
Daylight, // [RFC5545, Section 3.6.5]
VAvailability, // [RFC7953, Section 3.1]
Available, // [RFC7953, Section 3.1]
Participant, // [RFC9073, Section 7.1]
VLocation, // [RFC9073, Section 7.2] [RFC Errata 7381]
VResource, // [RFC9073, Section 7.3]
VStatus, // draft-ietf-calext-ical-tasks-14
Other,
}
pub const CALENDAR_SUBSCRIBED: u16 = 1;
pub const CALENDAR_INVISIBLE: u16 = 1 << 1;
pub const CALENDAR_AVAILABILITY_NONE: u16 = 1 << 2;
@ -319,3 +343,105 @@ impl Default for ChangedBy {
ChangedBy::CalendarAddress("".into())
}
}
impl From<u64> for SupportedComponent {
fn from(value: u64) -> Self {
match value {
0 => SupportedComponent::VCalendar,
1 => SupportedComponent::VEvent,
2 => SupportedComponent::VTodo,
3 => SupportedComponent::VJournal,
4 => SupportedComponent::VFreebusy,
5 => SupportedComponent::VTimezone,
6 => SupportedComponent::VAlarm,
7 => SupportedComponent::Standard,
8 => SupportedComponent::Daylight,
9 => SupportedComponent::VAvailability,
10 => SupportedComponent::Available,
11 => SupportedComponent::Participant,
12 => SupportedComponent::VLocation,
13 => SupportedComponent::VResource,
14 => SupportedComponent::VStatus,
_ => SupportedComponent::Other,
}
}
}
impl From<SupportedComponent> for u64 {
fn from(value: SupportedComponent) -> Self {
match value {
SupportedComponent::VCalendar => 0,
SupportedComponent::VEvent => 1,
SupportedComponent::VTodo => 2,
SupportedComponent::VJournal => 3,
SupportedComponent::VFreebusy => 4,
SupportedComponent::VTimezone => 5,
SupportedComponent::VAlarm => 6,
SupportedComponent::Standard => 7,
SupportedComponent::Daylight => 8,
SupportedComponent::VAvailability => 9,
SupportedComponent::Available => 10,
SupportedComponent::Participant => 11,
SupportedComponent::VLocation => 12,
SupportedComponent::VResource => 13,
SupportedComponent::VStatus => 14,
SupportedComponent::Other => 15,
}
}
}
impl BitmapItem for SupportedComponent {
fn max() -> u64 {
u64::from(SupportedComponent::Other)
}
fn is_valid(&self) -> bool {
!matches!(self, SupportedComponent::Other)
}
}
impl From<ICalendarComponentType> for SupportedComponent {
fn from(value: ICalendarComponentType) -> Self {
match value {
ICalendarComponentType::VCalendar => SupportedComponent::VCalendar,
ICalendarComponentType::VEvent => SupportedComponent::VEvent,
ICalendarComponentType::VTodo => SupportedComponent::VTodo,
ICalendarComponentType::VJournal => SupportedComponent::VJournal,
ICalendarComponentType::VFreebusy => SupportedComponent::VFreebusy,
ICalendarComponentType::VTimezone => SupportedComponent::VTimezone,
ICalendarComponentType::VAlarm => SupportedComponent::VAlarm,
ICalendarComponentType::Standard => SupportedComponent::Standard,
ICalendarComponentType::Daylight => SupportedComponent::Daylight,
ICalendarComponentType::VAvailability => SupportedComponent::VAvailability,
ICalendarComponentType::Available => SupportedComponent::Available,
ICalendarComponentType::Participant => SupportedComponent::Participant,
ICalendarComponentType::VLocation => SupportedComponent::VLocation,
ICalendarComponentType::VResource => SupportedComponent::VResource,
ICalendarComponentType::VStatus => SupportedComponent::VStatus,
_ => SupportedComponent::Other,
}
}
}
impl From<SupportedComponent> for ICalendarComponentType {
fn from(value: SupportedComponent) -> Self {
match value {
SupportedComponent::VCalendar => ICalendarComponentType::VCalendar,
SupportedComponent::VEvent => ICalendarComponentType::VEvent,
SupportedComponent::VTodo => ICalendarComponentType::VTodo,
SupportedComponent::VJournal => ICalendarComponentType::VJournal,
SupportedComponent::VFreebusy => ICalendarComponentType::VFreebusy,
SupportedComponent::VTimezone => ICalendarComponentType::VTimezone,
SupportedComponent::VAlarm => ICalendarComponentType::VAlarm,
SupportedComponent::Standard => ICalendarComponentType::Standard,
SupportedComponent::Daylight => ICalendarComponentType::Daylight,
SupportedComponent::VAvailability => ICalendarComponentType::VAvailability,
SupportedComponent::Available => ICalendarComponentType::Available,
SupportedComponent::Participant => ICalendarComponentType::Participant,
SupportedComponent::VLocation => ICalendarComponentType::VLocation,
SupportedComponent::VResource => ICalendarComponentType::VResource,
SupportedComponent::VStatus => ICalendarComponentType::VStatus,
SupportedComponent::Other => ICalendarComponentType::Other(Default::default()),
}
}
}

View file

@ -4,17 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use aes_gcm::{
Aes128Gcm, Nonce,
aead::{Aead, generic_array::GenericArray},
};
use aes_gcm::{Aes128Gcm, Nonce, aead::Aead};
use hkdf::Hkdf;
use p256::{
PublicKey,
ecdh::EphemeralSecret,
elliptic_curve::{rand_core::OsRng, sec1::ToEncodedPoint},
};
use sha2::Sha256;
use sha2::{Sha256, digest::generic_array::GenericArray};
use store::rand::Rng;
/*

View file

@ -141,6 +141,10 @@ impl<T: BitmapItem> Bitmap<T> {
_state: std::marker::PhantomData,
}
}
pub fn into_inner(self) -> u64 {
self.bitmap
}
}
impl<T: BitmapItem> From<ArchivedBitmap<T>> for Bitmap<T> {

View file

@ -171,7 +171,6 @@ pub async fn test(test: &WebDavTest) {
[
("D:displayname", "Named Events 2"),
("A:calendar-description", ""),
("A:supported-calendar-component-set", ""),
],
)
.await
@ -181,6 +180,61 @@ pub async fn test(test: &WebDavTest) {
"A:mkcalendar-response.D:propstat.D:status",
["HTTP/1.1 200 OK"],
);
client
.mkcol(
"MKCALENDAR",
"/dav/cal/john/my-named-events3",
[],
[
("D:displayname", "Named Events 3"),
(
"A:supported-calendar-component-set",
"<A:comp name=\"VEVENT\"/><A:comp name=\"VTODO\"/>",
),
],
)
.await
.with_status(StatusCode::CREATED)
.with_value("A:mkcalendar-response.D:propstat.D:prop.D:displayname", "")
.with_values(
"A:mkcalendar-response.D:propstat.D:status",
["HTTP/1.1 200 OK"],
);
// Check the properties of the created calendars
client
.propfind(
"/dav/cal/john/my-named-events2/",
["A:supported-calendar-component-set"],
)
.await
.properties("/dav/cal/john/my-named-events2/")
.get("A:supported-calendar-component-set")
.with_status(StatusCode::OK)
.with_values([
"A:comp.[name]:VJOURNAL",
"A:comp.[name]:VTIMEZONE",
"A:comp.[name]:VAVAILABILITY",
"A:comp.[name]:VALARM",
"A:comp.[name]:VRESOURCE",
"A:comp.[name]:AVAILABLE",
"A:comp.[name]:VTODO",
"A:comp.[name]:VFREEBUSY",
"A:comp.[name]:VEVENT",
"A:comp.[name]:STANDARD",
"A:comp.[name]:DAYLIGHT",
"A:comp.[name]:VLOCATION",
"A:comp.[name]:PARTICIPANT",
]);
client
.propfind(
"/dav/cal/john/my-named-events3/",
["A:supported-calendar-component-set"],
)
.await
.properties("/dav/cal/john/my-named-events3/")
.get("A:supported-calendar-component-set")
.with_status(StatusCode::OK)
.with_values(["A:comp.[name]:VEVENT", "A:comp.[name]:VTODO"]);
// Delete everything
for path in [
@ -191,6 +245,7 @@ pub async fn test(test: &WebDavTest) {
"/dav/card/john/my-named-cards",
"/dav/cal/john/my-named-events",
"/dav/cal/john/my-named-events2",
"/dav/cal/john/my-named-events3",
] {
client
.request("DELETE", path, "")