mailrs-dav 1.0.1

CalDAV (RFC 4791) and CardDAV (RFC 6352) server-side handlers — framework-agnostic, BYO data layer via the CalendarStore / AddressBookStore traits.
Documentation

mailrs-dav

Crates.io docs.rs License Downloads MSRV

Server-side CalDAV (RFC 4791) and CardDAV (RFC 6352) handlers for Rust mail / calendar / contacts servers — framework-agnostic, BYO data layer via the CalendarStore and AddressBookStore traits.

Extracted from mailrs so any project that wants to expose CalDAV / CardDAV can do so without re-implementing the multistatus / propstat builder, multiget UID extraction, iCalendar / vCard field scrapers, or the per-resource precondition handling (If-Match, If-None-Match: *).

This is, at the time of writing, the only standalone server-side CalDAV / CardDAV library on crates.io.

Highlights

  • Framework-free — no axum / actix / tower / hyper. Each handler returns a DavResponse { status, headers, body } your server-side adapter translates into your framework's response type.
  • Store-free — implement CalendarStore (8 async methods) and / or AddressBookStore (8 async methods); every handler works.
  • Preconditions handledIf-Match (etag must equal current) and If-None-Match: * (resource must not exist) honored on PUT per RFC 4791 §5.3.2 / RFC 6352 §6.3.2.
  • Standard error envelopeDavError enum with .to_dav_response() for a 4xx/5xx fallback your server can emit unchanged.
  • Pure helpers exposedxml_escape, multistatus, etag_of, options_response, extract_ical_field, extract_ical_datetime, extract_vcard_field, extract_multiget_uids, parse_depth. Use the handlers or grab the pieces.

Methods covered (1.0)

HTTP verb Resource scope RFC section Notes
OPTIONS any RFC 4918 §9.1 Advertises DAV: 1, 2, 3, calendar-access, addressbook + verbs.
PROPFIND /dav/ RFC 5397 current-user-principal, *-home-set, principal-URL, supported-report-set.
PROPFIND calendar home / calendar collection RFC 4791 §4 Auto-creates a default calendar on first hit.
PROPFIND addressbook home / addressbook RFC 6352 §5 Auto-creates a default address book on first hit.
REPORT calendar-multiget RFC 4791 §7.9 UIDs extracted from <C:href> children.
REPORT calendar-query RFC 4791 §7.8 Returns all events; time-range filter is 1.x roadmap (see below).
REPORT addressbook-multiget RFC 6352 §8.7 UIDs extracted from <CR:href> children.
REPORT addressbook-query RFC 6352 §8.6 Returns all contacts.
GET event / contact RFC 4791 §5.3.4 / RFC 6352 §6.3.4 Verbatim icalendar / vcard body + etag.
PUT event / contact RFC 4791 §5.3.2 / RFC 6352 §6.3.2 201 on create, 204 on update, 412 on precondition fail.
DELETE event / contact RFC 4918 §9.6 204 on delete, 404 when missing.

Quick start

use async_trait::async_trait;
use mailrs_dav::{
    caldav,
    store::{CalendarStore, StoreError},
    types::{Calendar, Event, PutResult},
};

struct MyStore;

#[async_trait]
impl CalendarStore for MyStore {
    async fn list_calendars(&self, _user: &str) -> Result<Vec<Calendar>, StoreError> {
        Ok(vec![Calendar {
            id: 1,
            name: "Work".into(),
            color: "#3366cc".into(),
            description: "".into(),
        }])
    }

    // ... 7 more methods, see docs.rs/mailrs-dav
#   async fn get_calendar(&self, _: &str, _: &str) -> Result<Option<Calendar>, StoreError> { Ok(None) }
#   async fn list_events(&self, _: i64) -> Result<Vec<Event>, StoreError> { Ok(vec![]) }
#   async fn get_event(&self, _: i64, _: &str) -> Result<Option<Event>, StoreError> { Ok(None) }
#   async fn event_etag(&self, _: i64, _: &str) -> Result<Option<String>, StoreError> { Ok(None) }
#   async fn put_event(&self, _: i64, _: &str, _: &str, etag: &str) -> Result<PutResult, StoreError> {
#       Ok(PutResult { created: true, etag: etag.into() })
#   }
#   async fn delete_event(&self, _: i64, _: &str) -> Result<bool, StoreError> { Ok(false) }
#   async fn ensure_default_calendar(&self, _: &str) -> Result<(), StoreError> { Ok(()) }
}

