Virtual domains and mailboxes (database-backed)
Purpose
Section titled “Purpose”Domains, mailboxes, aliases, forwarders and catch-alls are rows in Postgres. Postfix and Dovecot read them live through views, so creating a mailbox is an insert — no config file is regenerated and no daemon is reloaded.
Behaviour
Section titled “Behaviour”- Postfix (
docker/postfix/sql/*.cf):virtual_mailbox_domains←v_virtual_domains(only domains routedlocal);virtual_mailbox_maps←v_virtual_mailboxes(returns the Maildir path);virtual_alias_maps←v_virtual_aliases, which unions explicit aliases, forwarders (mailbox → itself + destination whenkeep_copy), and catch-alls (@domain), each only for domains routedlocal. Delivery goes to Dovecot over LMTP on port 24. - Dovecot (
docker/dovecot/dovecot-sql.conf.ext.tmpl): passdb ←v_dovecot_credentials(user,password, and theapp_tokenthe query selects on), userdb and iterate ←v_dovecot_users(home,uid,gid,quota_rule). The passdb query additionally refuses any identity or IP listed inv_dovecot_auth_bans. Passwords are Argon2id with the{ARGON2ID}prefix (packages/db/src/password.ts).quota_ruleis*:bytes=N, or*:bytes=0for unlimited, never NULL or''. - Dovecot → Postgres (
docker/dovecot/dovecot-dict-quota.conf.ext.tmpl): thequota_cloneplugin writespriv/quota/storageandpriv/quota/messagesinto tablequota_usage(keyed by login name = mailbox address) through thedictservice (quota_clone_dict = proxy::quotaclone). This is the only table the daemons write; the API reads it for/mailboxes/{id}/usage. - API → Dovecot, over ManageSieve: mailbox filters are pushed to
dovecot:4190as the master usermailserver-api(/etc/dovecot/master-users, hashed by the entrypoint fromMASTER_PASSWORD=DOVECOT_MASTER_PASSWORDin.env;auth_master_user_separator = *; the master passdb accepts it only from loopback and the container’s own Compose subnet, written by the entrypoint). Sieve scripts are stored in the mailbox home, not in Postgres. - Deactivating a domain or tenant, or suspending a mailbox, takes effect immediately without touching the daemons. A deactivated domain drops out of
v_virtual_domains,v_virtual_mailboxes,v_dovecot_usersandv_dovecot_credentials, so Postfix stops accepting its mail and its logins fail; some rows inv_virtual_aliasescan remain (a forwarder is filtered on its mailbox, not its domain, and nothing there checks the tenant), but Postfix no longer accepts the domain. A suspended mailbox leaves the mailbox, user, credentials and forwarder views. - Mail is stored as Maildir under
/var/vmail/<domain>/<local>/Maildir(volumevmail), uid/gid 5000.
Configuration
Section titled “Configuration”Limits & edge cases
Section titled “Limits & edge cases”- Addresses are stored lower-cased; the API is responsible for normalising input.
- Catch-all is consulted only after an exact address miss (Postfix semantics).
quota_usagerows are written only when Dovecot touches the mailbox;doveadm quota recalc -u <address>forces one (needed after a Maildir restore).
Sender-login maps and the supplementary lookups
Section titled “Sender-login maps and the supplementary lookups”docker/postfix/sql/sender-login.cf—smtpd_sender_login_maps: which SASL login may use an address as envelope sender (the mailbox itself, or the destinations of an alias). Used byreject_sender_login_mismatchon the submission ports.- Server-level configuration (hostname, TLS, limits, relay, spam thresholds) is no longer static: the API renders it from
ServerConfigand the in-container watcher applies it. Per-mailbox data still flows live through the views — no render on mailbox changes.