SMTP

本页目录

SMTP 负责把邮件交给下一跳,直到接收系统接管并投递;它不是收件箱同步协议,也不负责证明收件人已经阅读。理解它需要同时跟踪两组状态:连接上的命令与响应,以及服务器队列中尚未完成投递的邮件。

查阅点核心理解
MAIL FROM 与 From投递信封与消息内容是不同层
RCPT TO 成功是否已经收到邮件只接受该收件人,正文仍未提交
DATA 后 250 是否等于用户收到下一跳接管责任,后续仍可能失败
超时重发是否自动去重SMTP 不提供端到端 exactly-once
465 与 587隐式 TLS 提交与常见 STARTTLS 提交
SPF / DKIM / DMARC来源授权、签名验证、与 From 域对齐

一封邮件的路径与两个地址层

客户端通常向 MSA 提交邮件,MTA 负责队列与中继,接收侧再交给邮箱投递系统。SMTP 中的 client/server 是当前连接角色:同一台 MTA 收邮件时是 server,向下一跳发送时又是 client。

每一跳接管投递责任,最后仍要进入收件人的邮箱 用户客户端 提交邮件 发送方服务 排队 / 选择下一跳 接收方服务 接收 / 过滤 / 投递 提交 SMTP 最终 DATA 应答 250 该跳已接受责任,发件端可移除队列 接收后仍可能继续处理 转发、隔离或后续投递失败 250 不表示收件人已阅读;若应答丢失,发送方重试可能产生重复邮件。

SMTP 信封由 MAIL FROM 与一个或多个 RCPT TO 定义。前者是返回路径,后者决定这次事务的实际收件人;消息正文部分包含 RFC 5322 头部和内容,其中 From、To、Cc 是用户看到的消息字段。它们可能不同,例如退信地址使用专门的 bounce 子域,密送收件人存在于信封但不出现在公开收件人列表。

Return-Path 通常由最终投递系统根据信封返回路径生成,不应靠发件客户端随手增加一个头来改变退信路由。Message-ID 是消息标识,也不是 SMTP 的全局幂等事务号。SMTP RFC 5321

MTA 根据接收域的 MX 选择候选服务器,优先使用数值较小的 preference。没有 MX 与明确声明不接收邮件不同:前者可能按规则回退到地址记录,MX 0 . 的 Null MX 则明确表示不接收。Null MX RFC 7505

从 EHLO 到接受正文

下面展示已完成 TLS 和必要认证之后的一次事务。每一行实际以 CRLF 结束;S: 与 C: 是阅读标记,不属于线上字节。

方向命令或响应含义
C→SEHLO sender.example请求扩展能力
S→C250-...,最后 250 ...多行响应用连字符继续、空格结束
C→SMAIL FROM:<bounce@example.com>开始事务、声明返回路径
S→C250 2.1.0 OK接受发件路径
C→SRCPT TO:<alice@example.net>请求投给 Alice
S→C250 2.1.5 OK接受该收件人
C→SRCPT TO:<missing@example.net>请求第二位收件人
S→C550 5.1.1 No such user该地址被拒绝,Alice 不因此自动失效
C→SDATA请求发送消息内容
S→C354 Start mail input允许传正文,尚未接受整封邮件
C→S消息头、空行、正文、终止行传送实际内容
S→C250 2.0.0 Queued接受这次正文及已获准收件人的投递责任

多收件人事务可能部分成功;客户端要按收件人和阶段保留结果,不能只保存一个“发送失败”布尔值。4xx 表示临时失败,5xx 表示该请求的永久失败性质,重试策略要结合具体阶段;例如凭据配置被拒绝,应先修正配置,而非无限重放邮件。

DATA 怎样避免正文中的句点提前结束消息

DATA 使用独占一行的句点结束。如果消息某行本来以句点开头,发送时额外补一个句点,接收时移除这一个透明编码字符。以下模型要求输入已经使用规范 CRLF,并以 CRLF 结束;它不是任意邮件的 MIME 生成器。

message = (b'From: sender@example.com\r\n'
           b'To: alice@example.net\r\n'
           b'Subject: dot example\r\n\r\n'
           b'.first\r\n.\r\n..third\r\n')
lines = message[:-2].split(b'\r\n')
wire = b'\r\n'.join(b'.' + line if line.startswith(b'.') else line
                    for line in lines) + b'\r\n.\r\n'
assert b'\r\n..first\r\n..\r\n...third\r\n.\r\n' in wire
received = []
for line in wire.split(b'\r\n'):
    if line == b'.':
        break
    received.append(line[1:] if line.startswith(b'.') else line)
