上传文件
  1. base64hub
  2. 学习 Base64
  3. 电子邮件中的 Base64
指南

电子邮件中的 Base64:MIME、附件与邮件头

每个邮件附件都以 Base64 文本的形式传输,每 76 个字符换一行。只要看懂它周围的 MIME 结构,从原始邮件中取回文件只需要一分钟。

为什么电子邮件需要 Base64

互联网邮件最初是为行长有限的 7 位 ASCII 文本设计的。RFC 5322 至今仍把每行限制在 998 个字符,而且除非接收服务器声明支持 8BITMIME 扩展,SMTP 只保证 7 位数据能安全送达。即使是 8 位传输,邮件中也不能包含 NUL 字节或单独的回车符和换行符,而任何二进制文件迟早都会包含这些。发送原始字节需要 BINARYMIME 扩展,发件方无法指望服务器之间的整条传输路径都支持它。

MIME(RFC 2045)用 Content-Transfer-Encoding 头解决了这个问题。它告诉接收方,某个部分的正文是如何转换成安全文本的。它有五个取值:

取值含义
7bit纯 ASCII 文本行,不编码
8bit短行的 8 位文本,需要 8BITMIME
binary原始字节,需要 BINARYMIME
quoted-printable大体可读的文本,非 ASCII 字节转义为 =XX
base64每行最多 76 个字符的 Base64

对于附件,邮件软件几乎总会选择 base64。算法本身见 Base64 的工作原理。

Base64 与 quoted-printable 的对比

quoted-printable 保留 ASCII 字符原样不动,把其他每个字节写成 = 加两位 hex 数字。UTF-8 的 é(字节 C3 A9)会变成 =C3=A9。超过 76 个字符的行会用软换行断开,即在行尾加一个 =。

这让它非常适合偶尔带重音字母的英文文本,而对其他内容则很浪费,因为每个非 ASCII 字节要占 3 个字符。Base64 则不管内容是什么,开销都是固定的 4/3。我们用 Python 的 quopri.encodestring 和 base64.encodebytes(均使用 LF 行尾)对三种语言的句子各重复 20 次进行了编码:

样本(UTF-8)字节数Quoted-printableBase64
英文全字母句9009261,216
带重音字母的法语句子1,0601,8301,435
俄文全字母句1,6004,6022,165

邮件客户端一般会替你做这个选择:以 ASCII 为主的文本正文用 quoted-printable 或 7bit,附件和非拉丁文字的文本用 Base64。如果需要手动读写 quoted-printable,quoted-printable 工具 支持双向转换。

原始邮件中的附件

带附件的邮件是一个 multipart/mixed 正文。每个部分都有自己的头部、一个空行,然后是内容。下面是用 Python 的 email 包生成的一封完整的最小邮件(为便于阅读,缩短了边界字符串):

From: Ana <ana@example.com>
To: bo@example.org
Subject: =?utf-8?b?Q2Fmw6kgbWVudSDigJMgR3LDtsOfZQ==?=
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="b1"

--b1
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable

Hi Bo,

The caf=C3=A9 menu is attached.

--b1
Content-Type: application/pdf
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="menu.pdf"

JVBERi0xLjcKJeLjz9MKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyA+PgplbmRvYmoKdHJhaWxl
cgo8PCAvUm9vdCAxIDAgUiA+PgolJUVPRgo=

--b1--

需要关注的部分:

  • Content-Type 给出文件类型,Content-Disposition 给出文件名。
  • Content-Transfer-Encoding: base64 表明正文是 Base64。
  • Base64 从该部分的第一个空行之后开始,到下一个 --boundary 行之前结束。
  • 除最后一行外,每行恰好 76 个字符。每个完整的行包含文件的 57 个字节。
  • 开头的 JVBERi0 是 %PDF- 的 Base64,所以这是一个 PDF。其他文件签名见 常见的 Base64 前缀。

HTML 邮件中内嵌的图片,在 multipart/related 部分中使用同样的机制。每张图片都有一个 Content-ID 头,HTML 以 <img src="cid:..."> 的形式引用它。传输时这些行以 CRLF 结尾,所以每 76 个 Base64 字符要占 78 个字节;完整的计算见 Base64 体积开销。想自己生成分行的输出,可以用 MIME Base64 编码器。

编码的邮件头:=?UTF-8?B?…?=

Subject、From 中的显示名以及某些文件名等邮件头必须是 ASCII。RFC 2047 把其他文本编码为编码字(encoded-word):

=?charset?encoding?encoded-text?=

