From 8ed4db58245ccecb6687960a3d79f1dd9f810f8e Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:59:00 +0100 Subject: [PATCH 1/9] work on new comp-filter implementation --- .../report/calendar_query/comp_filter.rs | 106 ++++++++++++++++++ .../methods/report/calendar_query/mod.rs | 1 + 2 files changed, 107 insertions(+) create mode 100644 crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs new file mode 100644 index 0000000..1ed53bc --- /dev/null +++ b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs @@ -0,0 +1,106 @@ +use ical::generator::IcalEvent; +use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType}; + +use crate::calendar::methods::report::calendar_query::{ + CompFilterElement, PropFilterElement, TimeRangeElement, +}; + +pub trait CompFilterable { + fn get_comp_name(&self) -> &'static str; + + fn match_time_range(&self, time_range: &TimeRangeElement) -> bool; + + fn match_prop_filter(&self, prop_filter: &PropFilterElement) -> bool; + + fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool; + + fn matches(&self, comp_filter: &CompFilterElement) -> bool { + let name_matches = self.get_comp_name() != comp_filter.name; + match (comp_filter.is_not_defined.is_some(), name_matches) { + // We are the component that's not supposed to be defined + (true, true) => return false, + // We shall not be and indeed we aren't + (true, false) => return true, + // We don't match + (false, false) => return false, + _ => {} + } + + if let Some(time_range) = comp_filter.time_range.as_ref() + && !self.match_time_range(time_range) + { + return false; + } + + for prop_filter in &comp_filter.prop_filter { + if !self.match_prop_filter(prop_filter) { + return false; + } + } + + // let subcomponents = self.get_subcomponents(); + // for sub_comp_filter in &comp_filter.comp_filter { + // if sub_comp_filter.is_not_defined.is_some() { + // // If is_not_defined: Filter shuold match for all + // // Confusing logic but matching also means not being the component that + // // shouldn't be defined + // if subcomponents + // .iter() + // .any(|sub| !sub.matches(sub_comp_filter)) + // { + // return false; + // } + // } else { + // // otherwise if no component matches return false + // if !subcomponents.iter().any(|sub| sub.matches(sub_comp_filter)) { + // return false; + // } + // } + // } + + comp_filter + .comp_filter + .iter() + .all(|filter| self.match_subcomponents(filter)) + } +} + +impl CompFilterable for CalendarObject { + fn get_comp_name(&self) -> &'static str { + "VCALENDAR" + } + + fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool { + false + } + + fn match_prop_filter(&self, _prop_filter: &PropFilterElement) -> bool { + // TODO + true + } + + fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool { + self.get_data().matches(comp_filter) + } +} + +impl CompFilterable for CalendarObjectComponent { + fn get_comp_name(&self) -> &'static str { + CalendarObjectType::from(self).as_str() + } + + fn match_time_range(&self, time_range: &TimeRangeElement) -> bool { + // TODO + true + } + + fn match_prop_filter(&self, _prop_filter: &PropFilterElement) -> bool { + // TODO + true + } + + fn match_subcomponents(&self, _comp_filter: &CompFilterElement) -> bool { + // TODO: Properly check subcomponents + true + } +} diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs b/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs index 302f33d..22b34a0 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs @@ -4,6 +4,7 @@ use rustical_store::CalendarStore; mod elements; pub(crate) use elements::*; +mod comp_filter; pub async fn get_objects_calendar_query( cal_query: &CalendarQueryRequest, From 0c0be859f95cff3e6b5dbaa335f14b04da93bf26 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:00:13 +0100 Subject: [PATCH 2/9] calendar object: Move occurence methods to CalendarObjectComponent and add get_property method --- crates/ical/src/icalendar/object.rs | 59 ++++++++++++++++++----------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/crates/ical/src/icalendar/object.rs b/crates/ical/src/icalendar/object.rs index d31a0a0..d720d20 100644 --- a/crates/ical/src/icalendar/object.rs +++ b/crates/ical/src/icalendar/object.rs @@ -148,6 +148,34 @@ impl CalendarObjectComponent { } Ok(Self::Journal(main_journal, overrides)) } + + pub fn get_first_occurence(&self) -> Result, Error> { + match &self { + Self::Event(main_event, overrides) => Ok(overrides + .iter() + .chain(std::iter::once(main_event)) + .map(super::event::EventObject::get_dtstart) + .collect::, _>>()? + .into_iter() + .flatten() + .min()), + _ => Ok(None), + } + } + + pub fn get_last_occurence(&self) -> Result, Error> { + match &self { + Self::Event(main_event, overrides) => Ok(overrides + .iter() + .chain(std::iter::once(main_event)) + .map(super::event::EventObject::get_last_occurence) + .collect::, _>>()? + .into_iter() + .flatten() + .max()), + _ => Ok(None), + } + } } #[derive(Debug, Clone)] @@ -268,31 +296,11 @@ impl CalendarObject { } pub fn get_first_occurence(&self) -> Result, Error> { - match &self.data { - CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides - .iter() - .chain(std::iter::once(main_event)) - .map(super::event::EventObject::get_dtstart) - .collect::, _>>()? - .into_iter() - .flatten() - .min()), - _ => Ok(None), - } + self.data.get_first_occurence() } pub fn get_last_occurence(&self) -> Result, Error> { - match &self.data { - CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides - .iter() - .chain(std::iter::once(main_event)) - .map(super::event::EventObject::get_last_occurence) - .collect::, _>>()? - .into_iter() - .flatten() - .max()), - _ => Ok(None), - } + self.data.get_last_occurence() } pub fn expand_recurrence( @@ -313,4 +321,11 @@ impl CalendarObject { _ => Ok(self.get_ics().to_string()), } } + + #[must_use] + pub fn get_property(&self, name: &str) -> Option<&Property> { + self.properties + .iter() + .find(|property| property.name == name) + } } From 6361907152d2cc74c66d82bac3e42f480cd39f6a Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:00:53 +0100 Subject: [PATCH 3/9] re-implement comp-filter and add property filtering --- .../report/calendar_query/comp_filter.rs | 265 +++++++++++++++--- .../methods/report/calendar_query/elements.rs | 107 +------ .../methods/report/calendar_query/mod.rs | 19 +- .../report/calendar_query/prop_filter.rs | 123 ++++++++ 4 files changed, 366 insertions(+), 148 deletions(-) create mode 100644 crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs index 1ed53bc..f716c60 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs @@ -1,28 +1,49 @@ -use ical::generator::IcalEvent; -use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType}; - use crate::calendar::methods::report::calendar_query::{ - CompFilterElement, PropFilterElement, TimeRangeElement, + TimeRangeElement, + prop_filter::{PropFilterElement, PropFilterable}, }; +use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType}; +use rustical_xml::XmlDeserialize; -pub trait CompFilterable { +#[derive(XmlDeserialize, Clone, Debug, PartialEq)] +#[allow(dead_code)] +// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1 +pub struct CompFilterElement { + #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] + pub(crate) is_not_defined: Option<()>, + #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] + pub(crate) time_range: Option, + #[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)] + pub(crate) prop_filter: Vec, + #[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)] + pub(crate) comp_filter: Vec, + + #[xml(ty = "attr")] + pub(crate) name: String, +} + +pub trait CompFilterable: PropFilterable + Sized { fn get_comp_name(&self) -> &'static str; fn match_time_range(&self, time_range: &TimeRangeElement) -> bool; - fn match_prop_filter(&self, prop_filter: &PropFilterElement) -> bool; - fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool; + // https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1 + // The scope of the + // CALDAV:comp-filter XML element is the calendar object when used as + // a child of the CALDAV:filter XML element. The scope of the + // CALDAV:comp-filter XML element is the enclosing calendar component + // when used as a child of another CALDAV:comp-filter XML element fn matches(&self, comp_filter: &CompFilterElement) -> bool { - let name_matches = self.get_comp_name() != comp_filter.name; + let name_matches = self.get_comp_name() == comp_filter.name; match (comp_filter.is_not_defined.is_some(), name_matches) { // We are the component that's not supposed to be defined - (true, true) => return false, + (true, true) + // We don't match + | (false, false) => return false, // We shall not be and indeed we aren't (true, false) => return true, - // We don't match - (false, false) => return false, _ => {} } @@ -33,31 +54,11 @@ pub trait CompFilterable { } for prop_filter in &comp_filter.prop_filter { - if !self.match_prop_filter(prop_filter) { + if !prop_filter.match_component(self) { return false; } } - // let subcomponents = self.get_subcomponents(); - // for sub_comp_filter in &comp_filter.comp_filter { - // if sub_comp_filter.is_not_defined.is_some() { - // // If is_not_defined: Filter shuold match for all - // // Confusing logic but matching also means not being the component that - // // shouldn't be defined - // if subcomponents - // .iter() - // .any(|sub| !sub.matches(sub_comp_filter)) - // { - // return false; - // } - // } else { - // // otherwise if no component matches return false - // if !subcomponents.iter().any(|sub| sub.matches(sub_comp_filter)) { - // return false; - // } - // } - // } - comp_filter .comp_filter .iter() @@ -71,14 +72,10 @@ impl CompFilterable for CalendarObject { } fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool { + // VCALENDAR has no concept of time range false } - fn match_prop_filter(&self, _prop_filter: &PropFilterElement) -> bool { - // TODO - true - } - fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool { self.get_data().matches(comp_filter) } @@ -90,12 +87,18 @@ impl CompFilterable for CalendarObjectComponent { } fn match_time_range(&self, time_range: &TimeRangeElement) -> bool { - // TODO - true - } - - fn match_prop_filter(&self, _prop_filter: &PropFilterElement) -> bool { - // TODO + if let Some(start) = &time_range.start + && let Some(last_occurence) = self.get_last_occurence().unwrap_or(None) + && **start > last_occurence.utc() + { + return false; + } + if let Some(end) = &time_range.end + && let Some(first_occurence) = self.get_first_occurence().unwrap_or(None) + && **end < first_occurence.utc() + { + return false; + } true } @@ -104,3 +107,179 @@ impl CompFilterable for CalendarObjectComponent { true } } + +#[cfg(test)] +mod tests { + use chrono::{TimeZone, Utc}; + use rustical_ical::{CalendarObject, UtcDateTime}; + + use crate::calendar::methods::report::calendar_query::{ + CompFilterable, TextMatchElement, TimeRangeElement, comp_filter::CompFilterElement, + prop_filter::PropFilterElement, + }; + + const ICS: &str = r"BEGIN:VCALENDAR +CALSCALE:GREGORIAN +VERSION:2.0 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +END:VTIMEZONE + +BEGIN:VEVENT +UID:318ec6503573d9576818daf93dac07317058d95c +DTSTAMP:20250502T132758Z +DTSTART;TZID=Europe/Berlin:20250506T090000 +DTEND;TZID=Europe/Berlin:20250506T092500 +SEQUENCE:2 +SUMMARY:weekly stuff +TRANSP:OPAQUE +RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU +END:VEVENT +END:VCALENDAR"; + + #[test] + fn test_comp_filter_matching() { + let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap(); + + let comp_filter = CompFilterElement { + is_not_defined: Some(()), + name: "VCALENDAR".to_string(), + time_range: None, + prop_filter: vec![], + comp_filter: vec![], + }; + assert!(!object.matches(&comp_filter), "filter: wants no VCALENDAR"); + + let comp_filter = CompFilterElement { + is_not_defined: None, + name: "VCALENDAR".to_string(), + time_range: None, + prop_filter: vec![], + comp_filter: vec![CompFilterElement { + name: "VTODO".to_string(), + is_not_defined: None, + time_range: None, + prop_filter: vec![], + comp_filter: vec![], + }], + }; + assert!(!object.matches(&comp_filter), "filter matches VTODO"); + + let comp_filter = CompFilterElement { + is_not_defined: None, + name: "VCALENDAR".to_string(), + time_range: None, + prop_filter: vec![], + comp_filter: vec![CompFilterElement { + name: "VEVENT".to_string(), + is_not_defined: None, + time_range: None, + prop_filter: vec![], + comp_filter: vec![], + }], + }; + assert!(object.matches(&comp_filter), "filter matches VEVENT"); + + let comp_filter = CompFilterElement { + is_not_defined: None, + name: "VCALENDAR".to_string(), + time_range: None, + prop_filter: vec![ + PropFilterElement { + is_not_defined: None, + name: "VERSION".to_string(), + time_range: None, + text_match: Some(TextMatchElement { + needle: "2.0".to_string(), + collation: None, + negate_condition: None, + }), + param_filter: vec![], + }, + PropFilterElement { + is_not_defined: Some(()), + name: "STUFF".to_string(), + time_range: None, + text_match: None, + param_filter: vec![], + }, + ], + comp_filter: vec![CompFilterElement { + name: "VEVENT".to_string(), + is_not_defined: None, + time_range: None, + prop_filter: vec![PropFilterElement { + is_not_defined: None, + name: "SUMMARY".to_string(), + time_range: None, + text_match: Some(TextMatchElement { + collation: None, + negate_condition: None, + needle: "weekly".to_string(), + }), + param_filter: vec![], + }], + comp_filter: vec![], + }], + }; + assert!( + object.matches(&comp_filter), + "Some prop filters on VCALENDAR and VEVENT" + ); + } + #[test] + fn test_comp_filter_time_range() { + let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap(); + + let comp_filter = CompFilterElement { + is_not_defined: None, + name: "VCALENDAR".to_string(), + time_range: None, + prop_filter: vec![], + comp_filter: vec![CompFilterElement { + name: "VEVENT".to_string(), + is_not_defined: None, + time_range: Some(TimeRangeElement { + start: Some(UtcDateTime( + Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap(), + )), + end: Some(UtcDateTime( + Utc.with_ymd_and_hms(2025, 8, 1, 0, 0, 0).unwrap(), + )), + }), + prop_filter: vec![], + comp_filter: vec![], + }], + }; + assert!( + object.matches(&comp_filter), + "event should lie in time range" + ); + + let comp_filter = CompFilterElement { + is_not_defined: None, + name: "VCALENDAR".to_string(), + time_range: None, + prop_filter: vec![], + comp_filter: vec![CompFilterElement { + name: "VEVENT".to_string(), + is_not_defined: None, + time_range: Some(TimeRangeElement { + start: Some(UtcDateTime( + Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap(), + )), + end: Some(UtcDateTime( + Utc.with_ymd_and_hms(2024, 8, 1, 0, 0, 0).unwrap(), + )), + }), + prop_filter: vec![], + comp_filter: vec![], + }], + }; + assert!( + !object.matches(&comp_filter), + "event should not lie in time range" + ); + } +} diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs b/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs index 37a8648..fa86b0c 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs @@ -1,4 +1,7 @@ -use crate::calendar_object::CalendarObjectPropWrapperName; +use crate::{ + calendar::methods::report::calendar_query::comp_filter::{CompFilterElement, CompFilterable}, + calendar_object::CalendarObjectPropWrapperName, +}; use rustical_dav::xml::PropfindType; use rustical_ical::{CalendarObject, UtcDateTime}; use rustical_store::calendar_store::CalendarQuery; @@ -30,106 +33,12 @@ pub struct ParamFilterElement { #[allow(dead_code)] pub struct TextMatchElement { #[xml(ty = "attr")] - pub(crate) collation: String, + pub(crate) collation: Option, #[xml(ty = "attr")] // "yes" or "no", default: "no" pub(crate) negate_condition: Option, -} - -#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)] -#[allow(dead_code)] -// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2 -pub struct PropFilterElement { - #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] - pub(crate) is_not_defined: Option<()>, - #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] - pub(crate) time_range: Option, - #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] - pub(crate) text_match: Option, - #[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)] - pub(crate) param_filter: Vec, - - #[xml(ty = "attr")] - pub(crate) name: String, -} - -#[derive(XmlDeserialize, Clone, Debug, PartialEq)] -#[allow(dead_code)] -// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1 -pub struct CompFilterElement { - #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] - pub(crate) is_not_defined: Option<()>, - #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] - pub(crate) time_range: Option, - #[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)] - pub(crate) prop_filter: Vec, - #[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)] - pub(crate) comp_filter: Vec, - - #[xml(ty = "attr")] - pub(crate) name: String, -} - -impl CompFilterElement { - // match the VCALENDAR part - pub fn matches_root(&self, cal_object: &CalendarObject) -> bool { - let comp_vcal = self.name == "VCALENDAR"; - match (self.is_not_defined, comp_vcal) { - // Client wants VCALENDAR to not exist but we are a VCALENDAR - (Some(()), true) | - // Client is asking for something different than a vcalendar - (None, false) => return false, - _ => {} - } - - if self.time_range.is_some() { - // should be applied on VEVENT/VTODO but not on VCALENDAR - return false; - } - - // TODO: Implement prop-filter at some point - - // Apply sub-comp-filters on VEVENT/VTODO/VJOURNAL component - if self - .comp_filter - .iter() - .all(|filter| filter.matches(cal_object)) - { - return true; - } - - false - } - - // match the VEVENT/VTODO/VJOURNAL part - pub fn matches(&self, cal_object: &CalendarObject) -> bool { - let comp_name_matches = self.name == cal_object.get_component_name(); - match (self.is_not_defined, comp_name_matches) { - // Client wants VCALENDAR to not exist but we are a VCALENDAR - (Some(()), true) | - // Client is asking for something different than a vcalendar - (None, false) => return false, - _ => {} - } - - // TODO: Implement prop-filter (and comp-filter?) at some point - - if let Some(time_range) = &self.time_range { - if let Some(start) = &time_range.start - && let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) - && **start > last_occurence.utc() - { - return false; - } - if let Some(end) = &time_range.end - && let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None) - && **end < first_occurence.utc() - { - return false; - } - } - true - } + #[xml(ty = "text")] + pub(crate) needle: String, } #[derive(XmlDeserialize, Clone, Debug, PartialEq)] @@ -143,7 +52,7 @@ pub struct FilterElement { impl FilterElement { pub fn matches(&self, cal_object: &CalendarObject) -> bool { - self.comp_filter.matches_root(cal_object) + cal_object.matches(&self.comp_filter) } } diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs b/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs index 248e47c..16c6e59 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs @@ -4,7 +4,12 @@ use rustical_store::CalendarStore; mod comp_filter; mod elements; +mod prop_filter; +#[allow(unused_imports)] +pub use comp_filter::{CompFilterElement, CompFilterable}; pub use elements::*; +#[allow(unused_imports)] +pub use prop_filter::{PropFilterElement, PropFilterable}; pub async fn get_objects_calendar_query( cal_query: &CalendarQueryRequest, @@ -30,8 +35,8 @@ mod tests { calendar::methods::report::{ ReportRequest, calendar_query::{ - CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement, - PropFilterElement, TextMatchElement, + CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement, + comp_filter::CompFilterElement, prop_filter::PropFilterElement, }, }, calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName}, @@ -91,16 +96,18 @@ mod tests { prop_filter: vec![PropFilterElement { name: "ATTENDEE".to_owned(), text_match: Some(TextMatchElement { - collation: "i;ascii-casemap".to_owned(), - negate_condition: None + collation: Some("i;ascii-casemap".to_owned()), + negate_condition: None, + needle: "mailto:lisa@example.com".to_string() }), is_not_defined: None, param_filter: vec![ParamFilterElement { is_not_defined: None, name: "PARTSTAT".to_owned(), text_match: Some(TextMatchElement { - collation: "i;ascii-casemap".to_owned(), - negate_condition: None + collation: Some("i;ascii-casemap".to_owned()), + negate_condition: None, + needle: "NEEDS-ACTION".to_string() }), }], time_range: None diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs new file mode 100644 index 0000000..c431496 --- /dev/null +++ b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs @@ -0,0 +1,123 @@ +use ical::{ + generator::{IcalCalendar, IcalEvent}, + parser::{ + Component, + ical::component::{IcalJournal, IcalTodo}, + }, + property::Property, +}; +use rustical_ical::{CalendarObject, CalendarObjectComponent}; +use rustical_xml::XmlDeserialize; + +use crate::calendar::methods::report::calendar_query::{ + ParamFilterElement, TextMatchElement, TimeRangeElement, +}; + +#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)] +#[allow(dead_code)] +// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2 +pub struct PropFilterElement { + #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] + pub(crate) is_not_defined: Option<()>, + #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] + pub(crate) time_range: Option, + #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] + pub(crate) text_match: Option, + #[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)] + pub(crate) param_filter: Vec, + + #[xml(ty = "attr")] + pub(crate) name: String, +} + +impl PropFilterElement { + pub fn match_component(&self, comp: &impl PropFilterable) -> bool { + let property = comp.get_property(&self.name); + let property = match (self.is_not_defined.is_some(), property) { + // We are the component that's not supposed to be defined + (true, Some(_)) + // We don't match + | (false, None) => return false, + // We shall not be and indeed we aren't + (true, None) => return true, + (false, Some(property)) => property + }; + + if let Some(_time_range) = &self.time_range { + // TODO: implement + return true; + } + + if let Some(TextMatchElement { + collation: _collation, + negate_condition, + needle, + }) = &self.text_match + { + let mut matches = property + .value + .as_ref() + .is_some_and(|haystack| haystack.contains(needle)); + match negate_condition.as_deref() { + None | Some("no") => {} + Some("yes") => { + matches = !matches; + } + // Invalid value + _ => return false, + } + + if !matches { + return false; + } + } + + // TODO: param-filter + + true + } +} + +pub trait PropFilterable { + fn get_property(&self, name: &str) -> Option<&Property>; +} + +impl PropFilterable for CalendarObject { + fn get_property(&self, name: &str) -> Option<&Property> { + Self::get_property(self, name) + } +} + +impl PropFilterable for IcalEvent { + fn get_property(&self, name: &str) -> Option<&Property> { + Component::get_property(self, name) + } +} + +impl PropFilterable for IcalTodo { + fn get_property(&self, name: &str) -> Option<&Property> { + Component::get_property(self, name) + } +} + +impl PropFilterable for IcalJournal { + fn get_property(&self, name: &str) -> Option<&Property> { + Component::get_property(self, name) + } +} + +impl PropFilterable for IcalCalendar { + fn get_property(&self, name: &str) -> Option<&Property> { + Component::get_property(self, name) + } +} + +impl PropFilterable for CalendarObjectComponent { + fn get_property(&self, name: &str) -> Option<&Property> { + match self { + Self::Event(event, _) => PropFilterable::get_property(&event.event, name), + Self::Todo(todo, _) => PropFilterable::get_property(todo, name), + Self::Journal(journal, _) => PropFilterable::get_property(journal, name), + } + } +} From 9f911fe5d78f35da5cf1fc40152ce9c9aac06c4e Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 2 Nov 2025 15:09:31 +0100 Subject: [PATCH 4/9] prop-filter: Add time-range checking --- .../report/calendar_query/prop_filter.rs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs index c431496..8cf2545 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use ical::{ generator::{IcalCalendar, IcalEvent}, parser::{ @@ -6,7 +8,7 @@ use ical::{ }, property::Property, }; -use rustical_ical::{CalendarObject, CalendarObjectComponent}; +use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectComponent, UtcDateTime}; use rustical_xml::XmlDeserialize; use crate::calendar::methods::report::calendar_query::{ @@ -43,8 +45,22 @@ impl PropFilterElement { (false, Some(property)) => property }; - if let Some(_time_range) = &self.time_range { - // TODO: implement + if let Some(TimeRangeElement { start, end }) = &self.time_range { + // TODO: Respect timezones + let Ok(timestamp) = CalDateTime::parse_prop(property, &HashMap::default()) else { + return false; + }; + let timestamp = timestamp.utc(); + if let Some(UtcDateTime(start)) = start + && start > ×tamp + { + return false; + } + if let Some(UtcDateTime(end)) = end + && end < ×tamp + { + return false; + } return true; } From cd9993cd979b08f6347c2d7fb0edd9c9c7c07df6 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:21:44 +0100 Subject: [PATCH 5/9] implement comp-filter matching for VTIMEZONE --- .../report/calendar_query/comp_filter.rs | 60 ++++++++++++++++++- .../report/calendar_query/prop_filter.rs | 8 ++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs index f716c60..7cae2ce 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs @@ -2,6 +2,7 @@ use crate::calendar::methods::report::calendar_query::{ TimeRangeElement, prop_filter::{PropFilterElement, PropFilterable}, }; +use ical::parser::ical::component::IcalTimeZone; use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType}; use rustical_xml::XmlDeserialize; @@ -77,7 +78,31 @@ impl CompFilterable for CalendarObject { } fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool { - self.get_data().matches(comp_filter) + let mut matches = self + .get_vtimezones() + .values() + .map(|tz| tz.matches(comp_filter)) + .chain([self.get_data().matches(comp_filter)]); + + if comp_filter.is_not_defined.is_some() { + matches.all(|x| x) + } else { + matches.any(|x| x) + } + } +} + +impl CompFilterable for IcalTimeZone { + fn get_comp_name(&self) -> &'static str { + "VTIMEZONE" + } + + fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool { + false + } + + fn match_subcomponents(&self, _comp_filter: &CompFilterElement) -> bool { + true } } @@ -282,4 +307,37 @@ END:VCALENDAR"; "event should not lie in time range" ); } + + #[test] + fn test_match_timezone() { + let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap(); + + let comp_filter = CompFilterElement { + is_not_defined: None, + name: "VCALENDAR".to_string(), + time_range: None, + prop_filter: vec![], + comp_filter: vec![CompFilterElement { + name: "VTIMEZONE".to_string(), + is_not_defined: None, + time_range: None, + prop_filter: vec![PropFilterElement { + is_not_defined: None, + name: "TZID".to_string(), + time_range: None, + text_match: Some(TextMatchElement { + collation: None, + negate_condition: None, + needle: "Europe/Berlin".to_string(), + }), + param_filter: vec![], + }], + comp_filter: vec![], + }], + }; + assert!( + object.matches(&comp_filter), + "Timezone should be Europe/Berlin" + ); + } } diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs index 8cf2545..c90139a 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs @@ -4,7 +4,7 @@ use ical::{ generator::{IcalCalendar, IcalEvent}, parser::{ Component, - ical::component::{IcalJournal, IcalTodo}, + ical::component::{IcalJournal, IcalTimeZone, IcalTodo}, }, property::Property, }; @@ -128,6 +128,12 @@ impl PropFilterable for IcalCalendar { } } +impl PropFilterable for IcalTimeZone { + fn get_property(&self, name: &str) -> Option<&Property> { + Component::get_property(self, name) + } +} + impl PropFilterable for CalendarObjectComponent { fn get_property(&self, name: &str) -> Option<&Property> { match self { From 32f43951acdab3129e8391879c53c819fe37c860 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 2 Nov 2025 17:48:35 +0100 Subject: [PATCH 6/9] refactor text-match to support collations --- .../report/calendar_query/comp_filter.rs | 16 ++-- .../methods/report/calendar_query/elements.rs | 17 +--- .../methods/report/calendar_query/mod.rs | 15 ++- .../report/calendar_query/prop_filter.rs | 24 +---- .../report/calendar_query/text_match.rs | 92 +++++++++++++++++++ 5 files changed, 118 insertions(+), 46 deletions(-) create mode 100644 crates/caldav/src/calendar/methods/report/calendar_query/text_match.rs diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs index 7cae2ce..51f6bcf 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/comp_filter.rs @@ -139,8 +139,10 @@ mod tests { use rustical_ical::{CalendarObject, UtcDateTime}; use crate::calendar::methods::report::calendar_query::{ - CompFilterable, TextMatchElement, TimeRangeElement, comp_filter::CompFilterElement, + CompFilterable, TextMatchElement, TimeRangeElement, + comp_filter::CompFilterElement, prop_filter::PropFilterElement, + text_match::{NegateCondition, TextCollation}, }; const ICS: &str = r"BEGIN:VCALENDAR @@ -217,8 +219,8 @@ END:VCALENDAR"; time_range: None, text_match: Some(TextMatchElement { needle: "2.0".to_string(), - collation: None, - negate_condition: None, + collation: TextCollation::default(), + negate_condition: NegateCondition::default(), }), param_filter: vec![], }, @@ -239,8 +241,8 @@ END:VCALENDAR"; name: "SUMMARY".to_string(), time_range: None, text_match: Some(TextMatchElement { - collation: None, - negate_condition: None, + collation: TextCollation::default(), + negate_condition: NegateCondition(false), needle: "weekly".to_string(), }), param_filter: vec![], @@ -326,8 +328,8 @@ END:VCALENDAR"; name: "TZID".to_string(), time_range: None, text_match: Some(TextMatchElement { - collation: None, - negate_condition: None, + collation: TextCollation::AsciiCasemap, + negate_condition: NegateCondition::default(), needle: "Europe/Berlin".to_string(), }), param_filter: vec![], diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs b/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs index fa86b0c..0404394 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs @@ -1,5 +1,8 @@ use crate::{ - calendar::methods::report::calendar_query::comp_filter::{CompFilterElement, CompFilterable}, + calendar::methods::report::calendar_query::{ + TextMatchElement, + comp_filter::{CompFilterElement, CompFilterable}, + }, calendar_object::CalendarObjectPropWrapperName, }; use rustical_dav::xml::PropfindType; @@ -29,18 +32,6 @@ pub struct ParamFilterElement { pub(crate) name: String, } -#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)] -#[allow(dead_code)] -pub struct TextMatchElement { - #[xml(ty = "attr")] - pub(crate) collation: Option, - #[xml(ty = "attr")] - // "yes" or "no", default: "no" - pub(crate) negate_condition: Option, - #[xml(ty = "text")] - pub(crate) needle: String, -} - #[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] // https://datatracker.ietf.org/doc/html/rfc4791#section-9.7 diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs b/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs index 16c6e59..9e12a3f 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/mod.rs @@ -5,11 +5,14 @@ use rustical_store::CalendarStore; mod comp_filter; mod elements; mod prop_filter; +pub mod text_match; #[allow(unused_imports)] pub use comp_filter::{CompFilterElement, CompFilterable}; pub use elements::*; #[allow(unused_imports)] pub use prop_filter::{PropFilterElement, PropFilterable}; +#[allow(unused_imports)] +pub use text_match::TextMatchElement; pub async fn get_objects_calendar_query( cal_query: &CalendarQueryRequest, @@ -36,7 +39,9 @@ mod tests { ReportRequest, calendar_query::{ CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement, - comp_filter::CompFilterElement, prop_filter::PropFilterElement, + comp_filter::CompFilterElement, + prop_filter::PropFilterElement, + text_match::{NegateCondition, TextCollation}, }, }, calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName}, @@ -96,8 +101,8 @@ mod tests { prop_filter: vec![PropFilterElement { name: "ATTENDEE".to_owned(), text_match: Some(TextMatchElement { - collation: Some("i;ascii-casemap".to_owned()), - negate_condition: None, + collation: TextCollation::AsciiCasemap, + negate_condition: NegateCondition(false), needle: "mailto:lisa@example.com".to_string() }), is_not_defined: None, @@ -105,8 +110,8 @@ mod tests { is_not_defined: None, name: "PARTSTAT".to_owned(), text_match: Some(TextMatchElement { - collation: Some("i;ascii-casemap".to_owned()), - negate_condition: None, + collation: TextCollation::AsciiCasemap, + negate_condition: NegateCondition(false), needle: "NEEDS-ACTION".to_string() }), }], diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs index c90139a..9da7480 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/prop_filter.rs @@ -64,28 +64,10 @@ impl PropFilterElement { return true; } - if let Some(TextMatchElement { - collation: _collation, - negate_condition, - needle, - }) = &self.text_match + if let Some(text_match) = &self.text_match + && !text_match.match_property(property) { - let mut matches = property - .value - .as_ref() - .is_some_and(|haystack| haystack.contains(needle)); - match negate_condition.as_deref() { - None | Some("no") => {} - Some("yes") => { - matches = !matches; - } - // Invalid value - _ => return false, - } - - if !matches { - return false; - } + return false; } // TODO: param-filter diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/text_match.rs b/crates/caldav/src/calendar/methods/report/calendar_query/text_match.rs new file mode 100644 index 0000000..b8c4f27 --- /dev/null +++ b/crates/caldav/src/calendar/methods/report/calendar_query/text_match.rs @@ -0,0 +1,92 @@ +use ical::property::Property; +use rustical_xml::{ValueDeserialize, XmlDeserialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub enum TextCollation { + #[default] + AsciiCasemap, + Octet, +} + +impl TextCollation { + // Check whether a haystack contains a needle respecting the collation + pub fn match_text(&self, needle: &str, haystack: &str) -> bool { + match self { + // https://datatracker.ietf.org/doc/html/rfc4790#section-9.2 + Self::AsciiCasemap => haystack + .to_ascii_uppercase() + .contains(&needle.to_ascii_uppercase()), + Self::Octet => haystack.contains(needle), + } + } +} + +impl ValueDeserialize for TextCollation { + fn deserialize(val: &str) -> Result { + match val { + "i;ascii-casemap" => Ok(Self::AsciiCasemap), + "i;octet" => Ok(Self::Octet), + _ => Err(rustical_xml::XmlError::InvalidVariant(format!( + "Invalid collation: {val}" + ))), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct NegateCondition(pub bool); + +impl ValueDeserialize for NegateCondition { + fn deserialize(val: &str) -> Result { + match val { + "yes" => Ok(Self(true)), + "no" => Ok(Self(false)), + _ => Err(rustical_xml::XmlError::InvalidVariant(format!( + "Invalid negate-condition parameter: {val}" + ))), + } + } +} + +#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)] +#[allow(dead_code)] +pub struct TextMatchElement { + #[xml(ty = "attr", default = "Default::default")] + pub collation: TextCollation, + #[xml(ty = "attr", default = "Default::default")] + pub(crate) negate_condition: NegateCondition, + #[xml(ty = "text")] + pub(crate) needle: String, +} + +impl TextMatchElement { + pub fn match_property(&self, property: &Property) -> bool { + let Self { + collation, + negate_condition, + needle, + } = self; + + let matches = property + .value + .as_ref() + .is_some_and(|haystack| collation.match_text(needle, haystack)); + + // XOR + negate_condition.0 ^ matches + } +} + +#[cfg(test)] +mod tests { + use crate::calendar::methods::report::calendar_query::text_match::TextCollation; + + #[test] + fn test_collation() { + assert!(TextCollation::AsciiCasemap.match_text("GrüN", "grün")); + assert!(!TextCollation::AsciiCasemap.match_text("GrÜN", "grün")); + assert!(!TextCollation::Octet.match_text("GrÜN", "grün")); + assert!(TextCollation::Octet.match_text("hallo", "hallo")); + assert!(TextCollation::AsciiCasemap.match_text("HaLlo", "hAllo")); + } +} From 167492318f7906cd76f112fa1c368e556439006f Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:33:30 +0100 Subject: [PATCH 7/9] xml: serialize: Support non-string text fields --- crates/xml/derive/src/field.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/xml/derive/src/field.rs b/crates/xml/derive/src/field.rs index f73e116..47c1d54 100644 --- a/crates/xml/derive/src/field.rs +++ b/crates/xml/derive/src/field.rs @@ -313,7 +313,7 @@ impl Field { } }), (FieldType::Text, false) => Some(quote! { - writer.write_event(Event::Text(BytesText::new(&self.#target_field_index)))?; + writer.write_event(Event::Text(BytesText::new(self.#target_field_index.as_ref())))?; }), (FieldType::Tag, true) => { let field_name = self.xml_name(); From f72559d027c3394105fc4a21d7d819d3c65310b3 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:33:54 +0100 Subject: [PATCH 8/9] caldav: Add supported-collation-set property --- .../report/calendar_query/text_match.rs | 11 +++++++++ .../caldav/src/calendar/methods/report/mod.rs | 2 +- crates/caldav/src/calendar/prop.rs | 24 +++++++++++++++++++ crates/caldav/src/calendar/resource.rs | 9 ++++++- .../src/calendar/test_files/propfind.outputs | 5 ++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/text_match.rs b/crates/caldav/src/calendar/methods/report/calendar_query/text_match.rs index b8c4f27..9d8af6f 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/text_match.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/text_match.rs @@ -10,6 +10,7 @@ pub enum TextCollation { impl TextCollation { // Check whether a haystack contains a needle respecting the collation + #[must_use] pub fn match_text(&self, needle: &str, haystack: &str) -> bool { match self { // https://datatracker.ietf.org/doc/html/rfc4790#section-9.2 @@ -21,6 +22,15 @@ impl TextCollation { } } +impl AsRef for TextCollation { + fn as_ref(&self) -> &str { + match self { + Self::AsciiCasemap => "i;ascii-casemap", + Self::Octet => "i;octet", + } + } +} + impl ValueDeserialize for TextCollation { fn deserialize(val: &str) -> Result { match val { @@ -60,6 +70,7 @@ pub struct TextMatchElement { } impl TextMatchElement { + #[must_use] pub fn match_property(&self, property: &Property) -> bool { let Self { collation, diff --git a/crates/caldav/src/calendar/methods/report/mod.rs b/crates/caldav/src/calendar/methods/report/mod.rs index 2703f01..4d7ef6b 100644 --- a/crates/caldav/src/calendar/methods/report/mod.rs +++ b/crates/caldav/src/calendar/methods/report/mod.rs @@ -27,7 +27,7 @@ use sync_collection::handle_sync_collection; use tracing::instrument; mod calendar_multiget; -mod calendar_query; +pub mod calendar_query; mod sync_collection; #[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)] diff --git a/crates/caldav/src/calendar/prop.rs b/crates/caldav/src/calendar/prop.rs index fa74bf6..cf3a0a7 100644 --- a/crates/caldav/src/calendar/prop.rs +++ b/crates/caldav/src/calendar/prop.rs @@ -3,6 +3,8 @@ use rustical_ical::CalendarObjectType; use rustical_xml::{XmlDeserialize, XmlSerialize}; use strum_macros::VariantArray; +use crate::calendar::methods::report::calendar_query::text_match::TextCollation; + #[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)] pub struct SupportedCalendarComponent { #[xml(ty = "attr")] @@ -36,6 +38,28 @@ impl From for Vec { } } +#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)] +pub struct SupportedCollation(#[xml(ty = "text")] pub TextCollation); + +#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq)] +pub struct SupportedCollationSet( + #[xml( + ns = "rustical_dav::namespace::NS_CALDAV", + flatten, + rename = "supported-collation" + )] + pub Vec, +); + +impl Default for SupportedCollationSet { + fn default() -> Self { + Self(vec![ + SupportedCollation(TextCollation::AsciiCasemap), + SupportedCollation(TextCollation::Octet), + ]) + } +} + #[derive(Debug, Clone, XmlSerialize, PartialEq, Eq)] pub struct CalendarData { #[xml(ty = "attr")] diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index bec5de3..2c891eb 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -1,6 +1,6 @@ use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData}; use crate::Error; -use crate::calendar::prop::ReportMethod; +use crate::calendar::prop::{ReportMethod, SupportedCollationSet}; use chrono::{DateTime, Utc}; use derive_more::derive::{From, Into}; use ical::IcalParser; @@ -39,6 +39,8 @@ pub enum CalendarProp { SupportedCalendarComponentSet(SupportedCalendarComponentSet), #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)] SupportedCalendarData(SupportedCalendarData), + #[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)] + SupportedCollationSet(SupportedCollationSet), #[xml(ns = "rustical_dav::namespace::NS_DAV")] MaxResourceSize(i64), #[xml(skip_deserializing)] @@ -156,6 +158,9 @@ impl Resource for CalendarResource { CalendarPropName::SupportedCalendarData => { CalendarProp::SupportedCalendarData(SupportedCalendarData::default()) } + CalendarPropName::SupportedCollationSet => { + CalendarProp::SupportedCollationSet(SupportedCollationSet::default()) + } CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10_000_000), CalendarPropName::SupportedReportSet => { CalendarProp::SupportedReportSet(SupportedReportSet::all()) @@ -244,6 +249,7 @@ impl Resource for CalendarResource { } CalendarProp::TimezoneServiceSet(_) | CalendarProp::SupportedCalendarData(_) + | CalendarProp::SupportedCollationSet(_) | CalendarProp::MaxResourceSize(_) | CalendarProp::SupportedReportSet(_) | CalendarProp::Source(_) @@ -283,6 +289,7 @@ impl Resource for CalendarResource { } CalendarPropName::TimezoneServiceSet | CalendarPropName::SupportedCalendarData + | CalendarPropName::SupportedCollationSet | CalendarPropName::MaxResourceSize | CalendarPropName::SupportedReportSet | CalendarPropName::Source diff --git a/crates/caldav/src/calendar/test_files/propfind.outputs b/crates/caldav/src/calendar/test_files/propfind.outputs index 881ee37..9330cdf 100644 --- a/crates/caldav/src/calendar/test_files/propfind.outputs +++ b/crates/caldav/src/calendar/test_files/propfind.outputs @@ -11,6 +11,7 @@ + @@ -160,6 +161,10 @@ END:VCALENDAR + + i;ascii-casemap + i;octet + 10000000 From c42f8e56145742a2c43b35266959ed162fcb22af Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:42:55 +0100 Subject: [PATCH 9/9] clippy appeasement --- .../src/calendar/methods/report/calendar_query/elements.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs b/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs index 0404394..8a8857d 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query/elements.rs @@ -42,6 +42,7 @@ pub struct FilterElement { } impl FilterElement { + #[must_use] pub fn matches(&self, cal_object: &CalendarObject) -> bool { cal_object.matches(&self.comp_filter) }