assert b'\r\n'.join(received) + b'\r\n' == message
assert len(wire) - len(message) == 6  # 三个补点字节 + 最后的句点行3字节
print('DATA 补点、终止与恢复通过')

协商了 CHUNKING 的连接可以使用 BDAT 按明确字节数传内容,最后块用 LAST 标记。它不是在 DATA 内换一种正文写法,也不再按 DATA 的句点终止规则定界。CHUNKING RFC 3030

队列、250 与重复邮件的故障窗口

发送方把正文发完但未收到最终响应时,有两种可能:对方尚未接受,或者对方已经排队而 250 在途中丢失。发送方无法只凭超时区分二者;为避免丢信而重试,就可能产生重复邮件。保持相同 Message-ID 有助关联,但所有收件系统并不因此保证去重。

接收方最终 250 之后若无法投递,通常应按协议生成非投递通知;退信使用空反向路径 MAIL FROM:<>,避免失败通知再无限互相退信。接收服务不应先对任意地址承诺成功、再向可能被伪造的发件人制造大量退信。SMTP 接受责任与失败处理

应用发送验证码或订单通知时,业务侧仍应记录通知事件和投递尝试。SMTP 成功响应不是支付事务的一部分,不能在发送超时后重新扣款;邮件重复与业务副作用重复应分别处理。

同一个超时,为什么可能已经有一封邮件?

两种故障都让发送方停在TIMEOUT,但接收队列分别是零封和一封。各重试一次后变成一封和两封。这个不可观察窗口解释了为什么“成功发完正文”与“确认对方接受”之间必须保留不确定性。

正在呈现知识画面
同一个超时,为什么可能已经有一封邮件?

发送方看到的超时相同,接收队列却可能不同。重试可以补救未接受的情况,也可能重复已经接受的邮件;Message-ID不提供通用去重。

提交、服务器中继与 TLS

端口常见角色TLS 行为
TCP 25MTA 到 MTA 中继可协商 STARTTLS;要求还取决于接收域策略
TCP 587邮件提交常用 STARTTLS,客户端应要求安全连接再认证
TCP 465submissions建连后先 TLS,再开始 SMTP

465 已由 RFC 8314 注册为邮件提交的隐式 TLS 服务,不应继续标为“非标准 SMTPS”。提交与公开 MTA 中继的认证模型不同,不能要求互联网上每个发件 MTA 先持有收件服务器账号。RFC 8314、Submission RFC 6409

STARTTLS 返回就绪后进行 TLS 握手,双方清除之前学到的 SMTP 状态,客户端重新 EHLO 获取当前能力;不能继续把明文阶段的 AUTH 能力缓存视为可信。AUTH PLAIN 的 Base64 只是编码,不是密码加密;是否允许认证以及使用何种 SASL 机制由服务器能力和策略确定。STARTTLS RFC 3207

普通机会式 TLS 可能遭遇降级。MTA-STS 通过 HTTPS 发布并缓存接收域的 TLS/MX 策略;DANE for SMTP 依赖 DNSSEC 认证的 TLSA 信息。它们用于约束中继链路,不能替代消息本身的签名,也不会使所有中转服务器都无法阅读邮件。MTA-STS RFC 8461、SMTP DANE RFC 7672

SPF:来源 IP 是否获准使用这个信封域

SPF 依据本次 SMTP 连接的来源 IP 和信封域评估授权;空返回路径场景使用 HELO 身份规则。它不直接校验用户看到的 From,也不证明正文未经修改。

例如 v=spf1 ip4:192.0.2.0/24 -all 表示该教学域授权指定 IPv4 范围,其余匹配 -all 为 fail。结果还包括 softfail、neutral、none、temperror 和 permerror;DNS 临时故障与明确不授权不能混成一种失败。SPF RFC 7208

include 不是文本粘贴。递归求值中的 include、a、mx、ptr、exists、redirect 等受 DNS 查询类 term 总数上限约束,超过 10 返回 permerror;不是只数最外层 include,也不是简单统计抓包里的所有 DNS 包。维护多家发送供应商时,要追踪实际展开路径和记录失效,而非不断追加 include。

转发会改变连接来源,因此原始信封域 SPF 可能失败;这不说明原邮件必定伪造,也解释了为什么还需要内容签名及对齐判断。

DKIM:签名域为消息的选定内容背书

