Expand description
Fast CalDAV/CardDAV client library for Rust.
This library provides a high-performance, asynchronous CalDAV and CardDAV client built on modern Rust ecosystem components including hyper 1.x, rustls, and tokio.
§Why this library?
- CalDAV and CardDAV discovery, queries, and sync with a consistent API.
- HTTP/2, connection pooling, and compression for efficient client workloads.
- Streaming XML parsing and batch operations for large collections.
§Stability and maturity
Core discovery, CRUD, and query flows are covered by unit and e2e tests. Streaming parsing and sync are stable, but server quirks vary and feedback is welcome.
§Versioning and backward compatibility
This project follows Semantic Versioning. Patch releases fix bugs, minor releases add compatible features, and major releases introduce breaking changes when needed.
§Features
- HTTP/2 multiplexing and connection pooling
- Automatic response decompression (br/zstd/gzip)
- Streaming-friendly APIs for large WebDAV responses
- Batch operations with bounded concurrency
- ETag helpers for safe conditional writes/deletes
- Streaming XML parsing with minimal memory footprint
- CardDAV addressbook discovery and vCard operations
§Examples
§Basic Setup and Calendar Discovery
use fast_dav_rs::{CalDavClient, Depth};
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CalDavClient::new(
"https://caldav.example.com/user/",
Some("username"),
Some("password"),
)?;
// Discover the current user's principal
let principal = client.discover_current_user_principal().await?
.ok_or_else(|| anyhow::anyhow!("No principal found"))?;
// Find calendar home sets
let homes = client.discover_calendar_home_set(&principal).await?;
let home = homes.first().ok_or_else(|| anyhow::anyhow!("No calendar home found"))?;
// List all calendars
let calendars = client.list_calendars(home).await?;
for calendar in &calendars {
println!("Found calendar: {:?}", calendar.displayname);
}
Ok(())
}§Calendar Operations
use fast_dav_rs::{CalDavClient, Depth};
use bytes::Bytes;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CalDavClient::new(
"https://caldav.example.com/user/",
Some("username"),
Some("password"),
)?;
// Create a new calendar
let calendar_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:set>
<D:prop>
<D:displayname>My New Calendar</D:displayname>
<C:calendar-description>Calendar created with fast-dav-rs</C:calendar-description>
</D:prop>
</D:set>
</C:mkcalendar>"#;
let response = client.mkcalendar("my-new-calendar/", calendar_xml).await?;
println!("Created calendar with status: {}", response.status());
// Delete a calendar
let delete_response = client.delete("my-new-calendar/").await?;
println!("Deleted calendar with status: {}", delete_response.status());
Ok(())
}§Event Operations
use fast_dav_rs::{CalDavClient, Depth};
use bytes::Bytes;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CalDavClient::new(
"https://caldav.example.com/user/",
Some("username"),
Some("password"),
)?;
// Create a new event
let event_ics = Bytes::from(r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//fast-dav-rs//EN
BEGIN:VEVENT
UID:event-123@example.com
DTSTAMP:20230101T000000Z
DTSTART:20231225T100000Z
DTEND:20231225T110000Z
SUMMARY:Christmas Day Event
DESCRIPTION:Celebrate Christmas with family
END:VEVENT
END:VCALENDAR
"#);
// Safe creation with If-None-Match to prevent overwriting
let response = client.put_if_none_match("my-calendar/christmas-event.ics", event_ics).await?;
println!("Created event with status: {}", response.status());
// Query events in a date range
let events = client.calendar_query_timerange(
"my-calendar/",
"VEVENT",
Some("20231201T000000Z"), // Start date
Some("20231231T235959Z"), // End date
true // Include event data
).await?;
for event in &events {
println!("Event: {:?}, ETag: {:?}", event.href, event.etag);
}
// Update an existing event (if we found one)
if let Some(first_event) = events.first() {
if let Some(etag) = &first_event.etag {
let updated_ics = Bytes::from(format!(r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//fast-dav-rs//EN
BEGIN:VEVENT
UID:event-123@example.com
DTSTAMP:20230102T000000Z
DTSTART:20231225T100000Z
DTEND:20231225T120000Z // Extended end time
SUMMARY:Christmas Day Event (Extended)
DESCRIPTION:Celebrate Christmas with extended family
END:VEVENT
END:VCALENDAR
"#));
// Safe update with If-Match
let update_response = client.put_if_match(
&first_event.href,
updated_ics,
etag
).await?;
println!("Updated event with status: {}", update_response.status());
}
}
// Delete an event (using conditional delete for safety)
if let Some(first_event) = events.first() {
if let Some(etag) = &first_event.etag {
let delete_response = client.delete_if_match(&first_event.href, etag).await?;
println!("Deleted event with status: {}", delete_response.status());
}
}
Ok(())
}§Working with ETags for Safe Operations
use fast_dav_rs::CalDavClient;
use bytes::Bytes;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CalDavClient::new(
"https://caldav.example.com/user/",
Some("username"),
Some("password"),
)?;
// Get current ETag before modifying a resource
let head_response = client.head("my-calendar/some-event.ics").await?;
if let Some(etag) = CalDavClient::etag_from_headers(head_response.headers()) {
// Now we can safely update with If-Match
let updated_ics = Bytes::from(r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//fast-dav-rs//EN
BEGIN:VEVENT
UID:some-event@example.com
DTSTAMP:20230101T000000Z
DTSTART:20231225T100000Z
DTEND:20231225T110000Z
SUMMARY:Updated Event Title
END:VEVENT
END:VCALENDAR
"#);
let response = client.put_if_match("my-calendar/some-event.ics", updated_ics, &etag).await?;
if response.status().is_success() {
println!("Successfully updated event");
} else {
println!("Failed to update event: {}", response.status());
}
}
Ok(())
}§Streaming Large Responses
For processing large collections without loading everything into memory:
use fast_dav_rs::{CalDavClient, Depth, detect_encoding};
use fast_dav_rs::caldav::parse_multistatus_stream;
use bytes::Bytes;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CalDavClient::new(
"https://caldav.example.com/user/",
Some("username"),
Some("password"),
)?;
// Stream a large PROPFIND response
let propfind_xml = r#"
<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:displayname/>
<D:getetag/>
<C:calendar-data/>
</D:prop>
</D:propfind>"#;
let response = client.propfind_stream("large-calendar/", Depth::One, propfind_xml).await?;
let encoding = detect_encoding(response.headers());
let result = parse_multistatus_stream(response.into_body(), &[encoding]).await?;
// Process items one by one without loading everything into memory
for item in result.items {
println!("Found item: {} with etag: {:?}",
item.displayname.unwrap_or_default(),
item.etag);
// Process calendar data if present
if let Some(data) = item.calendar_data {
// Handle large iCalendar data efficiently
println!("Processing calendar data of length: {}", data.len());
}
}
Ok(())
}§Concurrent Batch Operations
Execute multiple operations concurrently with controlled parallelism:
use fast_dav_rs::{CalDavClient, Depth};
use bytes::Bytes;
use std::sync::Arc;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CalDavClient::new(
"https://caldav.example.com/user/",
Some("username"),
Some("password"),
)?;
// Prepare a common PROPFIND request for multiple calendars
let propfind_body = Arc::new(Bytes::from(r#"
<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<D:displayname/>
<D:getetag/>
<C:supported-calendar-component-set/>
</D:prop>
</D:propfind>"#));
// Execute PROPFIND on multiple calendars concurrently (max 5 in parallel)
let results = client.propfind_many(
calendar_paths, // Vector of calendar paths
Depth::Zero,
propfind_body,
5 // Maximum concurrency
).await;
// Process results in the same order as input
for result in results {
match result.result {
Ok(response) => {
if response.status().is_success() {
println!("Successfully queried: {}", result.pub_path);
// Parse response body as needed
} else {
println!("Query failed for {}: {}", result.pub_path, response.status());
}
}
Err(e) => {
println!("Error querying {}: {}", result.pub_path, e);
}
}
}
// Batch event updates
let event_updates = vec![
("event1.ics", "BEGIN:VCALENDAR...END:VCALENDAR"),
("event2.ics", "BEGIN:VCALENDAR...END:VCALENDAR"),
];
// Upload multiple events concurrently
let mut upload_tasks = Vec::new();
for (filename, ical_data) in event_updates {
let client_clone = client.clone();
let path = format!("{}/{}", calendar_path, filename);
let data = Bytes::from(ical_data);
upload_tasks.push(tokio::spawn(async move {
client_clone.put(&path, data).await
}));
}
// Wait for all uploads to complete
let upload_results = futures::future::join_all(upload_tasks).await;
for (i, result) in upload_results.into_iter().enumerate() {
match result {
Ok(Ok(response)) => {
println!("Upload {} completed with status: {}", i, response.status());
}
Ok(Err(e)) => {
println!("Upload {} failed with error: {}", i, e);
}
Err(e) => {
println!("Upload {} panicked: {}", i, e);
}
}
}
Ok(())
}§Bootstrap and Capability Detection
Discover server capabilities and choose appropriate synchronization methods:
use fast_dav_rs::{CalDavClient, Depth};
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CalDavClient::new(
"https://caldav.example.com/user/",
Some("username"),
Some("password"),
)?;
// Bootstrap: Discover server capabilities
println!("Detecting server capabilities...");
// Check if server supports WebDAV-Sync (RFC 6578)
let has_sync_support = client.supports_webdav_sync().await?;
println!("WebDAV-Sync support: {}", has_sync_support);
// Discover user principal
let principal = client.discover_current_user_principal().await?
.ok_or_else(|| anyhow::anyhow!("No principal found"))?;
println!("User principal: {}", principal);
// Discover calendar homes
let homes = client.discover_calendar_home_set(&principal).await?;
let home = homes.first().ok_or_else(|| anyhow::anyhow!("No calendar home found"))?;
println!("Calendar home: {}", home);
// List calendars with detailed info
let calendars = client.list_calendars(home).await?;
for calendar in &calendars {
println!("Calendar: {} (sync token: {:?})",
calendar.displayname.as_deref().unwrap_or("unnamed"),
calendar.sync_token.as_ref().map(|s| &s[..20]));
}
// Choose synchronization strategy based on capabilities
if has_sync_support && !calendars.is_empty() {
println!("Using efficient WebDAV-Sync for synchronization");
sync_with_webdav_sync(&client, &calendars[0]).await?;
} else {
println!("Using traditional polling for synchronization");
sync_with_polling(&client, home).await?;
}
Ok(())
}
/// Efficient synchronization using WebDAV-Sync
async fn sync_with_webdav_sync(client: &CalDavClient, calendar: &fast_dav_rs::CalendarInfo) -> Result<()> {
let mut sync_token = calendar.sync_token.clone();
loop {
println!("Syncing with token: {:?}", sync_token.as_ref().map(|s| &s[..20]));
// Perform incremental sync
let sync_response = client.sync_collection(
&calendar.href,
sync_token.as_deref(),
Some(100), // Limit results
true // Include data
).await?;
println!("Received {} updates", sync_response.items.len());
// Process changes
for item in &sync_response.items {
if item.is_deleted {
println!("Deleted: {}", item.href);
} else if let Some(data) = &item.calendar_data {
println!("Updated: {} ({} chars)", item.href, data.len());
} else {
println!("Changed: {} (no data)", item.href);
}
}
// Update sync token for next iteration
sync_token = sync_response.sync_token;
// Break if no more changes or implement your own exit condition
if sync_response.items.is_empty() {
break;
}
// In a real application, you'd probably want to sleep between syncs
// tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
}
Ok(())
}
/// Traditional synchronization using polling
async fn sync_with_polling(client: &CalDavClient, calendar_home: &str) -> Result<()> {
// Get all calendars
let calendars = client.list_calendars(calendar_home).await?;
for calendar in calendars {
println!("Polling calendar: {:?}", calendar.displayname);
// Query recent events (example with fixed dates)
let start = "20240101T000000Z";
let end = "20240201T000000Z";
let events = client.calendar_query_timerange(
&calendar.href,
"VEVENT",
Some(&start),
Some(&end),
true // Include data
).await?;
println!("Found {} events in {}", events.len(), calendar.displayname.unwrap_or_default());
// Process events (in a real app, you'd compare with local cache)
for event in events {
if let Some(data) = event.calendar_data {
println!("Event: {} ({})", event.href, data.lines().next().unwrap_or(""));
}
}
}
Ok(())
}§CardDAV Addressbook Discovery
use fast_dav_rs::CardDavClient;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CardDavClient::new(
"https://carddav.example.com/user/",
Some("username"),
Some("password"),
)?;
let principal = client.discover_current_user_principal().await?
.ok_or_else(|| anyhow::anyhow!("No principal found"))?;
let homes = client.discover_addressbook_home_set(&principal).await?;
let home = homes.first().ok_or_else(|| anyhow::anyhow!("No addressbook home found"))?;
let books = client.list_addressbooks(home).await?;
for book in &books {
println!("Found addressbook: {:?}", book.displayname);
}
Ok(())
}§CardDAV Contact Operations
use fast_dav_rs::CardDavClient;
use bytes::Bytes;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CardDavClient::new(
"https://carddav.example.com/user/",
Some("username"),
Some("password"),
)?;
let addressbook_path = "addressbooks/user/team/";
let addressbook_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<C:mkaddressbook xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:set>
<D:prop>
<D:displayname>Team Contacts</D:displayname>
</D:prop>
</D:set>
</C:mkaddressbook>"#;
let _ = client.mkaddressbook(addressbook_path, addressbook_xml).await?;
let contact_path = format!("{addressbook_path}jane.vcf");
let vcard = Bytes::from("BEGIN:VCARD\nVERSION:3.0\nFN:Jane Doe\nUID:jane-1\nEMAIL:jane@example.com\nEND:VCARD\n");
let create_resp = client.put_if_none_match(&contact_path, vcard).await?;
println!("Create contact: {}", create_resp.status());
let head = client.head(&contact_path).await?;
if let Some(etag) = CardDavClient::etag_from_headers(head.headers()) {
let updated = Bytes::from("BEGIN:VCARD\nVERSION:3.0\nFN:Jane Doe\nUID:jane-1\nEMAIL:jane@example.com\nTEL:+1-555-0100\nEND:VCARD\n");
let update_resp = client.put_if_match(&contact_path, updated, &etag).await?;
println!("Update contact: {}", update_resp.status());
}
Ok(())
}§CardDAV Queries and Multiget
use fast_dav_rs::CardDavClient;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CardDavClient::new(
"https://carddav.example.com/user/",
Some("username"),
Some("password"),
)?;
let addressbook_path = "addressbooks/user/team/";
let matches = client
.addressbook_query_email(addressbook_path, "jane@example.com", true)
.await?;
let hrefs: Vec<String> = matches.iter().map(|c| c.href.clone()).collect();
let contacts = client.addressbook_multiget(addressbook_path, hrefs, true).await?;
for contact in contacts {
if let Some(data) = contact.address_data {
let first_line = data.lines().next().unwrap_or("");
println!("{} -> {}", contact.href, first_line);
}
}
Ok(())
}§CardDAV Streaming Large Responses
use fast_dav_rs::{CardDavClient, Depth, detect_encoding};
use fast_dav_rs::carddav::parse_multistatus_stream;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CardDavClient::new(
"https://carddav.example.com/user/",
Some("username"),
Some("password"),
)?;
let addressbook_path = "addressbooks/user/team/";
let report_xml = r#"
<C:addressbook-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
<D:prop>
<D:getetag/>
<C:address-data/>
</D:prop>
<C:filter>
<C:prop-filter name="FN">
<C:text-match>Jane</C:text-match>
</C:prop-filter>
</C:filter>
</C:addressbook-query>"#;
let response = client
.report_stream(addressbook_path, Depth::One, report_xml)
.await?;
let encoding = detect_encoding(response.headers());
let result = parse_multistatus_stream(response.into_body(), &[encoding]).await?;
for item in result.items {
if let Some(data) = item.address_data {
println!("{} -> {} bytes", item.href, data.len());
}
}
Ok(())
}§CardDAV Sync Collection
use fast_dav_rs::CardDavClient;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CardDavClient::new(
"https://carddav.example.com/user/",
Some("username"),
Some("password"),
)?;
let addressbook_path = "addressbooks/user/team/";
let sync = client
.sync_collection(addressbook_path, None, Some(100), true)
.await?;
println!("Sync token: {:?}", sync.sync_token);
for item in sync.items {
if item.is_deleted {
println!("Deleted: {}", item.href);
} else if let Some(data) = item.address_data {
println!("Updated: {} ({} bytes)", item.href, data.len());
}
}
Ok(())
}Re-exports§
pub use caldav::streaming::parse_multistatus_bytes;pub use caldav::streaming::parse_multistatus_bytes_visit;pub use caldav::streaming::parse_multistatus_stream;pub use caldav::streaming::parse_multistatus_stream_visit;pub use caldav::BatchItem;pub use caldav::CalDavClient;pub use caldav::CalendarInfo;pub use caldav::CalendarObject;pub use caldav::DavItem;pub use caldav::Depth;pub use caldav::SyncItem;pub use caldav::SyncResponse;pub use caldav::build_calendar_multiget_body;pub use caldav::build_calendar_query_body;pub use caldav::build_sync_collection_body;pub use caldav::map_calendar_list;pub use caldav::map_calendar_objects;pub use caldav::map_sync_response;pub use carddav::AddressBookInfo;pub use carddav::AddressObject;pub use carddav::CardDavClient;pub use common::compression::ContentEncoding;pub use common::compression::add_accept_encoding;pub use common::compression::add_content_encoding;pub use common::compression::compress_payload;pub use common::compression::detect_encoding;pub use common::compression::detect_encodings;pub use common::compression::detect_request_compression_preference;