Mail CalDAV / CardDAV
CalDAV (RFC 4791) + CardDAV (RFC 6352) HTTP route handlers. Where the mail-server primitives mount as TCP listeners, the DAV stack rides the existing HTTP surface: operators mount the returned handlers under b.router / b.createApp and reuse their auth middleware, TLS termination, and rate-limit posture.
The framework owns the wire protocol — method dispatch, XML body parsing (via b.xmlC14n.parse), per-tenant URL isolation, If-Match / If-None-Match ETag invariants, response-shape generation, and (critically) PUT-body validation through b.safeIcal.parse (CalDAV) and b.safeVcard.parse (CardDAV). Operators wire the storage backend — listCalendars, getComponent, listComponents, putComponent, deleteComponent for CalDAV; the equivalent contact-shaped set for CardDAV — so the framework never assumes a single DB shape.
## Public surface
var dav = b.mail.dav.create({
storage: {
calendar: {
listCalendars: async function (principalId) { ... },
getComponent: async function (principalId, calendarId, componentId) { ... },
listComponents: async function (principalId, calendarId, filter) { ... },
putComponent: async function (principalId, calendarId, componentId, icalBytes, ifMatch) { ... },
deleteComponent: async function (principalId, calendarId, componentId, ifMatch) { ... },
mkcalendar: async function (principalId, calendarId, props) { ... },
},
addressbook: {
listAddressbooks: async function (principalId) { ... },
getCard: async function (principalId, addressbookId, cardId) { ... },
listCards: async function (principalId, addressbookId, filter) { ... },
putCard: async function (principalId, addressbookId, cardId, vcardBytes, ifMatch) { ... },
deleteCard: async function (principalId, addressbookId, cardId, ifMatch) { ... },
mkcol: async function (principalId, addressbookId, props) { ... },
},
},
profile: "strict", // safeIcal / safeVcard
audit: b.audit,
});
app.use("/.well-known/caldav", dav.discoveryHandler);
app.use("/.well-known/carddav", dav.discoveryHandler);
app.use("/caldav", b.middleware.bearerAuth({...}), dav.caldavHandler);
app.use("/carddav", b.middleware.bearerAuth({...}), dav.carddavHandler);
## URL shape
- CalDAV:
/caldav// / .ics - CardDAV:
/carddav// / .vcf
Every URL carries the principal ID at the first path segment. Cross-principal access is refused at the handler boundary; the storage backend never sees a principal ID it did not authorize.
## Verbs (v1)
Common: OPTIONS, PROPFIND, REPORT, GET, PUT, DELETE. CalDAV-specific: MKCALENDAR (RFC 4791 §5.2.1). CardDAV-specific: MKCOL (RFC 4918 §9.3).
PROPFIND responds Multi-Status (207) for Depth: 0 (resource props) / Depth: 1 (collection contents). REPORT bodies supported: calendar-query, calendar-multiget, addressbook-query, addressbook-multiget. sync-collection (RFC 6578) ships when the storage backend declares its sync-token capability.
## Status codes
- 200 — GET / OPTIONS success
- 201 — PUT created / MKCALENDAR / MKCOL success
- 204 — PUT / DELETE success (existing resource)
- 207 — PROPFIND / REPORT Multi-Status (RFC 4918 §13)
- 401 — auth required (operator middleware did not populate actor)
- 403 — cross-principal access / forbidden by storage
- 404 — resource not found
- 412 —
If-MatchETag mismatch on PUT / DELETE (RFC 4918 §10.4) - 415 — PUT body failed safeIcal / safeVcard validation
## Explicitly deferred (v1)
- **WebDAV ACL (RFC 3744)** — operator wires authorization at their HTTP middleware (per-principal scoping is already enforced by the URL invariant; richer ACE / privilege grammar is opt-in). - **CalDAV scheduling (RFC 6638)** — the iTIP scheduling outbox / inbox routes call back into b.mail.submission; ships in a later slice so the cross-protocol contract is settled first. - **Free-busy reports (RFC 4791 §7.10)** — basic free-busy shape parses; the full availability merge across attendees defers to the scheduling slice. - **VTIMEZONE inline composition** — operators reference IANA timezone names; full VTIMEZONE generation lives in JSCalendar. - **iMIP (RFC 6047)** — iTIP-over-mail handler defers to the scheduling slice with its MX hook.
## CVE defense composition
- b.safeIcal rejects RRULE COUNT > 10000 / BYxxx list > 24 → defends the ical4j RRULE-recursion / recurrence-expansion DoS class (unbounded RRULE expansion exhausts CPU/memory) on the PUT path. - b.xmlC14n.parse rejects DOCTYPE / ENTITY in the PROPFIND / REPORT body → defends XXE / billion-laughs on the query path. - URL-encoded path traversal (.., %2e%2e, null bytes) is refused before the storage backend sees the IDs.
b.mail.dav.create(opts) #
{
storage: { calendar, addressbook }, // operator-supplied
profile: "strict" | "balanced" | "permissive", // default strict
compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2", // optional
maxRequestBodyBytes: number, // default 8 MiB
audit: b.audit, // optional
}
Build a CalDAV + CardDAV route-handler bundle. Returns a handle exposing caldavHandler / carddavHandler / discoveryHandler (Express-style (req, res, next) functions) plus dispatchCaldav / dispatchCarddav for operators on a non-Express transport.
var dav = b.mail.dav.create({
storage: {
calendar: { listCalendars, getComponent, listComponents,
putComponent, deleteComponent, mkcalendar },
addressbook: { listAddressbooks, getCard, listCards,
putCard, deleteCard, mkcol },
},
profile: "strict",
});
app.use("/.well-known/caldav", dav.discoveryHandler);
app.use("/.well-known/carddav", dav.discoveryHandler);
app.use("/caldav", bearerAuth, dav.caldavHandler);
app.use("/carddav", bearerAuth, dav.carddavHandler);
Last updated 2026-08-08T16:39:49.652Z by seeder.