「发送」这个词太轻了。按下它的那一刻,我其实只是让 PHP 把一个字符串交给了一台机器;而在这个字符串躺进别人的收件箱之前,它还要经过四台机器、盖上三个章,最后通过一道不由我决定的审核。
这篇文章把这段路走了一遍。用的全是我自己服务器上的真实数据:邮件头、DNS 记录、谷歌发来的聚合报告,以及写这篇文章时现场做的两封投递实验。
一、先把「发送」这个词拆开
9 月 14 日晚上 23:15,我从博客服务器发出一封自检信,收件人是我自己另一个邮箱。这封信后来成了本文的主要证据——它的邮件头里,每一跳都留了签名。

四段分别是:WordPress 侧,wp_mail() 把信交给 SMTP 插件;博客服务器,用主账号认证、连上 smtp-relay.gmail.com 的 587 端口,全程 TLS 1.3;Google 中继,替我用自己的域名密钥盖了章,再从 209.85.220.101 这个出口把信投出去;对方收件方,收下它,并决定它究竟进收件箱还是垃圾箱。
三年前我会以为「发送成功」是最后一个环节。事实是:它只是第一个——wp_mail() 返回 true,只代表我把信交给了中继,不代表收件人收到了。
二、三个印章,各管一件事
SPF、DKIM、DMARC 常被并称「三件套」,但它们在做三件不同的事,失败的样子也完全不同:
- SPF:回答「这台机器有没有资格代表我的域名发信」。它读的是 DNS 里的 TXT 记录,检查的对象是信封上的发件人。
- DKIM:回答「这封信在路上有没有被动过」。签发时算出正文与头部的哈希、用私钥签名,收件方拿 DNS 里的公钥验签。
- DMARC:回答「前两项和信纸上署的域名对得上吗,对不上怎么处置」。它不自己验证,它是那个拿主意的。

真正容易被忽略的是对齐这件事:SPF 验的是信封上的域,DKIM 验的是签名里的域,而收件人在屏幕上看到的是信纸上的 From。三者必须指向同一个域,DMARC 才认账。我们用的是宽松模式(adkim=r / aspf=r):同组织域即可,不必逐字一致。
三、读一封真信:证据全在邮件头里
如果你只打算学一件事,就学这个:把邮件头打开。验证结果、每一跳经手人、退信地址,全在里面,比任何第三方检测工具都可信——因为它就是收件方自己写下的记录。

三个最容易读错的地方:Received 链要从下往上读,最下面那行才是真正的发信人;Return-Path 是信封上的发件人,SPF 检查的就是它,也是退信的去处,而 From 只是信纸上的署名;Authentication-Results 是收件方服务器在收信那一刻给出的判决书。
还要说清一个分寸:那一行 dkim=pass 并不是「对方保证你没问题」,它只是「它当时的验签结论」。认证通过,不等于会被放行——这就引出下面两节。
四、谷歌每天都在给我的域名做体检
因为我在 DMARC 记录里写了 rua=mailto:…,谷歌每天会把「谁在用我的域名发信、通过没通过认证」打包成一份 XML 报告发到那个地址。最近这一份的内容是这样:
report_id: 3894261694774059778
窗口: 2026-09-14 00:00 → 23:59 UTC(= 北京时间 09-14 08:00 → 09-15 07:59)
策略: p=none sp=none adkim=r aspf=r pct=100
209.85.220.69 6 封 dkim=pass spf=pass disposition=none
209.85.220.101 48 封 dkim=pass spf=pass disposition=none
合计 54 封,全部通过,没有一个陌生来源
两点值得说明。第一,为什么只有两个 IP:因为这一天的信都走 Google 中继出去,出口就这两个,能对上我们自己抓到的邮件头。第二,为什么处置是 none:因为我写的是 p=none,这条策略目前只记账、不处置。
所以它的价值不在「被警告」,而在于这是一个可见性开关。哪天有人冒用这个域名发信,会先在这份日报里现形——而不是等你收到客户的投诉电话。等它连续几周都干净,再考虑把策略收紧到 quarantine 也不迟。
五、那么,为什么三项全 pass 还是进了垃圾箱
那几天里确实有一封信被送进了垃圾箱,而那封信在垃圾箱里显示的状态仍然是 dkim=pass、spf=pass。听上去矛盾,其实一点都不矛盾:认证只负责回答「你是谁」,不负责回答「你是不是好人」。(那封信现在已不在邮箱里,无法复验,这里只能作为当时的一次实测记录。)

收件方判断一封信,至少要过五道关:认证、IP 与域名的历史信誉、内容与结构、收件人的行为、以及它自己的最终策略。我们只控制得住第一道,以及第三道的一部分。所以「全部 pass」的正确读法是:你没有在这道关卡被拦下,而不是你一定会被放行。
六、写这篇文章时,现场做的两封实验
为了让上面这节不全是推理,我现场从同一台服务器、用同一套认证、同一个 From 发出了两封信,收件人都是我另一个邮箱:
- A 信:纯文本,无链接、无图片、无营销词。
- B 信:HTML,4 个链接,带「限时免费」「立即领取」这类典型紧迫用语。
结果是:两封都进了收件箱。
这个结果说明什么?它至少说明:在认证齐备、域名没有前科的前提下,一些刻意堆上去的「垃圾特征」并不会立刻触发拦截。但它只是一次样本,我不敢拿它反推成「这样写也没事」——真实判定还牵着发送频率、域名年龄、收件人的行为历史。样本不足时,任何结论都是过度解读。
顺便一提,这两封测试信的标题里都写着「取证」两个字,收件人是我自己,没打扰任何人。
七、抄得走的四件事

如果只想要一份能干活的清单,那就是:三条认证记录(SPF 说明谁有权发信、 DMARC 说明对不上怎么办、DKIM 公钥供人验签,外加一条 MX)+四步排查顺序(查 DNS → 发一封看 Authentication-Results → 翻对方垃圾箱 → 再回来调内容与信誉)。
还有一件顺手记下的事:WordPress 会强制把 PHP 的时区设为 UTC,所以在邮件代码里写 date(),得到的时间会比北京时间少 8 小时。我第一次看到自己发的邮件主题里写着 06:12、而墙上钟明明是 14:12 的时候,愣了三秒才反应过来——要本地时间,得用 current_time()。
八、一封邮件教会我的
写这篇的起因很简单:有人看到谷歌发来的报告,问我「它为什么给我发这个」。我一开始也以为那是某种警告,读懂之后才发现——那是一份我自己订购的日报,只不过我忘了自己订过。
把这件事拉高一点看:邮件大概是互联网上少数还在认真执行「先证明你是谁,再谈内容」的协议。它在门口就要了身份,之后才轮到内容说话。而我们平时写代码、发内容、做产品,往往把顺序搞反了:先急着把话说漂亮,再回头补身份。
认证齐备不等于被信任;但认证不齐备,连被信任的机会都没有。
如果你也在折腾自建站发信,可以顺路看看上一篇《我用一个下午修好了自己的发信身份》,那篇讲的是这条链路上另一头的事:发信别名与身份。