DKIM-Signature 的 d= 是签名域,s= 是 selector,验证器到 s._domainkey.d 查公开密钥。h= 指定参与签名的头,bh= 是规范化正文的摘要,b= 是签名值。签名输入还涉及按规则处理的 DKIM-Signature 头,不能简化成“把 header hash 和 body hash 随便拼起来做 RSA”。From 头必须参与签名。DKIM RFC 6376

规范化规则决定哪些空白调整可以容忍;邮件列表加页脚、改 Subject 或转换正文,仍可能破坏签名。DKIM pass 证明持有该域密钥的一方签署了所验证内容,不证明签名域就是可见 From 域,也不证明邮件内容善意。

转发只改变外部信封而保留被签内容时,DKIM 有机会继续通过。多个签名可以共存,验证某个无关域的签名通过,不能直接推出 DMARC 通过。

DMARC:认证结果与可见 From 域绑定

DMARC:认证通过,还必须与可见 From 域对齐 From: billing@example.com SPF:验证本次连接来源 MAIL FROM 域通过 + 与 From 对齐 DKIM:验证签名与正文摘要 至少一个通过签名的 d= 与 From 对齐 任一路径成立 → DMARC pass 只有 SPF pass 或 DKIM pass、却没有对应对齐,并不足以通过 DMARC。

DMARC 的通过条件是至少一条路径同时满足“认证 pass”和“与 Author Domain 对齐”。strict 要求域名相同;relaxed 依据组织域关系判断,不能用字符串后缀匹配,也不能简单取最后两段域名。DMARC RFC 9989

截至 2026 年 9 月,RFC 9989 已于 2026 年 5 月替代 RFC 7489/9091:组织域与策略发现采用 DNS Tree Walk,删除旧 pct 标签并引入测试用 t,聚合与失败报告拆至 RFC 9990/9991。实际接收实现可能仍处于迁移期,不能假设旧 PSL 判定与新树遍历在所有域结构上得到相同结果。新版变化

下面是严格对齐的局部教学模型,不实现 DNS 策略发现;用它能看清“pass”与“aligned”必须属于同一条验证路径。

def dmarc_strict(author, spf_result, mail_from_domain, dkim_results):
    spf_ok = spf_result == 'pass' and mail_from_domain.lower() == author.lower()
    dkim_ok = any(result == 'pass' and domain.lower() == author.lower()
                  for result, domain in dkim_results)
    return spf_ok or dkim_ok

assert dmarc_strict('example.com', 'fail', 'example.com', [('pass', 'example.com')])
assert not dmarc_strict('example.com', 'pass', 'vendor.example', [('fail', 'example.com')])
assert not dmarc_strict('example.com', 'pass', 'bounce.example.com', [])
assert dmarc_strict('example.com', 'pass', 'example.com', [])
print('严格对齐及 SPF 失败但 DKIM 通过的分支通过')

发布 v=DMARC1; p=reject; rua=mailto:dmarc@example.com 表达域所有者的处理偏好与报告请求,接收者仍结合本地策略判断。p=none 不要求按 DMARC 失败隔离或拒绝,也不保证邮件进入收件箱;p=reject 不能令所有接收者无条件拒绝所有 SPF fail 邮件,因为对齐的 DKIM pass 仍能使 DMARC pass。

ARC 可记录邮件经过中间系统时的认证结果链,帮助接收者评估转发场景;它不把已失败的 DKIM 签名神奇修复,也不要求接收者信任任意 ARC 封印者。ARC RFC 8617

SPF通过、DKIM对齐,为什么合起来仍可能失败?

初始设置故意把两个成立条件拆到不同路径,整体仍失败。只要补齐其中一行就可通过,另一行不必同时通过。这里得到的是认证结果,接收者最终如何处理邮件仍有自己的策略。

正在呈现知识画面
SPF通过、DKIM对齐,为什么合起来仍可能失败?

pass和aligned必须出现在同一条路径。不能拿SPF的pass与失败DKIM的对齐拼成成功。

MIME、国际化与最终诊断

SMTP 传送消息,MIME 描述正文类型、分段与传输编码。Base64 附件通常使数据体积约增加三分之一,还要算折行和消息头;8BITMIME 不等于任意国际化地址都合法,非 ASCII 邮箱地址需要 SMTPUTF8 等对应支持。MIME RFC 2045、SMTPUTF8 RFC 6531

排查时先取得准确阶段、响应码和队列 ID:没有连接响应看网络;AUTH 失败看提交凭据;RCPT 拒绝看地址与策略;最终 DATA 后排队成功看后续队列和接收侧处理。认证结果头也要区分由可信接收边界生成的记录与发件人自行插入的文本,不能见到一行 spf=pass 就当作验证结论。