Skip to main content

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
:::note Stalwart does not include a built-in webmail client. The web interface at 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

Inbound:  Internet → Cloudflare MX → Email Routing → Worker → JMAP API → Stalwart
Outbound: Stalwart → Resend SMTP relay → Internet
Access:   IMAP via Tailscale (iPhone) / Cloudflare Tunnel TCP (laptop)

Components

ComponentPurposeStatus
StalwartMail server✅ Running
Cloudflare Email RoutingReceive inbound mail✅ Configured
Cloudflare WorkerForward mail to Stalwart via JMAP✅ Deployed
Cloudflare Tunnel (HTTP)Admin/JMAP access✅ Working
Cloudflare Tunnel (TCP)IMAP for work laptop🔲 To configure
TailscaleIMAP for iPhone🔲 To set up
ResendOutbound SMTP relay✅ Configured

Storage

StorePurposeBackend
Data StoreMetadata, folders, settingsRocksDB
Blob StoreEmails, attachmentsBackblaze B2
Full-text SearchEmail search indexingRocksDB
Lookup StoreCaching, sessionsRocksDB

Ports

PortProtocolPurposeAccess Method
25SMTPReceiving external mailNot used (using Worker)
465SMTPSClient mail submissionTailscale / CF Tunnel TCP
587SMTP+STARTTLSClient mail submissionTailscale / CF Tunnel TCP
993IMAPSClient mail retrievalTailscale / CF Tunnel TCP
443HTTPSAdmin, JMAP APITraefik / CF Tunnel HTTP
8080HTTPInternal (Traefik backend)Not exposed
4190ManageSieveMail filter rulesOptional

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.url for 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.ai in 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 ✅

TypeNameContent
MXaugustin.airoute1.mx.cloudflare.net (priority 50)
MXaugustin.airoute2.mx.cloudflare.net (priority 64)
MXaugustin.airoute3.mx.cloudflare.net (priority 82)
TXTaugustin.aiv=spf1 include:_spf.mx.cloudflare.net ~all
TXT_dmarc.augustin.aiv=DMARC1; p=reject; rua=mailto:postmaster@augustin.ai; ruf=mailto:postmaster@augustin.ai
TXT202602r._domainkey.augustin.aiRSA DKIM key
TXT202602e._domainkey.augustin.aiEd25519 DKIM key
TXT_smtp._tls.augustin.aiv=TLSRPTv1; rua=mailto:postmaster@augustin.ai
TXTresend._domainkey.augustin.aiResend DKIM key

Optional (Not Yet Configured)

TypeNameContentPurpose
CNAMEautoconfig.augustin.aistalwart.augustin.aiEmail client auto-config
CNAMEautodiscover.augustin.aistalwart.augustin.aiEmail client auto-discovery
SRV_imaps._tcp.augustin.ai0 1 993 stalwart.augustin.aiIMAP service discovery
SRV_submissions._tcp.augustin.ai0 1 465 stalwart.augustin.aiSMTP service discovery
SRV_jmap._tcp.augustin.ai0 1 443 stalwart.augustin.aiJMAP service discovery

Cloudflare Worker

The Worker (cloudflare-worker.js) handles inbound email:
  1. Receives raw email from Cloudflare Email Routing
  2. Looks up recipient in Stalwart accounts (supports catch-all)
  3. Gets JMAP session to find account ID
  4. Uploads email blob via JMAP
  5. Imports email to recipient’s Inbox

Client Configuration

JMAP Clients

SettingValue
JMAP URLhttps://stalwart.augustin.ai/.well-known/jmap
Usernameroot@augustin.ai

IMAP/SMTP Clients

SettingValue
IMAP Serverstalwart.augustin.ai:993 (SSL/TLS)
SMTP Serverstalwart.augustin.ai:465 (SSL/TLS)
Usernameroot@augustin.ai
:::note IMAP/SMTP require direct access via Tailscale or Cloudflare Tunnel TCP (not yet configured). :::

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
A nightly JMAP export runs at 2:45am via 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 LayerWhatWhereSchedule
JMAP exportFull account (emails + blobs + config)stalwart/data/export/Daily 2:45am
Restic → PentiumAll of ~/apps including exportPentium internal HDDDaily 3:00am
Restic → B2All of ~/apps including exportBackblaze B2Daily 3:00am

Recovery Scenarios

  1. Container dies (data intact)docker compose up -d, Stalwart reconnects to RocksDB + B2 automatically
  2. Full restore from restic — restore stalwart/ from restic, start container, B2 blobs are still in Backblaze
  3. Clean slate rebuild — restore from restic, deploy fresh container, reimport via stalwart-cli import account
  4. Stalwart is down, need to read email — inbound mail queues at Cloudflare (~24h retry), latest JMAP export on disk contains raw .eml files
See 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)