bigywater.tech
网络协议 · 98

SMTP:一封邮件在服务器之间怎么传

一封邮件在服务器之间怎么传?答案是一场 40 年历史的文本对话:HELO、MAIL FROM、RCPT TO、DATA。这篇在本机起一个 SMTP 服务器,逐行看这场对话、信封和信件头为什么不一样、错误码怎么分类、STARTTLS 怎么把明文连接升级成加密,最后到真实的 Gmail、QQ、163 邮件服务器上只做问候和 EHLO。

2 个收件人 vs 1 个
信封里有 bob 和 hidden-bcc 两个收件人,而信件头 To 里只有 bob:Bcc 的原理
552 两种
声明 SIZE=5000 在 MAIL FROM 阶段就被拒绝;不声明直接发大正文要到 DATA 结束才被拒绝
3 / 3
Gmail、QQ、163 三台真实邮件服务器宣告 STARTTLS 的个数

一场对话

实测 · 本机执行输出
    S: 220 mail.w98.test Python SMTP 1.4.6
    C: EHLO client.w98.test
    S: 250-mail.w98.test
    S: 250-SIZE 2000
    S: 250-8BITMIME
    S: 250-SMTPUTF8
    S: 250-STARTTLS
    S: 250 HELP
    C: MAIL FROM:<envelope-sender@w98.test>
    S: 250 OK
    C: RCPT TO:<bob@w98.test>
    S: 250 OK
    C: RCPT TO:<hidden-bcc@w98.test>
    S: 250 OK
    C: DATA
    S: 354 End data with <CR><LF>.<CR><LF>
    C: <信件(带点开头的行已转义)>
    S: 250 OK: queued as w98-1
    C: QUIT
    S: 221 Bye
  EHLO 的多行答复:前面各行用 “250-”,最后一行用 “250 ”(横线表示后面还有,空格表示最后一行);列出的扩展有 ['mail.w98.test', 'SIZE', '8BITMIME', 'SMTPUTF8', 'STARTTLS', 'HELP']
  服务器收到的信封:MAIL FROM = 'envelope-sender@w98.test',RCPT TO = ['bob@w98.test', 'hidden-bcc@w98.test']——信封里有 hidden-bcc@w98.test,但信件头里的 To 只写了 bob@w98.test(这就是 Bcc 的实现原理:信封收件人 ≠ 信件头里的收件人)
  信件头里的 From = '"Alice" <alice@example.org>',与信封的 MAIL FROM 'envelope-sender@w98.test' 完全不同:SMTP 不核对两者,这是伪造发件人的基础
  点转义:我发出的正文里有一行以点开头 ".dot-line",在线上写成 "..dot-line"(上面 C: 行里显示的是转义前的说明;实际字节见下);服务器还原后收到的正文里这一行是 ['.dot-line']
  线上字节(转义后正文的一部分):b'First line.\r\n..dot-line\r\nLast '

220 → EHLO(多行 250)→ MAIL FROM → RCPT TO → DATA(354)→ 正文以 CRLF.CRLF 结束 → 250 → QUIT。

信封与信件头

实测 · 本机执行输出
    S: 220 mail.w98.test Python SMTP 1.4.6
    C: EHLO client.w98.test
    S: 250 HELP
    C: MAIL FROM:<a@w98.test>
    S: 250 OK
    C: RCPT TO:<blocked@w98.test>
    S: 550 5.7.1 Relaying denied
    C: RCPT TO:<later@w98.test>
    S: 450 4.2.0 Greylisted, try again later
    C: RCPT TO:<ok@w98.test>
    S: 250 OK
    C: FOOBAR
    S: 500 Error: command "FOOBAR" not recognized
    C: RSET
    S: 250 OK
    C: DATA
    S: 503 Error: need RCPT command
    C: QUIT
    S: 221 Bye
  550 永久失败:发送方的邮件系统应该退信;450 临时失败(灰名单):应该稍后重试;500 未知命令;503 命令顺序不对(这里 RSET 之后还没有 MAIL FROM 就 DATA)

收件人和发件人都有两套:信封给传输用,信件头给人看,SMTP 不核对它们。

状态码

实测 · 本机执行输出
    C: EHLO client.w98.test
    S: 250-SIZE 2000
    C: MAIL FROM:<a@w98.test> SIZE=5000
    S: 552 Error: message size exceeds fixed maximum message size
    C: <约 3000 字节正文,每行 98 字节>
    S: 552 Error: Too much mail data
    C: QUIT
    S: 221 Bye

2 成功、4 临时失败、5 永久失败;增强状态码 5.7.1 更精确。

SIZE

实测 · 本机执行输出
  升级前 EHLO 答复里有 STARTTLS:True;STARTTLS 命令的答复:220 Ready to start TLS;升级后再次 EHLO 的答复里还有 STARTTLS:False(已经加密就不再宣告)
  整个连接的字节里:能找到明文 "EHLO client.w98.test" 吗?能(升级前的那一次);能找到明文 "STARTTLS" 命令吗?能;能找到升级后才发的 "secret-sender@w98.test" 吗?找不到(已加密)
  注意 STARTTLS 的弱点:升级前一切是明文,路径上的设备可以把 “250 STARTTLS” 从 EHLO 答复里删掉(降级攻击),发送方若是“尽量用 TLS”策略就会继续明文

EHLO 宣告上限,MAIL FROM 带 SIZE 可以提前被拒绝。

STARTTLS 与真实服务器

实测 · 本机执行输出
  gmail-smtp-in.l.google.com:问候 '220 mx.google.com ESMTP 41be03b00d2f7-cd635e4c53dsi3795446a1'(996 ms);EHLO 扩展 ['SIZE', '8BITMIME', 'STARTTLS', 'ENHANCEDSTATUSCODES', 'PIPELINING', 'CHUNKING', 'SMTPUTF8']
  mx1.qq.com:问候 '220 newxmmxszc26-0.qq.com MX QQ Mail Server.'(146 ms);EHLO 扩展 ['STARTTLS', '8BITMIME', 'SMTPUTF8', 'SIZE', 'OK']
  smtp.163.com:问候 '220 163.com Anti-spam GT for Coremail System (163com[2014120'(90 ms);EHLO 扩展 ['AUTH', 'AUTH=LOGIN', 'coremail', 'STARTTLS', 'ID', '8BITMIME']

把已有的明文连接升级成 TLS,升级前一切明文,存在降级攻击;Gmail、QQ、163 都宣告 STARTTLS。

分步演示

SMTP
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。前四节在本机 lo0 上,服务器是 aiosmtpd(Python),STARTTLS 用临时生成的自签名证书(客户端不校验);第五节对 Gmail、QQ、163 的 25 端口只收问候和 EHLO 的答复,不使用 MAIL FROM,不发任何邮件
    • 没有验证:AUTH(认证,163 宣告了 AUTH LOGIN);SPF、DKIM、DMARC;MTA-STS 和 DANE 对 STARTTLS 降级的防护;端口 465(隐式 TLS)与 587(提交端口);邮件队列、重试间隔和退信格式(DSN);PIPELINING 与 CHUNKING 的具体行为;从本机真实发出邮件(没有做)
    京ICP备2026067502号-1京公网安备11010502063487号