N100 自建邮件系统全指南:Stalwart + SnappyMail + SES

背景

在 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
# ~/mail-server/docker-compose.yml
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
# 访问 http://N100:8080/admin 完成 5 步向导
# 主机名: mail.chenyun.org
# 域名: chenyun.org
# 存储: 全部选 RocksDB,路径 /var/lib/stalwart

第二步:部署 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" # 注意:不要绑 127.0.0.1
volumes:
- ./snappy-data:/var/lib/snappymail

SnappyMail 初始化

  1. 访问 http://N100:8091/?admin
  2. 默认密码在容器日志:docker logs snappymail | grep admin_password
  3. 添加域名 chenyun.org
  4. IMAP: stalwart:993 SSL
  5. 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/*;
# ... 原有 SNI 配置 ...
}

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

# 获取 DKIM tokens
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

  1. 设置邮件账户添加账户其他添加邮件账户
  2. 填写姓名、邮箱地址、密码
  3. 收件服务器:
    • 主机名:mail.chenyun.org
    • 用户名:你的邮箱@chenyun.org
    • 密码:你的密码
  4. 发件服务器:
    • 主机名:mail.chenyun.org
    • 用户名:你的邮箱@chenyun.org
    • 密码:你的密码
  5. 保存后会提示 SSL 证书验证,点「继续」即可

Android (Gmail / 系统邮件)

  1. 打开 Gmail → 右上角头像 → 添加其他账户
  2. 选择 IMAP
  3. 填入邮箱地址,点「手动设置」
  4. 收件服务器: mail.chenyun.org,端口 993,安全类型 SSL/TLS
  5. 发件服务器: mail.chenyun.org,端口 587,安全类型 STARTTLS(或 465 / SSL/TLS
  6. 用户名填完整邮箱地址

Outlook / Thunderbird

设置 收件 发件
服务器 mail.chenyun.org mail.chenyun.org
端口 993 587(优先)或 465
加密 SSL/TLS STARTTLS 或 SSL/TLS
认证方式 普通密码 普通密码

macOS 邮件

  1. 邮件添加账户其他邮件账户
  2. 填入姓名、邮箱、密码
  3. 收件:mail.chenyun.org:993 SSL
  4. 发件: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
2
# 创建 Configuration Set + SNS Topic + SQS Queue
# 发信时加 header: X-SES-CONFIGURATION-SET: chenyun-mail-events

踩坑清单

# 问题 解决
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) 协助完成。