深入 CPython 的 email.message.Message 与 Compat32 策略:兼容 Python 3.2 的邮件对象模型全解析
本篇技术指南围绕 CPython 标准库 email 包中最核心的类 —— email.message.Message 展开,它是 Python 3.2 时代起就一直存在的"基础邮件消息类",其行为在默认策略 email.policy.Compat32 下与历史实现保持兼容。读者读完本文将掌握:Message 的 headers + payload 概念模型、构造与序列化(as_string / as_bytes)、类字典的头部访问接口、get_payload / attach / set_charset 等 payload 操作、Content-Type 与 RFC 2231 参数处理、walk() 深度遍历,以及 preamble / epilogue / defects 等实例属性;同时对照现代 EmailMessage 新 API,了解哪些方法是"legacy 方法"以及如何迁移,从而能够在维护旧代码与开发新代码之间做出正确选择。
Message 在 email 模块中的定位:与 EmailMessage 的分工
email.message.Message 被设计为表示一封 RFC 5322 邮件消息的基类。官方文档明确指出它与 EmailMessage(在现代 API 中推荐使用的类)非常相似,区别在于:
Message没有EmailMessage额外加入的高层方法(如set_content、make_alternative、get_body等);Message上某些方法的默认行为略有不同;- 本文档还会讲解一些虽然在
EmailMessage上也支持、但除非处理遗留代码否则不推荐使用的方法(这些方法在文档中被明确标注为 legacy)。
两者在哲学与结构上是一致的:EmailMessage 继承自 MIMEPart,而 MIMEPart 又继承自 Message。从 源码 Lib/email/message.py 的类定义顺序(Message 在第 141 行,EmailMessage 在第 1212 行,中间是作为消息树中各 MIME 部件基类的 MIMEPart,第 989 行)可以清楚看到这一继承关系。
Compat32 策略:为什么它决定本文的行为描述
本文描述的是 Message 在默认策略 compat32 下的行为。compat32 是 Compat32 策略类的一个模块级单例,定义于 Lib/email/_policybase.py#L289-L392,其 docstring 说明它是"向后兼容策略,复刻 email 包 5.1(即 Python 3.2 时代)的行为"。因此:
- 若你使用其他策略(如
policy.default或policy.SMTP),应当改用EmailMessage类,因为新策略下Message的若干行为(尤其是头部存储与折叠)已不再被精确保留; - 若你写的是面向新项目的新代码,也请优先选择
EmailMessage;Message的真正战场是解析存量邮件、维护老代码库。
概念模型:headers + payload
一封消息由**头部(headers)与载荷(payload)**构成:
- 头部是 RFC 5322 风格的
名字: 值,字段名与值之间以冒号分隔,冒号本身不属于任何一方; - payload 可以是一段简单的文本、一个二进制对象,也可以是"结构化的子消息序列"——每个子消息拥有自己的一组头部和自己的 payload。后者由消息具有 MIME 类型(如
multipart/*或message/rfc822)来标识。
Message 对象提供的概念模型是:一个头部有序字典,外加若干方法用于:
- 从头部中读取专门化信息(content type、charset、filename、boundary 等);
- 访问 payload;
- 生成消息的序列化表示;
- 在对象树上递归遍历。
头部字典的特殊语义与 envelope header
Message 的伪字典以头部字段名(必须是 ASCII)为索引,值为"应当只包含 ASCII"的字符串(对非 ASCII 输入有特殊处理,但未必总能得到正确结果)。头部以保留大小写形式存储,但字段名匹配时大小写不敏感。此外还有几点与普通 dict 的关键差异,在 源码中 Message 的类注释 也有印证:
- 允许重复头部:普通字典不允许重复键,但一封邮件可以合法出现多个
Received之类字段,需要get_all等专门方法访问; - 顺序有保证:
keys()等总是按头部在原始消息中出现(或被添加)的顺序返回,任何被删除后重新添加的头部会被追加到末尾; - envelope header 不参与映射接口:每封消息还可携带一个独立的信封头部,又称 Unix-From 头部或
From_头部。
payload 则分两种情况:
- 简单消息对象:payload 是一个
str或bytes; - MIME 容器文档(
multipart/*、message/rfc822):payload 是Message对象列表。
Message.__init__ 的源码(Lib/email/message.py#L156-L166)展示了这些字段在实例上的落地形式:self._headers = []、self._unixfrom = None、self._payload = None、self._charset = None、self.preamble = self.epilogue = None、self.defects = [],以及默认内容类型 self._default_type = 'text/plain'。
构造:Message(policy=compat32)
class email.message.Message(policy=compat32)
- 若不指定 policy,使用
Compat32策略(兼容 Python 3.2 的 email 包行为); - 若指定,则 policy 必须是某个 email.policy 类的实例,消息的更新与序列化将遵循该策略的规则。
该 policy 关键字参数于 Python 3.3 加入。
在实践中,你通常不会直接 Message() 构造对象,而是经由解析器产出——例如使用 email.parser 的 Parser/BytesParser,或其便捷函数 message_from_string、message_from_bytes、message_from_file、message_from_binary_file,它们默认即返回策略为 compat32 的 Message 实例。若要收到 EmailMessage,则需传入 policy=policy.default。
序列化:as_string、as_bytes 与 Generator
as_string(unixfrom=False, maxheaderlen=0, policy=None)
将整封消息"压平"(flatten)为一个字符串:
- unixfrom 为真时,返回的字符串包含信封头部,默认
False; - 向后兼容的关键默认值:maxheaderlen 默认是
0(禁用头部换行折叠),若需折叠必须显式传入其他值;此方法会忽略策略中配置的max_line_length; - policy 参数(Python 3.4 加入)用于覆盖消息实例自带的策略,传入的策略会被交给
Generator,从而控制部分输出格式。
源码(Lib/email/message.py#L173-L195)显示它内部创建 StringIO 与 Generator(fp, mangle_from_=False, maxheaderlen=maxheaderlen, policy=policy) 后调用 flatten。
需要留意两个易踩的坑:
- 压平可能改写消息:若为了完成向字符串的转换而需要填充默认值(例如自动生成或修改 MIME boundary),调用方持有的
Message会被就地修改; - 默认不处理 mbox 转义:
as_string默认不做 Unix mbox 格式要求的"行首From转义(mangling)"。若你需要更精细的控制,应直接实例化 email.generator.Generator 并调用其flatten:
from io import StringIO
from email.generator import Generator
fp = StringIO()
g = Generator(fp, mangle_from_=True, maxheaderlen=60)
g.flatten(msg)
text = fp.getvalue()
- 二进制数据兜底替换:若消息包含未按 RFC 标准编码的二进制数据,这些不合规字节会被替换为 Unicode "未知字符"码点(U+FFFD 之类)。要无损序列化二进制,请改用
as_bytes或BytesGenerator(见下)。
__str__() 等价于 as_string(),因此 str(msg) 即可得到格式化字符串。
as_bytes(unixfrom=False, policy=None)(3.4 新增)
与 as_string 对应,但返回 bytes 对象;policy(同样 3.4 加入)会被交给 BytesGenerator。源码(Lib/email/message.py#L202-L215)内部使用 BytesIO 与 BytesGenerator(fp, mangle_from_=False, policy=policy)。
对需要 mbox 转义或更细控制的二进制输出,同样建议绕过便捷方法而直接用 BytesGenerator:
from io import BytesIO
from email.generator import BytesGenerator
fp = BytesIO()
g = BytesGenerator(fp, mangle_from_=True, maxheaderlen=60)
g.flatten(msg)
text = fp.getvalue()
__bytes__() 等价于 as_bytes(),即 bytes(msg)。
在 email.generator 的 源码 Lib/email/generator.py 中可以看到,Generator.flatten(msg, unixfrom=False, linesep=None) 与 BytesGenerator.flatten 是真正递归写出消息树的地方;mangle_from_ 的默认行为也取决于是否显式传入策略(Lib/email/generator.py#L38-L68)。
结构与载荷操作
is_multipart()
若消息 payload 是子 Message 的列表则返回 True,否则 False。当它为 False 时,payload 应当是字符串对象(可能是 CTE 编码过的二进制载荷)。
一个值得注意的细节:is_multipart() 返回 True 并不一定意味着 msg.get_content_maintype() == 'multipart'。例如 message/rfc822 类型的 Message,其 is_multipart() 同样为 True(源码实现是判断 self._payload 是否为 list,Lib/email/message.py#L217-L219)。这一点在 walk() 一节会再次体现。
信封头部:set_unixfrom / get_unixfrom
set_unixfrom(unixfrom) # unixfrom 应为字符串
get_unixfrom() # 从未设置时返回 None
attach(payload)
把给定 payload 追加到当前载荷中。调用前当前载荷必须是 None 或 Message 列表;调用后 payload 始终变成 Message 列表。若想把载荷设置为标量(如字符串),请改用 set_payload。在非 multipart 的字符串载荷上调用 attach 会抛出 TypeError——源码(Lib/email/message.py#L233-L247)在 payload 已存在且不可 append 时抛出 TypeError("Attach is not valid on a message with a non-multipart payload")。
这是 legacy 方法,在 EmailMessage 上由 set_content 及相关的 make_*、add_* 方法取代。
get_payload(i=None, decode=False)
返回当前载荷:
is_multipart()为True时返回Message列表;为False时返回字符串。若返回的是列表而你直接修改该列表对象,将就地修改消息的载荷;- 传 i:在 multipart 情况下返回第 i 个元素(从 0 计数),越界(
i < 0或i >= 数量)抛IndexError;若载荷是字符串(非 multipart)却给了 i,抛TypeError; - decode 为
True时:若消息非 multipart,依据Content-Transfer-Encoding头部解码载荷——值为quoted-printable或base64时解码,其他编码或缺少该头部时原样返回;任何情况下返回值都是二进制数据(bytes);若消息是 multipart 且 decode 为True,返回None。若 base64 载荷不完整(缺少 padding、出现 base64 字母表外字符),相应缺陷会被追加到消息的defects属性(分别是 email.errors 中的InvalidBase64PaddingDefect与InvalidBase64CharactersDefect); - decode 为
False(默认)时:正文以字符串形式返回、不对 CTE 解码。但对Content-Transfer-Encoding为8bit的情况,会尝试用Content-Type头部指定的charset以及replace错误处理器解码原始字节;若未指定charset,或指定了 email 包不识别的charset,则按默认 ASCII 字符集解码。
源码 Lib/email/message.py#L249-L341 中保留了一张注释形式的逻辑表,清晰总结了 (i, decode, is_multipart) 三者的所有组合结果,例如:非 multipart 且 decode=True 时 payload 被解码为 bytes;base64 分支调用 decode_b 并将返回的缺陷逐一通过 self.policy.handle_defect(self, defect) 上报。
这是 legacy 方法,在 EmailMessage 上由 get_content() 与 iter_parts() 取代。
set_payload(payload, charset=None)
将整个消息对象的载荷设为 payload;设置字符集的任务交给客户端,需自行保证载荷不变量。可选的 charset 设置消息的默认字符集,细节见 set_charset。这也是 legacy 方法,在 EmailMessage 上由 set_content 取代。
set_charset(charset)
设置载荷字符集,charset 可以是 email.charset.Charset 实例、命名字符集的字符串或 None:
- 字符串会被转换为
Charset实例; None:从Content-Type头移除charset参数(消息其余部分不改动);- 其他类型抛
TypeError。
设置时的副作用(可从 源码 Lib/email/message.py#L363-L408 逐行印证):
- 若没有现有
MIME-Version头,则添加一个; - 若没有现有
Content-Type头,则添加值为text/plain的一个; - 无论
Content-Type头是否已存在,其charset参数都会被设为charset.output_charset; - 若
charset.input_charset与charset.output_charset不同,payload 会被重新编码到output_charset; - 若没有现有
Content-Transfer-Encoding头,则按需使用指定Charset对 payload 做传输编码,并添加相应头部;若该头已存在,则假定 payload 已用该 CTE 正确编码,不做改动。
在 EmailMessage 上,此能力由 set_content 的 charset 参数取代。
get_charset()
返回与消息载荷关联的 Charset 实例。注意它是 legacy 方法——在 EmailMessage 上总是返回 None。它与 get_content_charset() 的区别:前者返回正文默认编码对应的 Charset 实例,后者返回 Content-Type 头 charset 参数的小写字符串。
映射接口:像字典一样读写头部
Message 实现了类字典(mapping-like)接口来读写 RFC 5322 头部,但有别于普通字典的若干语义差异(前面概念模型一节已述,其中最关键的是:允许重复头部、顺序有保证)。这些差异是有意设计的,取向是"最大程度的便捷"。
注意:在任何情况下,消息中存在的 envelope header 都不参与该映射接口;由 bytes 解析出的模型中,若某些头部值(违反 RFC)包含非 ASCII 字节,通过此接口取回时会以
charset="unknown-8bit"的 email.header.Header 对象表示。
方法与语义
| 方法 | 语义与差异 |
|---|---|
__len__() |
返回头部总数,含重复项 |
__contains__(name) |
字段名大小写不敏感匹配,返回消息是否有该字段;name 不应含冒号 |
__getitem__(name) |
返回字段值;头缺失时返回 None,从不抛 KeyError;重名时"返回哪一个"未定义,请用 get_all |
__setitem__(name, val) |
把字段追加到末尾;不会覆盖或删除同名已有头。想让新值成为唯一同名头,先删再加 |
__delitem__(name) |
删除消息中该字段名的所有出现;字段不存在也不抛异常 |
keys() / values() / items() |
按原始顺序返回字段名 / 值 / (名, 值) 二元组列表 |
get(name, failobj=None) |
同 __getitem__,但头缺失时返回 failobj(默认 None) |
文档给出的典型惯用法:
if 'message-id' in myMessage:
print('Message-ID:', myMessage['message-id'])
以及"先删后设以保证唯一":
del msg['subject']
msg['subject'] = 'Python roolz!'
值得补充的源码细节:__setitem__ 会先查询 self.policy.header_max_count(name)——若策略规定某头最多出现 N 次(如 Received、From 等在新策略下有数量上限),超限会抛 ValueError;随后以 self.policy.header_store_parse(name, val) 存储(Lib/email/message.py#L433-L449)。而 values()/items()/get()/get_all() 取回值时都会经过 policy.header_fetch_parse,这正是 compat32 下非 ASCII 原始字节会被转换为 unknown-8bit Header 的钩子。在 Lib/email/_policybase.py#L329-L334 的 header_fetch_parse 中可以看到它调用 _sanitize_header,凡检测到代理字符(surrogates)的值都会被包装为使用 unknown-8bit 字符集的 Header。
附加常用方法
get_all(name, failobj=None)
返回指定 name 字段的全部取值列表(保持出现顺序、可含重复);若消息中不存在该字段,返回 failobj(默认 None)。这是访问 Received 等重复头部的正规途径。
add_header(_name, _value, **_params)
扩展版"设头"。类似 __setitem__,但可把附加参数以关键字形式一并写入,_name 是要添加的字段名、_value 是主值。规则:
- 每个关键字参数的键作为参数名,下划线转换为连字符(因为连字符在 Python 标识符中非法);
- 通常参数写作
key="value";若值为None则只写键; - 值含非 ASCII 字符时,可传三元组
(CHARSET, LANGUAGE, VALUE)(charset 命名编码用的字符集,LANGUAGE 通常为None或空串,见 RFC 2231);若传了含非 ASCII 的普通字符串而非三元组,则自动按 RFC 2231 以utf-8/ 语言None编码。
示例与产出:
msg.add_header('Content-Disposition', 'attachment', filename='bud.gif')
Content-Disposition: attachment; filename="bud.gif"
msg.add_header('Content-Disposition', 'attachment',
filename=('iso-8859-1', '', 'Fußballer.ppt'))
Content-Disposition: attachment; filename*="iso-8859-1''Fu%DFballer.ppt"
源码(Lib/email/message.py#L559-L587)逐项遍历 _params,值为 None 时仅输出 key(下划线转连字符),否则交给 _formatparam 格式化,最终经 self[_name] = '; '.join(parts) 走标准的 __setitem__ 通道。
replace_header(_name, _value)
替换头:替换消息中第一个匹配 _name 的头,同时保留头顺序与字段名原有大小写;若找不到匹配头,抛 KeyError。源码(Lib/email/message.py#L589-L602)在遍历中命中即改写对应槽位并 break,遍历完仍未命中才抛异常。
Content-Type 与内容类型 API
get_content_type() / get_content_maintype() / get_content_subtype()
get_content_type():返回小写形式的maintype/subtype;无Content-Type头时返回get_default_type()给出的默认类型。依 RFC 2045,消息总有默认类型,因此本方法总会返回一个值——RFC 2045 规定默认类型是text/plain,除非消息位于multipart/digest容器内(此时是message/rfc822);若Content-Type头是无效类型说明,RFC 2045 强制回落到text/plain;- 后两者分别返回主类型与子类型部分。
源码(Lib/email/message.py#L608-L648)显示 get_content_type 在缺头时返回默认类型,在有头时取 = 前的主值转小写,并检查是否恰好含一个 /(ctype.count('/') != 1 时回落 text/plain)。
get_default_type() / set_default_type(ctype)
- 返回默认内容类型:大多数消息为
text/plain;作为multipart/digest容器子部件的消息为message/rfc822; - 设置默认内容类型:ctype 应为
text/plain或message/rfc822(但不强制校验)。默认类型不存储在Content-Type头中(实例字段_default_type初值为'text/plain')。
set_type(type, header='Content-Type', requote=True)
设置 Content-Type 头的主/子类型。type 必须是 maintype/subtype 形式的字符串,否则抛 ValueError。该方法替换 Content-Type 头,但保留原有参数;requote 为 False 时保留现有引号风格,否则重新引号(默认)。可经 header 指定替代头。设置 Content-Type 时同时会添加 MIME-Version 头。它是 legacy 方法,在 EmailMessage 上由 make_* 与 add_* 方法取代。
MIME 参数(RFC 2045 / RFC 2231)API
get_params(failobj=None, header='content-type', unquote=True)
返回 Content-Type 头参数的列表,元素是按 = 切分的键值二元组;参数中没有 = 时值为空串;有 = 时取值规则同 get_param,且 unquote 为 True(默认)时解除引号。failobj 用于没有该头时返回;header 指定要搜索的替代头。这是 legacy 方法,在 EmailMessage 上由头部对象自带的 params 属性取代。
get_param(param, failobj=None, header='content-type', unquote=True)
返回 Content-Type 头参数 param 的字符串值;无该头或无该参数时返回 failobj(默认 None)。规则要点:
- 参数键总是大小写不敏感比较;
- 返回值可为字符串,也可为 RFC 2231 编码时的三元组
(CHARSET, LANGUAGE, VALUE)。CHARSET与LANGUAGE都可能为None,此时应认为VALUE以us-ascii编码,LANGUAGE通常可忽略; - 若应用不在乎是否 RFC 2231 编码,可用
email.utils.collapse_rfc2231_value(email.utils)把返回值折叠为解码后的 Unicode 字符串:
rawparam = msg.get_param('foo')
param = email.utils.collapse_rfc2231_value(rawparam)
- 无论哪种情况,参数值(返回的字符串或三元组的
VALUE)除非 unquote 为False,否则总是解除引号的。
set_param(param, value, header='Content-Type', requote=True, charset=None, language='', replace=False)
在 Content-Type 头中设置参数:
- 参数已存在则用 value 替换其值;若消息尚无
Content-Type头,则先置为text/plain,再依 RFC 2045 追加新参数; - header 可指定替代头;requote 为
False时关闭必要的引号(默认True); - 指定 charset 时按 RFC 2231 对参数编码,language 指定 RFC 2231 语言(默认空串),两者应为字符串;
- replace 为
False(默认)时头部被移动到头部列表末尾;为True时原位更新(replace关键字于 3.4 加入)。
del_param(param, header='content-type', requote=True)
从 Content-Type 头彻底移除指定参数(含其值),头被就地重写;默认在必要时给所有值加引号(requote 为 False 可关闭)。
文件名、边界与 charset 便捷方法
get_filename(failobj=None)
返回消息 Content-Disposition 头 filename 参数的值;若该头没有 filename,回退查找 Content-Type 头的 name 参数;两者都无或头缺失则返回 failobj。返回值总是按 email.utils.unquote 解除引号的字符串。源码(Lib/email/message.py#L850-L864)进一步调用 collapse_rfc2231_value(...).strip() 处理 RFC 2231 编码名。
get_boundary(failobj=None)
返回 Content-Type 头 boundary 参数值;头缺失或无此参数返回 failobj;返回字符串总是解除引号(源码 Lib/email/message.py#L866-L877 还会对折叠结果做 rstrip(),因为 RFC 2046 说 boundary 可以以空白开头但不能以空白结尾)。
set_boundary(boundary)
把 Content-Type 头的 boundary 参数设为 boundary,必要时总会加引号;若消息没有 Content-Type 头,抛 email.errors.HeaderParseError(源码 Lib/email/message.py#L879-L923)。
值得注意:它与"先删除 Content-Type 头再用 add_header 加新 boundary"存在细微差别——set_boundary 保持 Content-Type 头在头部列表中的原有顺序,但不保留原始 Content-Type 头中可能存在的续行。
get_content_charset(failobj=None)
返回 Content-Type 头 charset 参数、强制转为小写后的值;无该头或无 charset 参数时返回 failobj。注意此方法区别于 get_charset(后者返回正文默认编码的 Charset 实例)。
get_charsets(failobj=None)
返回消息中的字符集名列表:若为 multipart,对 payload 中每个子部件各有一个元素;否则列表长度为 1(只含消息自身)。列表元素是对应子部件 Content-Type 头 charset 参数的值(字符串);若子部件没有 Content-Type 头、没有 charset 参数、或主 MIME 类型不是 text,则该元素为 failobj。源码(Lib/email/message.py#L955-L971)实现为对 self.walk() 逐部件调用 get_content_charset 的列表推导。
get_content_disposition()(3.5 新增)
返回消息 Content-Disposition 头去掉参数后的小写值(若遵循 RFC 2183,取值为 inline、attachment),无该头时返回 None。
深度遍历:walk()
walk() 是一个通用生成器,用于以深度优先的顺序遍历消息对象树的所有部件与子部件,典型用法是作为 for 循环的迭代器。
官方文档示例(使用 CPython 测试数据中的一份 multipart/report 报文)打印每个部件的 MIME 类型:
>>> for part in msg.walk():
... print(part.get_content_type())
multipart/report
text/plain
message/delivery-status
text/plain
text/plain
message/rfc822
text/plain
walk 会进入任何 is_multipart() 为 True 的部件的子部件,即便 msg.get_content_maintype() == 'multipart' 为 False——这正是前面 is_multipart 一节强调的语义。示例用 _structure 调试辅助函数展示树形结构即可看明白:
>>> for part in msg.walk():
... print(part.get_content_maintype() == 'multipart',
... part.is_multipart())
True True
False False
False True
False False
False False
False True
False False
>>> _structure(msg)
multipart/report
text/plain
message/delivery-status
text/plain
text/plain
message/rfc822
text/plain
其中 message 部件不是 multipart,却包含子部件——is_multipart() 返回 True,walk 因此继续下钻。
实现层面,Message.walk 是直接以 email.iterators 中 walk 函数作为方法导入的(见 Lib/email/message.py#L985-L986 的 from email.iterators import walk)。email.iterators 模块还提供 _structure(用于打印树形结构)、body_line_iterator、typed_subpart_iterator 等辅助迭代工具,实测报文可来自 CPython 测试套件 Lib/test/test_email/ 的数据文件。
三个实例属性:preamble、epilogue、defects
Message 对象可选携带两个用于生成 MIME 明文时使用的属性,外加一个记录解析问题的属性。
preamble
MIME 文档格式允许在"头部后的空行与第一个 multipart boundary 串之间"存在一段文本。正常情况下,MIME 感知的邮件阅读器看不到这段文本(它在标准 MIME 盔甲之外),但在查看原始文本或使用非 MIME 感知阅读器时会显现。preamble 属性存放这段"盔甲外的前导文本":
- 当 email.parser.Parser 解析到头部之后、第一个 boundary 串之前的文本时,将其赋给
preamble; - 当 email.generator.Generator 写出 MIME 消息明文时,若发现消息带有
preamble,会把它写在头部与第一个 boundary 之间的区域; - 若消息没有 preamble,该属性为
None。
epilogue
与 preamble 行为相同,但内容是最后一个 boundary 之后到消息结束之间的文本。你不需要为了在文件末尾得到换行而把 epilogue 设为空串——Generator 会自行处理结尾换行。
defects
包含解析该消息时发现的所有问题(缺陷)列表,具体缺陷类型见 email.errors。前面 get_payload 一节提到的不合规 base64 也会在此登记。
Legacy 方法速查:Message → EmailMessage 迁移对照
文档在多个 legacy 方法上标注了替代关系,汇总如下,便于在新代码中快速迁移:
Message(compat32)legacy 方法 |
EmailMessage 上的替代 |
|---|---|
attach(payload) |
set_content + make_* / add_* |
get_payload(i, decode) |
get_content()、iter_parts() |
set_payload(payload, charset) |
set_content() |
set_charset(charset) |
set_content(..., charset=...) |
get_charset() |
总是返回 None(无对应物) |
get_params / get_param |
各头部对象返回的 params 属性 |
set_type |
make_* / add_* 方法 |
若确认只在 compat32 策略下工作(存量代码、老式脚本、测试夹具),继续使用 Message 与上述 legacy 方法完全可行;一旦切换到新策略或开始新项目,请直接使用 EmailMessage。
综合实战:构建 → 序列化 → 解析 → 遍历
将上述 API 串成一个完整流程示例:先手工构造一个带附件的 multipart 消息,序列化为文本,再解析回来并遍历其结构。这演示了 Message 的典型使用闭环(注意构造/解析默认均处于 compat32 语义下):
from email.message import Message
from email.mime.text import MIMEText
# --- 构建:容器消息 + 两个子部分 ---
outer = Message()
outer['Subject'] = '示例:compat32 下的 Message 树'
outer['From'] = 'alice@example.org'
outer['To'] = 'bob@example.net'
text_part = MIMEText('你好,这是正文。', 'plain', 'utf-8')
text_part.add_header('Content-Disposition', 'inline')
attach_part = Message()
attach_part['Content-Type'] = 'application/octet-stream'
attach_part.set_payload(b'\x00\x01binary payload', charset=None)
attach_part.add_header('Content-Disposition', 'attachment',
filename=('utf-8', '', '示例.bin'))
outer.set_type('multipart/mixed') # 自动补 MIME-Version
outer.attach(text_part)
outer.attach(attach_part)
# --- 序列化:字符串与字节两种途径 ---
print(outer.as_string(maxheaderlen=78))
raw_bytes = outer.as_bytes() # 二进制载荷无损输出
# --- 解析回树 ---
from email import message_from_bytes
msg = message_from_bytes(raw_bytes)
# --- 遍历全部子部件并输出类型 ---
for part in msg.walk():
print(part.get_content_type(),
part.get_filename(),
len(part.get_payload()) if part.is_multipart() else '-')
# --- 结构级调试输出(_structure 来自 email.iterators) ---
from email.iterators import _structure
_structure(msg)
几点说明:
set_type('multipart/mixed')会同时补写MIME-Version: 1.0,这是文档与源码都确认的行为;- 用
Generator(..., mangle_from_=True, maxheaderlen=60)手动压平可额外获得 mbox 行首From转义与头部折叠控制;as_string默认不做这两件事; as_string默认maxheaderlen=0(不折叠、忽略策略max_line_length),想要折叠必须显式传值或改用EmailMessage(MIMEPart.as_string在 Lib/email/message.py#L998-L1012 中对maxheaderlen=None的情形回落到policy.max_line_length,这是新旧两套默认值差异的典型缩影)。
延伸阅读
- 现代推荐 API:email.message.EmailMessage 与 MIMEPart;
- 策略体系:email.policy 与 Compat32 的行为差异;
- 解析入口:email.parser;
- 序列化出口:email.generator;
- 头部编码对象:email.header、email.headerregistry;
- 字符集与编码器:email.charset、email.encoders;
- 工具函数:email.utils(含 collapse_rfc2231_value)、email.iterators(walk / _structure);
- 缺陷类型:email.errors;
- 相关源码与测试:
Message实现位于 Lib/email/message.py,Compat32策略位于 Lib/email/_policybase.py#L289-L392,Generator/BytesGenerator位于 Lib/email/generator.py,遍历实现在 Lib/email/iterators.py,完整测试套件位于 Lib/test/test_email/(可重点关注test_email.py中对 Message 映射接口、payload 解码与策略行为的覆盖)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00