oximail subcommand. This page maps the surface; oximail <command> --help is always the authoritative reference for flags, and the per-feature pages explain the workflows.
Conventions
--config <path>— every command that touches the database takes it; default/etc/oximail/oximail.toml.--tenant-id <id>(alias--tenant) — defaults todefault. When a command takes--account <email>instead, the email lookup resolves its own organization.- Account selection — commands accept
--account <email>or--account-id <id>, mutually exclusive. - Destructive commands prompt.
[y/N]confirmation, skipped only with an explicit--yes(for scripts). - Repair commands are dry-run by default.
dedupe-folders,dedupe-tasks,blob promote-room-blobs, and their kin print their plan and touch nothing until--apply. - A closed pipe ends the verb like any Unix tool. A verb writing into a pipe whose reader has gone (
oximail bimi backfill | head) stops on SIGPIPE, not on a panic with exit 101; the server itself keeps ignoring the signal.
The command map
Serve & setup
| Command | Purpose |
|---|---|
serve | Run the server (what the systemd unit calls). |
setup | The first-boot wizard, full screen on a capable terminal (--tui / --no-tui force one rendering or the other); subcommands dns, dkim, admin, verify, relay-preflight re-run single steps. |
setup health-timer [--apply] | Install or refresh oximail-health.service and oximail-health.timer, which record the doctor round every 15 minutes as the service user (doctor --json --record), for a box installed before the timer existed; --apply writes the two units and enables the timer. |
setup-dkim | Generate a DKIM keypair and print the DNS record. |
firstboot, print-systemd-unit | Headless provisioning helpers. |
setup dns --domain … --hostname … [--cloudflare-api-token-file …] [--replace-mx] [--replace-spf] [--replace-dmarc] [--public-ipv4 auto|forwarded|none] | Re-run the DNS step: reads the zone first and creates only what is missing, never doubles a record; runs without a terminal, where a question no flag answered keeps the existing record (exit 1) and an undecided public IPv4 stops before anything is published (exit 2) (publishing). |
check-dns | Live DNS scoring for a mail domain (email auth); --hostname (default: [server] hostname) is the server’s own name its MX, SRV and PTR records must carry, and a backup MX is asked only for what it needs. |
check-config | Validate a TOML file without starting the server: an unknown key is an error, a loopback listener behind an unrecognised proxy and a backup MX without a relay spool are refused. |
schema check | Does this binary accept the database? Exit 0 yes, 2 newer or unreadable; read-only. Run with the previous binary before a rollback (operations). |
Accounts & organizations
| Command | Purpose |
|---|---|
account create / create-shared / list | Provision human accounts and shared team accounts. account create --service declares a service account (a connector, not a person): its grants carry no expiry, doctor never counts it as a departure, and its role stays member. |
account set-password / set-role / enable / disable / quota | Lifecycle and limits. enable --role refuses a role the server does not know rather than reporting the account enabled while it stays blocked. On a disabled account, set-role (like the REST role route and Principal/set over JMAP) is the reopening: it goes through enable, so an account held by a policy is refused and the directory, the mandates and the card come back. |
app-password create|list|revoke | App passwords of an account; create takes a mandatory --scope, and --read-only makes a password that opens JMAP only, where every writing method answers accountReadOnly whatever the scope (IMAP, SMTP, CalDAV/CardDAV and SMB refuse it at login). |
account delete [--dsar <ref>] | Enqueue the erasure phase machine. |
account custodian show|reattach | Shared-drive custody. |
account offboard --account … --forward-to … [--grant-mailbox-to … --grant-until DATE] [--apply] | A leaver in one command: full freeze (tokens revoked, scheduled sends cancelled, iTIP purged), the address redirected to a colleague by an exact alias that outranks the frozen account at delivery and never expires on its own, an optional read grant on the mailbox for the successor (at most one year), and the distribution groups the leaver belongs to named, not changed. With --grant-mailbox-to, the verb reminds that the grant opens every folder of the leaver’s mail, a private one included, and points to the law where you operate (privacyReminder, and privacyReminderNational when a national variant carries one). Dry-run by default, --json for supervision. |
account grant --owner-email … --grantee-email … --collection … --level readOnly|readWrite [--until DATE] [--apply], account revoke | The operator’s delegation of a whole collection of one account to another (account grants): under [rights] enforce it opens every object of the collection over JMAP only, and the verb says so (opensObjects, faces in --json). --collection Contact is refused, since JMAP serves no contacts on a shared account. Dry-run by default; revoke is guarded by --yes. |
account import --from-backup <repository> --identity <file> --account <address> [--into <address>] [--apply] | Bring one account’s mail, contacts, events, tasks, Drive files and notes back from a backup, beside what it holds, under Restored <day> (without a hypervisor). Dry-run by default. |
account relay add-domain|verify|list|remove-domain|mint-token|revoke-tokens | Smarthost relay accounts: the proven domains of the tenant a relay account may send for, and the one-time enrolment tokens. |
account envelope-domain add|verify|list|remove | Envelope-sender domain grants (Return-Path ≠ From, proven by the domain’s MX naming this server). Both list verbs show a domain proven once but not re-verified for 72 hours as stale, and their --json carries last_verified_ok_at and proof_stale. |
account signing-domain add|verify|list|remove | Delegated DKIM signing: the relay signs d= the client’s exact domain behind two CNAMEs it publishes. add is a dry run until --apply, remove needs --yes, verify exits 1 until the key signs; verify and list take --json. |
account flow create|list|show|set-app-password|set-pool|suspend|resume|delete | Sending flows: the unit that sends, chosen by the credential of the session. Every writing verb is a dry run until --apply (--yes for suspend and delete); set-pool names the egress pool its mail leaves from, --none for the default address. |
account dedupe-folders / dedupe-tasks | Post-migration repairs (groupware). |
tenant create|update|list|delete | Organizations; update --default-timezone sets the calendar fallback zone, update --default-language en|fr|de|it (or --clear-default-language) the language an account inherits (multi-tenancy). delete removes an empty organization only: dry-run by default, --force deletes, and an organization that still holds accounts or tenant blobs is refused whatever the flags (exit 2). |
tenant add-domain --tenant-id … --domain … [--kind web|mail|both|send] [--primary] / tenant remove-domain --domain … --yes | Attach a domain to an organization. --kind send is a domain used to send (a newsletter or bounce sub-domain) that hosts no mailbox and needs no MX or autoconfiguration names. A sender-domain proof (relay, return path, delegated DKIM) can only be declared on a domain registered here, and a domain that carries proofs is not removed from under them (outbound relay, bulk sending). |
tenant identity show|set | The organization’s declared legal identity: kind, legal name, postal address, country and subdivision of establishment, supervisory authority, contacts; show gives each effective value with its source, set replaces the fields it names (--clear <field>), dry-run by default, --apply writes one audit line. |
admin, alias, rights, token, sessions | Admin principals, address aliases, rights inspection, token and session management. rights explain --object … --account … lists the rights an account grant gives on that object by the server’s own translation, rights audit flags a grant row written as admin with the level applied (appliedLevel). |
admin organization list|set|create|set-entity | The tenant’s entities (the employers a profile card may cite): list sorts the organizations the cards carry (free text, sound references, dangling ones) and shows the state of the account behind each card, set attaches a free-text spelling to an entity, set-entity --group <id> --value true|false declares or withdraws an entity, create --tenant-id … --name … [--email …] makes a group already declared an entity, with its own backing account and, without --email, a generated address that receives nothing until it is enabled. Dry-run by default, --apply writes. |
admin contact normalize-phones [--tenant-id …] [--limit N] [--apply] [--json] | Puts the phone numbers already stored into their international form, the form every writer now stores: a national number only in an organization that declared its own country, a number that is not valid never. The report counts by shape and never prints a number; it names what it left as written (foreignLayout, ambiguous, severalNumbers, …). Dry-run by default; --apply writes each card in one transaction with its history version and its change line, and --limit counts the cards written (1000 by default). Exit 1 while numbers remain or a card failed. doctor contact_phone_form warns while numbers remain. |
admin instance identity show|set | Who operates the instance, the countries where its data is processed (--processing-country, the main server’s first) and the basis of each transfer (--transfer-basis CC=basis): the facts every organization inherits unless it declares its own. Written at the console only; the dry run lists the organizations whose effective identity would change. |
admin instance grant|revoke|list | Instance operators, the accounts that may read the periodic health report across every tenant of the box: grant --account … --by <name> is refused for an account that is not a human admin of its tenant or has no passkey, revoke takes effect within 30 s; dry-run by default, --apply writes. The console is the only writer. |
principal-reconcile | Offline one-shot: re-mint principal ids from the legacy import cohort (--dry-run supported). |
Mail, filtering & groupware
| Command | Purpose |
|---|---|
mailbox, email | Inspect and manage folders and messages. mailbox recount --account … | --all recomputes the folder counters from the stored mail; email reindex-bump [--all] queues messages for the background reindex worker, which derives their preview and search document again (by default those without a preview, every message with --all; --unindexed makes due, for the running server’s search catch-up, the messages absent from the committed index). email normalize-received-at rewrites the received dates an old import stored in another format than RFC 3339 UTC (they sorted ahead of the day’s mail in a newest-first mailbox); dry-run by default, --apply writes, exit 1 when a value could not be read. email backfill-encrypted [--account …] [--apply] gives the $encrypted keyword to the encrypted messages delivered before the server set it (keywords). |
smime verify|backfill | Received S/MIME signatures: backfill marks the messages stored before verification existed so the running server verifies them, verify verifies them again after a trust-store change. The whole box, or one --account; dry-run by default, --apply writes. |
bimi backfill --account … | Give the messages already in a mailbox the brand logo the resolver would have given them (configuration); dry-run by default. |
rule | The account’s filter Rules. rule forward-confirmation list --address … finds every confirmation of a forwarding destination in every organization, with the fingerprint the audit lines carry; revoke --address … --apply removes them and blocks the address for good, at its holder’s request (forwarding to a confirmed address). |
queue | The outbound delivery queue: list, inspect, cancel. |
spam | Spam corpus and model management, plus the read-only spam check (anti-spam). |
calendar | Calendar maintenance verbs. |
migrate | The import tool (--from-imap, --from-jmap, --from-dump, --from-mbox, --from-google-takeout, --from-vcf, --from-ics). |
Compliance
| Command | Purpose |
|---|---|
journaling list|add|remove|enable|disable | Journaling rules. |
erasure list|undo|prove|replay | Erasure jobs, the reversal buffer, DSAR proof, and the replay of the erasure ledger after a restore (dry-run by default, --apply; exit 1 while erased people are back). The completion record prove --dsar reads lives twelve months; when it finds nothing, the verb names the three readings (not completed, run without --dsar, or completed and purged, the tombstone remaining without a reference). |
audit-log list|verify|attest | The admin journal: every administrative act, chained per account and in the tenant’s own chain. |
access-log list|verify | The data-access journal (opt-in per tenant): who read, shared or exported another person’s data. |
journal list | Both journals read together, newest first, for a person: select by --account, by --object-type and --object-id, or by --actor with --since. Each line names its journal, the accounts by address, the client address and the device session. It proves nothing: the two verify verbs do. |
Federation
The federation verbs. Each one that writes is a dry run until--yes (identity, peers, grants) or --apply (shares, mirrors); --tenant is needed when the box hosts several organizations.
| Command | Purpose |
|---|---|
federation identity [--realm <domain> --custody <labels>] | Show this organization’s identity: the root fingerprints, the jurisdiction (its declared country of establishment) and the transport fingerprint of [federation]. The first run with --realm is the whole ceremony: it mints the sister roots (--roots, 2 to 5, three by default), prints their paper keys once, and certifies a working key (--valid-days, 365). |
federation pair --realm … --key … --jurisdiction CC [--endpoint … --tls-pin … --address …] --transport-fingerprint … --yes | Pin a peer organization: every root it read out (--key repeats), its jurisdiction, its endpoint and TLS pins, an optional tunnel address. --yes is refused unless the transport fingerprint the other administrator read out is confirmed. |
federation peers list|unpair|suspend|resume | The peerings, with their state and recorded transport; unpair suspends and never deletes, suspend --cause names the missing fact, resume lifts an administrative break only. |
federation grants list|revoke | The availability grants, with their last use; an administrator may revoke one (--id), never create one. |
federation shares add|list|remove | Share one Drive folder or file, for reading, with a paired organization (--owner-email or --owner-account-id, --path or --node, --realm, optional --expires-at); refused by name (drive-root, node-trashed, peering-not-active, already-shared, …). --json on each. |
federation mirror add|list|remove | The mirror account that shows, read-only, what a paired organization shares with this server (--email, --realm, --display-name); remove withdraws the capability and keeps the account. |
federation working-key rotate|repair | Rotate the working key with one root paper key on standard input; repair --realm … re-certifies an identity whose chain no longer verifies. |
federation roots enrol|retire|delete | Add a sister root (--custody), retire a compromised one (--root; its escrow is erased), or delete the whole identity (--tenant-id … --realm …, after which every peer pins again). |
Security & TLS
| Command | Purpose |
|---|---|
ban list|add, unban <ip> | The in-binary fail2ban. |
cert, dane, dkim, tls-rpt, vapid | Certificates, TLSA records, DKIM keys, TLS-RPT, web-push keys. |
resolver check [--json], resolver ensure [--apply] | The box’s local validating resolver (email auth): check runs the server’s own probe, server stopped or not (exit 0 reliable, 1 unmeasured, 2 unreliable or mute); ensure compares the unbound configuration with the one this binary writes, and --apply rewrites it (identity kept) and restarts unbound. |
dane suspend [--for <span> --reason <text> | --end] [--yes] | While the resolver is not proven, mail to the domains known to use DANE waits; --for (1m to 7d) lets it leave without DANE, each delivery logged; --end ends it; bare, shows the state. Dry-run without --yes. |
dane rotate | Change the served TLS key without a DANE failure window: --apply stages a new key and publishes its 3 1 1 pin beside the current one (cloudflare or rfc2136 provider with DANE on), or prints the records to publish by hand; the server switches once the pin has been visible in public DNS on every pinned port (the SMTP ports 25/465/587, 25 alone on a backup MX) for two TTLs. Run it again to see where the rotation stands, --cancel --apply abandons it before the switch. Needs [tls] acme_enabled. Exit 0, 1 when the pin is not published on every port, 2 refused or failed (TLS & ACME). |
key | At-rest key operations (rotation, escrow drill) — see encryption at rest. key escrow --guided replays the wizard’s recovery ceremony on an installed box (the recovery key shown once, typed back, the drill recorded, the scp that takes the blob off the box); key upgrade-escrow --yes turns a v1 escrow into one that also carries the instance secret, under the same recovery key; key adopt-identity --yes accepts a changed instance secret that doctor reported. |
hash-password | Argon2id hash for config files. |
config get-override|list-overrides|set-override|clear-override | Per-tenant runtime overrides (ADR-007): read one, list all, set (dry-run by default, --apply; the value is JSON-typed, and the verb prints the resulting type so a "true" string is never mistaken for a boolean), clear. Unknown tenant refused before the dry-run; a value the admin API would refuse (a trash horizon out of range, negative days, a non-boolean switch, an unknown IMAP namespace mode) is refused too, dry run included, exit 2 with the same details (configuration). |
config externalize-secrets | Moves the secrets still written inline in the TOML ([srs] secret_key, previous_secret_key, [meet] turn_shared_secret) into 0600 sidecars owned by the service user and rewrites each line to its _file form; the server refuses to boot on an inline one. Dry-run by default, --apply writes after saving the TOML; an existing sidecar with a different value is never overwritten; the result is re-checked by every boot-time validator (configuration). |
passkey list|revoke | Enumerate and revoke an account’s WebAuthn passkeys — the out-of-band door when the webmail itself is what is compromised. |
Storage & maintenance
| Command | Purpose |
|---|---|
backup, restore, verify-backup | Backups; restore refuses a backup made by a newer binary and a sealed blob from before ADR-148. Without a hypervisor: backup --to <repository> (a directory or s3://bucket/prefix under Object Lock, checked before each run; age-encrypted, incremental; --recipient, --dry-run, --json), restore --from <repository> --identity <file> [--expect-sha256 <hash>] [--with-config], verify-backup --from <repository> [--identity <file>] [--full [--record]] (without a hypervisor). |
backup prune --repository <dir> --identity <file> [--keep-daily|--keep-weekly|--keep-monthly N] [--apply] | Apply a retention to a backup repository, where the identity lives (never on the box). Dry-run by default. |
backup key generate --out <file> [--apply] | Write the age identity that opens this box’s backups (0600, never over a file), with the box’s snapshot-authentication key; move it off the box. |
backup mark --source hypervisor|external [--apply] | Record that something outside the box took a copy of it (the hypervisor’s snapshot, from its vzdump hook at backup-end, or another tool on the host), in the marker doctor backup_plan reads. Dry-run by default. |
relay-spool init|gc|reabsorb | The relay spool: create its root on the mounted volume, collect orphans (dry-run by default), retire it. |
drive share | Grant or revoke rights on one folder or file of a team’s drive toward a principal (a scanner’s service account), dry-run by default; account grant --collection FileNode opens the whole drive, this opens one node. rights audit sees files too. |
drive purge-trash | Destroy what the Drive’s trash holds past a horizon, the one exit the trash sweep also uses, respecting legal holds and announcing what it removed. Without --days, each tenant’s own horizon, as the sweep does: its drive.trash_retention_days override, else [drive] trash_retention_days (30); an override of 0 purges nothing. --days sets one horizon for the whole scope, 0 empties the trash. --json adds horizonSource (flag or policy) and tenantHorizons (per-tenant horizons). The four other trashes (tasks, contacts, events, notes) go through the same exit. |
tenant update --max-file-versions N | Per-tenant file version retention, applied hot (default 10, 0 disables history, instance ceiling 100). |
calendar availability, calendar backfill-end-dt | Replay an availability answer for an incident; give pre-existing events their stored end (calendar). |
reindex [--tenant <id>] [--json] | Rebuild the full-text index from the stored mail, notes and chat messages (contacts are not indexed), with the server stopped: refused while a server holds the index. Settles any index an earlier rebuild set aside, then indexes in batches of 2 048 rows; --tenant keeps the index and rewrites that tenant’s documents. Exit 0 complete, 1 rows still due, 2 refused or failed (the search index). |
blob | Blob maintenance: verify (--walk starts from the disk, --quarantine marks a file whose hash no longer gives its name), the unreferenced-blob collector blob gc (dry-run by default, --apply deletes, server stopped), promote-room-blobs. |
sql | A guarded SQL escape hatch on the plain SQLite database. |
backfill-*, decode-attachment-names | One-shot backfills for columns introduced after the data existed. |
volume-recover, rollback-guard | Disaster-recovery guards. |
chat-rekey | Chat identity re-keying machinery. |
chat | Chat maintenance: purge-trash and restore-room for the room trash (restore-room --from-backup takes a data.db or a backup repository with --identity) (purge-trash without --days applies each tenant’s horizon like drive purge-trash, with chat.trash_retention_days), recount, scan-hrefs. |
Diagnostics
| Command | Purpose |
|---|---|
status, health, doctor | Local process / port / config / storage checks, clock health included. doctor loads the extension sections, presents the metrics token to /metrics, verifies every audit chain, measures the volume (at_rest), the relay spool, the DST horizon and the rotation leftovers; an extension can add checks to the same round rather than open a second one. doctor --json gives each check its family, its dated facts and, on a warning or a failure, the remedy verb (operations); --reachability adds the network probes; --record stores the round for HealthReport/get (the timer’s job, refused as root). |
telemetry status|enable|disable|purge|id | The community reports: status [--json] prints the last report sent byte for byte and exits 1 when the channel cannot work (no answer recorded, last push failed, withdrawn, refused endpoint); enable [--crashes-only] and disable record the answer, --apply writes, no restart; purge --apply asks the receiver to delete what this installation sent; id --show-id prints the id the reports carry. |
setup verify | Exit 0 / 1 degraded / 2 failing; names who renews the certificate and what the box must reach. |
log level, log elevate --directives … --duration-secs N, log revert | The log level in force, a bounded elevation (60 to 3600 s, reverts on its own), the early return. These need the server running: the level is process state, not something on disk. log tail renders the journal one readable line per event, episode key first, and reads journald instead. |
log trace --account … | --peer-ip … --duration-secs N, log trace-status, log trace-stop | The protocol trace projector (ADR-140 R2): one account or one peer, 60 to 900 s, 0600 on the tmpfs, redacted, purged on stop (admin API). |
erasure list --json | Every erasure job with its phase, timestamps, error and persisted counters (additive fields); a store read error exits 2. |
Two behaviours of the maintenance verbs
sql routes on the shape of the statement, not on its first word. It used to sniff the prefix, so a CTE — WITH … SELECT — was taken for a mutation, executed, its rows thrown away, and reported as 0 rows affected. A statement that returns data being reported as a successful write is exactly the silent failure this project forbids, and it is worse on a verb that scripts trust. Each statement is now prepared and routed on whether it returns columns, which also gets VALUES, EXPLAIN and INSERT … RETURNING right, while a WITH … INSERT stays on the write path and is counted. Multi-statement input keeps batch semantics (everything runs, it stops at the first error, earlier writes stand) and now counts per statement. Result rows go to stdout, counts to stderr, so the output stays pipeable.
blob gc will not collect a fresh unreferenced blob. Between a client’s upload and the /set that names it, a blob is unreferenced by protocol (RFC 8620 §6.1). The collector skips anything unreferenced and younger than a 24-hour grace window, read from the file’s own timestamp — an unreadable or future timestamp keeps the file. The summary counts “unreferenced but in grace” separately, so the number is visible rather than implied.