Skip to content

Virtual domains and mailboxes (database-backed)

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.

  • Postfix (docker/postfix/sql/*.cf): virtual_mailbox_domains ← v_virtual_domains (only domains routed local); virtual_mailbox_maps ← v_virtual_mailboxes (returns the Maildir path); virtual_alias_maps ← v_virtual_aliases, which unions explicit aliases, forwarders (mailbox → itself + destination when keep_copy), and catch-alls (@domain), each only for domains routed local. Delivery goes to Dovecot over LMTP on port 24.
  • Dovecot (docker/dovecot/dovecot-sql.conf.ext.tmpl): passdb ← v_dovecot_credentials (user, password, and the app_token the query selects on), userdb and iterate ← v_dovecot_users (home, uid, gid, quota_rule). The passdb query additionally refuses any identity or IP listed in v_dovecot_auth_bans. Passwords are Argon2id with the {ARGON2ID} prefix (packages/db/src/password.ts). quota_rule is *:bytes=N, or *:bytes=0 for unlimited, never NULL or ''.
  • Dovecot → Postgres (docker/dovecot/dovecot-dict-quota.conf.ext.tmpl): the quota_clone plugin writes priv/quota/storage and priv/quota/messages into table quota_usage (keyed by login name = mailbox address) through the dict service (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:4190 as the master user mailserver-api (/etc/dovecot/master-users, hashed by the entrypoint from MASTER_PASSWORD = DOVECOT_MASTER_PASSWORD in .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_users and v_dovecot_credentials, so Postfix stops accepting its mail and its logins fail; some rows in v_virtual_aliases can 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 (volume vmail), uid/gid 5000.
  • 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_usage rows 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 by reject_sender_login_mismatch on the submission ports.
  • Server-level configuration (hostname, TLS, limits, relay, spam thresholds) is no longer static: the API renders it from ServerConfig and the in-container watcher applies it. Per-mailbox data still flows live through the views — no render on mailbox changes.