Skip to content

★ Enterprise Edition — not included in the Community Edition. Compare editions

DAV Worker (calendar and contacts)

The dav service is a sabre/dav 4.7.1 server. It speaks CalDAV (RFC 4791) and CardDAV (RFC 6352) to real clients — Apple Calendar, Thunderbird, DAVx⁵ — and stores everything in the mailserver MySQL database.

It replaced the caldav + radicale pair in phase-09, for two separate reasons. The management API in front of Radicale had no authentication on any of its routes. And Radicale does not implement RFC 6638 server-side scheduling, which matters less for what it prevents outright than for who it excludes: with no scheduling inbox on the server, Apple Calendar, iOS and DAVx⁵-plus-Android never see an invitation, because those clients read the scheduling inbox rather than the mail inbox. A desktop client that does iMIP itself, such as Thunderbird, can still send invitations over the user's own SMTP — so "Radicale cannot do meetings" would be too strong, and Radicale does have free-busy reports and coarse collection sharing through its rights file. What it has no answer for is scheduling delivery to phones, inbound reply handling done once rather than per-client, self-service sharing across tenants, and delegation.

What works today

  • Calendar and contact sync for every active mailbox, over CalDAV and CardDAV.
  • Authentication by IMAP LOGIN against Dovecot — the mailbox password, the same credential as IMAP and SMTP. There is no separate calendar password.
  • Automatic provisioning. A mailbox's default calendar and address book are created the first time a client asks for them, so there is no provisioning job to run and nothing to backfill for mailboxes that already exist.
  • Fast sync (RFC 6578 sync-collection), so a client asks what changed rather than re-listing every event.
  • Discovery (RFC 6764): /.well-known/caldav and /.well-known/carddav answer on every host this server terminates TLS for, and the DNS guidance the panel hands customers now includes matching SRV and TXT records.
  • Scheduling between mailboxes on this server (RFC 6638). An invitation to a colleague is delivered straight into their scheduling inbox, which is where Apple Calendar and DAVx⁵ look for one.
  • Per-calendar client settings (colour, display order) via WebDAV property storage.
  • Scheduling, both directions. An invitation to a mailbox here lands in its scheduling inbox; one to anyone else goes out as email (iMIP), when DAV_IMIP_ENABLED is on. Incoming invitations are filed onto the recipient's calendar by the /imip endpoint, but only when our own MTA authenticated the sender.
  • Free/busy inside an organisation, and RSVP from the webmail.
  • Delegation — calendar-proxy-read and calendar-proxy-write, which is what Apple Calendar reads to offer somebody else's calendar in its sidebar.
  • Meeting rooms and equipment that answer for themselves: accept when free, decline when already booked.
  • Read-only .ics subscription links, the one calendar path Outlook can follow.

What does not work yet

  • iMIP is OFF by default (DAV_IMIP_ENABLED=false). Until a deployment decides how its invitations should leave the building, an external attendee is honestly reported as undelivered rather than mailed from an unexpected address. This is the first thing to check when someone says invitations never arrive.
  • Inbound iMIP is not wired into delivery. The endpoint, the pipe script and the Sieve fragment all exist in worker/dav/examples/, but connecting them touches the mail delivery path — a maintenance-window change, not a config tweak. Until then, invitations from outside reach the webmail (which parses the message) but not a phone.
  • Moderated rooms. auto_accept = 0 is honoured, but nothing approves those bookings: there is no approval queue, and organisers are told the room has not answered.
  • Outlook two-way. Outlook does not speak CalDAV, and current Outlook gives an IMAP account no calendar at all. Subscription links (read-only) work everywhere; two-way needs ActiveSync — see ActiveSync, where the backend and router now exist but are off by default.
  • External calendar connect and booking pages. Not built. See the phase PRD.
  • Prometheus metrics. No exporter yet. Liveness is the compose healthcheck; /api/v1/platform/calendar/health and /calendar/stats are the operator's view.

Architecture

flowchart LR
    C["Apple Calendar<br/>Thunderbird<br/>DAVx5"] -->|"HTTPS<br/>dav.&lt;domain&gt;"| T[Traefik]
    T --> D["dav :8105<br/>(Apache + sabre/dav)"]
    D -->|"IMAP LOGIN"| V[Dovecot]
    D -->|"PDO"| M[("MySQL<br/>dav_* tables")]

Endpoints

sabre serves the DAV tree; there are no hand-written REST routes.

