1. 问题背景
使用硕士时候学校的邮箱注册了一些服务,但今年开始发现,无法通过校友邮箱转发服务接收验证码了。
我原来的邮箱链路是:
认证邮件 / OTP / MFA 验证码
-> 学校邮箱
-> 校友邮箱转发服务
-> 个人 Outlook / Gmail 邮箱
之前这条链路可以正常工作,但某个时间点之后,来自部分服务商的验证码邮件不再到达个人邮箱。典型现象是:
普通邮件还能转发
验证码 / OTP / MFA 邮件收不到
收件箱、垃圾箱、隔离区都没有
发件方也不一定能看到明显退信
后来确认原因与邮件安全策略有关:转发方启用了更严格的 DMARC / SPF / DKIM 相关策略后,邮件在自动转发过程中容易出现认证链路断裂。
简单说:
原始发件方 -> 学校邮箱:正常
学校邮箱 -> Outlook/Gmail:因为转发导致 SPF/DMARC 签名对齐失败
Outlook/Gmail:直接拒收、丢弃,甚至不会进垃圾箱
现代邮件系统对自动转发邮件越来越严格。尤其是 OTP / MFA / 验证码类邮件,主流邮箱服务商可能直接屏蔽或丢弃,用户侧没有任何显示。
解决方向:需要接收端不对经转发而来的第三方认证邮件执行不可控的硬拒或静默丢弃。
所以可行方案有两类:
1. 改用一个对转发邮件更宽松的收件服务商
2. 自建邮件服务,自己控制接收策略
我选择了第二种:用自己的域名和 VPS 搭一个只用于接收转发验证码邮件的小型邮箱。
2. 解决方案概览
最终链路改成:
认证邮件 / OTP / MFA 验证码
-> 学校邮箱
-> 校友EFL转发服务
-> 自建邮箱服务
自建邮箱只用于接收少量关键邮件,不作为主力邮箱,不参与大量发信。
最终域名规划类似:
邮箱域名:example.com
Web UI:mail.example.com
SMTP:smtp.example.com
IMAP:imap.example.com
可选 POP:pop.example.com
实际邮箱地址类似:
auth-forward@example.com
这里mail.example.com / smtp.example.com / imap.example.com 是服务器主机名,不是邮箱后缀。邮箱地址是:xxx@example.com
2.1 资源准备与为什么选择 PMail
我手里已有资源:
1. 一个自己的域名
2. 一台支持 rDNS/PTR 的 VPS
3. VPS 已安装 Docker
4. 服务器资源较小,约 2C / 2.5GB RAM
5. 邮箱用途很窄:主要接收转发来的 OTP / MFA 邮件
常见自建邮箱方案包括 Mailcow、Mailu、docker-mailserver、PMail 等。
我的选择是 PMail,原因很直接:
Mailcow:功能完整,但资源占用偏大,不适合小 VPS
Mailu:更完整,但组件也更多
docker-mailserver:标准、可控、适合严肃邮件服务,但配置相对重
PMail:轻量go服务,部署简单,内存占用仅10mb以内。适合低流量、收信为主的小邮箱
这里只是接收少量转发验证码,核心需求是:
1. 能收 SMTP
2. 支持 IMAP
3. 支持 DKIM/SPF/DMARC 相关基础能力
4. 能放宽过滤策略
5. 占用资源低
因此 PMail 足够。
2.1.1 如何购买服务器
我用的是此blog搭建所在的VPS,供应商是Racknerd,host博客+mail+一堆服务还只用了不到一半ram。有需要的可以看购买渠道。购买别的也是一样的,最好是海外的方便域名绑定。
重点是购买 VPS 时确认:
1. 25 端口没有被封
2. 支持设置 rDNS / PTR (Racknerd给服务商发一封邮件说明需要自建邮件服务,自己的ip以及目标主机记录即可)
3. 有固定公网 IPv4
4. IP 不在严重黑名单中
2.1.2 如何购买域名
此处略。
重点是域名服务商需要支持:
A 记录
MX 记录
TXT 记录
DKIM TXT
DMARC TXT
我自己用的是腾讯云的,100多十年。
2.2 配置 DNS 与 rDNS
假设域名是:
example.com
VPS IP 是:
203.0.113.10
基础 DNS 记录:
@ A 203.0.113.10
mail A 203.0.113.10
smtp A 203.0.113.10
imap A 203.0.113.10
pop A 203.0.113.10
@ MX 10 smtp.example.com.
@ TXT v=spf1 a mx ~all
_dmarc TXT v=DMARC1; p=none
DKIM 记录由 PMail 生成后再添加,格式通常类似:
default._domainkey TXT v=DKIM1; k=rsa; p=...
rDNS / PTR 需要找 VPS 服务商设置。
如果 MX 是 smtp.example.com,建议 rDNS 也设置成:
203.0.113.10 -> smtp.example.com
然后确认正反向都能解析:
dig +short A smtp.example.com
dig +short -x 203.0.113.10
期望结果:
smtp.example.com -> 203.0.113.10
203.0.113.10 -> smtp.example.com.
rDNS 主要影响发信可信度。收信不强依赖 rDNS,但正规邮件服务器最好配置。
2.3 配置 UFW 并放行端口
开启 UFW:
sudo ufw enable
sudo ufw status
放行 SSH、HTTP、HTTPS:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
放行邮件端口:
sudo ufw allow 25/tcp
sudo ufw allow 465/tcp
sudo ufw allow 587/tcp
sudo ufw allow 993/tcp
含义:
25/tcp SMTP 收信,其他邮件服务器投递邮件到这里
465/tcp SMTPS,客户端发信
587/tcp SMTP Submission / STARTTLS,客户端发信
993/tcp IMAPS,手机或电脑邮箱客户端收信
如果不用 POP3,不需要开放:
sudo ufw allow 110/tcp
sudo ufw allow 995/tcp
检查状态:
sudo ufw status
2.4 Docker 拉起 PMail
如果 VPS 上没有其他网站,也不需要 Caddy / Nginx 作为统一入口,可以直接让 PMail 使用 80/443,并使用 PMail 自己的 HTTP 自动证书获取。
docker-compose.yml:
services:
pmail:
image: ghcr.io/jinnrry/pmail:latest
container_name: pmail
restart: unless-stopped
ports:
- "25:25"
- "465:465"
- "587:587"
- "993:993"
- "80:80"
- "443:443"
volumes:
- ./config:/work/config
启动:
mkdir -p /opt/pmail/config
cd /opt/pmail
nano docker-compose.yml
docker compose up -d
docker logs -f pmail
首次启动如果没有配置文件,PMail 会进入初始化流程,这是正常现象。
2.5 可选:已有 Caddy 时的反代配置
如果服务器上已经有 Caddy/Nginx 等反代工具负责网站入口,不能让 PMail 直接占用宿主机的 80/443。我个人用caddy顺手,这里提供思路,nginx操作大差不差。
推荐做法是:
Caddy 继续占用宿主机 80/443
PMail 容器内部仍然监听 80
宿主机把 PMail 的 80 映射到 127.0.0.1:8081(或者其他端口,自选)
PMail 使用 HTTP-01 自动申请和续期证书
Caddy 只把 ACME challenge 路径转发给 PMail
PMail compose:
services:
pmail:
image: ghcr.io/jinnrry/pmail:latest
container_name: pmail
restart: unless-stopped
ports:
- "25:25"
- "465:465"
- "587:587"
- "993:993"
- "127.0.0.1:8081:80"
volumes:
- ./config:/work/config
这里不需要把 PMail 的 443 映射出来。Web 后台 HTTPS 由 Caddy 对外提供,PMail 自己只需要 HTTP 服务和邮件协议端口。
PMail 的 config.json 中建议设置:
"sslType":"0",
"httpsEnabled":2
含义:
sslType = 0:使用 HTTP-01 自动申请/续期证书
httpsEnabled = 2:PMail 自己不启用 WebUI 的 HTTPS,由 Caddy 负责外层 HTTPS
Caddyfile 示例:
http://mail.example.com {
@pmail_acme path /.well-known/acme-challenge/*
handle @pmail_acme {
reverse_proxy 127.0.0.1:8081
}
handle {
redir https://{host}{uri} 308
}
}
http://smtp.example.com,
http://imap.example.com,
http://pop.example.com {
@pmail_acme path /.well-known/acme-challenge/*
handle @pmail_acme {
reverse_proxy 127.0.0.1:8081
}
handle {
respond 404
}
}
mail.example.com {
reverse_proxy 127.0.0.1:8081
}
这里的关键点是:
1. mail.example.com 是 Web UI 域名,普通 HTTP 请求可以跳转到 HTTPS
2. smtp/imap/pop.example.com 不是网页域名,只需要放行 ACME challenge,其它 HTTP 请求直接 404 即可
3. 不要对 /.well-known/acme-challenge/* 使用 handle_path,因为 PMail 需要收到完整路径
4. 不要让该路径被重定向到 HTTPS,否则 HTTP-01 验证会失败
5. PMail 生成的证书仍然会用于 SMTP/IMAP/POP 的 TLS 端口
2.6 PMail 初始化配置
进入 PMail 管理页面后,按实际域名填写:
邮箱域名:example.com
Web 域名:mail.example.com
SMTP 域名:smtp.example.com
IMAP 域名:imap.example.com
POP 域名:pop.example.com
数据库:SQLite
过滤策略:关闭或最低
SSL:Auto
证书验证方式:HTTP Request
如果前面有 Caddy,确认 Caddy 已经把以下域名的 ACME challenge 路径转发到 PMail:
http://smtp.example.com/.well-known/acme-challenge/*
http://imap.example.com/.well-known/acme-challenge/*
http://pop.example.com/.well-known/acme-challenge/*
如果只是收少量转发验证码,可以关闭或降低过滤级别。
这个邮箱不是主力邮箱,使用私密地址即可,例如:
auth-forward@example.com
不要使用容易被扫到的地址:
admin@example.com
contact@example.com
test@example.com
2.7 证书验证与续期
PMail 会为以下域名申请同一张 SAN 证书:
mail.example.com
smtp.example.com
imap.example.com
pop.example.com
如果使用 HTTP-01,Let’s Encrypt 会访问:
http://对应域名/.well-known/acme-challenge/<token>
因此,哪怕 Web UI 只使用 mail.example.com,也必须让 smtp/imap/pop 这些域名的 80 端口 challenge 路径能到达 PMail。
测试 Caddy 转发是否正确:
curl -i http://smtp.example.com/.well-known/acme-challenge/test
curl -i http://imap.example.com/.well-known/acme-challenge/test
curl -i http://pop.example.com/.well-known/acme-challenge/test
这里返回 404 是正常的,因为 test 不是真实 ACME token。重点是不要返回 301/308 跳转,也不要被 Caddy 自己的规则拦截。
同时可以看 PMail 日志:
docker logs -f pmail
如果看到类似:
AcmeChallenge: /.well-known/acme-challenge/test
AcmeChallenge Error Token Infos:map[]
说明请求已经进了 PMail,HTTP-01 路径是通的。
证书签发后可以检查:
openssl x509 -in ./config/ssl/public.crt -noout -issuer -dates -ext subjectAltName
正常情况下应看到 Let’s Encrypt issuer,并且 SAN 中包含 smtp/imap/pop 四个域名。
PMail 会定期检查证书,在证书剩余时间较短时自动续期。只要 Caddy 的 challenge 转发一直有效,后续不需要手动添加 DNS TXT 记录。
2.8 创建邮箱并测试
创建邮箱:
auth-forward@example.com
测试 DNS:
dig +short MX example.com
dig +short A smtp.example.com
dig +short A imap.example.com
dig +short TXT example.com
dig +short TXT _dmarc.example.com
dig +short -x 203.0.113.10
测试端口监听:
sudo ss -lntp | egrep ':25|:465|:587|:993|:8081|:8443'
测试 IMAP TLS:
openssl s_client -connect imap.example.com:993 -servername imap.example.com
测试 SMTP 465:
openssl s_client -connect smtp.example.com:465 -servername smtp.example.com
测试 SMTP 587 STARTTLS:
openssl s_client -starttls smtp -connect smtp.example.com:587 -servername smtp.example.com
证书正常时应看到:
Verify return code: 0 (ok)
从外部邮箱发一封测试邮件到:
auth-forward@example.com
同时看日志:
docker logs -f pmail
如果 Web UI 能看到邮件,说明收信链路已经打通:
外部发件方 -> MX -> smtp.example.com:25 -> PMail -> auth-forward@example.com
2.9 手机邮箱客户端配置
手机邮箱 App 中手动添加:
邮箱地址:auth-forward@example.com
用户名:auth-forward@example.com
密码:PMail 中设置的邮箱密码
IMAP:
主机名:imap.example.com
端口:993
安全:SSL/TLS
SMTP:
主机名:smtp.example.com
端口:465
安全:SSL/TLS
如果 465 不通,可试:
端口:587
安全:STARTTLS
用户名填写完整邮箱地址。
2.10 切换转发目标
确认直收测试成功后,把校友邮箱转发目标从原来的 Outlook / Gmail 改成:
auth-forward@example.com
然后触发一次验证码邮件,同时开日志:
docker logs -f pmail
判断方式:
1. PMail 日志有连接、有投递:说明转发链路已到达自建邮箱
2. PMail 日志完全没有连接:说明转发方没有把邮件转出来
3. PMail 日志有连接但没进邮箱:检查过滤、规则、收件人地址
3. 最终效果
改造前:
OTP / MFA 邮件经学校转发后,被 Outlook/Gmail 等主流邮箱服务商屏蔽或丢弃
改造后:
OTP / MFA 邮件经学校转发后,投递到自建邮箱
接收策略由自己控制
可通过 Web UI 或 IMAP 客户端读取
这个方案的重点不是把自建邮箱做成主力邮箱,而是建立一个可控的低流量收信终点,专门接收因 DMARC/SPF/DKIM 转发链路问题而无法进入主流邮箱的验证码类邮件。
4. 注意事项
- 自建邮箱不等于高送达率邮箱。收信容易,发信难。
- 如果只用于收验证码,不建议把它当主力发信邮箱。
- 25 端口必须可用,否则外部邮件服务器无法投递。
- rDNS/PTR 建议配置,尤其是未来需要发信时。
- DKIM 用 PMail 生成的记录,不要手写。
- PMail 推荐使用 HTTP-01 自动续期。DNS-01 手动验证可以跑通,但如果 PMail 不能通过 DNS API 自动修改 TXT 记录,续期时仍可能需要手动操作。
- 如果只是收少量转发验证码,可以降低过滤强度,但邮箱地址应尽量私密。
- 如果转发方本身就不再转发 OTP/MFA 邮件,自建邮箱也收不到;自建邮箱只能解决“目标邮箱拒收/丢弃”的问题,不能强迫转发方转发。