★ 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/caldavand/.well-known/carddavanswer 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_ENABLEDis on. Incoming invitations are filed onto the recipient's calendar by the/imipendpoint, 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 = 0is 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/healthand/calendar/statsare the operator's view.
Architecture¶
flowchart LR
C["Apple Calendar<br/>Thunderbird<br/>DAVx5"] -->|"HTTPS<br/>dav.<domain>"| 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.
/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:
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
Authorizationheader through. It consumes the header for its own auth modules, and sabre reads it from the request. Without theRewriteRulethat copies it into the environment (inconfig/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.
findByUriresolves 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.