邮件进了垃圾箱:SPF、DKIM与DMARC三层排查

邮件发出去才算数——这句话我以前不信,直到站点的注册验证邮件在三家邮箱服务商那里同时进了垃圾箱。查了三天,最后发现问题不在 Postfix,也不在内容,而是发件域的身份认证少了一层:只配了 SPF,DKIM 签名被代理层改坏了,DMARC 策略又是最松的 p=none,等于告诉收件方「随便吧」。这篇文章把邮件送达率这件事按「DNS 身份认证 → 内容与信誉 → 退信与黑名单」三层拆开,每一层给出可执行的检查和修复顺序,适合已经能发信、但信经常进垃圾箱的自建站。

第一层:SPF 只是「谁可以代发」,不解决被冒名

很多人把 SPF 当成万能的,实际上 SPF 只做一件事:声明白名单 IP。收件方拿到一封声称来自 @example.com 的信,会去查 SPF 记录,看发信 IP 在不在名单里。在名单里 = PASS,不在 = FAIL。它验证的是「发信服务器」,完全不涉及「这封信的内容有没有被改过」。

dig +short TXT example.com | grep spf1
# 正确写法示例(注意 -all 而不是 ~all)
"v=spf1 ip4:95.216.247.102 include:_spf.mx.example.com -all"

SPF 有三个高频坑,踩了就等于白配:

一是 DNS 查询次数超过 10 次。SPF 里的 include、a、mx、ptr、exists 每一项都可能触发额外的 DNS 查询,RFC 7208 硬性规定不能超过 10 次,超了直接返回 permerror,收件方一律当失败处理。很多站点用了第三方邮件服务的 include,再加上自己的 mx,很容易到 12、13 次。用下面这条命令数一下,实际查询次数比这个还多(include 内部还有嵌套):

dig +short TXT example.com | grep spf1 | tr ' ' '\n' | grep -cE '^(include:|a:|mx:|ptr:|exists:)'

二是 多个 SPF 记录并存。DNS 面板里改记录时不小心新建了一条新的,而没有删旧的,结果 TXT 里出现两条 v=spf1 开头的记录。RFC 规定出现多条必须返回 permerror,而且这个错误在部分收件方那边表现为「SPF 无记录」,排查时很容易看漏。检查方式是把所有 TXT 记录拉全:

dig +short TXT example.com | grep -c 'v=spf1'
# 必须等于 1

三是 用了 ~all 而不是 -all。软失败(softfail)在收件方看来是「可能不是本人」,多数情况下不会拒信,但会给垃圾评分加一点权重。只有确认自己的发信 IP 全部在记录里,才敢写 -all。改动前先用 ~all 跑一周,看有没有意外的发信源(比如站点上某个插件用另一台机器发通知邮件)。

第二层:DKIM 签名被谁改坏了

DKIM 的作用是给邮件盖一个「防篡改章」:发送方用私钥对指定的头字段和正文算哈希并签名,收件方用 DNS 里的公钥验证。这条链路里最容易出事的地方是中间有代理或过滤器改写了邮件。

我踩过的事故是:站点前面挂了 Nginx 做 TLS 终结,后端是 Postfix。Nginx 转发时默认会重写一些头,如果配置里有 proxy_set_header Host $host; 这类指令,或者更隐蔽地加了 add_header,邮件头就可能被改,导致签名校验失败。判断方法很直接——看收到的信里 Authentication-Results 头:

# 在收件端查看完整头(Gmail 里是「显示原始邮件」)
grep -iE 'authentication-results|dkim|spf|dmarc' /tmp/mail.eml

正常的输出长这样:

Authentication-Results: mx.example.net;
  spf=pass smtp.mailfrom=example.com;
  dkim=pass header.d=example.com;
  dmarc=pass action=none

如果 DKIM 是 fail 或 temperror,先别急着重新生成密钥,按这个顺序查:

第 1 步,确认签名本身有效。用 opendkim-testkey 检查密钥和 DNS 公钥是否匹配:

opendkim-testkey -d example.com -s mail -vvv
# 期望输出:key OK

如果这里就报 key not secure,通常是 DNS 记录里的公钥被换行符截断了。DKIM 公钥的 TXT 记录经常超过 255 字符,写记录时必须拆成多段用引号拼接,有些面板不认这种写法,会把整条记录截断。

第 2 步,确认选择器(selector)对得上。签名头里的 s= 参数就是选择器,DNS 记录名必须是 选择器._domainkey.域名。换过密钥但没换选择器,或者反过来,都会导致查不到记录。

第 3 步,确认签名覆盖的头字段没有被中间件追加。DKIM 只能签它「看到」的头,如果代理层在签名之后加了一个头,而签名算法里又把这个头算进去了(或者干脆是签名之后正文被改了),校验必然失败。常见加害者:文件系统自动加签名尾巴、企业网关插入免责声明、反向代理改写首行。

第 4 步,检查本地是否有多个 DKIM 过滤条目。Postfix 的 smtpd_milters 只留一个 OpenDKIM 实例,重复挂载会让后一个覆盖前一个的签名,白折腾。

第三层:DMARC 策略怎么从 none 走到 reject

