Postfix 为什么发不出信?用 postsleuth 排查卡住的邮件
队列越积越多,用户说发到 Gmail 的信"没收到",mailq 打出来一大屏。本文演示如何用 postsleuth(一个只读的 Postfix 诊断 CLI,同时是 MCP 服务器)从这一大屏里找到原因和下一步。
输出来源说明:第 1–5 节是 postsleuth v0.1.0 在仓库自带样例数据上的真实运行结果(testdata/queue.json 和 testdata/mail.log,由 testdata/gen.py 生成,模拟一台出站中继"糟糕的一天";域名和 IP 都是文档保留段)。"在真实 Postfix 上"一节摘自项目 CI 日志,CI 会在 Docker 里起一个一次性的 Postfix 3.7。下文输出除去掉颜色外没有任何改动。
常见的排查流程
手工排查 Postfix 卡信一般是:mailq | less,挑一个看起来常见的原因,去邮件日志里 grep,再用 sort | uniq -c 计数(但每行地址和 IP 都不同,根本聚不起来),跑 qshape 看是新问题还是老问题,最后 dig 查 SPF、DMARC 的 TXT 记录。postsleuth 读的是同样的东西(postqueue -j、邮件日志、DNS),把归并这一步替你做了。除非启动时加 --allow-mutations,它不会改动队列。
go install github.com/Blackman99/postsleuth/cmd/postsleuth@latest
# 或者从 GitHub Releases 下载静态二进制
1. 按原因归并队列
在邮件服务器上直接运行 postsleuth queue 即可。这里改为读取保存下来的 postqueue -j 输出:
postsleuth queue --queue-file testdata/queue.json
QUEUE 212 messages · deferred 205 · active 6 · hold 1 · oldest 1d23h (F5EC72BA69)
✗ 150 rate_limit gmail.com, googlemail.com
421-4.7.28 Gmail has detected an unusual rate of unsolicited mail originating from your IP addr…
→ The receiver is throttling you: lower smtp_destination_concurrency_limit / add
smtp_destination_rate_delay for that domain, and check your sending reputation.
✗ 34 blocklist hotmail.com, outlook.com
550 5.7.1 Unfortunately, messages from [ip] weren't sent. Please contact your Internet service …
→ The receiver rejects your IP or domain because of a blocklist entry: look the IP up (Spamhaus,
Microsoft SNDS) and request delisting after fixing the cause.
✗ 14 timeout oldcorp.example
connect to mx.oldcorp.example[ip]: Connection timed out
→ Connections to port 25 time out: many providers block outbound port 25, so test with nc -vz MX 25
and use a relayhost if blocked.
✗ 4 dns acme-typo.example
Host or domain name not found. Name service error for name=acme-typo.example type=MX: Host not …
→ DNS lookup for the destination failed: check the domain exists and has MX/A records, and that this
server's resolver works (dig MX domain).
! 3 greylisting smallbiz.example
450 4.7.1 <addr>: Recipient address rejected: Greylisted, see http://postgrey.schweikert.ch/hel…
→ Greylisting: normal, Postfix retries automatically. Only a problem if it persists for hours.
212 封邮件归成了 5 行。归并前会先把延迟原因里的地址、IP 和追踪 ID 去掉,所以只差在收件人上的 150 条 Gmail 延迟会合成一个 rate_limit 组。每组都有分类(共 17 类,从 rate_limit、blocklist 到 ptr、auth、tls)和处理建议。灰名单标为警告(!)而不是问题(✗),因为 Postfix 会自动重试。
要看某一组背后的具体邮件,按分类过滤:
postsleuth queue --queue-file testdata/queue.json --category blocklist --list --limit 1
78715BBD26 deferred 20h6m 85428 billing@example.com
→ j.smith141@hotmail.com
host outlook-com.olc.protection.outlook.com[52.101.42.3] said: 550 5.7.1 Unfortunately, messages from [203.0.113.7] weren't sent. Please contact your Internet service provider since part of their network is on our block list (S3150). (in reply to MAIL FROM command)
--list 会保留完整的回复。微软的 S3150 代码说明了原因(发信 IP 在其黑名单上),搜索或提工单时就用它。
2. 从什么时候开始的?(qshape)
postsleuth shape --queue-file testdata/queue.json
SHAPE deferred queue by recipient domain, age in minutes
T 5 10 20 40 80 160 320 640 1280 1280+
TOTAL 205 0 0 0 9 22 45 80 12 21 16
gmail.com 141 0 0 0 7 19 39 73 3 0 0
outlook.com 23 0 0 0 0 0 1 4 6 8 4
oldcorp.example 14 0 0 0 0 0 0 0 1 5 8
hotmail.com 11 0 0 0 0 0 0 1 2 6 2
googlemail.com 9 0 0 0 0 2 5 2 0 0 0
acme-typo.example 4 0 0 0 0 0 0 0 0 2 2
smallbiz.example 3 0 0 0 2 1 0 0 0 0 0
列是邮件在队列里的时长(分钟),与 Postfix 自带的 qshape 一致。从左往右看:Gmail 的延迟集中在 40 到 640 分钟这几列,没有更老的,说明限速大约是最近十一个小时内开始的,而且新邮件仍在被拦。oldcorp.example 全在右侧几列:这个目的地已经大半天连不上,如果情况不变,这些邮件会在 maximal_queue_lifetime 到期后退回。怀疑某个被盗账号在刷信时用 shape --by sender;这份样例里所有邮件都来自 example.com,所以用不上。
3. 日志里发生了什么
队列只反映还卡着的邮件,日志则记录了已经发生的事,包括退信和已送达的邮件:
postsleuth log --log testdata/mail.log --since all
LOG testdata/mail.log · 792 sent · 125 deferred attempts · 26 bounced · 1 expired · 30 rejected
! still failing: gmail.com (82), outlook.com (38), oldcorp.example (5)
✗ bounce 14 blocklist 550 5.7.1 Unfortunately, messages from [ip] weren't sent. Please cont… · outlook.com
✗ bounce 12 recipient_unknown 554 5.1.1 <addr>: Recipient address rejected: User unknown · yahoo.com
✗ deferral 82 rate_limit 421-4.7.28 Gmail has detected an unusual rate of unsolicited mail ori… · gmail.com
✗ deferral 38 blocklist 550 5.7.1 Unfortunately, messages from [ip] weren't sent. Please cont… · outlook.com
✗ deferral 5 timeout connect to mx.oldcorp.example[ip]: Connection timed out · oldcorp.example
✗ reject 30 relay_denied 554 5.7.1 <addr>: Relay access denied
domain sent deferred bounced last
gmail.com 355 82 0 deferred
outlook.com 30 38 14 deferred
yahoo.com 91 0 12 sent
oldcorp.example 0 5 0 deferred
fastmail.com 103 0 0 sent
proton.me 97 0 0 sent
qq.com 90 0 0 sent
example.com 26 0 0 sent
这里有两点是队列看不出来的。第一,Gmail 仍在接收大部分邮件(成功 355 次,延迟 82 次),符合按发送量限速、而不是彻底封禁的特征。第二,Outlook 已经直接退回了 14 封,最近一次尝试仍是延迟:黑名单已经造成了实际的丢信。30 条 relay_denied 是 smtpd 拒绝了试图借道转发的外部客户端,这是服务器在正常工作,但如果这个数字突然暴涨就值得留意。
在真实服务器上可以省略 --log:postsleuth 会先从 postconf 读取 maillog_file,再依次尝试 /var/log/mail.log、/var/log/maillog 和 journald。syslog、RFC 3339、journald 三种时间格式以及 .gz 轮转文件都支持。
4. 追踪单封邮件
用户问起某一封信时,按队列 ID 或地址追踪。如果邮件被退回,Postfix 生成的退信通知也会一并列出:
postsleuth trace 85C1D13765 --log testdata/mail.log
85C1D13765 bounced
Oct 11 09:11:44 mail postfix/smtpd[3005]: 85C1D13765: client=app1.internal[10.0.0.5]
Oct 11 09:11:44 mail postfix/cleanup[2671]: 85C1D13765: message-id=<ab8c2ecd20@example.com>
Oct 11 09:11:45 mail postfix/qmgr[1201]: 85C1D13765: from=<noreply@example.com>, size=5120, nrcpt=1 (queue active)
Oct 11 09:11:46 mail postfix/smtp[2757]: 85C1D13765: to=<old.user8@yahoo.com>, relay=mta7.am0.yahoodns.net[67.195.204.72]:25, delay=1.2, delays=0.1/0/0.6/0.5, dsn=5.1.1, status=bounced (host mta7.am0.yahoodns.net[67.195.204.72] said: 554 5.1.1 <old.user@yahoo.com>: Recipient address rejected: User unknown (in reply to RCPT TO command))
Oct 11 09:11:46 mail postfix/bounce[3301]: 85C1D13765: sender non-delivery notification: B4EE96B1C7
Oct 11 09:11:47 mail postfix/qmgr[1201]: 85C1D13765: removed
B4EE96B1C7 sent (bounce notice for 85C1D13765)
Oct 11 09:11:46 mail postfix/qmgr[1201]: B4EE96B1C7: from=<>, size=7200, nrcpt=1 (queue active)
Oct 11 09:11:47 mail postfix/local[4410]: B4EE96B1C7: to=<noreply@example.com>, relay=local, delay=0.1, delays=0/0/0/0.1, dsn=2.0.0, status=sent (delivered to mailbox)
Oct 11 09:11:47 mail postfix/qmgr[1201]: B4EE96B1C7: removed
5. 一次看全
postsleuth doctor 会依次输出队列摘要、qshape、日志摘要和发信域名的 DNS 检查(MX、SPF 及其 10 次查询上限、DKIM、DMARC、PTR/正反解一致性),最后给出结论行。发现问题时退出码为 1,可以直接放进 cron 或监控检查。由于样例里的域名不是真实域名,这里跳过 DNS:
postsleuth doctor --queue-file testdata/queue.json --log testdata/mail.log --since all --no-dns
echo $? # 1
输出就是第 1–3 节合在一份报告里,最后一行是:
RESULT 6 problem(s), 4 warning(s)
在真实 Postfix 上
CI 会构建一个 Debian Postfix 3.7 容器,用 transport 表把 retry.test 指向一个只会延迟的 retry: 传输,把 bounce.test 指向 error:,把 timeout.test 指向永远不会应答的 192.0.2.1。然后发送 6 封邮件,在容器外用 --wrap "docker exec -i postsleuth-it" 运行 postsleuth,跟你对 docker-mailserver 或 mailcow 的用法完全一样。容器里的 mailq 是这样的:
-Queue ID- --Size-- ----Arrival Time---- -Sender/Recipient-------
8CFA98C2D24 284 Sun Oct 11 23:24:29 app@example.test
(Too many messages from your IP, rate limit exceeded, try again later)
user1@retry.test
BB3F58C2D39 284 Sun Oct 11 23:24:29 app@example.test
(connect to 192.0.2.1[192.0.2.1]:25: Connection timed out)
ceo@timeout.test
9D3388C2D2E 284 Sun Oct 11 23:24:29 app@example.test
(Too many messages from your IP, rate limit exceeded, try again later)
user2@retry.test
同一容器上的 postsleuth doctor(取自提交 fd6a2f8 的 CI 运行,2026-10-11):
postsleuth doctor · mail.example.test · 2026-10-11 23:24 UTC
QUEUE 3 messages · deferred 3 · oldest 0m (8CFA98C2D24)
✗ 2 rate_limit retry.test
Too many messages from your IP, rate limit exceeded, try again later
→ The receiver is throttling you: lower smtp_destination_concurrency_limit / add
smtp_destination_rate_delay for that domain, and check your sending reputation.
✗ 1 timeout timeout.test
connect to ip[ip]: Connection timed out
→ Connections to port 25 time out: many providers block outbound port 25, so test with nc -vz MX 25
and use a relayhost if blocked.
SHAPE deferred queue by recipient domain, age in minutes
T 5 10 20 40 80 160 320 640 1280 1280+
TOTAL 3 3 0 0 0 0 0 0 0 0 0
retry.test 2 2 0 0 0 0 0 0 0 0 0
timeout.test 1 1 0 0 0 0 0 0 0 0 0
LOG docker exec -i postsleuth-it cat /var/log/postfix.log · 1 sent · 3 deferred attempts · 4 bounced · 0 expired · 0 rejected
! still failing: retry.test (2), timeout.test (1)
✗ bounce 2 dns Host or domain name not found. Name service error for name=example.te… · example.test
✗ bounce 1 dns Host or domain name not found. Name service error for name=nxdomain.i… · nxdomain.invalid
✗ bounce 1 recipient_unknown Recipient address rejected: User unknown in virtual mailbox table · bounce.test
✗ deferral 2 rate_limit Too many messages from your IP, rate limit exceeded, try again later · retry.test
✗ deferral 1 timeout connect to ip[ip]: Connection timed out · timeout.test
RESULT 1 problem(s), 7 warning(s)
可以看到 doctor 比 mailq 多给了什么:延迟被分类归并;日志部分还列出了已经离开队列的退信,包括 example.test 的二次退信(容器里发信域名没有 DNS,所以退信通知本身也被退回)。同一次运行随后还验证了不带 --allow-mutations 的 delete 和 delete ALL 都会被拒绝。
接下来怎么处理
不同分类对应不同处理方法,大多数都不是"flush 一下队列":
- rate_limit(Gmail 4.7.28):不要 flush,集中重试只会更糟。为该传输调低
smtp_destination_concurrency_limit或加上smtp_destination_rate_delay,检查 SPF/DKIM/DMARC 对齐和发信量异常的原因,然后等退避重试。 - blocklist(Outlook S3150):IP 信誉有问题。先修复根因(开放转发、被盗账号、缺 PTR),再走微软的解封表单并查看 SNDS。
- timeout:在服务器上执行
nc -vz <mx> 25。很多云厂商默认封出站 25 端口,这种情况要改用 relayhost。 - dns:通常是收件域名拼错了,直接告诉发件人,不要等到队列超时。
确实需要改动队列时,命令是有的,但默认锁着:
$ postsleuth delete 78715BBD26
postsleuth: delete changes the mail queue; re-run with --allow-mutations if you mean it
$ postsleuth delete ALL --allow-mutations
postsleuth: invalid queue ID "ALL"
给 AI 助手用
postsleuth mcp 通过 stdio 把同样的只读能力暴露为 MCP 工具,Claude、Cursor 等 MCP 客户端就能在你指定的服务器上(本机、--wrap "ssh mx sudo" 或容器内)回答"为什么发往 Outlook 的信卡住了?"。修改队列的工具默认根本不出现在工具列表里,只有启动时加 --allow-mutations 才会提供,并且每次调用还要 confirm: true。配置示例见文档。
如果你遇到的真实延迟原因被归到 other,欢迎提 issue 附上原文(去掉地址),分类器就是这样变准的。