# async fn run() {
let store = MyStore;
// PROPFIND on /dav/calendars/alice/Work/ with Depth: 1
let resp = caldav::calendar_propfind(&store, "alice@example.com", "Work", 1, 1)
    .await
    .unwrap();
println!("{} bytes of multistatus XML", resp.body.len());
# }

How it slots into axum

use std::sync::Arc;
use axum::{extract::{Path, State}, http::{HeaderMap, Method, StatusCode}, response::Response};
use mailrs_dav::{caldav, parse::parse_depth};

async fn calendar_route(
    method: Method,
    Path((user, calendar)): Path<(String, String)>,
    State(store): State<Arc<dyn mailrs_dav::CalendarStore>>,
    headers: HeaderMap,
    body: String,
) -> Response {
    let depth = parse_depth(headers.get("depth").and_then(|v| v.to_str().ok()));
    // ... resolve calendar_id from (user, calendar) via your auth/route layer
    let calendar_id = 1_i64;
    let result = match method.as_str() {
        "PROPFIND" => caldav::calendar_propfind(store.as_ref(), &user, &calendar, calendar_id, depth).await,
        "REPORT" => caldav::calendar_report(store.as_ref(), &user, &calendar, calendar_id, &body).await,
        _ => return StatusCode::METHOD_NOT_ALLOWED.into_response(),
    };
    let dav_resp = result.unwrap_or_else(|e| e.to_dav_response());
    // ... translate dav_resp into axum::Response
#   axum::http::Response::builder().status(dav_resp.status).body(axum::body::Body::from(dav_resp.body)).unwrap()
}
# use axum::response::IntoResponse;

The store impl is yours. The mailrs server wraps its sqlx::PgPool in a thin DavAdapter that bridges the schema's row types into mailrs_dav::types — about 250 LOC, worth a read as a reference implementation.

Roadmap

1.0 is the minimum viable surface — enough to drive Apple Calendar / Contacts, Thunderbird, DAVx⁵, and other mainstream clients for read + write of events and contacts. Items planned for 1.x, in rough priority:

  • Calendar-query time-range filtering (RFC 4791 §9.7). Today calendar-query returns the full event list; clients then filter locally. Adding server-side time-range narrows the wire payload for clients that send the filter.
  • MKCALENDAR / MKCOL (RFC 4791 §5.3.1). Lets clients create new calendars from the UI (currently calendars are created server-side by ensure_default_calendar or an admin path).
  • iTIP scheduling extensions (RFC 6638). Inbox / outbox collections for invite delivery. The raw iCalendar already round-trips intact, but the protocol-level scheduling collection wiring is out of 1.0.
  • getctag (CalendarServer ctag extension). A cheap "did anything change in this collection" pre-PROPFIND check for clients that poll.

These will land as additive helpers / new pub fns. The existing trait signatures will not change incompatibly within 1.x.

What's intentionally out of scope

  • HTTP auth. Basic / Bearer / OAuth is the wrapper's job — this crate takes a resolved user string.
  • Routing / URL parsing. Handlers take pre-resolved calendar_id / book_id; the URL → id lookup is the wrapper's call (and trivial — the store trait gives you get_calendar / get_address_book).
  • Free/busy reports (RFC 4791 §7.10). Separate spec; rarely implemented; clients fall back to fetching events.
  • ACL (RFC 3744). The handlers emit a fixed <D:all/> privilege set for the authenticated owner; multi-owner / shared-calendar ACLs would need a real authorization model your wrapper owns.

Versioning

1.x follows semver. The public API surface is:

  • CalendarStore / AddressBookStore trait method signatures
  • DavError enum variants
  • DavResponse field shapes
  • Per-handler pub fn signatures in caldav::*, carddav::*, principal::*
  • Pure helper signatures in parse::* and xml::*

The exact XML shape inside multistatus may evolve within a minor version as long as it stays compliant with the matching RFC.

License

Licensed under either Apache License, Version 2.0 or MIT license at your option.