Mail App#
The mail App runs a Stalwart mail server and Bulwark webmail as rootless Podman Quadlet services.
Setup flow#
0. Provision .env#
Copy the template and fill the user-specific values:
cp apps/mail/.env.example apps/mail/.env
Then edit apps/mail/.env and replace every CHANGE_ME placeholder:
STALWART_RECOVERY_ADMIN— the recovery admin address and password asadmin@<your-mail-domain>:<strong-random-password>.JMAP_SERVER_URL— public URL Bulwark uses to reach Stalwart's JMAP endpoint, e.g.https://mail.example.com.STALWART_PUBLIC_URL— public URL Stalwart advertises, e.g.https://mail.example.com(usually the same asJMAP_SERVER_URL).DOMAIN— the bare mail subdomain cert-refresh operates on, e.g.mail.example.com.SESSION_SECRET—*_SECRETvalues auto-generate on first deploy: leaveCHANGE_MEand the local sync wrapper (deploy-to.sh, viaensure_envinapps/lib/common.sh) replaces it with a random value, or set your own.
Prefer not to keep secrets in a static file? Provision .env from a secret
store instead — e.g. skate get … > apps/mail/.env — before running
deploy-to.sh. The deploy wrapper stays agnostic to how .env got its
contents: it syncs whatever .env exists and rejects any file that still
contains a CHANGE_ME placeholder.
Never commit .env; it is gitignored.
1. Recovery admin credentials#
The recovery admin user is admin@<domain> with the password set in .env
(see step 0). ensure_env (from apps/lib/common.sh) auto-provisions .env
from .env.example on first deploy if it does not exist.
2. Stalwart Setup Wizard#
Tunnel to the host and open the wizard:
ssh -L 8080:127.0.0.1:8080 admin@<host> -N
Open http://127.0.0.1:8080/ in your browser and log in with the recovery admin credentials
from .env.
3. Configure SMTP relay (SMTP2GO)#
In the Stalwart WebUI:
- Add SMTP2GO as an outbound relay (Settings → Routing → Outbound)
- Host:
mail.smtp2go.com, port 587 (STARTTLS) or 465 (TLS) - Credentials: your SMTP2GO API key or SMTP username + password
4. DNS records#
For your mail domain:
- MX record pointing to the mail subdomain (e.g.
mail.example.com) - A record for the mail subdomain → host IP
- SPF:
v=spf1 include:smtp2go.com ~all - DKIM: get the public key from SMTP2GO, add as TXT record
- DMARC:
v=DMARC1; p=none; rua=mailto:admin@<domain>
5. TLS certificates#
A weekly timer (mail-cert-refresh.timer) copies the proxy's Let's Encrypt cert into
~/apps/mail/certs/. Stalwart can use these for IMAPS (993) and SMTP STARTTLS (587).
In the WebUI, point Stalwart's TLS to:
- Certificate:
/etc/stalwart/certs/fullchain.pem - Private key:
/etc/stalwart/certs/privkey.pem
Note: the cert-refresh script writes cert.pem/key.pem — this is a known discrepancy
(see improvement issue 02). Until resolved, adjust the WebUI paths or rename the files after
the refresh.
The container bind-mounts ~/apps/mail/certs/ → /etc/stalwart/certs/.
6. Post-setup#
Once the wizard completes:
- Stalwart writes
config.jsoninto~/apps/mail/config/ - Restart Stalwart:
systemctl --user restart mail-stalwart - Bulwark auto-connects (it uses Stalwart's JMAP endpoint)
Troubleshooting#
podman logs mail-stalwart
podman logs mail-bulwark
systemctl --user status mail-stalwart mail-bulwark