邮件提交协议(MSA):与标准 SMTP 有何不同?

上周有个客户的运维把发信程序从 25 端口改到 587,账号密码都填了,日志里还是 530 5.7.0。他把日志甩过来,问是不是我们把认证关了。

认证没关。他那套代码从头到尾没发过 AUTH——EHLO、MAIL FROM、RCPT TO、DATA,四步走完就等 250。这套流程在 25 端口上跑了七八年,一次没出过事。同一个程序换个端口就死,这件事本身比故障有意思:25 和 587 之间隔着的,是两种角色,端口号只是外面那层皮。

我早年以为 MSA 就是“给客户端用的 SMTP”,把 25 那套东西换个号、加个登录框。后来把 RFC 6409 从头读了一遍才明白,它拆的是两种信任关系,跟代码路径没多大关系。

25 端口守的是收件人

一台 MTA 收到连接,要回答的问题是:这封信是不是发给我的用户的。看的是 RCPT TO 后面的域。属于自己管的域,收下;不属于,就问一句你凭什么让我用带宽和信誉帮你转发,答不上来就拒。整个判断里,发件人是谁根本不重要——外域任何一台服务器都可能来信,你不可能要求对方先证明身份,那等于把收信关掉。

麻烦出在早年所有人挤同一个端口。客户端连 25,服务器之间也连 25。前者要求只让内部人进,后者要求对全网开放。一个端口背两种信任级别,只能取宽的那个。开放中继的年代,这条缝被人拿来塞垃圾邮件,发信成本接近零,成本转嫁给被中继的那台机器。后来 ISP 陆续封掉 25 的出站连接,出差、换网络、住酒店,信发不出去。收信和发信第一次在物理上打架。

1998 年 12 月的 RFC 2476 干的事就是拆:新开 587,定义 MSA 这个角色,只接 MUA 提交,不负责往别的域投递。SMTP 的命令集本来够用,拆开的理由在信任模型上,跟协议能力无关。

拆开了,之后补了十三年

2476 有个硬伤,它出生时 SMTP AUTH 还没标准化(RFC 2554 要到 1999 年 3 月才发布,今天这套机制是 RFC 4954 定义的)。最早那批 MSA 手里没有客户端普遍支持的认证手段,只能靠 IP 白名单、内网位置,或者“先 POP 再 SMTP”这种土办法。能连上 587 的人多半已经在可信网段里,认证等于没做。端口分开了,门还虚掩着。

2006 年 4 月的 RFC 4409 把认证、地址规范、消息处理补齐,同时收进一个叫 SUBMITTER 的扩展,定义在 RFC 4405,实验状态。想法很直接:在 MAIL FROM 里加个参数,写明“最近一次把这封信塞进传输流的是谁”,接收方不用等收完整封信就能预判发件人认证该怎么判。听上去是给认证打了个提前量。

它没成。2012 年 RFC 6686 给 2006 年那批实验做总结,数据很干脆:全网扫下来,在 EHLO 里报 SUBMITTER 的 MTA 不到 5%,找到的实现只有三个。2011 年 11 月的 RFC 6409 取代了 4409(4409 当年取代的是 2476),成了今天 MSA 的定义文本。它把 SUBMITTER 相关内容整段删掉,现在翻全文,这个词一次都不出现。

失败的原因比它当初怎么设计更值得看。同一批实验里有 SPF,同样 2006 年、同样实验状态,SPF 活成了标准,SUBMITTER 死了。差别不在谁更聪明。SPF 落地只需要域名的管理者往 DNS 加一条记录,一个人一下午干完,干完立刻见效。SUBMITTER 要 MUA 改(改了不获益),要中间每一跳中继原样透传(不认识的参数按规矩该忽略,透传还得防伪造),要接收方愿意相信一个没有密码学依据的声明。三方都得先动,没有一方先动就能拿到好处。

更麻烦的是模型错位。SMTP 扩展是逐跳协商的:EHLO 报能力,每一跳自己决定支不支持。SUBMITTER 想表达的是端到端的事实——只有第一跳知道提交者是谁,这个事实要穿过整条链。拿逐跳的机制承载端到端的信息,中间哪一跳不配合,参数就退化成一句没人读的注释。实现层面没出什么问题,错在拿错了工具。

