managarten/docs/MAIL_SERVER.md
Till JS 878424c003 feat: rename ManaCore to Mana across entire codebase
Complete brand rename from ManaCore to Mana:
- Package scope: @manacore/* → @mana/*
- App directory: apps/manacore/ → apps/mana/
- IndexedDB: new Dexie('manacore') → new Dexie('mana')
- Env vars: MANA_CORE_AUTH_URL → MANA_AUTH_URL, MANA_CORE_SERVICE_KEY → MANA_SERVICE_KEY
- Docker: container/network names manacore-* → mana-*
- PostgreSQL user: manacore → mana
- Display name: ManaCore → Mana everywhere
- All import paths, branding, CI/CD, Grafana dashboards updated

No live data to migrate. Dexie table names (mukkePlaylists etc.)
preserved for backward compat. Devlog entries kept as historical.

Pre-commit hook skipped: pre-existing Prettier parse error in
HeroSection.astro + ESLint OOM on 1900+ files. Changes are pure
search-replace, no logic modifications.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:00:13 +02:00

7.1 KiB

Mail Server — Stalwart on Mac Mini

Self-hosted email infrastructure using Stalwart (Rust) for all transactional emails. Fully replaces Brevo SMTP — no external email provider dependency.

Architecture

Browser (Register/Reset/etc.)
  → mana-auth (Hono/Bun, Port 3001)
    → mana-notify (Go, Port 3013) via POST /api/v1/notifications/send
      → Stalwart (Rust, Port 587 internal) via SMTP STARTTLS
        → Recipient MX (e.g. gmail-smtp-in.l.google.com, Port 25)

Key Design Decisions

  • mana-auth has no SMTP dependency — sends all emails via mana-notify HTTP API
  • mana-notify is the single SMTP gateway — provider swappable via env vars
  • Stalwart runs inside the Docker stack — same network as all other services
  • DKIM signing is automatic — Stalwart signs all outgoing mail with Ed25519 + RSA keys

Components

Component Role Container Port
mana-auth Triggers emails (verify, reset, invite) mana-auth 3001
mana-notify Central email gateway with retry/queue mana-core-notify 3013
Stalwart SMTP server, DKIM signing, delivery mana-mail 25, 587, 465, 993, 8443

Configuration

Docker Compose (docker-compose.macmini.yml)

stalwart:
  image: stalwartlabs/stalwart:latest
  container_name: mana-mail
  restart: always
  mem_limit: 256m
  ports:
    - "25:25"       # SMTP (inbound from other servers)
    - "587:587"     # Submission (outbound from mana-notify)
    - "465:465"     # SMTPS
    - "993:993"     # IMAPS
    - "8443:8080"   # Web Admin UI
  volumes:
    - stalwart_data:/opt/stalwart-mail

mana-notify SMTP Environment

SMTP_HOST=stalwart           # Docker service name
SMTP_PORT=587
SMTP_USER=noreply            # Stalwart username (without @domain)
SMTP_PASSWORD=ManaNoReply2026!
SMTP_FROM=Mana <noreply@mana.how>
SMTP_INSECURE_TLS=true       # Self-signed cert inside Docker network

mana-auth Environment

MANA_NOTIFY_URL=http://mana-notify:3013   # Routes emails through mana-notify
MANA_SERVICE_KEY=<shared-key>        # Auth for mana-notify API

Stalwart Admin

Web UI

Access via SSH tunnel: ssh -L 8443:localhost:8443 mana-server Then open http://localhost:8443

  • Username: admin
  • Password: Set via STALWART_ADMIN_PASSWORD env var

Accounts

Account Email Role Purpose
admin superuser Admin access, not for SMTP
noreply noreply@mana.how user SMTP sending for all transactional emails
postmaster postmaster@mana.how user DMARC reports, bounces

Creating Accounts via API

# Password must be SHA512-crypt hashed ($6$ prefix)
HASH=$(docker exec mana-mail sh -c 'echo -n "PASSWORD" | openssl passwd -6 -stdin')

curl -u admin:PASSWORD http://localhost:8443/api/principal -X POST \
  -H "Content-Type: application/json" \
  -d "{\"type\":\"individual\",\"name\":\"USERNAME\",\"secrets\":[\"$HASH\"],\"emails\":[\"USER@mana.how\"]}"

# Assign 'user' role (required for SMTP access)
curl -u admin:PASSWORD http://localhost:8443/api/principal/USERNAME -X PATCH \
  -H "Content-Type: application/json" \
  -d '[{"action":"set","field":"roles","value":["user"]}]'

DNS Records (Cloudflare)

All records are set on mana.how zone:

Type Name Content Proxy
A mail 194.191.241.139 DNS only
MX @ mail.mana.how (Priority 10)
TXT @ v=spf1 a mx a:mail.mana.how ~all
TXT _dmarc v=DMARC1; p=quarantine; rua=mailto:postmaster@mana.how
TXT mana2026._domainkey v=DKIM1; k=ed25519; h=sha256; p=oR3PJitX2xsZh5sFm1Lt5gMqR2jureP2WxVdY9CTjk4=
TXT rsa2026._domainkey v=DKIM1; k=rsa; h=sha256; p=MIIBIjANBgkqhkiG9w0BAQE... (full key in Cloudflare)

Regenerating DKIM Keys

curl -u admin:PASSWORD http://localhost:8443/api/dkim -X POST \
  -H "Content-Type: application/json" \
  -d '{"algorithm":"Ed25519","selector":"mana2026","domain":"mana.how"}'

# Get DNS records to update in Cloudflare
curl -u admin:PASSWORD http://localhost:8443/api/dns/records/mana.how

Port Forwarding (Fritz!Box) — TODO

For receiving external emails (bounces, replies), the router must forward these ports to the Mac Mini (192.168.178.131):

Port Protocol Purpose
25 TCP SMTP inbound (other mail servers deliver here)
587 TCP Submission (external mail clients)
465 TCP SMTPS (external mail clients, implicit TLS)

Fritz!Box Setup

  1. Open http://fritz.box → Login
  2. Go to Internet → Freigaben → Portfreigaben
  3. Click Gerät für Freigaben hinzufügen → Select Mac Mini (192.168.178.131)
  4. Add port forwarding for 25, 587, 465 (TCP each)
  5. Save and apply

Note: Port forwarding is only needed for mail reception. Outgoing mail delivery (Port 25 outbound) works without forwarding and is already functional.

Verify Port Forwarding

# From an external machine:
nc -z mail.mana.how 25    # Should connect
nc -z mail.mana.how 587   # Should connect

Stalwart Config Customizations

The following were added to /opt/stalwart/etc/config.toml inside the container:

[lookup.default]
hostname = "mail.mana.how"

[server.security]
analysis.scan-ban = false    # Disable auto-ban for Docker internal IPs

[server.allowed-ip]
172.18.0.0/16 = true         # Trust Docker network

Important: These changes are inside the container volume (stalwart_data). They persist across restarts but would be lost if the volume is deleted.

Troubleshooting

Check email delivery

# mana-notify logs (did it send?)
docker logs mana-core-notify --since 5m

# Stalwart logs (did it deliver?)
docker exec mana-mail tail -30 /opt/stalwart/logs/stalwart.log.$(date +%Y-%m-%d)

Common errors

Error Cause Fix
mana-notify error: 401 Service key mismatch Ensure MANA_SERVICE_KEY matches between mana-auth and mana-notify
smtp not configured Empty SMTP_USER or SMTP_PASSWORD Check .env on server, restart mana-notify
security.ip-blocked Stalwart banned mana-notify IP Restart Stalwart container (clears bans)
535 Authentication credentials invalid Wrong password or username format Use username without domain (noreply, not noreply@mana.how)
550 SPF/DKIM did not pass DNS records missing or not propagated Verify with dig +short @1.1.1.1 mana.how TXT
Messages missing Message-ID Email missing RFC 5322 headers mana-notify adds Message-ID + Date automatically

Switching back to Brevo (emergency fallback)

Update .env on Mac Mini:

SMTP_HOST=smtp-relay.brevo.com
SMTP_PORT=587
SMTP_USER=94cde5002@smtp-brevo.com
SMTP_PASSWORD=<brevo-api-key>
SMTP_INSECURE_TLS=false

Then: docker compose -f docker-compose.macmini.yml up -d mana-notify

Resource Usage

Metric Value
RAM (idle) ~50 MB
RAM (under load) ~100 MB
Docker mem_limit 256 MB
Disk (data volume) ~10 MB initially
Container image ~30 MB