Chatmail Control is a lightweight self-hosted admin/control-plane panel for a chatmail server stack. It is a thin admin UI around existing chatmail/cmdeploy components such as Dovecot, Postfix, doveauth, and chatmail-metadata.
This is not webmail, not a mailbox client, and not a Mailcow/PostfixAdmin replacement.
- admin login/logout with cookie sessions and Argon2 password hashes;
- dashboard with service state, queue size, user count, active bans, and recent audit events;
- user/mailbox listing through built-in
doveadmcommands; - account lifecycle and auth-state controls for chatmail maildir users (
disable/enable login, lifecycle delete); - Dovecot mailbox operations (
list,expunge,quota recalc,force-resync); - block/unblock address, domain, IP, and subnet bans with file export and built-in reload behavior;
- service control page for
status/reload/restartand log tail; - registration settings stored in SQLite and exported into a generated policy file;
- invite management with token export;
- logs viewer backed by built-in
journalctlsources for chatmail services; - health page with service, port, DNS, TLS, queue, and disk checks;
- audit log for admin actions;
- CLI bootstrap for the initial admin user.
- Rust
- Axum
- Tokio
- Askama + HTMX
- SQLite via SQLx
- TOML config
- tracing + tracing-subscriber
- Session storage is persisted in SQLite via the
sessionstable. - Shell integration is always argv-based. Commands are never executed through a shell.
- The UI degrades to
unavailablewhen an external command is missing or fails. - Command catalog is built into the application for chatmail host deployments and is not configurable from TOML.
- Postfix ban-policy wiring can be synchronized from Settings with a safe merge strategy.
- Shell command timeout is fixed to 10 seconds in MVP.
- Health checks tolerate missing local tools such as
systemctl,postqueue, oropenssland render warnings instead of crashing. - Invite handling is storage/export only. Real auth-side invite enforcement is left as an integration hook and documented below.
- The supported deployment model is a native host install on the mail server, managed by systemd and running with host-level privileges.
Install Rust, SQLite headers/runtime, and OpenSSL. Then:
cargo build
cp config.example.toml config.tomlEdit config.toml before first run:
- set
server.public_url; - set
auth.session_secretto a long random secret; - set correct file paths for bans, settings, and invite exports;
- set
health.domainandhealth.dkim_selectorfor your relay.
Run the server:
chatmail-control serve --config ./config.tomlThe supported deployment model is a native binary on the same host as Postfix, Dovecot, and other chatmail services, managed by systemd.
The service is intended to run with host-level privileges because it needs access to:
doveadmpostqueuejournalctlsystemctl reload ...sudo -naccess for host commands when service user is non-root- local service state and host filesystem policy files
This is not an unprivileged sidecar service. Treat it as a host admin component.
Use this order on a real server:
- Install the release bundle.
- Edit
/etc/chatmail-control/config.toml. - Create the first admin.
- Start the systemd service.
- Put a reverse proxy with HTTPS in front of it.
For a rustup-style one-liner install flow:
curl -fsSL https://raw.githubusercontent.com/localzet/chatmail-control/main/scripts/install.sh | sudo bashInstall a specific version:
curl -fsSL https://raw.githubusercontent.com/localzet/chatmail-control/main/scripts/install.sh | \
sudo bash -s -- --version v0.1.0Upgrade an existing installation in place and restart the service:
curl -fsSL https://raw.githubusercontent.com/localzet/chatmail-control/main/scripts/update.sh | sudo bashWhat the installer does:
- resolves the requested GitHub release;
- downloads the
*-bundle.tar.gzrelease bundle and its.sha256; - verifies the checksum;
- installs the binary to
/usr/local/bin/chatmail-control; - installs static, templates, and migrations under
/opt/chatmail-control; - installs
config.example.tomland createsconfig.tomlif missing; - installs and reloads the systemd unit;
- enables the service by default;
- does not start the service unless
--startis passed.
Supported flags:
--version vX.Y.Zorlatest--install-root /opt/chatmail-control--binary-path /usr/local/bin/chatmail-control--config-dir /etc/chatmail-control--state-dir /var/lib/chatmail-control--start--no-enable
Install the release contents:
sudo install -d /opt/chatmail-control /etc/chatmail-control /var/lib/chatmail-control
sudo install -m 0755 target/release/chatmail-control /usr/local/bin/chatmail-control
sudo cp -r static templates migrations /opt/chatmail-control/
sudo install -m 0644 config.example.toml /etc/chatmail-control/config.example.toml
sudo install -m 0644 systemd/chatmail-control.service /etc/systemd/system/chatmail-control.service
sudo systemctl daemon-reload
sudo systemctl enable chatmail-controlEdit /etc/chatmail-control/config.toml.
Minimum temporary HTTP test setup:
[server]
bind = "127.0.0.1:8088"
public_url = "http://127.0.0.1:8088"
secure_cookies = false
database_url = "sqlite:///var/lib/chatmail-control/chatmail-control.db"
[auth]
session_secret = "REPLACE_WITH_A_LONG_RANDOM_SECRET"
session_ttl_hours = 12For real deployment:
- keep
bind = "127.0.0.1:8088"unless you absolutely need direct exposure; - put a reverse proxy with HTTPS in front;
- set
public_urlto the real external HTTPS URL; - set
secure_cookies = truewhen serving behind HTTPS; - replace the default
health.domain = "example.com"with your real domain.
Create the first admin after config is in place:
sudo /usr/local/bin/chatmail-control admin create \
--username admin \
--password 'CHANGE_ME'Reset password:
sudo /usr/local/bin/chatmail-control admin reset-password \
--username admin \
--password 'NEW_SECRET'sudo systemctl restart chatmail-control
sudo systemctl status chatmail-control --no-pagerThe provided unit intentionally runs as root. That is required for practical access to Dovecot, Postfix, queue inspection, log access, and reload commands.
Binary path in the provided unit:
/usr/local/bin/chatmail-control/etc/chatmail-control/config.toml(config)
Start command:
/usr/local/bin/chatmail-control serveUse config.example.toml as the baseline.
Built-in command catalog used by the app:
Users:
- doveadm user '*'
- doveadm quota get -u <address>
- doveadm mailbox status -u <address> messages INBOX
- doveadm user -u <address> *
- doveadm mailbox delete -u <address> -s INBOX
- doveadm user -u <address> -f home
- doveadm mailbox list -u <address>
- doveadm expunge -u <address> mailbox <mailbox> all
- doveadm quota recalc -u <address>
- doveadm force-resync -u <address> *
Bans reload:
- systemctl reload postfix
- systemctl reload dovecot
Settings reload:
- systemctl reload doveauth
Postfix policy sync:
- postconf -h smtpd_recipient_restrictions
- postconf -h smtpd_sender_restrictions
- postconf -h smtpd_client_restrictions
- postconf -e "<setting> = <merged_restrictions>"
- systemctl reload postfix
Logs:
- journalctl -u dovecot -n <N> --no-pager
- journalctl -u postfix -n <N> --no-pager
- journalctl -u doveauth -n <N> --no-pager
- journalctl -u chatmail-metadata -n <N> --no-pager
- journalctl -u chatmail-expire -n <N> --no-pager
- journalctl -u lastlogin -n <N> --no-pager
Important:
Delete userin UI removes the resolved maildir home path (account lifecycle deletion for file-based chatmail setups);- before lifecycle deletion, the app auto-activates an address ban and reloads mail services to reduce immediate mailbox recreation from incoming traffic;
Clear INBOXin UI only clears mailbox contents through Dovecot and does not remove the account itself;- many deployments will recreate or continue listing the user after that command;
- lifecycle delete only removes the resolved maildir home path and refuses paths outside
/home/vmail/and/var/vmail/.
server {
listen 443 ssl http2;
server_name admin.example.com;
ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8088;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Real-IP $remote_addr;
}
}With a reverse proxy in place:
- set
public_url = "https://your-real-admin-host" - set
secure_cookies = true - keep the app bound to
127.0.0.1:8088
The app writes active bans into files configured under [bans]:
blocked_addresses.txtblocked_domains.txtblocked_ips.txt
Expected line formats:
bad@example.com REJECT blocked by admin
example.org REJECT domain blocked by admin
192.0.2.1 REJECT ip blocked by admin
198.51.100.0/24 REJECT subnet blocked by admin
Typical integration path:
- Point your Postfix restriction maps or policy loader to these generated files.
- The app runs built-in
systemctl reload postfixandsystemctl reload dovecotafter ban changes. - Validate file ownership and permissions so the service user can update files safely.
Recommended Postfix wiring for this project:
sudo postconf -e 'smtpd_recipient_restrictions = check_recipient_access texthash:/etc/chatmail-control/blocked_addresses.txt, reject_unauth_destination'
sudo postconf -e 'smtpd_sender_restrictions = check_sender_access texthash:/etc/chatmail-control/blocked_addresses.txt, check_sender_access texthash:/etc/chatmail-control/blocked_domains.txt'
sudo postconf -e 'smtpd_client_restrictions = check_client_access texthash:/etc/chatmail-control/blocked_ips.txt'
sudo systemctl reload postfixNotes:
texthash:is intentional here so Postfix can read the generated text files directly without a separatepostmapstep.- address bans must be enforced through both
check_recipient_accessandcheck_sender_access, otherwise a blocked mailbox can still send mail; - domain bans are enforced through
check_sender_access; - IP and subnet bans are enforced through
check_client_access. - If you already have custom Postfix restrictions, merge these access checks into your existing chains instead of
replacing them blindly with
postconf -e.
The Health page verifies these postconf integrations automatically and reports a warning when ban files are generated
but not wired into Postfix on both directions.
Registration settings are stored in SQLite and exported to the file configured in [settings].generated_policy_file.
The generated file is a TOML snapshot of:
registration_modemax_accounts_per_ip_per_daymax_accounts_per_daycleanup_empty_mailboxes_after_daysnotes
The built-in systemctl reload doveauth command is executed after every save. If reload fails, settings still persist,
a warning is logged, and an audit event is written.
The MVP stores invites and exports active tokens to [invites].export_file.
To enforce invite-only registration in your auth pipeline:
- Read the exported token list from the auth component handling registration.
- Reject registrations when
registration_mode = "invite_only"and the token is absent or inactive. - Increment
used_countin the application database from your integration hook if you need hard enforcement.
The current MVP does not decrement or enforce invite usage from the chatmail auth path by itself.
The health page performs:
systemctl is-activechecks for configured services;- local TCP checks for configured ports;
- DNS MX lookup;
- TXT checks for SPF, DMARC, and the DKIM selector;
- TLS probe through
openssl s_client; postqueue -p;df -h.
If one of these tools is unavailable, the page still opens and shows a warning or error row.
- Default bind is
127.0.0.1:8088. - The service is expected to run with host-level privileges.
- Do not expose this panel directly to the internet without HTTPS, a reverse proxy, and an allowlist.
- Replace
auth.session_secretbefore production use. - Keep
secure_cookies = falseonly for temporary plain HTTP testing. - Keep
secure_cookies = truewhen served behind HTTPS. - Login rate limiting is in-memory only in MVP scope.
- Passwords are hashed with Argon2 and never logged.
- Command execution is argv-only with placeholder substitution and timeout protection.
- Askama templates escape values by default.
- Audit log stores login success/failure and admin actions.
- Login returns
401: verify that the admin exists and the password was set with the CLI. /adminreturns401: expected without a valid login session, use/login.- Users page is empty: run
doveadm user '*'manually on the host to verify permissions/output. - Delete user does nothing: use
Manageand verify thatdoveadm user -u <address> -f homereturns a real maildir path; this action removes that path. - Delete user fails with
Directory not empty: update to the latest version; deletion now uses rename-to-tombstone and retry cleanup to avoid race with in-flight mail writes. - Clear INBOX does nothing: this action only deletes/expunges mailbox contents and does not remove the account.
- Login disable/enable fails: verify user home contains
passwordorpassword.blockedand service has write access. - Lifecycle delete fails: verify
doveadm user -u <address> -f homereturns a path under/home/vmail/or/var/vmail/. - Expunge fails: verify mailbox exists in
doveadm mailbox list -u <address>and use an exact mailbox name. doveadm user '*'works asrootbut not as an unprivileged user: expected on many real systems; the provided deployment model usessudo -nfor host commands and installer-managed/etc/sudoers.d/chatmail-control.auth-master client: Auth client doesn't have permissions to list users: verify/etc/sudoers.d/chatmail-controlexists and includesdoveadmNOPASSWD rules; restartchatmail-control.- Mailbox metrics show
unavailable: the optional command failed or returned unsupported output. - Health page shows warnings: verify that required host tools and services are available on the mail server.
- Bans were saved but Postfix/Dovecot did not reload: inspect
audit_logand system journal for built-in reload command failures.
The repository includes:
.github/workflows/ci.yml.github/workflows/release.yml
ci.yml runs:
cargo fmt --checkcargo clippy --all-targets --all-features -- -D warningscargo testcargo build --locked
release.yml runs on tag v* or manual dispatch and:
- verifies formatting, clippy, and tests;
- builds a Linux AMD64 release bundle;
- publishes a standalone Linux AMD64 binary;
- publishes the installer script;
- uploads
.tar.gzand.sha256files to GitHub Release assets; - uploads the same bundle as a workflow artifact.
Expected tag flow:
git tag v0.1.0
git push origin v0.1.0After that, the GitHub Release includes:
chatmail-control-<version>-linux-amd64as a standalone binary;chatmail-control-<version>-linux-amd64-bundle.tar.gzas the full runtime bundle;.sha256files for both;install.sh.
Requirements:
- GitHub Actions must be enabled for the repository;
- the release workflow uses
GITHUB_TOKEN, so no extra PAT is required for the default case.