📚 第八章 应用协议
📄 一句话概括

SMTP 只认 7 位 ASCII,MIME 把任意内容编码成 ASCII 塞进去。 base64 最多膨胀 4/3,quoted-printable 的膨胀率看内容。

📝 本节和前面几节的关系

这一节会真构造一封带中文和附件的信,看编码后的体积。 顺便解释为什么邮件头里的中文变成了 =?utf-8?b?...?= 这一串。

一、它解决的是「7 位」这个限制

SMTP 诞生在 1980 年代,那时候的邮件只能传 7 位 ASCII。 一个字节的高位不能用,所以中文(UTF-8 每字 3 字节,高位都是 1)根本发不出去。 二进制附件更不用说。 MIME 的思路很干脆:不改造 SMTP,而是「把任意内容编码成 ASCII,再交给 SMTP」。 这是一个「在旧协议上加一层适配」的经典做法,和 NAT 在 IPv4 上加一层地址转换是同一个思路。 这个做法有一个必须承认的代价:体积变大。 而且这个代价不是偶然的、不是实现不好——它是「用 7 位表示 8 位信息」的必然结果。

二、两种编码,两种取舍

base64 把每 3 个字节变成 4 个字符,所以最多膨胀 4/3。 长度不是 3 的倍数时,末尾补等号凑满。 这个比例不依赖内容,只依赖长度——所以它适合「整块二进制」的附件。 quoted-printable 的规则是「能直接显示的字符就原样留,不能的写成 =XX」。 所以纯英文几乎不膨胀,纯中文每个字节变 3 个字符。 它适合「大部分是英文、偶尔夹几个中文」的正文。 这两个选择的分界线很清楚:内容以文本为主 → quoted-printable;内容以二进制为主 → base64。 选错了不会出错,只是白花流量。

三、头部也要能装下非 ASCII

MIME 不只管正文和附件,它还管头字段。 因为 Subject: 季度报告 里的中文一样是高位字节。 解决办法是「头字段的编码字」:写成 =?字符集?编码方式?编码后的内容?=。 b 表示 base64,q 表示 quoted-printable。 所以你会看到 =?utf-8?b?5a2j5bqm5oql5ZGK?= 这种看着像乱码的东西。 这个东西之所以存在,是因为「邮件头必须是 ASCII」这条老规矩, 而且它比正文的限制更硬——正文还能靠 Content-Transfer-Encoding 声明,头字段只能就地编码。 这也是为什么老邮件客户端显示中文标题会乱码:它不认识这个语法。

四、动手 Demo:MIME 让邮件能装下中文和附件,代价是变大

先量三种编码的体积,再真构造一封带附件的信,看头部长什么样。 重点是「膨胀率」和「谁在膨胀」。

import base64, email.mime.text, email.mime.multipart
import email.mime.base, email.encoders

text = '这是一段中文正文,里面还有 emoji 和符号:★'
raw = text.encode('utf-8')
b64 = base64.b64encode(raw)

qp = []
for b in raw:
    if 33 <= b <= 126 and b != 61:
        qp.append(chr(b))
    elif b == 32:
        qp.append(' ')
    else:
        qp.append('=%02X' % b)
qps = ''.join(qp)
print('原文', len(raw), 'qp', len(qps), 'base64', len(b64))

msg = email.mime.multipart.MIMEMultipart()
msg['Subject'] = '季度报告(附件)'
msg.attach(email.mime.text.MIMEText(text, 'plain', 'utf-8'))
att = email.mime.base.MIMEBase('application', 'octet-stream')
att.set_payload(b'\x00\x01\x02' * 40)
email.encoders.encode_base64(att)
msg.attach(att)
print(msg.as_string()[:400])

实跑输出:

