
Python email.utils 模块完全指南地址解析、Message-ID 与日期时间格式处理【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpythonemail.utils是 CPython 标准库email包中提供杂项工具的模块它负责把To/Cc等头部中的地址解析成姓名 邮箱元组、生成符合 RFC 2822 的Message-ID、在字符串与datetime对象之间转换邮件日期时间以及对 RFC 2231 编码的头部参数做编解码。无论你是用email包编写发信程序、解析收到的邮件还是在 HTTP、SMTP、日志告警等场景中复用这些工具掌握本模块都能让你少踩头部格式的坑。读完本文你将能独立完成收件人批量提取、RFC 2047/2231 头部编解码、标准日期头生成并理解其底层实现与安全校验边界。模块概览定位与源码结构email.utils的全部实现位于 Lib/email/utils.py约 500 行底层地址与日期解析复用了 Lib/email/_parseaddr.py 中的词法解析器该类源自 Python 2 时代的rfc822模块代码头部注释即写明 Lifted directly from rfc822.py。模块对外公开的__all__共 15 个名字collapse_rfc2231_value、decode_params、decode_rfc2231、encode_rfc2231、formataddr、formatdate、format_datetime、getaddresses、make_msgid、mktime_tz、parseaddr、parsedate、parsedate_tz、parsedate_to_datetime、unquotequote从email._parseaddr导入但未列入__all__仍可显式from email.utils import quote取得。模块内部还有一个supports_strict_parsing True标志位见 Lib/test/test_email/test_email.py 中的test_supports_strict_parsing标识当前解析器具备严格模式能力。值得注意的工程细节延迟导入优化启动速度make_msgid()在函数体内才import random与import socketformataddr()在需要时才惰性导入email.charset.Charsetmktime_tz内部延迟导入calendar。这是为了让email包只被 import 而不使用时避免引入重型模块。Lib/test/test_email/test_utils.py 的TestImportTime专门用ensure_lazy_imports(email.utils, {random, socket})验证了这一点。新 API / 旧 API 双轨制官方文档明确指出从formataddr往后的函数属于 legacyCompat32email API——在使用新的EmailMessage默认EmailPolicy时头部解析与格式化由新 API 自动完成通常无需手动调用。但事实是它们仍然大量出现在标准库的非邮件模块中属于旧而不废的核心工具见下文仓库内应用实例。_sanitize与_has_surrogates内部还有处理 surrogate-escaped 二进制数据的辅助函数被 Lib/email/message.py、Lib/email/generator.py 等引用用于把畸形的字节内容安全地转成可显示文本。邮箱地址解析与格式化parseaddr()把地址字段拆成 姓名 邮箱from email.utils import parseaddr parseaddr(Guido van Rossum guidopython.org) # (Guido van Rossum, guidopython.org) parseaddr(Guidopython.org) # 没有显示名 # (, Guidopython.org) parseaddr(invalid address, no mailbox) # 解析失败 # (, )输入应为某个地址类头部字段To、Cc、From、Resent-To等的单条值成功时返回(realname, email_address)二元组失败时按文档契约返回(, )从 Lib/email/utils.py 的parseaddr实现看它走_AddressList(...).addresslist解析器并在严格模式下额外执行_pre_parse_validation/_post_parse_validation两层校验。strict 严格模式3.13 新增默认开启底层是AddrlistClass状态机见 Lib/email/_parseaddr.py其宽松解析会把畸形的输入拼出不合理的地址。例如源码注释给出的反例aliceexample.com bobexample.com会被宽松解析拆成两个地址。为避免这种非法输出前置校验_check_parenthesis会剥离引号内内容后检查括号是否配对不配对则整条替换为(, )后置校验会把邮箱中含[表示域字面量解析失败残留的结果置空严格模式下若解析出的地址数量与输入中的逗号计数对不上去掉引号内逗号后按1 逗号数估算同样整体返回空元组。因此默认行为是宁可返回(, )也不吐出错误地址。需要旧版宽松行为时可显式传parseaddr(addr, strictFalse)。parsedate需要说明的是3.13 之前无此参数行为等同于今天的strictFalse。formataddr()parseaddr 的逆操作from email.utils import formataddr formataddr((Guido van Rossum, guidopython.org)) # Guido van Rossum guidopython.org formataddr((, guidopython.org)) # 姓名为假值时原样返回地址 # guidopython.org它接收(realname, email_address)二元组返回可直接放进To/Cc/From头部的字符串实现细节Lib/email/utils.pyformataddr地址必须纯 ASCII对address执行address.encode(ascii)非 ASCII 会抛UnicodeError测试test_unicode_address_raises_error可验证邮箱不能做 IDN 直写显示名含特殊字符时自动加引号转义源码用specialsre re.compile(r[][\\(),:;.])检测命中则外层加双引号并用escapesre把其中的\与转义为\\、\非 ASCII 显示名走 RFC 2047 编码显示名不是 ASCII 时按charset参数默认utf-8执行header_encode输出形如?utf-8?b?...? addr的 encoded-word。Lib/test/test_email/test_email.py 给出的可复现用例formataddr((H\u00e4ns W\u00fcrst, persondom.ain)) # ?utf-8?b?SMOkbnMgV8O8cnN0? persondom.ain formataddr((H\u00e4ns W\u00fcrst, persondom.ain), iso-8859-1) # ?iso-8859-1?q?HE4ns_WFCrst? persondom.aincharset既可以是str字符集名也可以是具有header_encode方法的类email.charset.Charset对象测试test_accepts_any_charset_like_object用一个只实现了header_encode的 mock 验证了鸭子类型要求。防头部注入strict默认开启formataddr是收信/发信前把数据变安全的最后一道闸。若姓名或地址中含有\r或\nCR/LF默认直接抛ValueError防止通过换行注入伪造头部例如persondom.ain\r\nBcc: victimdom.ain这类攻击载荷会被拒绝对应测试test_crlf_in_parts_raises_error。显式传strictFalse才放行保留旧行为测试test_crlf_in_parts_allowed_when_not_strict。该参数在文档中标记为versionchanged:: next即本仓库当前开发主线对应的下一个发布版本中加入。getaddresses()批量提取一条消息的所有收件人from email.utils import getaddresses tos msg.get_all(to, []) ccs msg.get_all(cc, []) resent_tos msg.get_all(resent-to, []) resent_ccs msg.get_all(resent-cc, []) all_recipients getaddresses(tos ccs resent_tos resent_ccs)getaddresses(fieldvalues, *, strictTrue)的输入是头部字段值的序列如Message.get_all()的返回输出是若干(realname, email)二元组等价于对每个值执行parseaddr语义。严格模式3.13 起默认下同样引入失败保护先用COMMASPACE , 拼接全部字段值再整体交给_AddressList解析然后做数量对账——如果解析出的地址数与按逗号预估值不符即输入包含畸形语法返回[(, )]而非可能被误解的错误列表。仓库内 Lib/smtplib.py 的mail/sendmail正是用getaddresses把from_addr与to_addrs解析为addr_spec再投递。quote() 与 unquote()引号与反斜杠转义from email.utils import quote, unquote quote(ab\\c) # 反斜杠翻倍、双引号加反斜杠 # a\\b\\\\c unquote(quoted) # 去掉首尾双引号并还原内部转义 # quoted unquote(addrdom) # 或去掉首尾尖括号 # addrdom实现约定quote只负责把字符串准备好用于放进引号串不负责加外层引号unquote则去除首尾配对的...或...定界符。它们是 Compat32 遗留 API新 API 的头部解析机制会自动完成等价工作因此一般不需要手动调用——但它们仍是formataddr引号处理与 RFC 2231 参数去引号的底层基石unquote在本模块内被decode_params/collapse_rfc2231_value使用quote定义于 Lib/email/_parseaddr.py。生成符合 RFC 2822 的 Message-IDmake_msgid()from email.utils import make_msgid make_msgid() # 17571231231234.20800.16526388040877946887nightshade.la.mastaler.com make_msgid(idstringpython-list) # 附加字符串增强唯一性 make_msgid(domainexample.org) # 指定 之后的域 # 17571231231234.20800.81341234431122123312.example.org 中的域部分示例函数返回包裹在尖括号中的字符串适合直接赋给Message-ID头。三个组成部分见 Lib/email/utils.pymake_msgid时间int(time.time() * 100)百分之一秒精度配合进程号与随机数形成时间线唯一性进程号os.getpid()随机位random.getrandbits(64)64 位随机整数跨进程碰撞概率可忽略。参数语义idstring非空时以.拼接进本地部分如….myidhost用于在同一进程连续生成消息时进一步区分domain提供后面的域名部分默认取socket.getfqdn()本机全限定域名。文档特别指出构建跨多台主机共享统一域名的分布式系统时自定义domain才真正有用——这也是该参数的典型动机一般单机场景不必覆盖默认值。时间与日期宽松解析与标准格式化宽松解析 parsedate() 与 parsedate_tz()from email.utils import parsedate, parsedate_tz parsedate(Mon, 20 Nov 1995 19:12:08 -0500) # (1995, 11, 20, 19, 12, 8, 0, 1, -1) parsedate_tz(Mon, 20 Nov 1995 19:12:08 -0500) # (1995, 11, 20, 19, 12, 8, 0, 1, -1, -18000)parsedate(date)按 RFC 2822 规则尝试解析但对不守规矩的邮件头采取能猜就猜策略成功返回 9 元组可直接传给time.mktime失败返回None。parsedate_tz(date)功能相同但返回10 元组前 9 个元素同样可直接交给time.mktime第 10 个元素是相对 UTC 的时区偏移单位秒。若输入不含时区则第 10 个元素为0表示 UTC。注意9 元组的第 6、7、8 个索引星期、年内日序、DST 标志不可用。底层_parsedate_tzLib/email/_parseaddr.py的猜体现在许多宽容细节上星期名Mon–Sun可缺失月份支持全拼january…兼容 RFC 850 风格的20-Nov-95与点号分隔时间19.12.08两位年份按 POSIX 规则换算69–99 → 1969–199900–68 → 2000–2068内建时区名表UT/UTC/GMT/Z → 0美加常用缩写EST:-500, EDT:-400, CST:-600, CDT:-500, MST:-700, MDT:-600, PST:-800, PDT:-700与AST/ADT。源码注释说明出于 RFC 1123 指出的 RFC 822 符号错误问题不支持军事时区仅保留ZRFC 1123 建议优先使用数字偏移特殊语义-0000时区被解析为偏移None后面会看到它对 datetime 转换的意义。转成 datetimeparsedate_to_datetime()from email.utils import parsedate_to_datetime parsedate_to_datetime(Mon, 20 Nov 1995 19:12:08 -0500) # datetime.datetime(1995, 11, 20, 19, 12, 8, tzinfodatetime.timezone(datetime.timedelta(days-1, seconds68400)))parsedate_to_datetime是format_datetime的逆操作也是把邮件日期字符串接入现代datetime生态的推荐入口。它直接调用内部_parsedate_tz而非parsedate_tz从而能区分两种语义-0000→ naivedatetime-0000表示时间上是 UTC但明确不透露来源时区因此源码在此分支返回不带tzinfo的 naive 对象对应测试test_parsedate_to_datetime_naive其它有效偏移 → awaredatetime用datetime.timezone(datetime.timedelta(secondstz))构造对应tzinfo对应测试test_parsedate_to_datetime非法输入 →ValueError与parsedate返回None不同这里对无法解析、越界值小时 23、偏移超出 ±24 小时区间及构造时OverflowError一律抛ValueError。完整反例清单见测试类DateTimeTests例如Tue, 06 Jun 2017 27:39:33 0600、Mon, 20 Nov 2017 12:00:00 24000000000000等。10 元组转时间戳mktime_tz()from email.utils import parsedate_tz, mktime_tz stamp mktime_tz(parsedate_tz(Mon, 20 Nov 1995 19:12:08 -0500)) # 返回 UTC 纪元秒POSIX timestamp可直接 time.ctime(stamp) 展示把parsedate_tz产出的 10 元组换算成 UTC 时间戳。实现约定若第 10 个元素为None则按本地时间处理time.mktime否则按calendar.timegm得到 UTC 基准后再扣除时区偏移。注意该函数的None分支是给直接调用它、绕过parsedate_tz后者会把缺失时区归一化为0的调用方保留的。生成标准日期头formatdate() 与 format_datetime()from email.utils import formatdate, format_datetime import datetime, time formatdate() # 当前 UTC 时间 # Fri, 09 Nov 2001 01:08:47 -0000 格式示例值为当前时刻 formatdate(time.time(), localtimeTrue) # 本地时区、含 DST 处理 # Sat, 01 Jan 2011 18:00:00 0200 值随系统时区变化 formatdate(time.time(), localtimeFalse, usegmtTrue) # HTTP 风格 # Thu, 01 Dec 2011 15:00:00 GMTformatdate(timevalNone, localtimeFalse, usegmtFalse)timeval浮点时间值time.time()一类缺省为当前时间localtimeTrue相对本地时区输出数字偏移并正确考虑夏令时实现为datetime.fromtimestamp(...).astimezone()同时强制usegmtFalseusegmtTrue把时区写成 ASCII 字符串GMT而非数字-0000供 HTTP 等协议使用仅当localtimeFalse时生效。format_datetime(dt, usegmtFalse)与之对应但直接接收datetimenaivedatetime被理解为UTC 但无来源时区信息输出-0000awaredatetime输出数字偏移dt.strftime(%z)如-0500aware 且为零偏移时usegmtTrue可输出GMT用于生成符合 HTTP 规范的Date头。注意源码中的严格检查usegmtTrue要求dt.tzinfo必须是datetime.timezone.utcdt.tzinfo is None or dt.tzinfo ! datetime.timezone.utc即抛ValueError仅仅偏移为零的其它 tzinfo并不满足测试test_usegmt_with_non_utc_datetime_raises用-0700aware 实例验证了这一约束。源码注释中一个有趣的细节格式化没有使用strftime()因为那会受 locale 影响而 RFC 2822 硬性要求英文缩写星期与月份名这正是为何 Lib/email/utils.py 手写[Mon,...]/[Jan,...]两张查表并结合timetuple拼字符串。测试类FormatDateTestsLib/test/test_email/test_utils.py在Europe/Minsk时区下验证了 UTC 与本地模式含 2011 年 Minsk 从 0200/带 DST 迁到 0300/无 DST 的历史案例。localtime()拿到带时区的本地时间from email.utils import localtime import datetime localtime() # 当前时刻aware localtime(datetime.datetime.now()) # naive 输入按系统本地时间解释返回带本地时区信息的 awaredatetime。实现只有一步dt.astimezone()Lib/email/utils.py无参调用以datetime.now()为输入传入 naive 对象时按它本就在系统本地时区解释后补上 tzinfo传入 aware 对象则换算到本地时区依赖系统时区数据库。因此等价效果是给定任意时刻返回同一时刻的本地墙上时间表示。测试覆盖了 DST 开/关两种time.daylight场景、Europe/Minsk/Europe/Kyiv等历史时区跳变如 1984 年基辅为MSK、1994 年为EET。该函数 3.3 加入文档同时记载旧的isdst参数已在 3.12 弃用、并于 3.14 移除deprecated-removed:: 3.12 3.14。仓库内的真实调用场景虽然文档把日期函数归入 legacy 邮件工具标准库却到处在用它们是最直接的实战证据Lib/http/server.py 的date_time_string()用formatdate(timestamp, usegmtTrue)生成 HTTPDate响应头并在If-Modified-Since条件请求里用parsedate_to_datetime解析客户端时间再与文件 mtime 比较含对无时区旧格式的 UTC 兜底处理Lib/logging/handlers.py 的SMTPHandler.emit()构造告警邮件时直接msg[Date] email.utils.localtime()Lib/smtplib.py 用getaddresses/parseaddr规范化收件人列表。RFC 2231 参数编解码处理 Content-Type 等头的扩展参数RFC 2231 解决的是头部参数如Content-Type的filename、name携带非 ASCII 与超长分段的问题编码形态如filename*utf-8%E4%B8%AD%E6%96%87.txt。email.utils提供四个配套函数。encode_rfc2231() 与 decode_rfc2231()from email.utils import encode_rfc2231, decode_rfc2231 encode_rfc2231(中文名.txt, charsetutf-8, languageNone) # utf-8%E4%B8%AD%E6%96%87%E5%90%8D.txt示意按 utf-8 百分号编码语言为空 decode_rfc2231(utf-8%E4%B8%AD%E6%96%87) # (utf-8, , %E4%B8%AD%E6%96%87)encode_rfc2231对内容做百分号编码实现用urllib.parse.quote(s, safe, encodingcharset or ascii)。charset、language均给出时输出charsetlanguageencoded三字段形式只给charset时language用空串占位。文档所述的两者皆缺则原样返回按实现理解是指不追加charsetlanguage前缀——纯 ASCII 字母数字串经过编码后与原文一致含空格等符号的内容仍会被百分号转义使用时需留意这一细节。decode_rfc2231按单引号maxsplit2切分。不足三段时返回(None, None, s)否则返回(charset, language, value)三元组。注意它只负责切分还原三字段不负责解百分号——解码 octets 的工作交给下文的collapse_rfc2231_value或在读取侧由Message机制完成。collapse_rfc2231_value()三元组变回字符串from email.utils import collapse_rfc2231_value collapse_rfc2231_value((utf-8, , %E4%B8%AD%E6%96%87)) # 示意 # 中文当头部参数按 RFC 2231 编码时Message.get_param()可能返回 3 元组(charset, language, value)。Lib/email/message.py 的get_param文档与源码直接推荐了收尾姿势rawparam msg.get_param(foo) param email.utils.collapse_rfc2231_value(rawparam)函数行为Lib/email/utils.py若传入的不是三元组视为普通字符串并unquote后返回去掉首尾引号三元组时charset为None则回退用fallback_charset默认us-ascii把 value 按raw-unicode-escape转成字节后以指定字符集解码未知字符集显式codecs.lookup(charset)查表查不到LookupError则退化为unquote(text)原样返回——也就是说遇到 Python 不认识的字符集声明不会崩溃errors参数透传给bytes.decode默认replace非法字节替换为 UFFFD。decode_params()批量还原参数序列from email.utils import decode_params # params 为 (参数名, 值) 序列首项是媒体类型本身 params [(text/plain, ), (name*0, a), (name*1, b)] decode_params(params)decode_params(params)把 RFC 2231 的分段参数列表还原为普通参数列表params是(name, value)二元组序列第一项通常即Content-Type的媒体类型实现上new_params [params[0]]原样保留它。随后它用正则rfc2231_continuation re.compile(r^(?Pname\w)\*((?Pnum[0-9])\*?)?$)识别name*、name*0、name*1这类延续片段按编号排序并拼接分段值has_zero逻辑处理无 0 号段但有未编号段的兼容情形带*的段先按latin-1解百分号再交给decode_rfc2231拆出charset/language最终产出(name, (charset, language, value))或普通value形式普通非延续参数仅做去引号并重新加引号的规范化值经quote转义后以…形式呈现相当于统一引号风格。它主要服务于兼容层Message._get_params_preserve等内部路径新EmailPolicyAPI 的用户通常不会直接面对它。版本演变与安全边界小结把文档与源码中的版本信息汇总函数关键版本说明localtime3.3 加入isdst3.12 弃用、3.14 移除返回 aware 本地时间make_msgiddomain参数 3.2 加入生成 RFC 2822Message-IDparseaddr/getaddressesstrict3.13 加入并默认True默认拒绝畸形输入失败给(, )formataddrcharset3.3 加入strict文档标记next当前开发主线默认拒绝含 CR/LF 的输入防头部注入parsedate_to_datetime/format_datetime/localtime3.3 加入datetime 与 RFC 2822 字符串互转从安全角度formataddr的 CR/LF 拦截与parseaddr/getaddresses的严格解析都是 3.13 前后针对地址伪造 / 头部注入 / 畸形输入导致错误解析的加固成果——相关回归测试同时存在于 Lib/test/test_email/test_email.pyFormatAddrTests与 Lib/test/test_email/test_utils.pyDateTimeTests/LocaltimeTests/FormatDateTests。运行python -m test test_email -k utils或直接python -m unittest test.test_email.test_utils即可在本地复现上述行为。小结何时用什么一句话使用清单要发信EmailMessage配合默认EmailPolicy时头部分析全自动但仍可借formatdate(usegmtTrue)/localtime()填Date、make_msgid()填Message-ID用formataddrstrict 保持默认安全写入带显示名的地址要解析收到的邮件新 API 直接读msg[to]等即可拿到结构化对象需要姓名 邮箱元组时可用parseaddr/getaddresses保持默认 strict需要旧式日期元组或时间戳时用parsedate/parsedate_tz/mktime_tz要把邮件日期与datetime生态打通parsedate_to_datetime/format_datetime是最佳双向桥梁HTTP 头日期用format_datetime(dt, usegmtTrue)要处理Content-Disposition的filename*等参数理解encode_rfc2231/decode_rfc2231/decode_params的分段与charsetlang结构并让collapse_rfc2231_value收尾还原成可读字符串。若想深入词法层原理地址状态机、域字面量、注释剥离、2 位年份换算、时区名表请直接研读 Lib/email/_parseaddr.py每个公开函数的可复现断言都能在 Lib/test/test_email/test_email.py 与 Lib/test/test_email/test_utils.py 中找到。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考