The model: TOML baseline, DB overrides
OxiMail does not store its whole configuration in the database. It reads a TOML file at boot as the baseline, and layers optional per-organization database overrides on top for runtime tuning without a restart. This is ADR-007. The lookup order for a setting is:- Database override (if one exists for this key) — wins.
- TOML file — the fallback, and the source of truth for everything else.
cat, versioned in
git, copied with scp, and edited over SSH. A full-database config cannot, and
it creates a chicken-and-egg problem: the database has to be configured before
you can read the configuration that configures the database. OxiMail keeps the
file as the authoritative baseline and uses the database only for runtime
overrides.
The TOML file always remains the source of truth for bootstrap and disaster
recovery. If you lose the database overrides, the server still boots from the
file. The reverse is not true.
Bootstrap vs runtime settings
Not every setting can be a database override. Anything OxiMail needs before the database is open has to come from the file. That is the chicken-and-egg rule:Never put bootstrap configuration in the database only.Bootstrap settings (TOML only) — needed before or in order to open storage and start listening:
[storage]sqlite_path,blob_path,search_path,encrypted— where and how the database itself lives.[server]bind,http_bind,admin_bind,hostname,base_url— the listening sockets and identity.[smtp]bind,submission_bind,smtps_bind,hostname— the SMTP listening sockets.[auth]default_tenant.
auth.session_lifetime_secs,
auth.refresh_reuse_leeway_secs, erasure.reversal_ttl_days, blob_gc.derived_sweep,
retention.trash_days (per tenant, from 1 day), and the rate_limit.* ceilings
(HTTP per IP and per account, login per account, the anonymous-surface budgets,
SMTP submission per tenant, inbound per IP, relay recipients per day, outbound
recipients per hour). retention.chat_days and calendar.working_hours_enforce are in the
set too, and so are the six trash horizons, drive., calendar., contacts., tasks.,
notes. and chat.trash_retention_days (per tenant; absent means the instance value, 0
means no purge, see per-tenant horizons). Fail2ban thresholds and [spam] options are not
overridable; they take a restart. See Setting a runtime override.
Secrets live outside the TOML
The configuration file is tracked, backed up, copied to.bak-* by the deploy
rail and quoted in journald in cleartext. No secret sits in it: every secret is
a sidecar file the TOML points at, one line, mode 0600, owned by the
service user, unreadable or empty is fatal at boot. The sidecar keys are
[[smtp.dkim_keys]] key_path, [[smtp.transport_maps]] auth_password_file,
[dns] cloudflare_api_token_file, [dns] rfc2136_tsig_key_file, [admin] metrics_token_file,
[srs] secret_key_file and previous_secret_key_file, and
[meet] turn_shared_secret_file.
The server refuses to boot on an inline [srs] secret_key,
previous_secret_key, [meet] turn_shared_secret or [dns] rfc2136_tsig_key, and the refusal names the
move to make. Before swapping in a binary that carries the refusal, run it on
each box:
0600 sidecar next to the TOML
(srs.secret, srs.previous.secret, turn.secret, rfc2136.tsig), hands it to the service
user, saves the TOML as oximail.toml.pre-externalize-<ts>, rewrites only the
matching lines to their _file form, and re-runs every boot-time validator on
the result. An existing sidecar holding a different value is never overwritten.
oximail doctor then checks that the service user can read every sidecar.
Hot-reload
Runtime overrides are designed to take effect without restarting the process. The mechanism isArcSwap, a lock-free atomic pointer swap
(ADR-016): the component holds an Arc to its current
state, and a reload atomically swaps in a new Arc. Readers are never blocked —
in-flight requests finish on the old value, and every subsequent read sees the
new one. This is used for the hot-reloadable components: TLS certificates (so an
ACME renewal applies without a restart), the spam blocklist, runtime config
overrides, and compiled Sieve scripts.
Because reads are never locked, hot-reload has no measurable cost on the request
path: a renewal or an override is a single pointer swap, and reloads are rare.
Setting a runtime override
Runtime overrides are stored per organization (a tenant in the storage model) and set through the admin REST API. See the Admin API for authentication (every admin endpoint requires the admin bearer token configured under[admin] token).
To set one or more overrides, PUT /admin/v1/config with a JSON body whose
overrides object maps each config key to its new value:
PUT /admin/v1/tenants/{id}/config takes the same body for another tenant
(admin API). Values keep
their JSON type (a number stays a number, a string stays a string). A key outside
the published set is refused with unknownKeys and the knownKeys list, never
stored: a typo cannot become a silent setting. A value the server would not read
as written (a trash horizon outside 0 to 36500, for example) is refused with
details, and nothing in the request is written.
GET /admin/v1/config returns the overrides in force and knownKeys. From the
CLI, oximail config get-override|list-overrides|set-override|clear-override
read and write per tenant (--tenant-id), dry-run by default, --apply writes
(CLI); set-override refuses the values this endpoint refuses, dry run
included, with the same details and exit 2.
The config file and its shape
The server reads/etc/oximail/oximail.toml by default. CLI subcommands and the
serve command accept --config <path> to point elsewhere. The
setup wizard writes this file for you on first boot; this page is
for tuning it afterwards.
A representative annotated file looks like this. It is not exhaustive — it
shows the shape and the most common sections. See the per-feature operator pages
for the full set of keys in each area.
The real top-level sections
The sections the server reads are:[server], [storage], [auth],
[mode], [smtp] (with the [[smtp.dkim_keys]], [[smtp.transport_maps]] and
[[smtp.egress_pool]] arrays, the last one in bulk sending), [tls], [spam], [security], [rate_limit]
(with the [rate_limit.destination_domains] table), [dmarc_reports],
[feedback_reports], [complaint_feedback] (bulk sending), [contacts], [quota], [branding], [rights],
[logging], [admin], [telemetry], [backup], [erasure], [geocode], [image_proxy],
[pgp_directory], [legacy], [passkey], [mta_sts], [dns], [sieve],
[distribution_groups], [scheduler], [chat], [calendar], [drive],
[tasks], [notes], [meet], [srs], [metrics], [push], [bimi],
[smime] and [federation]. An unknown key inside one of them refuses the file; a section outside
the list is logged as dead configuration at boot, unless an extension the binary
carries claims it.
The per-feature pages document the keys inside each:
- Mail & SMTP —
[smtp], transport maps, DKIM keys. - Email authentication & security —
[mta_sts],[dns],[srs];[smime](verify,revocation,trust_roots,trust_intermediates,sweep_secs: verifying received signatures);[pgp_directory](OpenPGP key lookup). - Operations —
[backup] strategy(backups),[telemetry](community telemetry),[admin] metrics_token_file. - Compliance —
[erasure] ledger_hook(erasures and restores). - Anti-spam —
[spam]. - Mailboxes & storage —
[storage]. - Encryption at rest —
[storage] encrypted. - TLS & ACME —
[tls]. - Legacy protocols —
[legacy]. - Filtering —
[sieve](the operator’s global script). - Multi-tenancy —
[auth],[chat]capacity caps, and organization overrides. - Federation —
[federation](below). - Chat —
[chat] transcript_policy, who may make the server transcribe a voice message:"on-demand"(the default, the first member who asks),"always"(at ingestion, by an engine only an extended build carries) or"never"(ChatMessage/transcribeanswersforbidden). - Compliance — journaling rules (stored in the database, managed by CLI/JMAP, not TOML) and the
erasure.reversal_ttl_daysper-organization override. - JMAP Core / authentication — the
[auth]DPoP knobs (dpop_nonce_required,dpop_resource = "off" | "monitor" | "require").
[bimi] — sender brand logos from external senders
Since 2026-09-20 the server resolves the brand logo of an external sender
whose message passed DMARC under an enforcing policy and whose domain
publishes a brand indicator backed by a mark certificate the server
validates itself (chain to an embedded set of roots, revocation lists,
certificate transparency). The logo reaches clients as the same
brandLogoBlobId property that already carries your own organization’s
logo for internal senders; internal senders keep yours. Next to it,
brandMarkType names the kind of certificate behind an external logo
(registered_mark or government_mark for a Verified Mark Certificate;
prior_use_mark, modified_registered_mark or provisional_mark for a
Common Mark Certificate; null for your own logo), so a client can show
a verified-brand mark for a VMC only. A BIMI-Selector
header is honoured only when the DMARC-aligned DKIM signature that passed
covers it; a sending domain resolves at most eight selectors a day.
bimi.external_resolution_enabled
(default on) switches the feature off for that organization only: its
messages stop being resolved AND its links already written stop being
served. oximail doctor reports the trust base (bimi_trust: every root,
issuing CA or log key that stops being usable within 90 days is named) and
the caches (bimi_cache). A trust file you name that does not load is
fatal at boot — the embedded base is never substituted for it silently.
The resolver works at ingestion: messages that were already in a mailbox
when it started carry no logo. oximail bimi backfill --account <email>
reports, per sender domain, which stored messages of that account would
qualify (same gates as the live path, no network call); --apply resolves
and links them. One account per run.
[federation] — the door paired organizations call
Off unless the section is written, and then enabled is required: a
[federation] without it is refused, never read as off. On, it mounts
/jmap/federation (busy time and shared Drive folders for paired
organizations) and the endpoint and tls_pins are what oximail federation identity reads to the other administrator (federation).
enabled = true the server refuses to start without an endpoint at
/jmap/federation, with a cache setting out of its bounds, or when a peer’s
recorded address falls inside [security] trusted_ips. check-config does not
run these checks; doctor reports the cache bounds (federation_content_cache).
Validating a config before you deploy
OxiMail fails loud on a bad config: rather than booting in a half-broken state, it refuses to start and prints what is wrong. You can run the same validators ahead of time without starting the server:dane_enabled
set with no DNS provider, and deprecated pre-v0.30 DKIM keys still in the file.
See the CLI reference for the full command set and
Operations for the day-to-day workflow.
Next steps
- First boot & setup wizard — what the wizard writes into this file.
- Admin API — the endpoint that sets runtime overrides.
- Operations — logs, backups, and upgrades.
- CLI reference — every
oximailsubcommand.