From 5cc85faf5bb1d99d94679b87512c95a7ba4e44f0 Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 22 Jul 2026 19:49:40 -0500 Subject: [PATCH] feat(cal): add CalDavClient for WebDAV REPORT calendar queries --- Cargo.toml | 3 + crates/nuncio-cal/Cargo.toml | 3 + crates/nuncio-cal/src/caldav.rs | 109 ++++++++++++++++++++++++++++++++ crates/nuncio-cal/src/lib.rs | 2 + 4 files changed, 117 insertions(+) create mode 100644 crates/nuncio-cal/src/caldav.rs diff --git a/Cargo.toml b/Cargo.toml index 02560df..9ac6593 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,9 @@ icalendar = "0.15" chrono-tz = "0.9" rrule = "0.10" chrono = "0.4" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +quick-xml = "0.36" + diff --git a/crates/nuncio-cal/Cargo.toml b/crates/nuncio-cal/Cargo.toml index 371dffa..22bb938 100644 --- a/crates/nuncio-cal/Cargo.toml +++ b/crates/nuncio-cal/Cargo.toml @@ -17,4 +17,7 @@ icalendar.workspace = true chrono-tz.workspace = true rrule.workspace = true chrono.workspace = true +reqwest.workspace = true +quick-xml.workspace = true + diff --git a/crates/nuncio-cal/src/caldav.rs b/crates/nuncio-cal/src/caldav.rs new file mode 100644 index 0000000..9d96a95 --- /dev/null +++ b/crates/nuncio-cal/src/caldav.rs @@ -0,0 +1,109 @@ +//! CalDAV (RFC 4791) / WebDAV REPORT client and XML query generator. + +use nuncio_core::model::CalendarEvent; + +use crate::parser::{CalendarError, IcalParserAdapter}; + +/// CalDAV client protocol engine managing WebDAV REPORT queries. +pub struct CalDavClient { + account_id: String, +} + +impl CalDavClient { + /// Create a new `CalDavClient` bound to an account ID. + pub fn new(account_id: &str) -> Self { + Self { + account_id: account_id.to_string(), + } + } + + /// Construct a standard CalDAV `` XML payload for a time range window. + pub fn build_report_query(start_iso: &str, end_iso: &str) -> String { + format!( + r#" + + + + + + + + + + + + +"#, + start_iso, end_iso + ) + } + + /// Parse CalDAV WebDAV XML `` response containing embedded VEVENT data. + pub fn parse_multistatus_response( + &self, + calendar_id: &str, + raw_xml: &str, + ) -> Result, CalendarError> { + let mut events = Vec::new(); + + // Extract or text blocks + for block in raw_xml.split("") { + if let Some((ics_data, _)) = block.split_once("") { + let clean_ics = ics_data.trim(); + if !clean_ics.is_empty() { + let event_id = format!("caldav-evt-{}", events.len() + 1); + if let Ok(event) = + IcalParserAdapter::parse_ical(&event_id, &self.account_id, calendar_id, clean_ics) + { + events.push(event); + } + } + } + } + + Ok(events) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_report_query_format() { + let query = CalDavClient::build_report_query("20240101T000000Z", "20240201T000000Z"); + assert!(query.contains("20240101T000000Z")); + assert!(query.contains("20240201T000000Z")); + assert!(query.contains(" + + + /calendars/user/work/evt1.ics + + + BEGIN:VCALENDAR +VERSION:2.0 +BEGIN:VEVENT +SUMMARY:Product Planning +LOCATION:Boardroom +END:VEVENT +END:VCALENDAR + + + + "#; + + let events = client + .parse_multistatus_response("cal-work", xml_response) + .expect("parse succeeds"); + + assert_eq!(events.len(), 1); + assert_eq!(events[0].summary, "Product Planning"); + assert_eq!(events[0].location, Some("Boardroom".to_string())); + } +} diff --git a/crates/nuncio-cal/src/lib.rs b/crates/nuncio-cal/src/lib.rs index 2d21c6d..6ec3c23 100644 --- a/crates/nuncio-cal/src/lib.rs +++ b/crates/nuncio-cal/src/lib.rs @@ -1,7 +1,9 @@ //! iCalendar (RFC 5545), CalDAV (RFC 4791), and recurrence engine for Nuncio. +pub mod caldav; pub mod parser; pub mod rrule; +pub use caldav::CalDavClient; pub use parser::{CalendarError, IcalParserAdapter}; pub use rrule::RecurrenceEngine; -- 2.54.0