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 秒实验
- 算一下:120 字节的附件 base64 之后是多少字符?
- 想一下:为什么 quoted-printable 适合文本、base64 适合二进制?
- 想一下:老客户端为什么显示不了中文标题?
常见误区
- 以为「base64 会把内容压缩」。 它只会变大,最大 4/3。
- 以为「MIME 是一种新的邮件协议」。 它只是 SMTP 上面的一层编码约定,SMTP 本身没改。
- 以为「中文邮件一定用 base64」。 纯文本正文常用 quoted-printable,因为英文部分不膨胀。
自测三问(点开对答案)
1. MIME 要解决什么限制? —— **SMTP 只能传 7 位 ASCII**,高位字节发不出去。 2. base64 的膨胀率是多少? —— **最多 4/3**,每 3 字节变 4 字符,末尾补等号。 3. 头部的 `=?utf-8?b?...?=` 是什么? —— **编码字**:字符集、编码方式、编码后的内容,三段用问号隔开。下一节真跟一台 SMTP 服务器对话一遍。
- ☐ (本节完,下一节 8.4.4 SMTP)