真正把发件人认证做成的是另一条路:SPF 查域、DKIM 签名、DMARC 定策略。它们不要求全链路改协议,只要求发信的域自己签字、收信的方自己查 DNS。成本落在愿意承担的人身上,所以推得动。

三个端口,三种关系

25 是 MTA 对 MTA,陌生机器之间说话,唯一一个必须无认证对全网开放的门。不开放就收不到外域来信。

587 是 MUA 对 MSA。先 STARTTLS 升级加密,再 AUTH,然后才能 MAIL FROM。RFC 6409 写得很清楚:默认情况下没认证就给 530,除非 MSA 已经用别的方式确认了授权,比如对方在受保护的内网里。

465 也是 MUA 对 MSA,但 TLS 从第一个字节就开始。它的命不太好:九十年代末作为 smtps 注册,后来 STARTTLS 成了标准路径,它被判定为重复实现,注册撤掉。客户端不管这套,十几年一直在用。2017 年底 IANA 重新分配,2018 年 1 月的 RFC 8314 正式确认它可以用于隐式 TLS 的提交,这份文档顺带还更新了 6409。

465 回来,我认为根子在 STARTTLS 的模型:先明文握手,再协商升级,中间人只要让客户端看不见服务端的能力,客户端就老老实实退回明文,协议里没有一个“必须加密、否则别发”的硬点。465 没有明文阶段,也就没有降级空间。RFC 8314 的口气很硬,明文提交该结束了。新部署我倾向 465——STARTTLS 当然也能配得很严,但它那个模型天生允许例外,465 没有例外可给,运维少一类事故。

两道门,各查各的

这是最容易混的地方。25 端口的 MTA 查收件人域:这封信是不是发给我的。是就收,不是就看有没有中继授权,没有就 554 5.7.1。MSA 查发件人身份:这封信是不是你发的。AUTH 上来的账号,和 MAIL FROM、From 里的地址对不对得上,对不上就 550 5.7.1。

一个问“给谁”,一个问“谁给的”,两道门正交,互不替代。MSA 基本不看收件人域,收件人写哪儿都行——它干的是替一个有账号的人发信,不负责帮陌生人转发。

这个分工有个常被忽略的后果:提交时返回 250,只代表“这封信我接了”,不代表“对方收到了”。MSA 把责任接过来,投递、重试、退信都在后面。很多业务系统把发信接口的返回值当送达回执用,这个误解在排障时能把人带沟里。

谁有权改这封信

RFC 6409 列了一串 MSA 可以做的修改:补 Date,补 Message-ID,身份已知而 From 没给时补一个 Sender,做传输编码,按本地策略改写地址,必要时加签名。补 From 这件事规范里没有明文,不少实现是拿认证身份直接补的——业界常见,边界也确实模糊。RFC 5321 对中继的要求是另一个方向:除了加 Received、Return-Path 这类轨迹头,不得改动消息内容。

我一开始以为这是权限大小的区别,后来发现是身份的区别。MSA 是作者这一侧的代理,替用户干活,而用户交上来的常常是半成品:客户端不知道自己的时区,Date 就是空的;有些客户端压根不生成 Message-ID。把这些补完是它的职责。中继是搬运工,跟内容没有关系,一改,端到端的完整性就没了。

还有一个规范没单列、但所有 MSA 都在做的动作:摘掉 Bcc。Bcc 在提交阶段是给服务器看的指令——这些人也要收,别让他们互相看见。信一旦拆成多份投递出去,这个头还留着,密送名单就跟着泄露。它必须在离开 MSA 之前消失。这种事不写进条款,因为不改就是事故。

改写权有后遗症,边界很模糊。补一个 From 和改一个 From,差在哪?不同实现给的答案不一样。上了 DKIM 之后,模糊变成硬约束:签名必须发生在所有改写之后,MSA 成了签名前最后一道闸口。位置放错,整条链的密码学校验全崩,而且崩得毫无提示,只在接收方的过滤器里表现为“签名无效”。

SIZE,和一个反直觉的膨胀

RFC 1870 给了 SIZE:EHLO 时服务端广播上限,客户端在 MAIL FROM 里带上预估字节数,超了直接 552 5.3.4。

这里有个几乎每张工单都会踩的坑:用户看到的附件大小,和线上真正传输的字节数,不是一回事。MIME 编码会膨胀。base64 用四个字符表示三个字节,光编码就多出三分之一,还没算按行折断插入的换行;quoted-printable 对二进制内容更差。一个 20MB 的 PDF,base64 之后接近 27MB。SIZE 算的是编码后的整封信,包含头部、分隔线和每一段的编码。

