One branding module (types::brand!) supplies the name to every protocol greeting, the HTTP realm, the startup banner, Received headers, the user agent, the IMAP ID response and calendar PRODIDs. The sign-in and calendar pages take ihasmail's palette and font and the INBUXA logo, and calendar emails embed the INBUXA lockup. Default calendar and address book names follow. Protocol identifiers (urn:stalwart:jmap, vnd.stalwart Sieve extensions) and upstream copyright notices are unchanged.
1201 lines
33 KiB
Rust
1201 lines
33 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use crate::utils::server::TestServer;
|
|
|
|
use ahash::AHashSet;
|
|
use calcard::{common::timezone::Tz, icalendar::ICalendar};
|
|
use groupware::{
|
|
DavResourceName,
|
|
calendar::{CalendarEventData, alarm::ExpandAlarm, expand::CalendarEventExpansion},
|
|
};
|
|
use hyper::StatusCode;
|
|
use std::str::FromStr;
|
|
use store::write::serialize::rkyv_unarchive;
|
|
use types::TimeRange;
|
|
|
|
pub async fn test(test: &TestServer) {
|
|
println!("Running REPORT calendar-query & free-busy-query tests...");
|
|
let client = test.account("[email protected]").webdav_client();
|
|
let cal_path = format!(
|
|
"{}/john%40example.com/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)
|
|
);
|
|
|
|
// Test 12: JMAP-style event stored with an entry-less VCALENDAR wrapper
|
|
client
|
|
.request("PUT", &rfc_file_name(9), ICAL_JMAP_NO_VERSION)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
let response = client
|
|
.request("REPORT", &cal_path, REPORT_12)
|
|
.await
|
|
.with_status(StatusCode::MULTI_STATUS)
|
|
.with_hrefs([rfc_file_name(9).as_str()])
|
|
.into_propfind_response(None);
|
|
response
|
|
.properties(&rfc_file_name(9))
|
|
.calendar_data()
|
|
.is_not_empty();
|
|
|
|
// Test 13: Unbounded yearly recurrences are visible far beyond their first instance
|
|
client
|
|
.request("PUT", &rfc_file_name(10), ICAL_UNBOUNDED_YEARLY_ICS)
|
|
.await
|
|
.with_status(StatusCode::CREATED);
|
|
client
|
|
.request("REPORT", &cal_path, REPORT_13)
|
|
.await
|
|
.with_status(StatusCode::MULTI_STATUS)
|
|
.with_hrefs([rfc_file_name(10).as_str()])
|
|
.into_propfind_response(None)
|
|
.properties(&rfc_file_name(10))
|
|
.calendar_data()
|
|
.is_not_empty();
|
|
|
|
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;
|
|
}
|
|
CalendarEventExpansion {
|
|
comp_id: e.comp_id,
|
|
own_recurrence_id: None,
|
|
start,
|
|
end,
|
|
start_naive: 0,
|
|
}
|
|
})
|
|
.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();
|
|
|
|
assert_eq!(
|
|
events_archive,
|
|
event_data
|
|
.expand_from_ids(
|
|
&mut events_archive
|
|
.iter()
|
|
.filter_map(|e| e.recurrence_key())
|
|
.collect::<AHashSet<_>>(),
|
|
Tz::UTC
|
|
)
|
|
.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)
|
|
}
|
|
});
|
|
for event in events.iter_mut().chain(events_archive.iter_mut()) {
|
|
event.own_recurrence_id = None;
|
|
event.start_naive = 0;
|
|
}
|
|
|
|
assert_eq!(events, events_archive);
|
|
}
|
|
|
|
#[test]
|
|
fn calendar_expand_dst_fallback() {
|
|
let akl = Tz::from_str("Pacific/Auckland").unwrap();
|
|
let ical = ICalendar::parse(ICAL_DST_FALLBACK_ICS).unwrap();
|
|
let event_data = CalendarEventData::new(ical, akl, 1000, &mut None);
|
|
let expanded_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&event_data).unwrap();
|
|
let archive = rkyv_unarchive::<CalendarEventData>(&expanded_bytes).unwrap();
|
|
|
|
let window = archive
|
|
.expand(
|
|
akl,
|
|
TimeRange {
|
|
start: 1782777600,
|
|
end: 1786579200,
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
!window.is_empty(),
|
|
"recurrence expansion aborted across the Pacific/Auckland DST fall-back overlap"
|
|
);
|
|
|
|
let across_overlap = archive
|
|
.expand(
|
|
akl,
|
|
TimeRange {
|
|
start: 1775260800,
|
|
end: 1775433600,
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
across_overlap.len(),
|
|
1,
|
|
"the ambiguous fall-back occurrence must resolve to its earliest instant"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn calendar_expand_beyond_indexable_span() {
|
|
let ical = ICalendar::parse(ICAL_UNBOUNDED_YEARLY_ICS).unwrap();
|
|
let event_data = CalendarEventData::new(ical, Tz::UTC, 3000, &mut None);
|
|
let expanded_bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&event_data).unwrap();
|
|
let archive = rkyv_unarchive::<CalendarEventData>(&expanded_bytes).unwrap();
|
|
|
|
let instances = archive
|
|
.expand(
|
|
Tz::UTC,
|
|
TimeRange {
|
|
start: i64::MIN,
|
|
end: i64::MAX,
|
|
},
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(instances.first().unwrap().start, FIRST_YEARLY_INSTANCE);
|
|
assert_eq!(instances.last().unwrap().start, LAST_YEARLY_INSTANCE);
|
|
assert_eq!(instances.len(), 137);
|
|
assert!(
|
|
instances.windows(2).all(|w| w[0].start < w[1].start),
|
|
"instance offsets wrapped around u32"
|
|
);
|
|
|
|
assert_eq!(event_data.event_range_start(), FIRST_YEARLY_INSTANCE);
|
|
assert!(
|
|
event_data.event_range_end() >= instances.last().unwrap().end,
|
|
"indexed range ends at {}, before its last instance at {}",
|
|
event_data.event_range_end(),
|
|
instances.last().unwrap().end
|
|
);
|
|
|
|
let in_2027 = archive
|
|
.expand(
|
|
Tz::UTC,
|
|
TimeRange {
|
|
start: JUNE_2027_START,
|
|
end: JUNE_2027_END,
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert_eq!(in_2027.len(), 1);
|
|
assert_eq!(in_2027[0].start, JUNE_2027_INSTANCE);
|
|
assert!(
|
|
event_data.event_range_start() < JUNE_2027_END
|
|
&& event_data.event_range_end() > JUNE_2027_START,
|
|
"indexed range {}..{} does not overlap June 2027",
|
|
event_data.event_range_start(),
|
|
event_data.event_range_end()
|
|
);
|
|
}
|
|
|
|
const FIRST_YEARLY_INSTANCE: i64 = 1527843600;
|
|
const LAST_YEARLY_INSTANCE: i64 = 5819590800;
|
|
const JUNE_2027_START: i64 = 1811808000;
|
|
const JUNE_2027_END: i64 = 1814400000;
|
|
const JUNE_2027_INSTANCE: i64 = 1811840400;
|
|
|
|
fn rfc_file_name(num: usize) -> String {
|
|
format!(
|
|
"{}/john%40example.com/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:[email protected]
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
DTSTART;TZID=US/Eastern:20060104T140000
|
|
DURATION:PT1H
|
|
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
|
|
SUMMARY:Event #2 bis
|
|
UID:[email protected]
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
DTSTART;TZID=US/Eastern:20060106T140000
|
|
DURATION:PT1H
|
|
RECURRENCE-ID;TZID=US/Eastern:20060106T120000
|
|
SUMMARY:Event #2 bis bis
|
|
UID:[email protected]
|
|
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:[email protected]
|
|
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:[email protected]
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
DTSTAMP:20060206T001121Z
|
|
DTSTART;TZID=US/Eastern:20060104T140000
|
|
DURATION:PT1H
|
|
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
|
|
SUMMARY:Event #2 bis
|
|
UID:[email protected]
|
|
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:[email protected]
|
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:[email protected]
|
|
DTSTAMP:20060206T001220Z
|
|
DTSTART;TZID=US/Eastern:20060104T100000
|
|
DURATION:PT1H
|
|
LAST-MODIFIED:20060206T001330Z
|
|
ORGANIZER:mailto:[email protected]
|
|
SEQUENCE:1
|
|
STATUS:TENTATIVE
|
|
SUMMARY:Event #3
|
|
UID:[email protected]
|
|
X-ABC-GUID:[email protected]
|
|
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:[email protected]
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
DTSTART:20060104T190000Z
|
|
RECURRENCE-ID:20060104T190000Z
|
|
DTSTAMP:20060206T001121Z
|
|
DURATION:PT1H
|
|
SUMMARY:Event #2 bis
|
|
UID:[email protected]
|
|
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:[email protected]
|
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:[email protected]
|
|
DTSTAMP:20060206T001220Z
|
|
DURATION:PT1H
|
|
LAST-MODIFIED:20060206T001330Z
|
|
ORGANIZER:mailto:[email protected]
|
|
SEQUENCE:1
|
|
STATUS:TENTATIVE
|
|
SUMMARY:Event #3
|
|
UID:[email protected]
|
|
X-ABC-GUID:[email protected]
|
|
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:[email protected]
|
|
UID:[email protected]
|
|
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"
|
|
>[email protected]</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:[email protected]</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_12: &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"
|
|
>[email protected]</C:text-match>
|
|
</C:prop-filter>
|
|
</C:comp-filter>
|
|
</C:comp-filter>
|
|
</C:filter>
|
|
</C:calendar-query>
|
|
"#;
|
|
|
|
const ICAL_JMAP_NO_VERSION: &str = r#"BEGIN:VCALENDAR
|
|
BEGIN:VEVENT
|
|
UID:[email protected]
|
|
SUMMARY:JMAP created event
|
|
DTSTART:20060107T120000Z
|
|
DTEND:20060107T130000Z
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
"#;
|
|
|
|
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:-//INBUXA//INBUXA Server//EN
|
|
BEGIN:VFREEBUSY
|
|
DTSTART:20060104T140000Z
|
|
DTEND:20060105T220000Z
|
|
FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060104T150000Z/20060104T160000Z
|
|
FREEBUSY;FBTYPE=BUSY:20060104T190000Z/20060104T200000Z,20060105T170000Z/200
|
|
60105T180000Z
|
|
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:-//INBUXA//INBUXA Server//EN
|
|
BEGIN:VFREEBUSY
|
|
DTSTART:20060101T000000Z
|
|
DTEND:20060104T140000Z
|
|
DTSTAMP:20250505T105255Z
|
|
FREEBUSY;FBTYPE=BUSY-TENTATIVE:20060102T100000Z/20060102T120000Z
|
|
FREEBUSY;FBTYPE=BUSY:20060102T150000Z/20060102T160000Z,20060102T170000Z/200
|
|
60102T180000Z,20060103T100000Z/20060103T120000Z,20060103T170000Z/20060103T
|
|
180000Z,20060104T100000Z/20060104T120000Z
|
|
END:VFREEBUSY
|
|
END:VCALENDAR
|
|
"#;
|
|
|
|
const REPORT_13: &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/>
|
|
</D:prop>
|
|
<C:filter>
|
|
<C:comp-filter name="VCALENDAR">
|
|
<C:comp-filter name="VEVENT">
|
|
<C:time-range start="20270601T000000Z"
|
|
end="20270701T000000Z"/>
|
|
</C:comp-filter>
|
|
</C:comp-filter>
|
|
</C:filter>
|
|
</C:calendar-query>
|
|
"#;
|
|
|
|
const ICAL_UNBOUNDED_YEARLY_ICS: &str = r#"BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//Example Corp.//CalDAV Client//EN
|
|
BEGIN:VEVENT
|
|
DTSTAMP:20180601T000000Z
|
|
DTSTART:20180601T090000Z
|
|
DTEND:20180601T100000Z
|
|
RRULE:FREQ=YEARLY
|
|
SUMMARY:Unbounded yearly event
|
|
UID:[email protected]
|
|
END:VEVENT
|
|
END:VCALENDAR
|
|
"#;
|
|
|
|
const ICAL_DST_FALLBACK_ICS: &str = r#"BEGIN:VCALENDAR
|
|
VERSION:2.0
|
|
PRODID:-//Example Corp.//CalDAV Client//EN
|
|
BEGIN:VEVENT
|
|
DTSTAMP:20260108T000000Z
|
|
DTSTART;TZID=Pacific/Auckland:20260108T022000
|
|
DURATION:PT2H10M
|
|
RRULE:FREQ=DAILY;INTERVAL=3
|
|
SUMMARY:Auckland DST fall-back recurrence
|
|
UID:[email protected]
|
|
END:VEVENT
|
|
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:[email protected]
|
|
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:[email protected]
|
|
END:VEVENT
|
|
BEGIN:VEVENT
|
|
DTSTAMP:20060206T001121Z
|
|
DTSTART;TZID=US/Eastern:20060104T140000
|
|
DURATION:PT1H
|
|
RECURRENCE-ID;TZID=US/Eastern:20060104T120000
|
|
SUMMARY:Event #2 bis
|
|
UID:[email protected]
|
|
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:[email protected]
|
|
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:[email protected]
|
|
ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:[email protected]
|
|
DTSTAMP:20060206T001220Z
|
|
DTSTART;TZID=US/Eastern:20060104T100000
|
|
DURATION:PT1H
|
|
LAST-MODIFIED:20060206T001330Z
|
|
ORGANIZER:mailto:[email protected]
|
|
SEQUENCE:1
|
|
STATUS:TENTATIVE
|
|
SUMMARY:Event #3
|
|
UID:[email protected]
|
|
X-ABC-GUID:[email protected]
|
|
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:[email protected]
|
|
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:[email protected]
|
|
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:[email protected]
|
|
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:[email protected]
|
|
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:[email protected]
|
|
UID:[email protected]
|
|
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
|
|
}
|