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-principal 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.

caldavHandler / carddavHandler read that principal from req.user.principalId (falling back to req.actor), or from opts.actorPrincipalId when the deployment names it differently. One field, not a search across several: a principal ID addresses a URL, so two actors named through two different fields would share one URL and one set of calendars. An authenticated request whose actor yields no principal is refused with 401 rather than served under a shared name. Callers on a non-Express transport pass the principal to dispatchCaldav / dispatchCarddav themselves.

Two principals are separate here exactly when their principal IDs differ, so a multi-tenant deployment whose tenants can hold the same ID returns opts.actorPrincipalId with the tenant included.

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

## Not served

- **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 and inbox collections are not served. A deployment that needs iTIP sends it through b.mail.server.submission itself. - **Free-busy reports (RFC 4791 §7.10)** — a basic free-busy shape parses. The availability merge across several attendees is not computed, so a caller reads the per-attendee shapes and merges them. - **VTIMEZONE inline composition** — operators reference IANA timezone names; full VTIMEZONE generation lives in JSCalendar. - **iMIP (RFC 6047)** — iTIP carried over mail is not handled.

## 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
  actorPrincipalId:  function,    // (actor, req) → string naming the principal this request acts for, where the deployment does not carry it on `actor.principalId`. It is the only field consulted when set, and a value that is not a non-empty string refuses the request with 401. A throw is treated the same way
  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-10-12T14:21:56.879Z by seeder.