/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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("john@example.com").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::() } }; 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::>(); // 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::(&event_data).unwrap(); let expanded_archive = rkyv_unarchive::(&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::>(), 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::(&event_data).unwrap(); let archive = rkyv_unarchive::(&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::(&event_data).unwrap(); let archive = rkyv_unarchive::(&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#" "#; 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#" "#; 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#" "#; 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#" "#; 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#" "#; const REPORT_6: &str = r#" DC6C50A017428C5216A2F1CD@example.com "#; const REPORT_7: &str = r#" mailto:lisa@example.com NEEDS-ACTION "#; const REPORT_8: &str = r#" "#; const REPORT_9: &str = r#" CANCELLED "#; const REPORT_12: &str = r#" JMAP-NO-VERSION-EVENT@example.com "#; const ICAL_JMAP_NO_VERSION: &str = r#"BEGIN:VCALENDAR BEGIN:VEVENT UID:JMAP-NO-VERSION-EVENT@example.com SUMMARY:JMAP created event DTSTART:20060107T120000Z DTEND:20060107T130000Z END:VEVENT END:VCALENDAR "#; const REPORT_10: &str = r#" "#; 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#" "#; 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#" "#; 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:yearly-unbounded@example.com 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:auckland-dst-fallback@example.com 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: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 { let mut result = AHashSet::new(); for line in ics.lines() { if !line.starts_with("DTSTAMP:") { result.insert(line.to_string()); } } result }