Python3 电子邮件模块中的解码不正确

Incorrect decoding in Python3 email module

我最近 运行 进入了一个我想用 Python 电子邮件模块解析的 EML 文件。 在 from header 中,有以下文字:

From: "=?utf-8?b?5b2t5Lul5Zu9L+esrOS6jOS6i+S4mumDqOmhueebrumDqC/nrKzkuozkuovkuJrp?=
=?utf-8?b?g6g=?=" <email@address.com>

所以这个名字被编码成两部分。当我连接代码并将其手动解码为十六进制时,我得到以下结果,这是正确的 UTF-8 字符串:

e5 bd ad e4 bb a5 e5 9b bd 2f e7 ac ac e4 ba 8c e4 ba 8b e4 b8 9a e9 83 a8 e9 a1 b9 e7 9b ae e9 83 a8 2f e7 ac ac e4 ba 8c e4 ba 8b e4 b8 9a e9 83 a8

但是,当我调用 Python 电子邮件解析器 parse 时,最后 3 个字节未正确解码。相反,当我读取 message['from'] 的值时,有代理项:

dce9:20:dc83:dca8

因此,例如,当我想打印字符串时,它以

结尾
UnicodeEncodeError('utf-8', '彭以国/第二事业部项目部/第二事业\udce9\udc83\udca8', 17, 18, 'surrogates not allowed')

当我将 From header 中的 2 个编码部分合并为一个时,它看起来像这样:

From: "=?utf-8?b?5b2t5Lul5Zu9L+esrOS6jOS6i+S4mumDqOmhueebrumDqC/nrKzkuozkuovkuJrpg6g=?=" <email@address.com>

字符串已被库正确解码,可以正常打印。

这是 Python 电子邮件模块中的错误吗? EML 标准是否允许 double-encoded 值?

这里是一个示例 EML 文件 + Python 重现错误解码的代码(这实际上不会触发异常,稍后会发生,即 SQLAlchemy 无法将字符串编码回 UTF-8 )

EML:

Content-Type: multipart/mixed; boundary="===============2193163039290138103=="
MIME-Version: 1.0
Date: Wed, 25 Aug 2018 19:21:23 +0100
From: "=?utf-8?b?5b2t5Lul5Zu9L+esrOS6jOS6i+S4mumDqOmhueebrumDqC/nrKzkuozkuovkuJrp?=
 =?utf-8?b?g6g=?=" <addr@addr.com>
Message-Id: <12312924463694945698.525C0AC435BA7D0E@xxxxx.com>
Subject: Sample subject
To: addr@addr.com

--===============2193163039290138103==
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: base64

VGhpcyBpcyBhIHNhbXBsZSB0ZXh0

--===============2193163039290138103==--

Python代码:

from email.parser import Parser
from email import policy
from sys import argv


with open(argv[1], 'r', encoding='utf-8') as eml_file:
    msg = Parser(policy=policy.default).parse(eml_file)

print(msg['from'])

结果:

彭以国/第二事业部项目部/第二事业���

这似乎是 email.parser 基础设施如何处理 multi-line header 展开的问题,其中包含来自 header 的 encoded-word 个令牌和其他结构化的 headers。它对 非结构化 header 正确执行此操作,例如 Subject.

您的 header 有两个 encoded word 部分,分两行。这是完全正常的,一个 encoded-word 令牌有限制 space (有最大长度限制)所以你的 UTF-8 数据被分成两个这样的词,并且有一个 line-separator加上 space in-between。一切都很好。无论生成的电子邮件在 UTF-8 字符的中间分割是错误的(RFC2047 声明严格禁止),此类数据的 解码器 不应插入 spaces在解码字节之间。正是额外的 space 阻止了 email header 处理加入代理和修复数据。

所以这似乎是在处理结构化 header 时解析 header 的方式中的错误;解析器不能正确处理编码单词之间的 spaces,这里 space 是由折叠的 header 行引入的。这导致 space 被保留在两个 encoded-word 部分之间,从而阻止了正确的解码。因此,虽然 RFC2047 确实声明 encoded-word 部分 必须 包含整个字符(multi-byte 编码不得拆分),但它还声明编码的单词可以拆分为CRLF SPACE 分隔符和编码字之间的任何 space 将被忽略。

您可以通过提供自定义策略 class 来解决此问题,该策略会从您自己的 Policy.header_fetch_parse() method.

实现中的行中删除前导白色 space
import re
from email.policy import EmailPolicy

class UnfoldingEncodedStringHeaderPolicy(EmailPolicy):
    def header_fetch_parse(self, name, value):
        # remove any leading white space from header lines
        # that separates apparent encoded-word tokens before further processing 
        # using somewhat crude CRLF-FWS-between-encoded-word matching
        value = re.sub(r'(?<=\?=)((?:\r\n|[\r\n])[\t ]+)(?==\?)', '', value)
        return super().header_fetch_parse(name, value)

并在加载时将其用作您的策略:

custom_policy = UnfoldingEncodedStringHeaderPolicy()

with open(argv[1], 'r', encoding='utf-8') as eml_file:
    msg = Parser(policy=custom_policy).parse(eml_file)

演示:

>>> from io import StringIO
>>> from email.parser import Parser
>>> from email.policy import default as default_policy
>>> custom_policy = UnfoldingEncodedStringHeaderPolicy()
>>> Parser(policy=default_policy).parse(StringIO(data))['from']
'彭以国/第二事业部项目部/第二事业� �� <addr@addr.com>'
>>> Parser(policy=custom_policy).parse(StringIO(data))['from']
'彭以国/第二事业部项目部/第二事业部 <addr@addr.com>'

我提交了 Python issue #35547 来跟踪这个。