Files
Stalwart/tests/src/webdav/cal_query.rs
2025-06-15 15:09:32 +02:00

975 lines
26 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::WebDavTest;
use ahash::AHashSet;
use calcard::{
common::timezone::Tz,
icalendar::{ICalendar, dates::CalendarEvent},
};
use dav_proto::schema::property::TimeRange;
use groupware::{
DavResourceName,
calendar::{CalendarEventData, alarm::ExpandAlarm},
};
use hyper::StatusCode;
use store::write::serialize::rkyv_unarchive;
pub async fn test(test: &WebDavTest) {
println!("Running REPORT calendar-query & free-busy-query tests...");
let client = test.client("john");
let cal_path = format!("{}/john/default/", DavResourceName::Cal.base_path());
#[allow(clippy::never_loop)]
for (num, ics) in [
(1, ICAL_RFC_ABCD1_ICS),
(2, ICAL_RFC_ABCD2_ICS),
(3, ICAL_RFC_ABCD3_ICS),
(4, ICAL_RFC_ABCD4_ICS),
(5, ICAL_RFC_ABCD5_ICS),
(6, ICAL_RFC_ABCD6_ICS),
(7, ICAL_RFC_ABCD7_ICS),
(8, ICAL_RFC_ABCD8_ICS),
] {
roundtrip_expansion(ics, false);
client
.request("PUT", &rfc_file_name(num), ics)
.await
.with_status(StatusCode::CREATED);
}
// Test 1: Partial Retrieval of Events by Time Range
let response = client
.request("REPORT", &cal_path, REPORT_1)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([rfc_file_name(2).as_str(), rfc_file_name(3).as_str()])
.into_propfind_response(None);
response
.properties(&rfc_file_name(2))
.calendar_data()
.with_values([REPORT_1_EXPECTED_ABCD2.replace('\n', "\r\n").as_str()]);
response
.properties(&rfc_file_name(3))
.calendar_data()
.with_values([REPORT_1_EXPECTED_ABCD3.replace('\n', "\r\n").as_str()]);
// Test 2: Partial Retrieval of Recurring Events
let response = client
.request("REPORT", &cal_path, REPORT_2)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([rfc_file_name(2).as_str(), rfc_file_name(3).as_str()])
.into_propfind_response(None);
response
.properties(&rfc_file_name(2))
.calendar_data()
.with_values([REPORT_2_EXPECTED_ABCD2.replace('\n', "\r\n").as_str()]);
response
.properties(&rfc_file_name(3))
.calendar_data()
.with_values([REPORT_2_EXPECTED_ABCD3.replace('\n', "\r\n").as_str()]);
// Test 3: Expanded Retrieval of Recurring Events
let response = client
.request("REPORT", &cal_path, REPORT_3)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([rfc_file_name(2).as_str(), rfc_file_name(3).as_str()])
.into_propfind_response(None);
response
.properties(&rfc_file_name(2))
.calendar_data()
.with_values([REPORT_3_EXPECTED_ABCD2.replace('\n', "\r\n").as_str()]);
response
.properties(&rfc_file_name(3))
.calendar_data()
.with_values([REPORT_3_EXPECTED_ABCD3.replace('\n', "\r\n").as_str()]);
// Test 4: Partial Retrieval of Stored Free Busy Components
let response = client
.request("REPORT", &cal_path, REPORT_4)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([rfc_file_name(8).as_str()])
.into_propfind_response(None);
response
.properties(&rfc_file_name(8))
.calendar_data()
.with_values([REPORT_4_EXPECTED_ABCD8.replace('\n', "\r\n").as_str()]);
// Test 5: Retrieval of To-Dos by Alarm Time Range
let response = client
.request("REPORT", &cal_path, REPORT_5)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([rfc_file_name(5).as_str()])
.into_propfind_response(None);
response
.properties(&rfc_file_name(5))
.calendar_data()
.with_values([ICAL_RFC_ABCD5_ICS.replace('\n', "\r\n").as_str()]);
// Test 6: Retrieval of Event by UID
client
.request("REPORT", &cal_path, REPORT_6)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([rfc_file_name(3).as_str()])
.into_propfind_response(None);
// Test 7: Retrieval of Events by PARTSTAT
client
.request("REPORT", &cal_path, REPORT_7)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([rfc_file_name(3).as_str()])
.into_propfind_response(None);
// Test 8: Retrieval of Events Only
client
.request("REPORT", &cal_path, REPORT_8)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([
rfc_file_name(1).as_str(),
rfc_file_name(2).as_str(),
rfc_file_name(3).as_str(),
])
.into_propfind_response(None);
// Test 9: Retrieval of All Pending To-Dos
client
.request("REPORT", &cal_path, REPORT_9)
.await
.with_status(StatusCode::MULTI_STATUS)
.with_hrefs([rfc_file_name(4).as_str(), rfc_file_name(5).as_str()])
.into_propfind_response(None);
// Test 10: Successful CALDAV:free-busy-query REPORT
assert_eq!(
remove_dtstamp(
client
.request("REPORT", &cal_path, REPORT_10)
.await
.with_status(StatusCode::OK)
.body
.as_ref()
.unwrap()
),
remove_dtstamp(REPORT_10_RESPONSE)
);
assert_eq!(
remove_dtstamp(
client
.request("REPORT", &cal_path, REPORT_11)
.await
.with_status(StatusCode::OK)
.body
.as_ref()
.unwrap()
),
remove_dtstamp(REPORT_11_RESPONSE)
);
client.delete_default_containers().await;
test.assert_is_empty().await;
}
#[test]
#[ignore]
fn ical_roundtrip_expansion() {
for entry in std::fs::read_dir("/Users/me/code/calcard/resources/ical").unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "ics") {
println!("Testing: {:?}", path);
let input = match String::from_utf8(std::fs::read(&path).unwrap()) {
Ok(input) => input,
Err(err) => {
// ISO-8859-1
err.as_bytes()
.iter()
.map(|&b| b as char)
.collect::<String>()
}
};
roundtrip_expansion(&input, true);
}
}
}
fn roundtrip_expansion(ics: &str, ignore_errors: bool) {
let ical = if let Ok(ical) = ICalendar::parse(ics) {
ical
} else if ignore_errors {
return;
} else {
panic!("Failed to parse ICalendar {}", ics);
};
let expanded = ical.expand_dates(Tz::UTC, 100);
if !ignore_errors {
assert!(expanded.errors.is_empty());
}
let mut min_utc = i64::MAX;
let mut max_utc = i64::MIN;
let mut events = expanded
.events
.into_iter()
.map(|e| {
let e = e.try_into_date_time().unwrap();
let start = e.start.timestamp();
let end = e.end.timestamp();
let mut min = std::cmp::min(start, end);
let mut max = std::cmp::max(start, end);
for alarm in ical.alarms_for_id(e.comp_id) {
if let Some(alarm_time) = alarm
.expand_alarm(0, 0)
.and_then(|alarm| alarm.delta.to_timestamp(start, end, Tz::UTC))
{
if alarm_time < min {
min = alarm_time;
}
if alarm_time > max {
max = alarm_time;
}
}
}
if min < min_utc {
min_utc = min;
}
if max > max_utc {
max_utc = max;
}
CalendarEvent {
comp_id: e.comp_id,
start,
end,
}
})
.collect::<Vec<_>>();
// Verify min/max UTC timestamps
let event_data = CalendarEventData::new(ical, Tz::UTC, 100, &mut None);
let from_time = event_data.base_time_utc as i64 + event_data.base_offset;
let to_time = from_time + event_data.duration as i64;
if min_utc != i64::MAX {
assert_eq!(
from_time,
min_utc,
"diff: {}, failed for {}",
from_time - min_utc,
ics
);
assert_eq!(
to_time,
max_utc,
"diff: {}, failed for {}",
to_time - max_utc,
ics
);
}
// Validate archive expansion
let expanded_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&event_data).unwrap();
let expanded_archive = rkyv_unarchive::<CalendarEventData>(&expanded_bytes).unwrap();
let mut events_archive = expanded_archive
.expand(
Tz::UTC,
TimeRange {
start: i64::MIN,
end: i64::MAX,
},
)
.unwrap();
events.sort_by(|a, b| {
if a.comp_id == b.comp_id {
a.start.cmp(&b.start)
} else {
a.comp_id.cmp(&b.comp_id)
}
});
events_archive.sort_by(|a, b| {
if a.comp_id == b.comp_id {
a.start.cmp(&b.start)
} else {
a.comp_id.cmp(&b.comp_id)
}
});
assert_eq!(events, events_archive);
}
fn rfc_file_name(num: usize) -> String {
format!(
"{}/john/default/abcd{num}.ics",
DavResourceName::Cal.base_path()
)
}
const REPORT_1: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:getetag/>
<C:calendar-data>
<C:comp name="VCALENDAR">
<C:prop name="VERSION"/>
<C:comp name="VEVENT">
<C:prop name="SUMMARY"/>
<C:prop name="UID"/>
<C:prop name="DTSTART"/>
<C:prop name="DTEND"/>
<C:prop name="DURATION"/>
<C:prop name="RRULE"/>
<C:prop name="RDATE"/>
<C:prop name="EXRULE"/>
<C:prop name="EXDATE"/>
<C:prop name="RECURRENCE-ID"/>
</C:comp>
<C:comp name="VTIMEZONE"/>
</C:comp>
</C:calendar-data>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20060104T000000Z"
end="20060105T000000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
const REPORT_1_EXPECTED_ABCD2: &str = r#"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
DTSTART;TZID=US/Eastern:20060102T120000
DURATION:PT1H
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Event #2
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060104T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
SUMMARY:Event #2 bis
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=US/Eastern:20060106T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060106T120000
SUMMARY:Event #2 bis bis
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
END:VCALENDAR
"#;
const REPORT_1_EXPECTED_ABCD3: &str = r#"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
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
SUMMARY:Event #3
UID:DC6C50A017428C5216A2F1CD@example.com
END:VEVENT
END:VCALENDAR
"#;
const REPORT_2: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data>
<C:limit-recurrence-set start="20060103T000000Z"
end="20060105T000000Z"/>
</C:calendar-data>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20060103T000000Z"
end="20060105T000000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
const REPORT_2_EXPECTED_ABCD2: &str = r#"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
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060102T120000
DURATION:PT1H
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Event #2
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060104T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
SUMMARY:Event #2 bis
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
END:VCALENDAR
"#;
const REPORT_2_EXPECTED_ABCD3: &str = r#"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:20060206T001220Z
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:DC6C50A017428C5216A2F1CD@example.com
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
END:VEVENT
END:VCALENDAR
"#;
const REPORT_3: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data>
<C:comp name="VCALENDAR">
<C:comp name="VEVENT"/>
</C:comp>
<C:expand start="20060103T000000Z"
end="20060105T000000Z"/>
</C:calendar-data>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:time-range start="20060103T000000Z"
end="20060105T000000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
const REPORT_3_EXPECTED_ABCD2: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
DTSTART:20060103T170000Z
RECURRENCE-ID:20060103T170000Z
DTSTAMP:20060206T001121Z
DURATION:PT1H
SUMMARY:Event #2
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
BEGIN:VEVENT
DTSTART:20060104T190000Z
RECURRENCE-ID:20060104T190000Z
DTSTAMP:20060206T001121Z
DURATION:PT1H
SUMMARY:Event #2 bis
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
END:VCALENDAR
"#;
const REPORT_3_EXPECTED_ABCD3: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
DTSTART:20060104T150000Z
ATTENDEE;PARTSTAT=ACCEPTED;ROLE=CHAIR:mailto:cyrus@example.com
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:lisa@example.com
DTSTAMP:20060206T001220Z
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:DC6C50A017428C5216A2F1CD@example.com
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
END:VEVENT
END:VCALENDAR
"#;
const REPORT_4: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:"
xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data>
<C:limit-freebusy-set start="20060102T000000Z"
end="20060103T000000Z"/>
</C:calendar-data>
</D:prop>
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VFREEBUSY">
<C:time-range start="20060102T000000Z"
end="20060103T000000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
const REPORT_4_EXPECTED_ABCD8: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VFREEBUSY
ORGANIZER;CN="Bernard Desruisseaux":mailto:bernard@example.com
UID:76ef34-54a3d2@example.com
DTSTAMP:20050530T123421Z
DTSTART:20060101T000000Z
DTEND:20060108T000000Z
FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060102T100000Z/20060102T120000Z
END:VFREEBUSY
END:VCALENDAR
"#;
const REPORT_5: &str = 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="VTODO">
<C:comp-filter name="VALARM">
<C:time-range start="20060106T100000Z"
end="20060107T100000Z"/>
</C:comp-filter>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
const REPORT_6: &str = 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>
"#;
const REPORT_7: &str = 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="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>
"#;
const REPORT_8: &str = 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:comp-filter>
</C:filter>
</C:calendar-query>
"#;
const REPORT_9: &str = 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="VTODO">
<C:prop-filter name="COMPLETED">
<C:is-not-defined/>
</C:prop-filter>
<C:prop-filter name="STATUS">
<C:text-match
negate-condition="yes">CANCELLED</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>
</C:filter>
</C:calendar-query>
"#;
const REPORT_10: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
<C:free-busy-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<C:time-range start="20060104T140000Z"
end="20060105T220000Z"/>
</C:free-busy-query>
"#;
const REPORT_10_RESPONSE: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Stalwart Labs LLC//Stalwart Server//EN
BEGIN:VFREEBUSY
DTSTART:20060104T140000Z
DTEND:20060105T220000Z
FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060104T150000Z/20060104T160000Z
FREEBUSY;FBTYPE=BUSY:20060104T190000Z/20060104T200000Z;20060105T170000Z/20060105T180000Z
FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20060105T100000Z/20060105T120000Z
END:VFREEBUSY
END:VCALENDAR
"#;
const REPORT_11: &str = r#"<?xml version="1.0" encoding="utf-8" ?>
<C:free-busy-query xmlns:C="urn:ietf:params:xml:ns:caldav">
<C:time-range start="20060101T000000Z"
end="20060104T140000Z"/>
</C:free-busy-query>
"#;
const REPORT_11_RESPONSE: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Stalwart Labs LLC//Stalwart Server//EN
BEGIN:VFREEBUSY
DTSTART:20060101T000000Z
DTEND:20060104T140000Z
DTSTAMP:20250505T105255Z
FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060102T100000Z/20060102T120000Z
FREEBUSY;FBTYPE=BUSY:20060102T150000Z/20060102T160000Z;20060102T170000Z/20060102T180000Z;
20060103T100000Z/20060103T120000Z;20060103T170000Z/20060103T180000Z;20060104T100000Z/20060104T120000Z
END:VFREEBUSY
END:VCALENDAR
"#;
const ICAL_RFC_ABCD1_ICS: &str = r#"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
DTSTAMP:20060206T001102Z
DTSTART;TZID=US/Eastern:20060102T100000
DURATION:PT1H
SUMMARY:Event #1
Description:Go Steelers!
UID:74855313FA803DA593CD579A@example.com
END:VEVENT
END:VCALENDAR
"#;
const ICAL_RFC_ABCD2_ICS: &str = r#"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
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060102T120000
DURATION:PT1H
RRULE:FREQ=DAILY;COUNT=5
SUMMARY:Event #2
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060104T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
SUMMARY:Event #2 bis
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20060206T001121Z
DTSTART;TZID=US/Eastern:20060106T140000
DURATION:PT1H
RECURRENCE-ID;TZID=US/Eastern:20060106T120000
SUMMARY:Event #2 bis bis
UID:00959BC664CA650E933C892C@example.com
END:VEVENT
END:VCALENDAR
"#;
const ICAL_RFC_ABCD3_ICS: &str = r#"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:20060206T001220Z
DTSTART;TZID=US/Eastern:20060104T100000
DURATION:PT1H
LAST-MODIFIED:20060206T001330Z
ORGANIZER:mailto:cyrus@example.com
SEQUENCE:1
STATUS:TENTATIVE
SUMMARY:Event #3
UID:DC6C50A017428C5216A2F1CD@example.com
X-ABC-GUID:E1CX5Dr-0007ym-Hz@example.com
END:VEVENT
END:VCALENDAR
"#;
const ICAL_RFC_ABCD4_ICS: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VTODO
DTSTAMP:20060205T235335Z
DUE;VALUE=DATE:20060104
STATUS:NEEDS-ACTION
SUMMARY:Task #1
UID:DDDEEB7915FA61233B861457@example.com
BEGIN:VALARM
ACTION:AUDIO
TRIGGER;RELATED=START:-PT10M
END:VALARM
END:VTODO
END:VCALENDAR
"#;
const ICAL_RFC_ABCD5_ICS: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VTODO
DTSTAMP:20060205T235300Z
DUE;TZID=US/Eastern:20060106T120000
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:NEEDS-ACTION
SUMMARY:Task #2
UID:E10BA47467C5C69BB74E8720@example.com
BEGIN:VALARM
ACTION:AUDIO
TRIGGER;RELATED=START:-PT10M
END:VALARM
END:VTODO
END:VCALENDAR
"#;
const ICAL_RFC_ABCD6_ICS: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VTODO
COMPLETED:20051223T122322Z
DTSTAMP:20060205T235400Z
DUE;VALUE=DATE:20051225
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:COMPLETED
SUMMARY:Task #3
UID:E10BA47467C5C69BB74E8722@example.com
END:VTODO
END:VCALENDAR
"#;
const ICAL_RFC_ABCD7_ICS: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VTODO
DTSTAMP:20060205T235600Z
DUE;VALUE=DATE:20060101
LAST-MODIFIED:20060205T235308Z
SEQUENCE:1
STATUS:CANCELLED
SUMMARY:Task #4
UID:E10BA47467C5C69BB74E8725@example.com
END:VTODO
END:VCALENDAR
"#;
const ICAL_RFC_ABCD8_ICS: &str = r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VFREEBUSY
ORGANIZER;CN="Bernard Desruisseaux":mailto:bernard@example.com
UID:76ef34-54a3d2@example.com
DTSTAMP:20050530T123421Z
DTSTART:20060101T000000Z
DTEND:20060108T000000Z
FREEBUSY:20050531T230000Z/20050601T010000Z
FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060102T100000Z/20060102T120000Z
FREEBUSY:20060103T100000Z/20060103T120000Z
FREEBUSY:20060104T100000Z/20060104T120000Z
FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:20060105T100000Z/20060105T120000Z
FREEBUSY:20060106T100000Z/20060106T120000Z
END:VFREEBUSY
END:VCALENDAR
"#;
fn remove_dtstamp(ics: &str) -> AHashSet<String> {
let mut result = AHashSet::new();
for line in ics.lines() {
if !line.starts_with("DTSTAMP:") {
result.insert(line.to_string());
}
}
result
}