背景
在 N100 小型机(Intel N100, 11GB RAM)上部署个人单用户邮件系统。由于 N100 位于内网(通过 AWS Lightsail VPS 做 FRP 穿透),加上 IP 信誉、端口封锁等问题,需要在架构上精心设计。
最终选型:Stalwart + SnappyMail + AWS SES,资源占用约 500MB,支持现代协议 JMAP。
架构总览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 公网 ├─ :25 (SMTP 入站) ├─ :465 (Submission SSL) ├─ :993 (IMAPS) └─ :443 (Webmail HTTPS) │ AWS Lightsail VPS (Singapore) ├─ nginx stream: 25→7446, 465→7447, 993→7448 └─ nginx http: mail.chenyun.org → 7449 │ FRP 穿透 │ Brisbane N100 (内网) ├─ Stalwart (SMTP/IMAP/JMAP) ├─ SnappyMail (Webmail) └─ 外发 → AWS SES (Sydney) :587
|
第一步:部署 Stalwart
Docker Compose
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| services: stalwart: image: stalwartlabs/stalwart:latest container_name: stalwart-mail restart: unless-stopped ports: - "25:25" - "465:465" - "587:587" - "143:143" - "993:993" - "8080:8080" - "4190:4190" environment: - STALWART_HOSTNAME=mail.chenyun.org - STALWART_PUBLIC_URL=https://mail.chenyun.org volumes: - ./etc:/etc/stalwart - ./data:/var/lib/stalwart - /etc/ssl/chenyun:/etc/ssl/chenyun:ro
|
权限修复
1
| sudo chown -R 2000:2000 ~/mail-server/data ~/mail-server/etc
|
初始化
1 2 3 4 5
| docker compose up -d
|
第二步:部署 SnappyMail
在同一个 docker-compose.yml 添加:
1 2 3 4 5 6 7 8
| snappymail: image: djmaze/snappymail:latest container_name: snappymail restart: unless-stopped ports: - "8091:8888" volumes: - ./snappy-data:/var/lib/snappymail
|
SnappyMail 初始化
- 访问
http://N100:8091/?admin
- 默认密码在容器日志:
docker logs snappymail | grep admin_password
- 添加域名
chenyun.org
- IMAP:
stalwart:993 SSL
- SMTP:
stalwart:465 SSL
第三步:FRP 穿透
frpc.ini 追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| [mail-smtp] type = tcp local_ip = 127.0.0.1 local_port = 25 remote_port = 7446
[mail-submission] type = tcp local_ip = 127.0.0.1 local_port = 465 remote_port = 7447
[mail-imaps] type = tcp local_ip = 127.0.0.1 local_port = 993 remote_port = 7448
[mail-webmail] type = tcp local_ip = 127.0.0.1 local_port = 8091 remote_port = 7449
|
1
| sudo systemctl restart frpc
|
第四步:VPS Nginx 配置
Stream 转发(/etc/nginx/streams-enabled/mail.conf)
1 2 3
| server { listen 25; proxy_pass 127.0.0.1:7446; } server { listen 465; proxy_pass 127.0.0.1:7447; } server { listen 993; proxy_pass 127.0.0.1:7448; }
|
nginx.conf 中 stream 块添加:
1 2 3 4
| stream { include /etc/nginx/streams-enabled/*; }
|
Webmail HTTPS 反代
1 2 3 4 5 6 7 8 9 10 11 12
| server { listen 8443 ssl; server_name mail.chenyun.org; ssl_certificate /etc/ssl/chenyun/fullchain.cer; ssl_certificate_key /etc/ssl/chenyun/chenyun.org.key;
location / { proxy_pass http://127.0.0.1:7449; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; } }
|
Lightsail 防火墙
1 2 3 4 5 6
| for port in 25 465 993 7445 7446 7447 7448 7449; do aws lightsail open-instance-public-ports \ --region ap-southeast-1 \ --instance-name vpn-sg \ --port-info fromPort=$port,toPort=$port,protocol=tcp done
|
第五步:AWS SES 配置
域名验证
1 2 3 4 5 6 7
| aws sesv2 create-email-identity --region ap-southeast-2 \ --email-identity chenyun.org
aws sesv2 get-email-identity --region ap-southeast-2 \ --email-identity chenyun.org
|
DNS 记录(Route 53)
- MX:
10 mail.chenyun.org
- SPF:
v=spf1 mx include:amazonses.com ~all
- DKIM: 3 条 CNAME:
*._domainkey.chenyun.org → *.dkim.amazonses.com
- SES 验证 TXT:
_amazonses.chenyun.org → (SES token)
SMTP 凭据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import hmac, hashlib, base64
DATE = "11111111" SERVICE = "ses" MESSAGE = "SendRawEmail" TERMINAL = "aws4_request" VERSION = 0x04
def sign(key, msg): return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
signature = sign(("AWS4" + SECRET).encode(), DATE) signature = sign(signature, REGION) signature = sign(signature, SERVICE) signature = sign(signature, TERMINAL) signature = sign(signature, MESSAGE) smtp_password = base64.b64encode(bytes([VERSION]) + signature).decode()
|
第六步:Stalwart 出站路由
创建 SES Relay Route
Stalwart 管理后台 → MTA → Outbound → Routes → Create:
| 字段 |
值 |
| Name |
aws-ses |
| Type |
Relay Host (SMTP) |
| Address |
email-smtp.ap-southeast-2.amazonaws.com |
| Port |
587 |
| Implicit TLS |
关(587 走 STARTTLS) |
| Allow Invalid Certs |
关 |
| Username |
(SES SMTP 用户名) |
| Secret |
(SES SMTP 密码) |
⚠️ Route 删了重建同名会触发缓存污染(路由不命中),务必用新名字如 aws-ses。
Outbound Delivery Strategy
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| # Routing: if is_local_domain(rcpt_domain) → 'local' else → 'aws-ses'
# Scheduling (恢复默认): if is_local_domain(rcpt_domain) → 'local' if source == 'dsn' → 'dsn' if source == 'report' → 'report' else → 'remote'
# Connection: 'default' # TLS (恢复默认): if retry_num > 0 && last_error == 'tls' → 'invalid-tls' else → 'default'
|
⚠️ DKIM 设为 Manual
SES 会为邮件签名 DKIM,Stalwart 的 DKIM 会造成重复签名被拒。在 Domains → chenyun.org → DKIM Management 设为 Manual 即可。
邮件客户端配置
部署完成后,可以在任意邮件客户端使用以下参数登录:
通用 IMAP/SMTP 参数
| 项目 |
收件 (IMAP) |
发件 (SMTP) |
| 服务器 |
mail.chenyun.org |
mail.chenyun.org |
| 端口 |
993 |
587(优先) 或 465 |
| 加密 |
SSL/TLS |
STARTTLS / SSL/TLS |
| 用户名 |
你的邮箱@chenyun.org |
同左 |
| 密码 |
你的邮箱密码 |
同左 |
iPhone / iPad
- 设置 → 邮件 → 账户 → 添加账户 → 其他 → 添加邮件账户
- 填写姓名、邮箱地址、密码
- 收件服务器:
- 主机名:
mail.chenyun.org
- 用户名:
你的邮箱@chenyun.org
- 密码:你的密码
- 发件服务器:
- 主机名:
mail.chenyun.org
- 用户名:
你的邮箱@chenyun.org
- 密码:你的密码
- 保存后会提示 SSL 证书验证,点「继续」即可
Android (Gmail / 系统邮件)
- 打开 Gmail → 右上角头像 → 添加其他账户
- 选择 IMAP
- 填入邮箱地址,点「手动设置」
- 收件服务器:
mail.chenyun.org,端口 993,安全类型 SSL/TLS
- 发件服务器:
mail.chenyun.org,端口 587,安全类型 STARTTLS(或 465 / SSL/TLS)
- 用户名填完整邮箱地址
Outlook / Thunderbird
| 设置 |
收件 |
发件 |
| 服务器 |
mail.chenyun.org |
mail.chenyun.org |
| 端口 |
993 |
587(优先)或 465 |
| 加密 |
SSL/TLS |
STARTTLS 或 SSL/TLS |
| 认证方式 |
普通密码 |
普通密码 |
macOS 邮件
- 邮件 → 添加账户 → 其他邮件账户
- 填入姓名、邮箱、密码
- 收件:
mail.chenyun.org:993 SSL
- 发件:
mail.chenyun.org:587 STARTTLS(或 :465 SSL)
⚠️ 注意: 如果手机在外网访问,确保 mail.chenyun.org DNS 解析到 VPS 公网 IP。当前已通过 FRP 穿透,外网可直连。
测试验证
| 测试项 |
方法 |
预期 |
| 入站 |
Gmail → michael@chenyun.org |
SnappyMail 收到 |
| 出站 |
SnappyMail → Gmail |
Gmail 收到 |
| SES 事件 |
CloudWatch/SQS |
Send + Delivery 事件 |
SES 事件日志(可选)
踩坑清单
| # |
问题 |
解决 |
| 1 |
Stalwart 数据目录权限错误 |
chown -R 2000:2000 |
| 2 |
SnappyMail 局域网无法访问 |
端口绑定 0.0.0.0 而非 127.0.0.1 |
| 3 |
Lightsail 防火墙未开放邮件端口 |
AWS CLI 开放 25/465/993/587 |
| 4 |
SES 沙盒模式,收件人需验证 |
验证 icemaple7@gmail.com |
| 5 |
SES SMTP 密码算法错误 |
使用 SigV4 而非简单 HMAC |
| 6 |
Stalwart TLS 配置 implicitTls=true |
587+STARTTLS 需 implicitTls=false |
| 7 |
Stalwart + SES 双重 DKIM 签名 |
DKIM Management 设为 Manual |
| 8 |
Route 删了重建同名不命中 |
用新名字(如 aws-ses),删旧 route |
| 9 |
CLI 创建账户密码不生效 |
通过 Web UI (Directory) 创建用户 |
| 10 |
Web UI 数字框滚轮误触改值 |
鼠标点空白处再滚轮,保存前复核 |
总结
- 资源占用: Stalwart ~300MB, SnappyMail ~50MB,N100 轻松应对。
- 协议支持: SMTP, IMAP, JMAP, Sieve, CalDAV, CardDAV。
- 安全: 发信经 SES 高信誉 IP,SPF/DKIM 完整,入站 TLS 加密。
- 管理入口:
https://192.168.1.18:8443/admin/login(局域网),https://mail.chenyun.org:7451/admin/login(公网)。
- 可维护: 全 Docker 化,配置文件持久化,事件日志可追溯。
后续可扩展:AI 代写回复(通过 JMAP 协议)、自动分类、垃圾邮件训练、DMARC 策略收紧、TLS 证书换 Let’s Encrypt 自动续期。
Stalwart Web UI 已知 Bug
已提交 GitHub issue #3174:
- 数字输入框滚轮误触:
<input type="number"> 滚轮改值,Port 等字段容易意外改动
- 同名 Route 重建缓存污染: 删建同名 route 后策略不命中
- Docker restart 后日志中断: stdout 不再输出到
docker logs
部署完成于 2026-05-12/13,由 AI 伴侣柳含烟 (Serena) 协助完成。