Text Only
/dav/principals/<email>/            Principal, and its calendar-proxy children
/dav/calendars/<email>/             Calendar home
/dav/calendars/<email>/default/     The auto-provisioned calendar
/dav/calendars/<email>/inbox/       Scheduling inbox  (RFC 6638)
/dav/calendars/<email>/outbox/      Scheduling outbox (RFC 6638)
/dav/addressbooks/<email>/default/  The auto-provisioned address book
/.well-known/caldav                 301 -> /dav/   (RFC 6764)
/.well-known/carddav                301 -> /dav/   (RFC 6764)
/health                             Liveness, unauthenticated
/imip                               POST: file an inbound invitation
                                    (internal credential; see below)

Plus the JSON surface the webmail uses, on the api service rather than this one:

Text Only
GET    /api/v1/mailbox/calendars                       Calendars in this mailbox
GET    /api/v1/mailbox/calendars/{cal}/events          Events in a time range
POST   /api/v1/mailbox/calendars/{cal}/events          Create
PUT    /api/v1/mailbox/calendars/{cal}/events/{id}     Replace (If-Match by etag)
DELETE /api/v1/mailbox/calendars/{cal}/events/{id}     Delete
POST   /api/v1/mailbox/calendar/free-busy              When are these people busy
GET    /api/v1/mailbox/calendar/invitations            Scheduling inbox
POST   /api/v1/mailbox/calendar/invitations/{id}/rsvp  Accept / decline / tentative
GET    /api/v1/mailbox/calendar-subscriptions          Read-only .ics links
GET    /ics/{token}.ics                                The feed itself (no auth)
GET    /api/v1/calendar-resources                      Meeting rooms (org admin)
GET    /api/v1/platform/calendar/health                Operator view
GET    /api/v1/platform/calendar/stats                 Operator view

Configuration

Variable Default Description
DB_HOST / DB_PORT / DB_NAME / DB_USER / DB_PASSWORD mysql / 3306 / mailserver / — / — MySQL connection. DB_PASSWORD is required; the service refuses to start without it
IMAP_HOST / IMAP_PORT dovecot / 993 Where credentials are verified
IMAP_AUTH_TIMEOUT 10 Seconds before an unreachable Dovecot becomes a 503
IMAP_TLS_VERIFY false Off because this reaches Dovecot by compose service name, which its certificate cannot cover. Set IMAP_HOST to the public hostname and this to true to verify properly
DAV_BASE_URI /dav/ The DAV root
DAV_BROWSER_PLUGIN false sabre's HTML browser. Development only — it lists every collection it can see
DAV_INTERNAL_SECRET — Shared with the api service so it can act for a mailbox it has already authenticated. Empty makes that path inert
DAV_IMIP_ENABLED false Send invitations to addresses outside this server
SMTP_HOST / SMTP_PORT postfix / 10587 Where invitations are submitted. 10587 is the internal listener — compose-network only, and carries ORIGINATING so the message is DKIM signed

DAV_BASE_URL is set on the api service, not this one: it is what /mailbox/capabilities reports calendar from, so a deployment that does not run dav advertises no calendar and the webmail renders no calendar control.

Storage

Ten tables, all prefixed dav_, created by Alembic revision 0031_dav_calendar_schema and transcribed from the schema sabre ships for the pinned version.

sabre is the only writer

The Python services may read these tables. Nothing outside worker/dav may write them. An event written directly to dav_calendarobjects bypasses the scheduling plugin — which is the thing that turns an event with attendees into actual invitations — so an event written behind sabre's back is an event nobody is invited to.

There is no foreign key to email_accounts: sabre addresses a user by principal URI string, and principaluri is VARBINARY, which cannot satisfy an FK against a CHAR(26) ULID primary key. Mailbox deletion therefore does not cascade; cleanup is phase-09 step 6.

Things to know

  • Apache must pass the Authorization header through. It consumes the header for its own auth modules, and sabre reads it from the request. Without the RewriteRule that copies it into the environment (in config/dav.conf), every client sits in an endless 401 loop that looks exactly like a wrong password.
  • Successful credentials are cached for 60 seconds. DAV clients are far chattier than browsers, and without a cache every request would open an IMAP connection. Revocation is therefore not instant here — but Dovecot's own auth cache already delays it by up to an hour, so flush both.
  • Tenancy is enforced in the principal backend. Every principal lookup is scoped to the authenticated mailbox's organization, so a principal in another organization cannot be resolved, listed or searched — not merely denied. That is what makes cross-organization free/busy impossible by construction.
  • Aliases matter. findByUri resolves aliases as well as primary addresses. An invitation sent to an alias whose principal cannot be found is treated as external mail to an address we own, and the RSVP never attaches to the event.