DMARC 是前两者的「裁判」:它告诉收件方,当 SPF 和 DKIM 都不通过时该怎么做。它的记录里还包含一个 rua 地址,收件方会定期把汇总报告发过来——这是整条链路上最有价值的数据源,很多人配完就不看了。

_dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com; ruf=mailto:dmarc@example.com; pct=100; adkim=r; aspf=r"

四个参数的含义:p 是策略(none / quarantine / reject),pct 是策略应用比例,adkim 和 aspf 是严格(s)还是宽松(r)的对齐模式。宽松模式允许子域和父域对齐,对多子域站点更宽容。

正确的推进节奏是分三步,每步观察两周:

第一步,p=none 只收报告不拦信。这一步的目的是拿到「谁在冒用我的域名发信」的清单。报告是 XML 压缩包,人眼看不了,用 parsedmarc 解析成表格:

pip install parsedmarc
parsedmarc -f report.xml.gz --output json
# 关注 source_ip 与 dkim/spf 结果,尤其是 pass 但在陌生 IP 上的记录

如果报告里出现你完全不认识的 IP 且 SPF 是 pass,说明 SPF 记录里混进了别人的 include,或者你用了某个 SaaS 的发信服务却忘了登记。这些 IP 在收紧策略后会全部失败,所以在第一步就要确认名单。

第二步,改成 p=quarantine; pct=10,只对 10% 的失败邮件执行隔离。观察一周没有误伤,再提到 pct=50,然后 pct=100。这一步的意义不是「快点拦掉」,而是「确认有没有正常邮件被误杀」。

第三步,p=reject。到这一步,冒用你域名发信的行为基本被堵死了——这也是防钓鱼最有效的一层。

内容与信誉:不是靠技巧,是靠不犯错

身份认证过了,进不进垃圾箱还要看内容评分和发件域信誉。这一层没有捷径,但有几条硬红线:

别用短链接和图片链接代替正文。整封信只有一张图、或者链接全部指向跳转服务,是垃圾邮件的典型特征。验证邮件、通知邮件的内容应该和域名一致、可读、有明确的发件人身份。

退信要处理,不要硬发。持续给不存在地址发信会迅速拉低信誉。Postfix 的退信可以从日志里统计:

grep -oE 'to=<[^>]+>,.*status=(bounced|deferred)' /var/log/mail.log | wc -l
# 更精细:按域名统计退信率,超过 5% 的域名直接停发

控制发信频率,注意突发。个人站最容易踩的坑是「一次群发几百封通知」。新域名的日发信量应该是渐进的,第一周每天不超过 50 封,之后逐步增加。突然从 0 跳到 500 封,多数收件方会直接限速或送垃圾箱。

反转 DNS(PTR)要有,且要指回发信主机名。很多 VPS 服务商默认不设 PTR,或者设成一个通用名字。PTR 和 HELO 名不一致时,部分严格的收件方(尤其是企业邮箱)会直接拒收。检查方式:

dig +short -x 95.216.247.102
# 期望返回类似 mail.example.com
# 同时确认 Postfix 的 myhostname 与之一致

退信与黑名单:出事之后的补救路径

如果信已经被退回,先看退信类型,不同原因处理方式完全不同。5.7.1 通常是内容或 IP 声誉被拒;5.1.1 是收件人不存在;4.7.1 是临时限流,会重试;5.7.26 是 SPF/DKIM 对齐失败。

被列入黑名单时,先查在哪几个名单里,再按各家的流程申诉。查黑名单不需要装任何工具:

# 以 Spamhaus Zen 为例,反转 IP 加域名后缀查询
dig +short 102.247.216.95.zen.spamhaus.org
# 返回 127.0.0.x 表示在名单里,x 代表具体子名单(2=Spamhaus SBL,3=PBL 等)
# 无返回表示不在名单

申诉时最容易失败的一点是:只写「请把我移出名单」,不给原因和整改说明。有效的申诉要包含三件事——问题根因(比如某个表单被滥用群发)、已采取的整改措施(加了频率限制、清理了被入侵的账号)、以及承诺。整改措施必须是真的,因为申诉通过后一般会被监控一段时间,再次出现同类问题会被永久列入。

一份可以直接抄的检查清单

把上面三层压缩成发布前必过的七项,每次改动发信配置后跑一遍:

1. SPF 记录唯一,且以 -all 结尾;查询次数不超过 10。

2. DKIM 选择器与 DNS 记录名一致,opendkim-testkey 返回 key OK。

3. DMARC 记录存在,rua 指向一个能收信的地址,策略已推进到 quarantine 以上。

4. PTR 记录存在并指向发信主机名,与 HELO 一致。

5. 发一封测试信到 Gmail、Outlook、QQ 邮箱三个目标,全部检查 Authentication-Results 是否为三项 pass。

6. 退信率低于 5%,无持续 deferred 的地址。

7. 发信频率没有超过域名当前的信誉容量,新域名前两周控制日发信量。

这套流程的核心不是配置技巧,而是「先看后改」:先把三份报告拿到手(DMARC 汇总、退信日志、认证结果头),知道自己在收件方眼里是什么样子,再决定改哪一层。跳过这一步直接调参数,多半会把问题从垃圾箱挪到拒收。

Last modification:October 27th, 2026 at 09:28 pm

Leave a Comment