Skip to main content

Crate fast_dav_rs

Crate fast_dav_rs 

Source
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;

Modules§

caldav
carddav
CardDAV client, streaming helpers, and types for addressbook discovery, queries, and sync.
client
common
compression
streaming
types
webdav