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

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

## 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) #

stable0.9.81
{
  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.