| Crates.io | fast-dav-rs |
| lib.rs | fast-dav-rs |
| version | 0.4.1 |
| created_at | 2025-10-19 13:08:25.578364+00 |
| updated_at | 2026-01-25 21:00:11.042095+00 |
| description | Fast CalDAV/CardDAV client with hyper + rustls, HTTP/2, compression, batching, and streaming XML parsing. |
| homepage | |
| repository | |
| max_upload_size | |
| id | 1890402 |
| size | 575,510 |
fast-dav-rs is a high-performance asynchronous CalDAV/CardDAV client for Rust. It blends hyper 1.x, tokio, rustls, and streaming XML tooling so your services can discover calendars, manage events, sync addressbooks, and keep remote DAV stores in sync without re-implementing the protocol by hand.
This library focuses on correctness and predictable behavior across CalDAV and CardDAV servers.
The project prioritizes correctness, performance, and a low-ceremony API. New features are welcome when they improve protocol compliance or compatibility without adding unnecessary abstraction.
This project follows Semantic Versioning. Patch releases fix bugs, minor releases add compatible features, and major releases introduce breaking changes when needed.
macros, rt-multi-thread, and time features.cargo add fast-dav-rs
use fast_dav_rs::CalDavClient;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CalDavClient::new(
"https://caldav.example.com/users/alice/",
Some("alice"),
Some("hunter2"),
)?;
let principal = client
.discover_current_user_principal()
.await?
.ok_or_else(|| anyhow::anyhow!("no principal returned"))?;
let homes = client.discover_calendar_home_set(&principal).await?;
let home = homes.first().expect("missing calendar-home-set");
for calendar in client.list_calendars(home).await? {
println!("Calendar: {:?}", calendar.displayname);
}
Ok(())
}
use fast_dav_rs::CardDavClient;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CardDavClient::new(
"https://carddav.example.com/users/alice/",
Some("alice"),
Some("hunter2"),
)?;
let principal = client
.discover_current_user_principal()
.await?
.ok_or_else(|| anyhow::anyhow!("no principal returned"))?;
let homes = client.discover_addressbook_home_set(&principal).await?;
let home = homes.first().expect("missing addressbook-home-set");
for book in client.list_addressbooks(home).await? {
println!("Addressbook: {:?}", book.displayname);
}
Ok(())
}
use fast_dav_rs::{CalDavClient, ContentEncoding};
use fast_dav_rs::webdav::RequestCompressionMode;
let mut client = CalDavClient::new("https://caldav.example.com/users/alice/", None, None)?;
client.set_request_compression_mode(RequestCompressionMode::Force(ContentEncoding::Gzip));
client.set_request_compression_auto();
client.disable_request_compression();
The low-level send and send_stream methods accept an optional per_req_timeout: Option<Duration>
so you can override the default timeout for specific requests.
propfind_many and report_many accept a max_concurrency parameter to bound the number of in-flight
requests while preserving input order in the result list.
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/users/alice/", None, None)?;
let calendar_path = "calendars/alice/work/";
let event_path = format!("{calendar_path}kickoff.ics");
let create = Bytes::from("BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:kickoff\nEND:VEVENT\nEND:VCALENDAR\n");
client.put_if_none_match(&event_path, create).await?;
let events = client
.calendar_query_timerange(calendar_path, "VEVENT", None, None, true)
.await?;
if let Some(event) = events.first() {
if let Some(etag) = &event.etag {
let updated = Bytes::from("BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nUID:kickoff\nSUMMARY:Updated\nEND:VEVENT\nEND:VCALENDAR\n");
client.put_if_match(&event.href, updated, etag).await?;
}
}
Ok(())
}
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/users/alice/", None, None)?;
let addressbook_path = "addressbooks/alice/team/";
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");
client.put_if_none_match(&contact_path, vcard).await?;
let matches = client
.addressbook_query_email(addressbook_path, "jane@example.com", true)
.await?;
if let Some(contact) = matches.first() {
if let Some(etag) = &contact.etag {
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");
client.put_if_match(&contact.href, updated, etag).await?;
}
}
Ok(())
}
caldav::parse_multistatus_stream for CalDAV responses and carddav::parse_multistatus_stream
for CardDAV responses.supports_webdav_sync and sync_collection work for both calendars and addressbooks.use fast_dav_rs::{CalDavClient, Depth, detect_encoding};
use fast_dav_rs::caldav::parse_multistatus_stream;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
let client = CalDavClient::new("https://caldav.example.com/users/alice/", None, None)?;
let propfind_xml = r#"<D:propfind xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\"><D:prop><D:getetag/><C:calendar-data/></D:prop></D:propfind>"#;
let response = client.propfind_stream("calendars/alice/work/", Depth::One, propfind_xml).await?;
let encoding = detect_encoding(response.headers());
let parsed = parse_multistatus_stream(response.into_body(), &[encoding]).await?;
for item in parsed.items {
if let Some(data) = item.calendar_data {
println!("{} -> {} bytes", item.href, data.len());
}
}
Ok(())
}
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/users/alice/", None, None)?;
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:addressbook-query>"#;
let response = client.report_stream("addressbooks/alice/team/", Depth::One, report_xml).await?;
let encoding = detect_encoding(response.headers());
let parsed = parse_multistatus_stream(response.into_body(), &[encoding]).await?;
for item in parsed.items {
if let Some(data) = item.address_data {
println!("{} -> {} bytes", item.href, data.len());
}
}
Ok(())
}
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/users/alice/", None, None)?;
let paths = vec!["calendars/alice/work/".to_string(), "calendars/alice/home/".to_string()];
let body = Arc::new(Bytes::from(r#"<D:propfind xmlns:D=\"DAV:\"><D:prop><D:displayname/></D:prop></D:propfind>"#));
let results = client.propfind_many(paths, Depth::Zero, body, 4).await;
for item in results {
println!("{} -> {:?}", item.pub_path, item.result.as_ref().map(|r| r.status()));
}
Ok(())
}
cargo test --all-features
cargo test --doc
./run-e2e-tests.sh
This project includes a complete e2e testing environment with a SabreDAV server that supports CalDAV and CardDAV features including compression.
sabredav-test/)cd sabredav-test
./setup.sh
This will start a complete SabreDAV environment with:
./run-e2e-tests.sh
Or manually:
cargo test --test e2e_tests -- --nocapture
To reset the database to a clean state:
cd sabredav-test
./reset-db.sh
This library focuses on being a fast, low-level CalDAV/CardDAV client.
Consider alternatives if:
sync_collection over full scans when WebDAV-Sync is supported.Auto unless your payloads are tiny.We welcome contributions. See CONTRIBUTING.md for the workflow and AGENTS.md for repository-specific guidelines.
fast-dav-rs builds on the Rust ecosystem, including hyper, tokio, rustls, quick-xml, and async-compression.
This package is licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0).
See LICENSE for details.