--------------------------------------------------------------------
MIME 让邮件能装下中文和附件,代价是变大
--------------------------------------------------------------------
**SMTP 诞生时只认 7 位 ASCII**:**一个字节的高位不能用,中文和二进制文件根本发不出去。**
**MIME 的思路是「把任意内容编码成 ASCII」。**
**代价是体积膨胀。**

  编码                  字节数     相对原文
  ------------------------------------------
  原文字节(UTF-8)     61         100.0%
  quoted-printable      169        277%
  base64                84         138%

**base64 的膨胀率最多是 4/3**:**每 3 个字节变成 4 个字符,所以最多多三分之一。**
**长度不是 3 的倍数时,末尾补等号凑满。**
**这个结论不依赖内容,只依赖长度。**

**quoted-printable 的膨胀率浮动很大**:
**纯英文几乎不膨胀,纯中文每个字节都变成 3 个字符(即 =XX)。**
**所以它适合「大部分是英文、偶尔夹几个中文」的正文**,**base64 适合「整块二进制」的附件。**

**真正构造一封带附件的信,看头部编码怎么处理中文**:

    Content-Type: multipart/mixed; boundary="===============2023136735260619768=="
    From: =?utf-8?b?5byg5LiJ?= <[email protected]>
    Subject: =?utf-8?b?5a2j5bqm5oql5ZGK77yI6ZmE5Lu277yJ?=
    Content-Type: text/plain; charset="utf-8"
    Content-Transfer-Encoding: base64
    Content-Type: application/octet-stream
    Content-Transfer-Encoding: base64

**看 `Subject` 那一行**:**中文被编码成了 `=utf-8?B?...?=` 的形式,B 表示 base64。**
**这是 MIME 的「头部扩展」。**不只正文和附件,连头字段的值也要能装下非 ASCII。

**整封信 830 字节**,而真正的内容只有正文 61 字节加附件 120 字节。
**附件 120 字节经 base64 变成 160 个字符,正好是 4/3。**
**再加上 boundary 分隔线、每段自己的 Content-Type,信封比信纸厚得多。**
**邮件的「变大」是结构性的,不是偶然的。**

原文 61 字节,quoted-printable 169 字节,base64 84 字节。 这一行全是中文,所以 quoted-printable 膨胀到 277%(接近理论上限 300%), 而 base64 只有 138%。 结论正好和直觉相反:纯中文的内容,base64 反而更省。 再看头部的 Subject:=?utf-8?b?5a2j5bqm5oql5ZGK77yI6ZmE5Lu277yJ?= 这一长串就是「季度报告(附件)」。b 表示 base64,中间那段是编码后的字节。 From 那一行也一样:中文名字变成了 =?utf-8?b?5byg5LiJ?=。 整封信 830 字节,而真正的内容只有正文 61 字节加附件 120 字节。 附件 120 字节经 base64 变成 160 个字符,正好是 4/3。 剩下的全是 boundary 分隔线和每段自己的 Content-Type。 「信封比信纸厚」在邮件里是字面意义上的事实。

30 秒实验

  1. 算一下:120 字节的附件 base64 之后是多少字符?
  2. 想一下:为什么 quoted-printable 适合文本、base64 适合二进制?
  3. 想一下:老客户端为什么显示不了中文标题?

常见误区

  1. 以为「base64 会把内容压缩」。 它只会变大,最大 4/3。
  2. 以为「MIME 是一种新的邮件协议」。 它只是 SMTP 上面的一层编码约定,SMTP 本身没改。
  3. 以为「中文邮件一定用 base64」。 纯文本正文常用 quoted-printable,因为英文部分不膨胀。
自测三问(点开对答案) 1. MIME 要解决什么限制? —— **SMTP 只能传 7 位 ASCII**,高位字节发不出去。 2. base64 的膨胀率是多少? —— **最多 4/3**,每 3 字节变 4 字符,末尾补等号。 3. 头部的 `=?utf-8?b?...?=` 是什么? —— **编码字**:字符集、编码方式、编码后的内容,三段用问号隔开。

下一节真跟一台 SMTP 服务器对话一遍。

相关笔记