望舒

人的一生注定会遇到两个人一个惊艳了时光,一个温柔了岁月

背景

在 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) 协助完成。

The first week of May 2026 will be remembered as the moment AI agents stopped being experimental toys and became enterprise infrastructure. Three simultaneous events—Anthropic’s Claude Agent SDK going public, Microsoft Agent 365 reaching General Availability, and OpenAI’s quiet pivot toward an “agent-only” interface paradigm—marked a decisive shift in how we build, deploy, and interact with software.

This isn’t just another product launch cycle. It’s a fundamental re-architecture of how enterprise software operates.

阅读全文 »
0%