所以“支持 25MB 附件”这种产品口径,和服务器上配的 SIZE 数值,中间必须留余量。反过来,SIZE 配 25MB 的服务器,用户实际能塞进去的附件大约 18MB。这两个数字在文档里被混用了十几年,每次都要重新解释。顺带一句,552 5.3.4 是永久错误,客户端不该重试;可不少客户端把 5xx 一律当网络抖动,原地重试三次,用户看到“卡住了”,我们看到三条一模一样的拒收日志。

限流对象是资源,跟人没关系

MSA 面对的是一个账号的多个客户端。手机在同步,桌面客户端挂着 IDLE,网页版开着标签页,三个会话同时发信是常态,再叠加群发,一个账号瞬间能占满连接。

所以要控制的东西很具体:单账号并发会话数、单位时间的发信量和收件人总数、单封信的收件人上限。超了返回 421 或 452,都是 4xx,让客户端等一会儿再来。

为什么给临时错误?账号按计费和信誉算,跟设备数量无关。一个账号被盗、被拿去群发,毁的是整个 IP 池的信誉,牵连所有用户。限流冲的是共享资源去的,谁撞上谁被拦。

代价是这套控制有一半掌握在客户端手里。421 是“我要关连接”,最粗暴,很多客户端收到就弹框,不重试;452 精细些,也要客户端认。我见过不少系统限流配得漂亮,实际效果接近零,因为客户端根本不退避,用户只觉得“这邮箱时好时坏”。

同一个 550,两种意思

响应码的语义在提交这条路上漂移了。策略类拒绝集中在 5.7.x 和 4.7.x:认证前发 MAIL FROM 得 530 5.7.0,没资格中继得 554 5.7.1,这些好认。

难认的是 550。在 MTA 之间,550 通常意味着这封信传不过去,比如 550 5.1.1 收件人不存在。在 MSA 上,550 很少是这个意思。RFC 6409 写明白了:MAIL FROM 的地址没有提交权限、或跟认证身份对不上,用 550 加 5.7.1;违反站点策略,用 550 加 5.7.0。它说的是“你这个身份、或者这份内容,不被允许”,卡在准入这一步,信压根没进传输。

所以看日志只看三位数字不够,得看增强状态码的第二段。5.1.x 是地址,5.2.x 是邮箱,5.3.x 是系统,5.7.x 是策略。分不清这个,排障会往反方向走:把策略拒绝当投递失败去查 DNS 和连接,能查一下午。

还有一件 MSA 故意不做的事:多数实现不会对收件人返回 5.1.1。返回了等于告诉提交者“你们公司有没有这个地址”,这是个目录枚举接口。它先收下,退信异步走。这是安全上的取舍,谈不上能力不够。

把信任边界画进协议

回头看,端口号只是表象,真正值得学的是它处理信任边界的方式:能靠端口分的,别靠参数分;能靠角色分的,别靠配置约定。

端口是硬的。连上 587 的那一刻,双方就知道这是提交会话,该认证、该限流、该改写。EHLO 是软的,能力靠协商,协商就可以不谈。SUBMITTER 死在软的那一层,MSA 活在硬的那一层。

这个判断在运维上还有一层后果。把提交和传递塞进同一个进程,短期省事,长期是把两个故障域焊在一起:线程池、队列、配置、限流全部共享,提交高峰会拖慢投递,25 端口被字典攻击打满连接,587 的用户跟着登不上。我遇到过一次,用户报的是“网页版发不了信”,最后查出来是 25 那边的连接数把进程撑满了。加机器没用,缺的是隔离。

再往上抬一点。今天做系统设计,把身份和权限的判断放到协议的第一跳,比放在业务层加校验更可靠。前者是结构,后者是纪律。纪律会被绕过,会被赶工期绕,会被“这个接口先上线”绕;结构绕不过去,不按结构走,连接根本建立不起来。

MSA 这套设计活了二十多年。RFC 写得漂亮这件事跟它活多久没关系——6409 读起来相当枯燥,一半篇幅在解释什么情况下可以用 554。真正管用的是它把“你是谁”这个问题,钉在了会话开始的那一刻。

posted @ 2026-11-05 16:05  TurboEx技术分享  阅读(8)  评论(0)    收藏  举报