encoding 为 B 表示 Base64,为 Q 表示 quoted-printable 的一种变体,其中 _ 代表空格。这两个字母不区分大小写,所以上面 Python 写的是 =?utf-8?b?。下面是一些真实的例子,均用 Python 的 email.header.decode_header 验证过:

编码后解码后
=?UTF-8?B?0J/RgNC40LLQtdGC?=Привет
=?utf-8?q?Caf=C3=A9_menu?=Café menu
=?ISO-8859-1?Q?Caf=E9_menu?=Café menu
=?utf-8?b?Q2Fmw6kgbWVudSDigJMgR3LDtsOfZQ==?=Café menu – Größe

一个编码字最多 75 个字符,所以较长的主题会被拆成多个编码字,每个占一行折叠的头部行。RFC 2047 规定,相邻编码字之间的空白会被忽略,而且每个编码字必须包含完整的字符,因此一个多字节的 UTF-8 字符永远不会被拆到两个编码字中。一个 44 个字符的俄文主题,Python 输出如下:

Subject: =?utf-8?b?0KHRh9GR0YIg0LfQsCDQvtC60YLRj9Cx0YDRjCDQuCDQsNC60YIg0YE=?=
 =?utf-8?b?0LLQtdGA0LrQuCDQtNC70Y8g0LHRg9GF0LPQsNC70YLQtdGA0LjQuA==?=

每个编码字都是一个带有自己填充的完整 Base64 字符串。分别解码后,第一个得到 Счёт за октябрь и акт с,第二个得到 верки для бухгалтерии;解码器会把它们拼回原来的主题。

附件文件名还有第二个标准 RFC 2231,形如 filename*=UTF-8''%D0%BE%D1%82%D1%87%D1%91%D1%82.pdf。许多邮件程序也会把 RFC 2047 编码字放在 filename="..." 里,所以解析器应该两种都能处理。

如何从 .eml 文件中提取附件

在浏览器中:用 EML 解码器 打开 .eml,即可阅读邮件正文并保存附件。它在页面中本地运行,当邮件包含发票或个人数据时,这一点很重要。

用 Python:标准库能解析 MIME,并同时解码 Base64 正文和 RFC 2047 邮件头:

import sys
from email import policy
from email.parser import BytesParser

with open(sys.argv[1], "rb") as f:
    msg = BytesParser(policy=policy.default).parse(f)

print("Subject:", msg["Subject"])        # encoded-words decoded
for part in msg.iter_attachments():
    name = part.get_filename()
    data = part.get_content()             # Base64 already decoded to bytes
    with open(name, "wb") as out:
        out.write(data)
    print("saved", name, len(data), "bytes")

对上面的示例邮件运行时,它输出了 Subject: Café menu – Größe 和 saved menu.pdf 83 bytes。在实际代码中,写文件前要先清理 name,因为它来自发件人。

手动处理:把该部分的 Base64 行复制到一个文件中,再进行解码。原始 .eml 文件使用 CRLF 行尾,而 GNU base64 -d 遇到第一个回车符就会以 invalid input 停止。请先去掉回车符,或者用 -i 跳过字母表之外的字符:

tr -d '\r' < block.txt | base64 -d > menu.pdf

在我们的测试中,openssl base64 -d -in block.txt -out menu.pdf 可以原样接受 CRLF 版本。如果你手里只有 Base64 文本,可以把它粘贴到 Base64 转文件 中,它会根据开头的字节识别类型;PDF 也可以直接粘贴到 Base64 转 PDF。

打开 .eml 文件

查看原始邮件的邮件头、正文和附件,全部在你的浏览器本地解码。

邮件解码器

常见问题

如何从邮件源码中打开 Base64 编码的附件?

复制该部分空行之后、下一个边界之前的 Base64 行,再把它们解码为文件。EML 解码器 可以直接从 .eml 文件中帮你提取附件。

邮件主题中的 =?UTF-8?B? 是什么意思?

它是 RFC 2047 编码字(encoded-word)的开头:字符集为 UTF-8,B 表示接下来 ? 和 ?= 之间的文本是 Base64。如果是 Q 而不是 B,则表示 quoted-printable 的一种变体。

为什么邮件中的 Base64 每行是 76 个字符?

RFC 2045 把 MIME 中 Base64 的每行长度限制为 76 个字符。76 个字符正好容纳 57 个字节,而且这个限制让每行远低于电子邮件允许的 998 个字符上限。

邮件正文用 quoted-printable 好还是 Base64 好?

以 ASCII 为主的文本用 quoted-printable,因为它保持可读,体积几乎不增加。二进制数据和非拉丁文字的文本用 Base64,对这类文本 quoted-printable 可能让体积增加到将近三倍。

继续阅读