Actions
55 actions, callable from the panel, the command palette and the API as
POST /api/v1/a/<id>. Internal actions used between modules are not listed.
| Action | What it does | Risk | Preview |
|---|---|---|---|
mail.server.enable | Enable mail on a server | critical | |
mail.hostname.ensure | Certificate for a mail hostname | medium | |
mail.server.config.apply | Apply mail server settings | high | |
mail.config.status | Hand-edited mail files (read) | low | No dry run |
mail.config.adopt | Keep a hand edit on top of the panel's configuration | medium | |
mail.config.reapply | Render the panel's version again | medium | |
mail.config.reset | Reset a file to defaults | medium | |
mail.config.unmanage | Hand a file off to the administrator | high | |
mail.server.selftest | Mail server self-test (read) | low | No dry run |
mail.server.get | Mail server settings (read) | low | No dry run |
mail.reconcile | Reconcile mail servers | medium | No dry run |
mail.domain.add | Add a mail domain | medium | |
mail.domain.list | List mail domains (read) | low | No dry run |
mail.domain.get | Get a mail domain (read) | low | No dry run |
mail.domain.delete | Remove a mail domain | high | |
mail.domain.dns.check | Check mail DNS records (read) | low | No dry run |
mail.domain.dns.fix | Publish mail DNS records | medium | No dry run |
mail.dkim.rotate | Rotate the DKIM key | medium | No dry run |
mail.dmarc.set | Set DMARC policy | medium | No dry run |
mail.mtasts.set | Set MTA-STS and TLS-RPT | medium | No dry run |
mail.catchall.set | Set the catch-all | medium | No dry run |
mail.mailbox.create | Create a mailbox | medium | |
mail.mailbox.list | List mailboxes (read) | low | No dry run |
mail.mailbox.update | Change a mailbox | medium | No dry run |
mail.mailbox.delete | Delete a mailbox | high | |
mail.mailbox.password.set | Reset a mailbox password | medium | No dry run |
mail.alias.set | Create or change an alias | low | No dry run |
mail.alias.list | List aliases and forwarders (read) | low | No dry run |
mail.alias.delete | Delete an alias | low | No dry run |
mail.forwarder.set | Create or change a forwarder | low | No dry run |
mail.forwarder.delete | Delete a forwarder | low | No dry run |
mail.autoresponder.set | Set an autoresponder | low | No dry run |
mail.autoresponder.delete | Delete an autoresponder | low | No dry run |
mail.queue.list | List the mail queue (read) | low | No dry run |
mail.queue.retry | Queue: retry messages | medium | No dry run |
mail.queue.hold | Queue: hold messages | medium | No dry run |
mail.queue.release | Queue: release messages | medium | No dry run |
mail.queue.delete | Queue: delete messages | high | No dry run |
mail.queue.flush | Queue: flush | medium | No dry run |
mail.limit.set | Set outbound limits | medium | No dry run |
mail.account.hold | Hold outbound mail of a mailbox | medium | No dry run |
mail.account.release | Release a held mailbox | medium | No dry run |
mail.sso.prepare | Single sign-on credentials for webmail | medium | No dry run |
mail.client.config | Mail client settings (read) | low | No dry run |
mail.config.reset_all | Reset the whole service to defaults | high | |
mail.config.snapshot.list | Snapshots before resets (read) | low | No dry run |
mail.config.snapshot.restore | Undo a reset (restore a snapshot) | high | |
mail.config.history.list | Earlier versions of a file (read) | low | No dry run |
mail.config.history.diff | Compare an earlier version (read) | low | No dry run |
mail.config.history.restore | Restore an earlier version of a file | high | |
mail.unmanaged.scan | Scan now for objects made outside the panel | low | No dry run |
mail.unmanaged.list | Objects made outside the panel (read) | low | No dry run |
mail.unmanaged.ignore | Ignore or un-ignore an unmanaged object | low | |
mail.unmanaged.adopt | Adopt an object made outside the panel | medium | |
mail.unmanaged.remove | Remove an object made outside the panel | high |
Permissions and limits
Permissions
mail.server.manageManage the mail servermail.server.viewView the mail servermail.domain.manageAdd and remove mail domainsmail.domain.viewView mail domainsmail.dkim.manageManage DKIM keysmail.dmarc.manageManage DMARC policymail.mtasts.manageManage MTA-STS and TLS-RPTmail.mailbox.manageCreate, change and delete mailboxesmail.mailbox.viewView mailboxesmail.mailbox.passwordReset mailbox passwordsmail.alias.manageManage aliases, forwarders and the catch-allmail.autoresponder.manageManage autorespondersmail.queue.viewView the mail queuemail.queue.manageAct on the mail queuemail.limit.manageSet outbound limitsmail.abuse.manageHold and release sendersmail.client.viewView mail client settings
Plan limits
mail.domainsMail domainsmail.mailboxesMailboxesmail.forwardersForwarders
Engineering notes
Generated from modules/mail/docs.md at build d90e9e2. These are the notes the engineers keep
next to the code: precise, technical, and honest about what is not done yet.
Desired state lives in the panel DB (mail_* tables, mailboxes). Every mutation re-renders the whole node
configuration in the control plane (cp/render.go, templates in templates/) and sends it to the agent with
mail.files.apply; the agent writes only a fixed set of file kinds (sdk/agentop/mail.go), atomically, validates with
postfix check / doveconf -n / rspamadm configtest / sievec, restores the previous files on failure, and reloads
only the services whose files changed. Idempotent; mail.reconcile re-applies everything.
Decisions
- Storage: all mailboxes are virtual users owned by one
vmailsystem user:/var/vmail/<domain>/<local>/Maildir. Per-account Unix ownership was rejected: Dovecot LMTP/IMAP run as one uid and hosting accounts are shell users who should not read mail. Quotas are Dovecot maildir quotas (userdb_quota_rule), not filesystem quotas. - Passwords: bcrypt (
{BLF-CRYPT}$2a$10$) hashes in the panel DB, exported to a Dovecot passwd-file (/etc/dovecot/rc-users, 0640 root:dovecot). One password store: Postfix authenticates through Dovecot SASL. Argon2 was skipped (memory cost per login under Dovecot's auth worker, libsodium dependency). - Maps: Postfix
texthash:files (no postmap step) for domains, mailboxes, aliases, sender-login, held senders. - DKIM: per-domain RSA keys generated on the node (
/var/lib/rspamd/dkim/<domain>.<selector>.key, 0640 root:_rspamd); the private key never enters the DB, results, logs or events (only the public key is stored). Rspamddkim_signing+arcusedkim_selectors.map. Ed25519 not yet. - Rotation (two steps):
mail.dkim.rotatemakes a pending key and publishes it;activate=true(orimmediate) switches signing and keeps the old selector published 7 days (retired, removed bymail.reconcile). - TLS: self-signed cert at
/etc/rc-mail/tls/{fullchain,privkey}.pemuntil ssl installs one at the same path. - Ports: 25 (STARTTLS), 587 (STARTTLS, auth), 465 (implicit TLS, auth), 143/993 IMAP, 4190 ManageSieve.
Submission requires login = sender (
reject_sender_login_mismatch) and refuses held / outbound-disabled senders. - Sieve layout (mail/012): two owners, two files. The mail module owns
~/.rc-vacation.sieve(the out-of-officevacation, date window via thedateextension with a UTC-offsettimezone; a comment-only script when off), rendered for every mailbox on every sync and loaded by Dovecot as a per-usersieve_beforescript (after the global spam filter, so the reply runs ahead of the user's rules and a rule that files or discards a message never silences it). The user's active script~/.dovecot.sievebelongs to ManageSieve clients (webmail saves its rules as scriptrc-webmailand activates it); the mail module never writes or includes it, so neither side can overwrite the other and there is noconfig_driftedon a file nobody edited. Mailboxes written by the old layout are migrated by the agent on the next sync: the old managed wrapper is removed, and webmail rules that had overwritten it move to~/sieve/rc-webmail.sieveand become the active script again. - Catch-all (mail/011):
@domain -> targetsits invirtual_alias_maps, which Postfix reads before the mailbox map and again for every alias destination. Each mailbox of a catch-all domain therefore gets an identity line (info@d info@d) ahead of it, so existing mailboxes and alias destinations are never swallowed; a mailbox that is itself an alias key keeps that mapping. - Auto-provision:
domains.created->mail.on_domain_createdonly when the single mail server hasauto_provisionon (never publish MX for a domain nobody asked mail for).domains.deletedremoves the mail domain but keeps the maildirs on disk (orphans under/var/vmail/<domain>). - Limits:
mail.domains,mail.mailboxes,mail.forwarders(Reserve/Release, compensated on failure).
DNS records (per mail domain)
MX @ -> 10 <hostname>.; SPF @ TXT v=spf1 mx ~all; DKIM <selector>._domainkey TXT v=DKIM1; k=rsa; p=… (pending/
active/retired selectors); DMARC _dmarc TXT (starts p=none); MTA-STS _mta-sts TXT + mta-sts CNAME -> host and TLS-RPT
_smtp._tls TXT when enabled; autoconfig and autodiscover CNAME -> host. Publishing calls
dns.record.upsert_managed with {domain, owner_module:"mail", key, record:{name,type,ttl,value}} for each record
(assumption from docs/specs/dns.md §6; zone resolved by domain since w2-01 may use zone_id). If that call fails for any
reason (module missing, zone not hosted here) the action returns dns: {published:false, records:[…]} to copy. The
DKIM value can exceed 255 chars: the dns module must split it into TXT strings.
MTA-STS policy text is written to /var/lib/respirecloud/mail/mta-sts/<domain>/.well-known/mta-sts.txt for the web
module to serve at https://mta-sts.<domain>/.well-known/mta-sts.txt.
Verified in the lab (2026-10-09, FAKENET=full, Ubuntu 24.04, Postfix 3.8.6, Dovecot 2.3, Rspamd 3.8.1)
enable (apt + config + self-test all green) · domain add (DKIM + files) · inbound from "outside" to a mailbox read over
IMAPS · bad IMAP password refused · submission 587 with auth -> relay -> Mailpit, DKIM-Signature + ARC seal present and
dkim.verify = True (also after rotation, new selector) · sender mismatch 553 · open relay refused · no auth without TLS
· alias, forwarder with keep-copy, catch-all, loop rejected, autoresponder reply received, held and send-disabled senders
rejected, quota-exceeded -> 552 bounce · usage via doveadm · queue list/hold/release/retry/delete/flush · dry-run config
diff · domain deletion event keeps the maildir.
Not done (follow-ups)
Relays/smarthost per domain, IP pools, suppression lists, send API/webhooks, distribution lists, raw Sieve/filters UI, app passwords, DMARC report parsing, deliverability checker, mail logs, IMAP import, POP3, DANE, per-mailbox/domain outbound limit enforcement (stored; only node limit via Rspamd ratelimit + Redis, and a per-IP submission rate are enforced), auto-hold on abuse, ClamAV, mailbox rename/move, mobileconfig/QR, UI, quota warnings at 80/90 %.