mailrs_dav/lib.rs
1#![deny(missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3
4//! # mailrs-dav
5//!
6//! Server-side **CalDAV** (RFC 4791) and **CardDAV** (RFC 6352) handlers for
7//! Rust mail / calendar / contacts servers — framework-agnostic, BYO data
8//! layer via the [`CalendarStore`] and [`AddressBookStore`] traits.
9//!
10//! At the time of writing this is the only standalone CalDAV / CardDAV
11//! **server-side** library on crates.io.
12//!
13//! ## What's covered
14//!
15//! - **Principal discovery** ([`principal::principal_propfind`]) — PROPFIND
16//! on `/dav/` returns `current-user-principal`, `calendar-home-set`,
17//! `addressbook-home-set`, `principal-URL`, `supported-report-set`.
18//! - **CalDAV** ([`caldav`]) — calendar home + collection PROPFIND, REPORT
19//! (calendar-multiget / calendar-query), GET / PUT / DELETE on events,
20//! `If-Match` / `If-None-Match: *` precondition handling.
21//! - **CardDAV** ([`carddav`]) — same shape for address books and contacts.
22//! - **Pure parsing helpers** ([`parse`]) — iCalendar / vCard field
23//! extraction, `Depth` header, multiget UID extraction.
24//! - **OPTIONS / multistatus / etag** ([`xml`]).
25//!
26//! ## What's not
27//!
28//! - **Calendar-query time-range filters** (RFC 4791 §9.7). Most clients work
29//! fine with the "return everything" form this crate emits; if you need
30//! filtered queries, layer them on top of [`crate::store::CalendarStore::list_events`].
31//! - **Free/busy reports** (RFC 4791 §7.10) — separate spec, not implemented.
32//! - **VTIMEZONE / scheduling extensions** (RFC 6638) — out of scope; the
33//! raw icalendar body round-trips intact, so clients that emit VTIMEZONE
34//! can still GET back what they PUT.
35//! - **ACL / privilege management** (RFC 3744). The handlers emit a fixed
36//! `<D:all/>` privilege set for the authenticated owner.
37//! - **HTTP auth, routing, framework integration.** This crate takes a
38//! resolved `user` and `calendar_id` / `book_id`; the auth and route
39//! parsing live in your server-side wrapper.
40//!
41//! ## Quick start
42//!
43//! ```no_run
44//! use async_trait::async_trait;
45//! use mailrs_dav::{
46//! caldav, carddav,
47//! store::{CalendarStore, AddressBookStore, StoreError},
48//! types::{Calendar, Event, AddressBook, Contact, PutResult},
49//! };
50//!
51//! struct MyStore;
52//!
53//! #[async_trait]
54//! impl CalendarStore for MyStore {
55//! async fn list_calendars(&self, _user: &str) -> Result<Vec<Calendar>, StoreError> { Ok(vec![]) }
56//! async fn get_calendar(&self, _: &str, _: &str) -> Result<Option<Calendar>, StoreError> { Ok(None) }
57//! async fn list_events(&self, _: i64) -> Result<Vec<Event>, StoreError> { Ok(vec![]) }
58//! async fn get_event(&self, _: i64, _: &str) -> Result<Option<Event>, StoreError> { Ok(None) }
59//! async fn event_etag(&self, _: i64, _: &str) -> Result<Option<String>, StoreError> { Ok(None) }
60//! async fn put_event(&self, _: i64, _: &str, _: &str, etag: &str) -> Result<PutResult, StoreError> {
61//! Ok(PutResult { created: true, etag: etag.into() })
62//! }
63//! async fn delete_event(&self, _: i64, _: &str) -> Result<bool, StoreError> { Ok(false) }
64//! async fn ensure_default_calendar(&self, _: &str) -> Result<(), StoreError> { Ok(()) }
65//! }
66//!
67//! # async fn run() {
68//! let store = MyStore;
69//! // PROPFIND on a calendar collection (calendar id 42, Depth: 1)
70//! let resp = caldav::calendar_propfind(&store, "alice@example.com", "Work", 42, 1)
71//! .await
72//! .unwrap();
73//! assert_eq!(resp.status, 207);
74//! # }
75//! ```
76//!
77//! ## How it slots into axum
78//!
79//! ```rust,ignore
80//! use std::sync::Arc;
81//! use axum::http::StatusCode;
82//! use mailrs_dav::{caldav, xml::DavResponse};
83//!
84//! async fn axum_event_get(
85//! // resolved by your auth + routing layer
86//! store: Arc<dyn mailrs_dav::store::CalendarStore>,
87//! calendar_id: i64,
88//! uid: String,
89//! ) -> impl axum::response::IntoResponse {
90//! match caldav::event_get(store.as_ref(), calendar_id, &uid).await {
91//! Ok(r) => into_axum(r),
92//! Err(e) => into_axum(e.to_dav_response()),
93//! }
94//! }
95//!
96//! fn into_axum(r: DavResponse) -> axum::response::Response {
97//! let mut builder = axum::http::Response::builder()
98//! .status(StatusCode::from_u16(r.status).unwrap());
99//! for (k, v) in r.headers { builder = builder.header(k, v); }
100//! builder.body(axum::body::Body::from(r.body)).unwrap()
101//! }
102//! ```
103
104pub mod caldav;
105pub mod carddav;
106pub mod error;
107pub mod fixtures;
108pub mod parse;
109pub mod principal;
110pub mod store;
111pub mod types;
112pub mod xml;
113
114pub use error::DavError;
115pub use store::{AddressBookStore, CalendarStore, StoreError};
116pub use types::{AddressBook, Calendar, Contact, Event, PutResult};
117pub use xml::{DavResponse, etag_of, multistatus, options_response, xml_escape};