aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-05-26 11:03:39 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-05-26 11:03:39 +0200
commitd5a222967dbc774ad04cff572a0d901c832b36bf (patch)
tree52467d2f4c98e0a8551aed46ad4ab4bc0115d75e
parent6b9720844aaa86ad25a77c0821dcdbc772937065 (diff)
downloadaerogramme-d5a222967dbc774ad04cff572a0d901c832b36bf.tar.gz
aerogramme-d5a222967dbc774ad04cff572a0d901c832b36bf.zip
support multiple same name components, properties & parameters
-rw-r--r--aero-ical/src/query.rs154
-rw-r--r--aerogramme/tests/behavior.rs154
-rw-r--r--aerogramme/tests/common/constants.rs38
3 files changed, 240 insertions, 106 deletions
diff --git a/aero-ical/src/query.rs b/aero-ical/src/query.rs
index 440441f..d69a919 100644
--- a/aero-ical/src/query.rs
+++ b/aero-ical/src/query.rs
@@ -7,19 +7,18 @@ pub fn is_component_match(
filter: &cal::CompFilter,
) -> bool {
// Find the component among the list
- //@FIXME do not handle correctly multiple entities (eg. 3 VEVENT)
- let maybe_comp = components
+ let maybe_comps = components
.iter()
- .find(|candidate| candidate.name.as_str() == filter.name.as_str());
+ .filter(|candidate| candidate.name.as_str() == filter.name.as_str())
+ .collect::<Vec<_>>();
// Filter according to rules
- match (maybe_comp, &filter.additional_rules) {
- (Some(_), None) => true,
- (None, Some(cal::CompFilterRules::IsNotDefined)) => true,
- (None, None) => false,
- (Some(_), Some(cal::CompFilterRules::IsNotDefined)) => false,
- (None, Some(cal::CompFilterRules::Matches(_))) => false,
- (Some(component), Some(cal::CompFilterRules::Matches(matcher))) => {
+ match (&maybe_comps[..], &filter.additional_rules) {
+ ([_, ..], None) => true,
+ ([], Some(cal::CompFilterRules::IsNotDefined)) => true,
+ ([], None) => false,
+ ([_, ..], Some(cal::CompFilterRules::IsNotDefined)) => false,
+ (comps, Some(cal::CompFilterRules::Matches(matcher))) => comps.iter().any(|component| {
// check time range
if let Some(time_range) = &matcher.time_range {
if !is_in_time_range(
@@ -41,7 +40,7 @@ pub fn is_component_match(
matcher.comp_filter.iter().all(|inner_filter| {
is_component_match(component, component.components.as_ref(), &inner_filter)
})
- }
+ }),
}
}
@@ -71,80 +70,89 @@ fn prop_parse<T: std::str::FromStr>(
fn is_properties_match(props: &[icalendar::parser::Property], filters: &[cal::PropFilter]) -> bool {
filters.iter().all(|single_filter| {
// Find the property
- let single_prop = props
+ let candidate_props = props
.iter()
- .find(|candidate| candidate.name.as_str() == single_filter.name.0.as_str());
- match (&single_filter.additional_rules, single_prop) {
- (None, Some(_)) | (Some(cal::PropFilterRules::IsNotDefined), None) => true,
- (None, None)
- | (Some(cal::PropFilterRules::IsNotDefined), Some(_))
- | (Some(cal::PropFilterRules::Match(_)), None) => false,
- (Some(cal::PropFilterRules::Match(pattern)), Some(prop)) => {
- // check value
- match &pattern.time_or_text {
- Some(cal::TimeOrText::Time(time_range)) => {
- let maybe_parsed_date = parser::date_time(prop.val.as_str());
+ .filter(|candidate| candidate.name.as_str() == single_filter.name.0.as_str())
+ .collect::<Vec<_>>();
+
+ match (&single_filter.additional_rules, &candidate_props[..]) {
+ (None, [_, ..]) | (Some(cal::PropFilterRules::IsNotDefined), []) => true,
+ (None, []) | (Some(cal::PropFilterRules::IsNotDefined), [_, ..]) => false,
+ (Some(cal::PropFilterRules::Match(pattern)), multi_props) => {
+ multi_props.iter().any(|prop| {
+ // check value
+ match &pattern.time_or_text {
+ Some(cal::TimeOrText::Time(time_range)) => {
+ let maybe_parsed_date = parser::date_time(prop.val.as_str());
- let parsed_date = match maybe_parsed_date {
- None => return false,
- Some(v) => v,
- };
+ let parsed_date = match maybe_parsed_date {
+ None => return false,
+ Some(v) => v,
+ };
- // see if entry is in range
- let is_in_range = match time_range {
- cal::TimeRange::OnlyStart(after) => &parsed_date >= after,
- cal::TimeRange::OnlyEnd(before) => &parsed_date <= before,
- cal::TimeRange::FullRange(after, before) => {
- &parsed_date >= after && &parsed_date <= before
+ // see if entry is in range
+ let is_in_range = match time_range {
+ cal::TimeRange::OnlyStart(after) => &parsed_date >= after,
+ cal::TimeRange::OnlyEnd(before) => &parsed_date <= before,
+ cal::TimeRange::FullRange(after, before) => {
+ &parsed_date >= after && &parsed_date <= before
+ }
+ };
+ if !is_in_range {
+ return false;
}
- };
- if !is_in_range {
- return false;
- }
- // if you are here, this subcondition is valid
- }
- Some(cal::TimeOrText::Text(txt_match)) => {
- //@FIXME ignoring collation
- let is_match = match txt_match.negate_condition {
- None | Some(false) => {
- prop.val.as_str().contains(txt_match.text.as_str())
+ // if you are here, this subcondition is valid
+ }
+ Some(cal::TimeOrText::Text(txt_match)) => {
+ //@FIXME ignoring collation
+ let is_match = match txt_match.negate_condition {
+ None | Some(false) => {
+ prop.val.as_str().contains(txt_match.text.as_str())
+ }
+ Some(true) => !prop.val.as_str().contains(txt_match.text.as_str()),
+ };
+ if !is_match {
+ return false;
}
- Some(true) => !prop.val.as_str().contains(txt_match.text.as_str()),
- };
- if !is_match {
- return false;
}
- }
- None => (), // if not filter on value is set, continue
- };
+ None => (), // if not filter on value is set, continue
+ };
- // check parameters
- pattern.param_filter.iter().all(|single_param_filter| {
- let maybe_param = prop.params.iter().find(|candidate| {
- candidate.key.as_str() == single_param_filter.name.as_str()
- });
+ // check parameters
+ pattern.param_filter.iter().all(|single_param_filter| {
+ let multi_param = prop
+ .params
+ .iter()
+ .filter(|candidate| {
+ candidate.key.as_str() == single_param_filter.name.as_str()
+ })
+ .collect::<Vec<_>>();
- match (maybe_param, &single_param_filter.additional_rules) {
- (Some(_), None) => true,
- (None, None) => false,
- (Some(_), Some(cal::ParamFilterMatch::IsNotDefined)) => false,
- (None, Some(cal::ParamFilterMatch::IsNotDefined)) => true,
- (None, Some(cal::ParamFilterMatch::Match(_))) => false,
- (Some(param), Some(cal::ParamFilterMatch::Match(txt_match))) => {
- let param_val = match &param.val {
- Some(v) => v,
- None => return false,
- };
+ match (&multi_param[..], &single_param_filter.additional_rules) {
+ ([.., _], None) => true,
+ ([], None) => false,
+ ([.., _], Some(cal::ParamFilterMatch::IsNotDefined)) => false,
+ ([], Some(cal::ParamFilterMatch::IsNotDefined)) => true,
+ (many_params, Some(cal::ParamFilterMatch::Match(txt_match))) => {
+ many_params.iter().any(|param| {
+ let param_val = match &param.val {
+ Some(v) => v,
+ None => return false,
+ };
- match txt_match.negate_condition {
- None | Some(false) => {
- param_val.as_str().contains(txt_match.text.as_str())
- }
- Some(true) => !param_val.as_str().contains(txt_match.text.as_str()),
+ match txt_match.negate_condition {
+ None | Some(false) => {
+ param_val.as_str().contains(txt_match.text.as_str())
+ }
+ Some(true) => {
+ !param_val.as_str().contains(txt_match.text.as_str())
+ }
+ }
+ })
}
}
- }
+ })
})
}
}
diff --git a/aerogramme/tests/behavior.rs b/aerogramme/tests/behavior.rs
index d6c73e3..ef58182 100644
--- a/aerogramme/tests/behavior.rs
+++ b/aerogramme/tests/behavior.rs
@@ -554,7 +554,7 @@ fn rfc4791_webdav_caldav() {
println!("🧪 rfc4791_webdav_caldav");
common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| {
// --- INITIAL TEST SETUP ---
- // Add entries (3 VEVENT, 1 FREEBUSY, 1 VTODO)
+ // Add entries
let resp = http
.put("http://localhost:8087/alice/calendar/Personal/rfc1.ics")
.header("If-None-Match", "*")
@@ -595,7 +595,14 @@ fn rfc4791_webdav_caldav() {
.header("If-None-Match", "*")
.body(ICAL_RFC6)
.send()?;
- let _obj6_etag = resp.headers().get("etag").expect("etag must be set");
+ let obj6_etag = resp.headers().get("etag").expect("etag must be set");
+ assert_eq!(resp.status(), 201);
+ let resp = http
+ .put("http://localhost:8087/alice/calendar/Personal/rfc7.ics")
+ .header("If-None-Match", "*")
+ .body(ICAL_RFC7)
+ .send()?;
+ let obj7_etag = resp.headers().get("etag").expect("etag must be set");
assert_eq!(resp.status(), 201);
// A generic function to check a <calendar-data/> query result
@@ -684,9 +691,44 @@ fn rfc4791_webdav_caldav() {
.send()?;
//@FIXME not yet supported. returns DAV: 1 ; expects DAV: 1 calendar-access
// Not used by any client I know, so not implementing it now.
+
+ // --- REPORT calendar-multiget ---
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop>
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <D:href>/alice/calendar/Personal/rfc1.ics</D:href>
+ <D:href>/alice/calendar/Personal/rfc3.ics</D:href>
+ </C:calendar-multiget>"#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 2);
+ [
+ ("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1),
+ ("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3),
+ ]
+ .iter()
+ .for_each(|(ref_path, ref_etag, ref_ical)| {
+ check_cal(
+ &multistatus,
+ (
+ ref_path,
+ Some(ref_etag.to_str().expect("etag header convertible to str")),
+ Some(ref_ical),
+ ),
+ )
+ });
- // --- REPORT calendar-query ---
- //@FIXME missing support for calendar-data...
+ // --- REPORT calendar-query, only filtering ---
// 7.8.8. Example: Retrieval of Events Only
let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
@@ -709,12 +751,13 @@ fn rfc4791_webdav_caldav() {
.send()?;
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
- assert_eq!(multistatus.responses.len(), 3);
+ assert_eq!(multistatus.responses.len(), 4);
[
("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1),
("/alice/calendar/Personal/rfc2.ics", obj2_etag, ICAL_RFC2),
("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3),
+ ("/alice/calendar/Personal/rfc7.ics", obj7_etag, ICAL_RFC7),
]
.iter()
.for_each(|(ref_path, ref_etag, ref_ical)| {
@@ -788,26 +831,72 @@ fn rfc4791_webdav_caldav() {
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
assert_eq!(multistatus.responses.len(), 1);
+ check_cal(
+ &multistatus,
+ (
+ "/alice/calendar/Personal/rfc6.ics",
+ Some(obj6_etag.to_str().expect("etag header convertible to str")),
+ Some(ICAL_RFC6),
+ ),
+ );
// 7.8.6. Example: Retrieval of Event by UID
- // @TODO
-
- // 7.8.7. Example: Retrieval of Events by PARTSTAT
- // @TODO
+ let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
+ <D:getetag/>
+ <C:calendar-data/>
+ </D:prop>
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:prop-filter name="UID">
+ <C:text-match collation="i;octet">DC6C50A017428C5216A2F1CD@example.com</C:text-match>
+ </C:prop-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
+ let resp = http
+ .request(
+ reqwest::Method::from_bytes(b"REPORT")?,
+ "http://localhost:8087/alice/calendar/Personal/",
+ )
+ .body(cal_query)
+ .send()?;
+ assert_eq!(resp.status(), 207);
+ let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
+ assert_eq!(multistatus.responses.len(), 1);
+ check_cal(
+ &multistatus,
+ (
+ "/alice/calendar/Personal/rfc3.ics",
+ Some(obj3_etag.to_str().expect("etag header convertible to str")),
+ Some(ICAL_RFC3),
+ ),
+ );
- // 7.8.9. Example: Retrieval of All Pending To-Dos
- // @TODO
- // --- REPORT calendar-multiget ---
+ // 7.8.7. Example: Retrieval of Events by PARTSTAT
let cal_query = r#"<?xml version="1.0" encoding="utf-8" ?>
- <C:calendar-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
- <D:prop>
+ <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+ <D:prop xmlns:D="DAV:">
<D:getetag/>
<C:calendar-data/>
</D:prop>
- <D:href>/alice/calendar/Personal/rfc1.ics</D:href>
- <D:href>/alice/calendar/Personal/rfc3.ics</D:href>
- </C:calendar-multiget>"#;
+ <C:filter>
+ <C:comp-filter name="VCALENDAR">
+ <C:comp-filter name="VEVENT">
+ <C:prop-filter name="ATTENDEE">
+ <C:text-match collation="i;ascii-casemap">mailto:lisa@example.com</C:text-match>
+ <C:param-filter name="PARTSTAT">
+ <C:text-match collation="i;ascii-casemap">NEEDS-ACTION</C:text-match>
+ </C:param-filter>
+ </C:prop-filter>
+ </C:comp-filter>
+ </C:comp-filter>
+ </C:filter>
+ </C:calendar-query>"#;
let resp = http
.request(
reqwest::Method::from_bytes(b"REPORT")?,
@@ -817,22 +906,21 @@ fn rfc4791_webdav_caldav() {
.send()?;
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
- assert_eq!(multistatus.responses.len(), 2);
- [
- ("/alice/calendar/Personal/rfc1.ics", obj1_etag, ICAL_RFC1),
- ("/alice/calendar/Personal/rfc3.ics", obj3_etag, ICAL_RFC3),
- ]
- .iter()
- .for_each(|(ref_path, ref_etag, ref_ical)| {
- check_cal(
- &multistatus,
- (
- ref_path,
- Some(ref_etag.to_str().expect("etag header convertible to str")),
- Some(ref_ical),
- ),
- )
- });
+ assert_eq!(multistatus.responses.len(), 1);
+ check_cal(
+ &multistatus,
+ (
+ "/alice/calendar/Personal/rfc7.ics",
+ Some(obj7_etag.to_str().expect("etag header convertible to str")),
+ Some(ICAL_RFC7),
+ ),
+ );
+
+ // 7.8.9. Example: Retrieval of All Pending To-Dos
+ // @TODO
+
+ // --- REPORT calendar-query, with calendar-data tx ---
+ //@FIXME add support for calendar-data...
Ok(())
})
diff --git a/aerogramme/tests/common/constants.rs b/aerogramme/tests/common/constants.rs
index 91ee159..c04bae0 100644
--- a/aerogramme/tests/common/constants.rs
+++ b/aerogramme/tests/common/constants.rs
@@ -175,3 +175,41 @@ END:VALARM
END:VTODO
END:VCALENDAR
"#;
+
+pub static ICAL_RFC7: &[u8] = br#"BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
+DTSTAMP:20090206T001220Z
+DTSTART;TZID=US/Eastern:20090104T100000
+DURATION:PT1H
+LAST-MODIFIED:20090206T001330Z
+ORGANIZER:mailto:cyrus@example.com
+SEQUENCE:1
+STATUS:TENTATIVE
+SUMMARY:Event #3
+UID:DC6C50A017428C5216A2F1CA@example.com
+X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
+END:VEVENT
+END:VCALENDAR
+"#;