压缩附件破坏 email.message.Message.get_payload()

dg9*_*g99 4 python email mime quoted-printable email-attachments

我经常收到带有附件的电子邮件,我必须将其提取并保存到磁盘。我基本上执行以下操作(在 Python 2.7 中):

message = email.message_from_file(sys.stdin)
for part in message.walk():
    path = email.header.decode_header(part.get_filename())[0][0]
    content = part.get_payload(decode=True)
    with open(path, 'w') as f:
        f.write(content)
Run Code Online (Sandbox Code Playgroud)

这种方法适用于我迄今为止收到的所有类型的附件和所有类型的内容传输编码,除非附件是 ZIP 文件并且Content-Transfer-Encoding是“引用可打印的”。在这些情况下,写入的 ZIP 文件比原始文件少一个字节(大约整个文件的 60-80%),并unzip报告如下错误:

% unzip -l foo.zip
Archive:  foo.zip
error [foo.zip]:  missing 1 bytes in zipfile
  (attempting to process anyway)
  Length      Date    Time    Name
---------  ---------- -----   ----
   440228  01-00-1980 00:00   foo - bar.csv
---------                     -------
   440228                     1 file
Run Code Online (Sandbox Code Playgroud)

% unzip foo.zip 
Archive:  foo.zip
error [foo.zip]:  missing 1 bytes in zipfile
  (attempting to process anyway)
error [foo.zip]:  attempt to seek before beginning of zipfile
  (please check that you have transferred or created the zipfile in the
  appropriate BINARY mode and that you have compiled UnZip properly)
  (attempting to re-compensate)
  inflating: foo - bar.csv   bad CRC 4c86de66  (should be a53f73b1)
Run Code Online (Sandbox Code Playgroud)

解压缩的结果与原始 CSV 的大小相差约 0.01%,文件的最后 20-40% 左右出现乱码。


现在,代码可以很好地处理附加为“base64”的 ZIP 文件,并且可以很好地处理附加为“引用可打印”的其他内容(Excel 文件、csv 文件)。我知道 ZIP 附件内容足够完整,以至于我的常规电子邮件阅读器可以将其保存到磁盘中,并完美地提取原始内容。(在保存我的 Python 没有做的附件时,真正的电子邮件阅读器是否可能正在执行一些错误更正?)

Python 是否存在无法读取以引用可打印形式发送的 ZIP 文件的已知问题?Pythonemail包中是否还有其他函数可以尝试正确破译此内容?

dg9*_*g99 5

这种情况下的问题是发件人的二进制附件(ZIP 文件)写得不好,以至于它们包含\r\n序列。也就是说,ZIP 格式的文件本身(不是被压缩的文件)偶尔包含CRLF对。我无法推测这些是如何进入 ZIP 输出的;我认为任何商业或开源拉链都不会包含CRLF在其输出中......

根据quoted-printable encoding 的规则#4,原始“文本”(在这种情况下是ZIP 附件)中的换行符必须\r\n在编码中表示为裸露的(然后根据解码器的区域设置进行解释)。显然,当换行符的确切形式有意义时(例如当它本身是一种编码时),这非常糟糕。RFC 甚至评论了包含文字换行符的二进制数据的奇怪之处:

由于文本以外的类型的规范表示通常不包括换行符的表示,因此在引用的可打印编码中不应出现硬换行符(即旨在有意义并显示给用户的换行符)这样的类型。

所以在 RFC 的末尾有一个巨大的警告:

对实现者的警告:如果二进制数据以带引号的可打印编码,则必须注意将 CR 和 LF 字符分别编码为“=0D”和“=0A”。特别地,二进制数据中的CRLF序列应编码为“=0D=0A”。否则,如果 CRLF 被表示为硬换行符,它可能会在具有不同换行符约定的平台上被错误地解码。

发件人在编码时显然没有遵守此警告,因此发件人和我之间的某些邮件传输代理或网关决定为我的语言环境设置适当的换行符\n(通常是这样)。

无论如何,我通过quopri逐字节比较我的解码附件与附件 ZIP 文件的原始副本,发现这是问题所在。两者是相同的,除了CRLF原版中的每一个都只是LF我解码中的一个。因为\r显然是有意义的,并且因为 QP 编码中的每个其他换行符都正确地以换行符开头=,所以我简单地为application来自该发件人的所有 QP 编码的MIME 类型编写了以下转换:

if part['Content-Disposition'].startswith('attachment') and \
   part['Content-Transfer-Encoding'] == 'quoted-printable':
    rawContent = part.get_payload(decode=False)
    fixedRawContent = re.sub(r'([^=])\n', r'\1=0D=0A=\n', rawContent)
    decodedContent = quopri.decodestring(fixedRawContent)
Run Code Online (Sandbox Code Playgroud)

通过将每个硬(意外)换行符转换为编码\r\n(后跟我自己的软换行符,这样我就不必担心创建任何过长的行),解码功能尽职尽责地将所述内容\r\n放入 ZIP 中,然后正确提取。