Stalwart Mail Server
Self-hosted mail server with a privacy-focused architecture that keeps the home IP hidden.Overview
Stalwart is a modern, open-source mail server written in Rust that supports JMAP, IMAP, POP3, SMTP, CalDAV, CardDAV, and WebDAV.- Hostname:
stalwart.augustin.ai - Primary Domain:
augustin.ai - Primary Email:
root@augustin.ai - Catch-all:
*@augustin.ai→ routes to primary account
stalwart.augustin.ai is for administration only. To read emails, use an IMAP client (Apple Mail, Thunderbird, etc.) or deploy a separate webmail container. Stalwart plans to add webmail in 2026.
:::
Architecture
Components
| Component | Purpose | Status |
|---|---|---|
| Stalwart | Mail server | ✅ Running |
| Cloudflare Email Routing | Receive inbound mail | ✅ Configured |
| Cloudflare Worker | Forward mail to Stalwart via JMAP | ✅ Deployed |
| Cloudflare Tunnel (HTTP) | Admin/JMAP access | ✅ Working |
| Cloudflare Tunnel (TCP) | IMAP for work laptop | 🔲 To configure |
| Tailscale | IMAP for iPhone | 🔲 To set up |
| Resend | Outbound SMTP relay | ✅ Configured |
Storage
| Store | Purpose | Backend |
|---|---|---|
| Data Store | Metadata, folders, settings | RocksDB |
| Blob Store | Emails, attachments | Backblaze B2 |
| Full-text Search | Email search indexing | RocksDB |
| Lookup Store | Caching, sessions | RocksDB |
Ports
| Port | Protocol | Purpose | Access Method |
|---|---|---|---|
| 25 | SMTP | Receiving external mail | Not used (using Worker) |
| 465 | SMTPS | Client mail submission | Tailscale / CF Tunnel TCP |
| 587 | SMTP+STARTTLS | Client mail submission | Tailscale / CF Tunnel TCP |
| 993 | IMAPS | Client mail retrieval | Tailscale / CF Tunnel TCP |
| 443 | HTTPS | Admin, JMAP API | Traefik / CF Tunnel HTTP |
| 8080 | HTTP | Internal (Traefik backend) | Not exposed |
| 4190 | ManageSieve | Mail filter rules | Optional |
Setup Progress
Phase 1: Core Deployment ✅
- Create compose.yaml
- Create .env file with initial config
- Deploy container
- Access web admin, get initial password
- Configure hostname in Stalwart
Phase 2: Storage Configuration ✅
- Configure storage in Stalwart web admin (B2 for blobs, RocksDB for data/fts/lookup)
- Test storage is working (verified email blob stored in B2)
Phase 3: Outbound Mail (Resend) ✅
- Create Resend account
- Verify domain in Resend (added DKIM record to Cloudflare)
- Get SMTP credentials
- Configure Stalwart to relay through Resend (smtp.resend.com:465)
- Test outbound mail
Phase 4: Client Access
- Configure Traefik labels for HTTP access
- Cloudflare Tunnel working for HTTP (admin/JMAP)
- Configure
http.urlfor reverse proxy (JMAP returns HTTPS URLs) - Set up Cloudflare Tunnel for TCP (IMAP)
- Install Tailscale on server
- Test iPhone Mail via Tailscale
- Test work laptop via CF Tunnel TCP
Phase 5: Inbound Mail (Cloudflare Worker) ✅
- Write Cloudflare Worker for email → JMAP
- Deploy Worker to Cloudflare
- Configure Cloudflare Email Routing
- Set up MX records
- Configure catch-all
*@augustin.aiin Stalwart - Test inbound mail delivery
Phase 6: Domain & DNS ✅
- Add domain in Stalwart
- Configure SPF, DKIM, DMARC, TLS reporting
- Create user account
- Test end-to-end email flow
DNS Records
Currently Configured ✅
| Type | Name | Content |
|---|---|---|
| MX | augustin.ai | route1.mx.cloudflare.net (priority 50) |
| MX | augustin.ai | route2.mx.cloudflare.net (priority 64) |
| MX | augustin.ai | route3.mx.cloudflare.net (priority 82) |
| TXT | augustin.ai | v=spf1 include:_spf.mx.cloudflare.net ~all |
| TXT | _dmarc.augustin.ai | v=DMARC1; p=reject; rua=mailto:postmaster@augustin.ai; ruf=mailto:postmaster@augustin.ai |
| TXT | 202602r._domainkey.augustin.ai | RSA DKIM key |
| TXT | 202602e._domainkey.augustin.ai | Ed25519 DKIM key |
| TXT | _smtp._tls.augustin.ai | v=TLSRPTv1; rua=mailto:postmaster@augustin.ai |
| TXT | resend._domainkey.augustin.ai | Resend DKIM key |
Optional (Not Yet Configured)
| Type | Name | Content | Purpose |
|---|---|---|---|
| CNAME | autoconfig.augustin.ai | stalwart.augustin.ai | Email client auto-config |
| CNAME | autodiscover.augustin.ai | stalwart.augustin.ai | Email client auto-discovery |
| SRV | _imaps._tcp.augustin.ai | 0 1 993 stalwart.augustin.ai | IMAP service discovery |
| SRV | _submissions._tcp.augustin.ai | 0 1 465 stalwart.augustin.ai | SMTP service discovery |
| SRV | _jmap._tcp.augustin.ai | 0 1 443 stalwart.augustin.ai | JMAP service discovery |
Cloudflare Worker
The Worker (cloudflare-worker.js) handles inbound email:
- Receives raw email from Cloudflare Email Routing
- Looks up recipient in Stalwart accounts (supports catch-all)
- Gets JMAP session to find account ID
- Uploads email blob via JMAP
- Imports email to recipient’s Inbox
Client Configuration
JMAP Clients
| Setting | Value |
|---|---|
| JMAP URL | https://stalwart.augustin.ai/.well-known/jmap |
| Username | root@augustin.ai |
IMAP/SMTP Clients
| Setting | Value |
|---|---|
| IMAP Server | stalwart.augustin.ai:993 (SSL/TLS) |
| SMTP Server | stalwart.augustin.ai:465 (SSL/TLS) |
| Username | root@augustin.ai |
Backup & Recovery
Backup Strategy
Email data lives in two places:- Backblaze B2 — email blobs (the actual messages and attachments)
- RocksDB (local) — metadata, mailbox structure, FTS index, settings
stalwart/backup-email.sh, using stalwart-cli export account to dump the full account (emails, mailboxes, identities, Sieve scripts, vacation responses, and all blobs from B2) into stalwart/data/export/. The 3:00am restic backup then sends this to both Pentium and B2.
| Backup Layer | What | Where | Schedule |
|---|---|---|---|
| JMAP export | Full account (emails + blobs + config) | stalwart/data/export/ | Daily 2:45am |
| Restic → Pentium | All of ~/apps including export | Pentium internal HDD | Daily 3:00am |
| Restic → B2 | All of ~/apps including export | Backblaze B2 | Daily 3:00am |
Recovery Scenarios
- Container dies (data intact) —
docker compose up -d, Stalwart reconnects to RocksDB + B2 automatically - Full restore from restic — restore
stalwart/from restic, start container, B2 blobs are still in Backblaze - Clean slate rebuild — restore from restic, deploy fresh container, reimport via
stalwart-cli import account - Stalwart is down, need to read email — inbound mail queues at Cloudflare (~24h retry), latest JMAP export on disk contains raw
.emlfiles
stalwart/README.md for detailed recovery commands.
Future Enhancements
- N8N integration via JMAP for AI email parsing
- CalDAV/CardDAV for calendar and contacts
- Pocket ID integration (OpenID Connect)
- Multiple domains
- Shared mailboxes
- Webmail container (Roundcube or SnappyMail)