7. Python email
库深度解析:MIME 邮件构建与解析的艺术
在前面的章节中,我们深入探讨了电子邮件的底层协议(SMTP, POP3, IMAP)以及如何使用imaplib
库从服务器接收和管理邮件。然而,邮件内容的实际格式和结构并非由这些传输协议定义,而是由MIME (Multipurpose Internet Mail Extensions) 标准规范。Python的email
库是处理MIME格式邮件的强大工具,它允许我们以编程方式解析传入的邮件内容,以及构建符合标准、包含富文本和附件的复杂邮件。
本章将聚焦于email
库的核心功能,从原始邮件字节串到可操作的Message
对象,再到从头开始构建多部分邮件,全面揭示邮件内容的内部机制。
7.1 email
库概览与核心概念
email
库是Python标准库的一部分,专门用于处理电子邮件消息的创建、解析、修改和发送。它提供了一个面向对象的模型来表示电子邮件,使得开发者无需直接处理复杂的RFC 822和MIME格式细节。
7.1.1 email.message.Message
对象:邮件的抽象表示
email.message.Message
类是email
库的核心。它代表了一封电子邮件的抽象概念,无论是整个邮件还是邮件的某个MIME部分。可以将其想象成一棵树形结构,其中每个节点都是一个Message
对象。
- 特点:
- 字典式访问头部:可以通过类似字典的方式访问和设置邮件头部字段(如
msg['Subject']
,msg['From']
)。 - 载荷 (Payload):
Message
对象的核心内容,可以是纯文本、二进制数据,或者另一个Message
对象(对于多部分邮件)。 - 多部分邮件的层次结构:对于
multipart
类型的邮件,一个Message
对象可以包含多个子Message
对象,形成一个树形结构。msg.get_payload()
方法在不同情况下会有不同的返回值:- 对于单部分邮件,返回其内容。
- 对于多部分邮件,返回一个包含子
Message
对象的列表。
- MIME 类型信息:提供了获取邮件
Content-Type
、Content-Transfer-Encoding
等MIME头部信息的方法。
- 字典式访问头部:可以通过类似字典的方式访问和设置邮件头部字段(如
7.1.2 MIME 邮件的层次结构:多部分邮件的树状模型
MIME标准允许将一封邮件划分为多个独立的部分,每个部分都可以有自己的内容类型和编码。这使得邮件能够同时包含纯文本、HTML、图片、附件等。这种多部分邮件形成了自然的层次结构,可以用一棵树来表示。
- 根
Message
对象:代表整个邮件。如果邮件是多部分的,它的Content-Type
将是multipart/*
,并且其载荷将是一个包含子Message
对象的列表。 - 子
Message
对象:代表邮件的各个MIME部分。这些子对象可以是:text/plain
:纯文本正文。text/html
:HTML正文。image/jpeg
,application/pdf
:各种类型的附件或内联内容。multipart/*
:如果某个部分自身也是一个多部分容器(例如,一个HTML邮件中包含内联图片,HTML部分会是multipart/related
,而其内部又包含text/html
和image/jpeg
)。
email
库的walk()
方法是遍历这个树形结构的关键。
7.1.3 头部与载荷 (Payload) 分离
每个Message
对象都清晰地分为两个主要部分:
- 头部 (Headers):
- 由一系列
Field-name: field-body
对组成。 - 例如
From
,To
,Subject
,Date
,Content-Type
,MIME-Version
等。 Message
对象提供了类似字典的接口来访问和操作这些头部。
- 由一系列
- 载荷 (Payload):
- 邮件的实际内容。
- 对于文本类型,通常是字符串。
- 对于二进制类型(如图片、附件),通常是字节串。
- 对于多部分类型,载荷是子
Message
对象的列表。
msg.get_payload()
用于获取载荷,而msg.get('Header-Name')
或 msg['Header-Name']
用于获取头部。
7.1.4 字符集与传输编码
为了支持非ASCII字符和二进制数据在邮件系统中的传输,MIME定义了字符集和传输编码:
- 字符集 (Charset):
- 在
Content-Type
头部字段中指定,例如Content-Type: text/plain; charset="utf-8"
。 - 告诉邮件客户端如何将字节数据解码为可读的字符。
email
库在解析时会尝试自动检测并使用正确的字符集,在构建时也会强制使用。
- 在
- 传输编码 (Content-Transfer-Encoding):
- 在
Content-Transfer-Encoding
头部字段中指定,例如Content-Transfer-Encoding: base64
。 - 将原始的字节数据编码成7位ASCII字符(或8位,但通常都转换为7位安全)。
- 常见的编码:
base64
,quoted-printable
,7bit
,8bit
,binary
。 email
库在get_payload(decode=True)
时会自动进行解码,在构建邮件时也会自动进行编码。
- 在
理解这些核心概念是有效使用email
库进行邮件处理的基础。
7.2 邮件解析:从原始字节到 Message
对象
解析邮件是将原始邮件内容(通常是从IMAP服务器获取的字节串)转换成Python Message
对象的过程,以便我们可以轻松地访问其头部、正文和附件。
7.2.1 email.message_from_bytes()
和 email.parser.BytesParser
最常用的邮件解析函数是 email.message_from_bytes()
。它是一个便捷函数,内部使用了 email.parser.BytesParser
。
email.message_from_bytes(binary_message, _class=Message, *, policy=policy.default)
:- 直接将字节串解析为
Message
对象。 binary_message
:从IMAP服务器获取的原始邮件字节串。_class
:可选,指定要创建的Message对象类。policy
:重要的参数,控制解析行为,如错误处理、MIME兼容性。policy.default
通常是安全的默认值。
- 直接将字节串解析为
import email # 导入email库
from email.message import Message # 从email.message模块导入Message类
def parse_raw_email_bytes(raw_email_bytes: bytes):
"""
演示如何从原始邮件字节串解析 Message 对象。
参数:
raw_email_bytes (bytes): 待解析的原始邮件字节串。
"""
print("\n--- 从原始字节解析邮件 ---") # 打印信息
try: # 尝试解析邮件
# 使用 email.message_from_bytes 函数将原始字节串解析为 Message 对象。
# 这是一个便捷函数,推荐用于简单直接的解析。
msg = email.message_from_bytes(raw_email_bytes) # 将原始字节数据解析成一个邮件对象
print("邮件解析成功。") # 打印解析成功信息
# 访问邮件头部
print("\n--- 邮件头部信息 ---") # 打印邮件头部信息标题
# msg.items() 返回一个列表,包含所有头部字段的 (name, value) 对。
for header_name, header_value in msg.items(): # 遍历邮件的所有头部字段
print(f"{
header_name}: {
header_value}") # 打印头部字段名和值
# 获取特定头部字段
print(f"\n主题 (Subject): {
msg.get('Subject', '无主题')}") # 获取主题字段,如果不存在则显示“无主题”
print(f"发件人 (From): {
msg.get('From', '无发件人')}") # 获取发件人字段
print(f"收件人 (To): {
msg.get('To', '无收件人')}") # 获取收件人字段
# 检查邮件是否是多部分邮件
if msg.is_multipart(): # 如果邮件是多部分邮件
print("\n这是一封多部分邮件。") # 打印多部分邮件信息
# 进一步处理将在 7.2.3 节的 walk() 方法中详细演示
else: # 如果是单部分邮件
print("\n这是一封单部分邮件。") # 打印单部分邮件信息
# 获取单部分邮件的 Content-Type 和 Payload
content_type = msg.get_content_type() # 获取邮件内容类型
charset = msg.get_content_charset() # 获取邮件内容字符集
print(f" Content-Type: {
content_type}") # 打印内容类型
print(f" Charset: {
charset}") # 打印字符集
# 获取载荷并尝试解码
payload = msg.get_payload(decode=True) # 获取解码后的邮件载荷(字节串)
if payload and content_type.startswith('text/'): # 如果载荷不为空且是文本类型
try: # 尝试解码
decoded_payload = payload.decode(charset if charset else 'utf-8', errors='replace') # 使用指定字符集或UTF-8解码,替换无法解码的字符
print(f" 载荷内容摘要:\n{
decoded_payload[:200]}...") # 打印载荷内容摘要
except Exception as e: # 捕获解码错误
print(f" 解码载荷失败: {
e}") # 打印解码失败信息
elif payload: # 如果载荷不为空但不是文本类型
print(f" 载荷是二进制数据,大小: {
len(payload)} 字节。") # 打印二进制数据大小
else: # 如果载荷为空
print(" 载荷为空。") # 打印载荷为空信息
except Exception as e: # 捕获其他所有异常
print(f"解析邮件时发生错误: {
e}") # 打印解析错误信息
# 示例原始邮件字节串 (包含头部和正文,模拟从IMAP获取)
# 这是一个非常简单的纯文本邮件,用于初次演示
sample_raw_email_1 = b"""From: Sender <sender@example.com>
To: Recipient <recipient@example.com>
Subject: Test Email - Hello World
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
这是一封测试邮件。
Hello, world!
这是中文内容。
"""
# sample_raw_email_2 模拟一个简单的HTML邮件
sample_raw_email_2 = b"""From: HTML Sender <html@example.com>
To: HTML Recipient <html_recip@example.com>
Subject: Test HTML Email
MIME-Version: 1.0
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
<html><body><h1>Hello HTML!</h1><p>=E8=BF=99=E6=98=AF<b>HTML</b>=E5=86=85=E5=AE=B9=E3=80=82</p></body></html>
"""
# if __name__ == "__main__": # 当脚本直接运行时
# print("--- 演示邮件解析 (纯文本邮件) ---") # 打印演示信息
# parse_raw_email_bytes(sample_raw_email_1) # 解析第一个示例邮件
#
# print("\n" + "="*50 + "\n") # 分隔符
#
# print("--- 演示邮件解析 (HTML邮件) ---") # 打印演示信息
# parse_raw_email_bytes(sample_raw_email_2) # 解析第二个示例邮件
这段代码展示了如何使用email.message_from_bytes()
函数将原始邮件字节串解析成email.message.Message
对象。它演示了如何访问邮件的头部字段(如From
, To
, Subject
)以及如何检查邮件是否为多部分,并初步获取单部分邮件的Content-Type
和解码后的载荷内容。这是所有邮件解析操作的起点。
7.2.2 处理编码问题:decode_header()
和 get_payload(decode=True)
邮件内容的编码是邮件解析中最常见的难点之一。email
库提供了强大的工具来处理这些问题。
-
decode_header(encoded_header)
(来自email.header
模块):- 用于解码可能包含“编码词”语法(
=?charset?encoding?text?=
)的邮件头部字段,如Subject
、From
、To
、Cc
等。 - 返回值:一个列表,其中每个元素是一个元组
(decoded_string_or_bytes, charset)
。decoded_string_or_bytes
:解码后的字符串或原始字节串(如果无法解码)。charset
:用于解码的字符集名称(如'utf-8'
),如果未指定或无法确定,则为None
。
- 你需要遍历这个列表,并将所有部分连接起来,同时处理
charset
。
- 用于解码可能包含“编码词”语法(
-
get_payload(decode=False)
:- 获取邮件或MIME部分的原始载荷。
- 如果
decode=True
(推荐):email
库会自动根据Content-Transfer-Encoding
头部(如base64
,quoted-printable
)对载荷进行解码。- 对于
text/*
类型,通常返回一个字符串(已根据charset
解码)。 - 对于非
text/*
类型(如附件),通常返回一个字节串。
- 如果
decode=False
:- 返回原始的、未解码的载荷(字符串或字节串),你需要手动处理
Content-Transfer-Encoding
和charset
。通常不推荐。
- 返回原始的、未解码的载荷(字符串或字节串),你需要手动处理
import email # 导入email库
from email.header import decode_header # 导入decode_header函数
from email.message import Message # 导入Message类
import base64 # 导入base64库,用于模拟Base64编码
def handle_email_encodings(raw_email_bytes: bytes):
"""
演示如何处理邮件中的编码问题,包括头部和载荷。
参数:
raw_email_bytes (bytes): 包含编码内容的原始邮件字节串。
"""
print("\n--- 处理邮件编码问题 ---") # 打印信息
try: # 尝试解析邮件
msg = email.message_from_bytes(raw_email_bytes) # 将原始字节数据解析成一个邮件对象
print("邮件解析成功。") # 打印解析成功信息
# 1. 解码头部字段 (Subject, From, To 等)
print("\n--- 头部解码 ---") # 打印头部解码标题
for header_name in ['Subject', 'From', 'To']: # 遍历需要解码的头部字段
header_value = msg.get(header_name) # 获取头部字段的值
if header_value: # 如果头部值存在
# decode_header() 返回一个列表,元素为 (decoded_bytes_or_str, charset)
decoded_parts = decode_header(header_value) # 解码头部值
decoded_header_str = "" # 初始化解码后的头部字符串
for part, charset in decoded_parts: # 遍历解码后的部分
if isinstance(part, bytes): # 如果部分是字节串
try: # 尝试解码
# 如果 charset 为 None,通常默认为 us-ascii 或 utf-8
decoded_header_str += part.decode(charset if charset else 'utf-8', errors='replace') # 使用指定字符集或UTF-8解码,替换无法解码的字符
except UnicodeDecodeError: # 捕获Unicode解码错误
# 尝试使用更通用的编码作为回退
decoded_header_str += part.decode('latin-1', errors='replace') # 尝试使用latin-1解码
else: # 如果部分已经是字符串
decoded_header_str += part # 直接添加字符串部分
print(f" {
header_name} (解码后): {
decoded_header_str}") # 打印解码后的头部字段值
else: # 如果头部值不存在
print(f" {
header_name}: (未找到)") # 打印未找到信息
# 2. 获取和解码邮件载荷 (正文或附件)
print("\n--- 载荷解码 ---") # 打印载荷解码标题
if msg.is_multipart(): # 如果是多部分邮件
print(" 多部分邮件,遍历各部分进行解码:") # 打印多部分邮件信息
for part_num, part in enumerate(msg.walk()): # 遍历邮件的所有MIME部分
if part.is_multipart(): # 如果是多部分容器
continue # 跳过容器,只处理叶子部分
content_type = part.get_content_type() # 获取内容类型
charset = part.get_content_charset() # 获取字符集
transfer_encoding = part.get('Content-Transfer-Encoding', '7bit').lower() # 获取传输编码
print(f" Part {
part_num}: Content-Type: {
content_type}, Charset: {
'{'}{
charset}{
'}'}, Transfer-Encoding: {
transfer_encoding}") # 打印当前部分信息
# 使用 get_payload(decode=True) 自动解码
payload_bytes = part.get_payload(decode=True) # 获取解码后的有效载荷字节
if payload_bytes: # 如果有效载荷不为空
if content_type.startswith('text/'): # 如果是文本类型
try: # 尝试解码文本内容
decoded_text = payload_bytes.decode(charset if charset else 'utf-8', errors='replace') # 解码文本内容
print(f" 文本内容摘要:\n {
decoded_text[:100]}...") # 打印文本内容摘要
except Exception as e: # 捕获解码错误
print(f" 无法解码文本内容 (charset: {
charset}): {
e}") # 打印解码错误信息
else: # 如果是非文本类型 (如附件)
filename = part.get_filename() # 获取文件名
print(f" 二进制内容 (可能是附件 '{
filename}'), 大小: {
len(payload_bytes)} 字节。") # 打印二进制内容信息
else: # 如果有效载荷为空
print(" 此部分没有有效载荷。") # 打印没有有效载荷信息
else: # 如果是单部分邮件
content_type = msg.get_content_type() # 获取内容类型
charset = msg.get_content_charset() # 获取字符集
transfer_encoding = msg.get('Content-Transfer-Encoding', '7bit').lower() # 获取传输编码
print(f" 单部分邮件: Content-Type: {
content_type}, Charset: {
'{'}{
charset}{
'}'}, Transfer-Encoding: {
transfer_encoding}") # 打印单部分邮件信息
payload_bytes = msg.get_payload(decode=True) # 获取解码后的邮件载荷字节
if payload_bytes and content_type.startswith('text/'): # 如果载荷不为空且是文本类型
try: # 尝试解码
decoded_text = payload_bytes.decode(charset if charset else 'utf-8', errors='replace') # 解码文本内容
print(f" 文本内容摘要:\n {
decoded_text[:100]}...") # 打印文本内容摘要
except Exception as e: # 捕获解码错误
print(f" 无法解码文本内容 (charset: {
charset}): {
e}") # 打印解码错误信息
elif payload_bytes: # 如果载荷不为空但不是文本类型
print(f" 二进制内容,大小: {
len(payload_bytes)} 字节。") # 打印二进制内容大小
else: # 如果载荷为空
print(" 载荷为空。") # 打印载荷为空信息
except Exception as e: # 捕获其他所有异常
print(f"处理邮件编码时发生错误: {
e}") # 打印处理错误信息
# 示例原始邮件字节串 (包含复杂头部编码和正文编码)
# 模拟主题和发件人包含中文的邮件
sample_raw_email_3 = b"""From: =?utf-8?B?5pys5omA?= <test.user@example.com>
To: recipient@example.com
Subject: =?utf-8?Q?=E6=B5=8B=E8=AF=95=E4=B8=BB=E9=A2=98?= with =?gbk?B?xczM?=
MIME-Version: 1.0
Content-Type: text/plain; charset="gbk"
Content-Transfer-Encoding: quoted-printable
Hello, =E4=BD=A0=E5=A5=BD=EF=BC=81
This is a test email with some Chinese characters.
"""
# 模拟一个带Base64编码附件的邮件
sample_raw_email_4 = b"""From: Attachment Sender <attach@example.com>
To: attach_recip@example.com
Subject: Email with an attachment
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_Boundary_12345"
------=_Boundary_12345
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
This is the main body of the email.
=E8=BF=99=E6=98=AF=E9=82=AE=E4=BB=B6=E4=B8=BB=E4=BD=93=E3=80=82
------=_Boundary_12345
Content-Type: application/octet-stream; name="test.txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test.txt"
SGVsbG8gV29ybGQhDQpUaGlzIGlzIGEgdGVzdCBmaWxlLg==
------=_Boundary_12345--
"""
# if __name__ == "__main__": # 当脚本直接运行时
# print("--- 演示邮件编码处理 (复杂头部和正文) ---") # 打印演示信息
# handle_email_encodings(sample_raw_email_3) # 处理第三个示例邮件
#
# print("\n" + "="*50 + "\n") # 分隔符
#
# print("--- 演示邮件编码处理 (Base64附件) ---") # 打印演示信息
# handle_email_encodings(sample_raw_email_4) # 处理第四个示例邮件
这段代码深入演示了email
库如何处理邮件中的各种编码问题。它详细展示了如何使用email.header.decode_header()
来解码包含特殊字符的邮件头部(如主题、发件人名称),以及如何使用part.get_payload(decode=True)
来自动解码邮件正文(纯文本、HTML)和附件的传输编码(如quoted-printable
, base64
),并进行字符集解码。这是正确解析和显示邮件内容的关键。
7.2.3 遍历多部分邮件:walk()
方法的强大功能
对于多部分邮件(multipart/*
类型),邮件内容被组织成一个树形结构。Message
对象的walk()
方法是一个生成器,它以深度优先的顺序遍历邮件的所有部分(包括嵌套的多部分容器),返回每个MIME部分的Message
对象。
这使得你可以轻松地找到纯文本正文、HTML正文、附件、内联图片等所有内容,而无需手动处理multipart
边界和嵌套结构。
import email # 导入email库
from email.message import Message # 导入Message类
from email.header import decode_header # 导入decode_header函数
import os # 导入os库,用于文件路径操作
# 定义附件保存目录
ATTACHMENT_SAVE_DIR = "downloaded_attachments" # 定义附件保存目录
if not os.path.exists(ATTACHMENT_SAVE_DIR): # 如果附件保存目录不存在
os.makedirs(ATTACHMENT_SAVE_DIR) # 创建附件保存目录
def process_multipart_email(raw_email_bytes: bytes):
"""
演示如何使用 walk() 方法遍历多部分邮件,提取纯文本、HTML和附件。
参数:
raw_email_bytes (bytes): 包含多部分内容的原始邮件字节串。
"""
print("\n--- 遍历多部分邮件并提取内容 ---") # 打印信息
try: # 尝试解析邮件
msg = email.message_from_bytes(raw_email_bytes) # 将原始字节数据解析成一个邮件对象
print("邮件解析成功。") # 打印解析成功信息
if not msg.is_multipart(): # 如果不是多部分邮件
print("这不是一封多部分邮件,跳过 walk() 演示。") # 打印跳过演示信息
return # 退出函数
plain_text_bodies = [] # 初始化纯文本正文列表
html_bodies = [] # 初始化HTML正文列表
attachments = [] # 初始化附件列表
inline_images = [] # 初始化内联图片列表
print("\n--- 遍历邮件各部分 ---") # 打印遍历邮件各部分标题
# msg.walk() 返回一个迭代器,以深度优先的顺序遍历所有MIME部分。
for part_num, part in enumerate(msg.walk()): # 遍历邮件的所有MIME部分,获取部分编号和部分对象
content_type = part.get_content_type() # 获取当前部分的内容类型
filename = part.get_filename() # 获取当前部分的文件名 (如果存在)
charset = part.get_content_charset() # 获取当前部分的字符集 (如果存在)
content_disposition = part.get('Content-Disposition') # 获取当前部分的Content-Disposition
content_id = part.get('Content-ID') # 获取当前部分的Content-ID
print(f"\n Part {
part_num}: Content-Type: {
content_type}, Filename: {
filename}, Disposition: {
content_disposition}, Content-ID: {
content_id}") # 打印当前部分详细信息
# 跳过多部分容器自身,我们只关心“叶子”部分
if part.is_multipart(): # 如果当前部分是多部分容器
print(" 这是一个多部分容器,继续遍历其子部分。") # 打印容器信息
continue # 跳过当前部分,处理其子部分
# 获取解码后的载荷
payload_bytes = part.get_payload(decode=True) # 获取解码后的有效载荷字节
if not payload_bytes: # 如果载荷为空
print(" 此部分没有有效载荷。") # 打印没有有效载荷信息
continue # 继续下一部分
# 提取文本内容
if content_type == 'text/plain': # 如果是纯文本类型
try: # 尝试解码
plain_text_bodies.append(payload_bytes.decode(charset if charset else 'utf-8', errors='replace')) # 解码并添加到纯文本列表
print(" 已提取纯文本内容。") # 打印提取信息
except Exception as e: # 捕获解码错误
print(f" 解码纯文本内容失败: {
e}") # 打印解码失败信息
elif content_type == 'text/html': # 如果是HTML类型
try: # 尝试解码
html_bodies.append(payload_bytes.decode(charset if charset else 'utf-8', errors='replace')) # 解码并添加到HTML列表
print(" 已提取HTML内容。") # 打印提取信息
except Exception as e: # 捕获解码错误
print(f" 解码HTML内容失败: {
e}") # 打印解码失败信息
# 提取附件和内联图片
elif filename: # 如果有文件名,通常是附件或内联内容
if content_disposition and content_disposition.lower().startswith('attachment'): # 如果是附件
attachments.append({
# 添加到附件列表
'filename': filename, # 文件名
'content_type': content_type, # 内容类型
'payload': payload_bytes # 载荷
})
print(f" 检测到附件: '{
filename}' ({
content_type}, 大小: {
len(payload_bytes)} 字节)。") # 打印附件信息
# 保存附件到本地
attachment_path = os.path.join(ATTACHMENT_SAVE_DIR, filename) # 构造附件本地路径
with open(attachment_path, 'wb') as f: # 以二进制写入模式打开文件
f.write(payload_bytes) # 写入附件内容
print(f" 附件已保存到: {
attachment_path}") # 打印保存路径
elif content_disposition and content_disposition.lower().startswith('inline'): # 如果是内联内容 (如 HTML 引用的图片)
inline_images.append({
# 添加到内联图片列表
'filename': filename, # 文件名
'content_type': content_type, # 内容类型
'content_id': content_id, # Content-ID
'payload': payload_bytes # 载荷
})
print(f" 检测到内联图片: '{
filename}' (Content-ID: {
content_id}, 大小: {
len(payload_bytes)} 字节)。") # 打印内联图片信息
# 内联图片通常根据 Content-ID 在 HTML 中引用,可以保存到临时目录以便后续渲染HTML
inline_image_path = os.path.join(ATTACHMENT_SAVE_DIR, filename) # 构造内联图片本地路径
with open(inline_image_path, 'wb') as f: # 以二进制写入模式打开文件
f.write(payload_bytes) # 写入内联图片内容
print(f" 内联图片已保存到: {
inline_image_path}") # 打印保存路径
else: # 其他有文件名的部分
print(f" 检测到其他有文件名的内容: '{
filename}' ({
content_type}, 大小: {
len(payload_bytes)} 字节)。") # 打印其他内容信息
elif content_type == 'application/octet-stream' and not filename: # 可能是未指定文件名的通用二进制附件
attachments.append({
# 添加到附件列表
'filename': f"unnamed_attachment_{
part_num}", # 生成一个默认文件名
'content_type': content_type, # 内容类型
'payload': payload_bytes # 载荷
})
print(f" 检测到无文件名附件 ({
content_type}, 大小: {
len(payload_bytes)} 字节)。") # 打印无文件名附件信息
print("\n--- 提取内容总结 ---") # 打印总结标题
if plain_text_bodies: # 如果有纯文本正文
print("\n纯文本正文:") # 打印纯文本正文标题
for i, body in enumerate(plain_text_bodies): # 遍历纯文本正文
print(f" Part {
i+1} 摘要:\n{
body[:300]}...") # 打印纯文本正文摘要
if html_bodies: # 如果有HTML正文
print("\nHTML正文:") # 打印HTML正文标题
for i, body in enumerate(html_bodies): # 遍历HTML正文
print(f" Part {
i+1} 摘要:\n{
body[:300]}...") # 打印HTML正文摘要
if attachments: # 如果有附件
print("\n附件:") # 打印附件标题
for i, attach in enumerate(attachments): # 遍历附件
print(f" {
i+1}. 文件名: {
attach['filename']}, 类型: {
attach['content_type']}, 大小: {
len(attach['payload'])} 字节") # 打印附件信息
if inline_images: # 如果有内联图片
print("\n内联图片:") # 打印内联图片标题
for i, img in enumerate(inline_images): # 遍历内联图片
print(f" {
i+1}. 文件名: {
img['filename']}, ID: {
img['content_id']}, 类型: {
img['content_type']}, 大小: {
len(img['payload'])} 字节") # 打印内联图片信息
except Exception as e: # 捕获其他所有异常
print(f"处理多部分邮件时发生错误: {
e}") # 打印处理错误信息
# 示例多部分邮件字节串 (包含纯文本、HTML和附件)
sample_raw_email_5 = b"""From: Complex Sender <complex@example.com>
To: Complex Recipient <complex_recip@example.com>
Subject: Multipart Email with Text, HTML, and Attachment - =?utf-8?B?5Lit5YaF?=
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="BOUNDARY_MIXED_001"
--BOUNDARY_MIXED_001
Content-Type: multipart/alternative; boundary="BOUNDARY_ALTERNATIVE_002"
--BOUNDARY_ALTERNATIVE_002
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Hello, this is the plain text version of the email.
This is a test.
=E8=BF=99=E6=98=AF=E7=BA=AF=E6=96=87=E6=9C=AC=E5=86=85=E5=AE=B9=E3=80=82
--BOUNDARY_ALTERNATIVE_002
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64
PGh0bWw+PGJvZHk+PGgxPkhlbGxvIEhUTUwhPC9oMT48c1Vvbj5UaGlzIGlzIDxiPmh0bWw8
L2I+IHZlcnNpb24gb2YgdGhlIGVtYWlsLjxzV28tY3o+R2hpc2lzIGlzIGEgdGVzdC48L1N3
by1jeD48c1dvLWN6PuS4reWGhOWPuOW/q+S9k+eOpeS6u+aWhy48L1N3by1jeD48L3NVo24+
PC9ib2R5PjwvaHRtbD4=
--BOUNDARY_ALTERNATIVE_002--
--BOUNDARY_MIXED_001
Content-Type: application/pdf; name="document.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="document.pdf"
JVBERi0xLjQKJcOgw7HDqwoKMSAwIG9iagpbL1BERiAvVGV4dF0KPj4KZW5kb2JqCjIgMCBj
YXQ8PC9UeXBlL1BhZ2UvUGFyZW50IDMgMCBSL1Jlc291cmNlczw8L0ZvbnQ8PC9GMTEgNSAw
IFI+Pi9Qcm9jU2V0Wy9QREYvVGV4dF1+Pi9NZWRpYUJveFswIDAgNTk1IDg0Ml0+Pi9Db250
ZW50cyA0IDAgUj4+CmVuZG9iagooJSAqKioqKioqKioqKioqKioqKiAqCiAgICAgICAgICAg
ICAgICAgICAgICAgKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg==
--BOUNDARY_MIXED_001
Content-Type: image/png; name="inline_image.png"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="inline_image.png"
Content-ID: <image1@example.com>
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=
--BOUNDARY_MIXED_001--
"""
# if __name__ == "__main__": # 当脚本直接运行时
# process_multipart_email(sample_raw_email_5) # 处理多部分邮件
# print(f"\n所有附件和内联图片已保存到: {ATTACHMENT_SAVE_DIR}") # 打印保存路径
这段代码是邮件解析的核心部分,它详细演示了如何使用email.message.Message.walk()
方法来递归遍历多部分邮件的树形结构。对于每个部分,它会识别其Content-Type
、filename
、Content-Disposition
和Content-ID
,然后自动解码其载荷并进行分类处理:提取纯文本正文、HTML正文,以及识别和保存附件和内联图片。这为构建能够完全处理复杂邮件内容的应用程序提供了基础。
7.2.4 提取纯文本、HTML 内容
在处理邮件时,我们通常最关心的是邮件正文。MIME邮件可能同时包含纯文本和HTML版本的正文(multipart/alternative
)。为了获得最佳的用户体验,我们需要智能地选择和提取这些内容。
- 提取策略:
- 遍历
msg.walk()
。 - 对于
text/plain
部分:提取其内容。如果存在多个纯文本部分(例如,一个multipart/alternative
内部,或在multipart/mixed
中),通常取第一个。 - 对于
text/html
部分:提取其内容。同样,如果存在多个HTML部分,通常取第一个。 - 优先级:如果邮件同时有纯文本和HTML版本(在
multipart/alternative
中),现代客户端通常会优先显示HTML版本。你的解析逻辑也应遵循这一原则,但提供纯文本作为HTML无法渲染时的备选。
- 遍历
import email # 导入email库
from email.message import Message # 导入Message类
from email.header import decode_header # 导入decode_header函数
def extract_text_html_bodies(raw_email_bytes: bytes):
"""
演示如何从邮件中提取纯文本和HTML正文。
参数:
raw_email_bytes (bytes): 待解析的原始邮件字节串。
"""
print("\n--- 提取纯文本和HTML正文 ---") # 打印信息
try: # 尝试解析邮件
msg = email.message_from_bytes(raw_email_bytes) # 将原始字节数据解析成一个邮件对象
print("邮件解析成功。") # 打印解析成功信息
plain_text_content = "" # 初始化纯文本内容
html_content = "" # 初始化HTML内容
# 遍历邮件的所有部分
for part in msg.walk(): # 遍历邮件的所有MIME部分
# 跳过最外层的 multipart/alternative 容器,直接处理其子部分
if part.is_multipart() and part.get_content_maintype() == 'multipart' and \
part.get_content_subtype() == 'alternative': # 如果是 multipart/alternative 容器
continue # 跳过,处理其子部分
content_type = part.get_content_type() # 获取内容类型
charset = part.get_content_charset() # 获取字符集
# 获取解码后的载荷
payload_bytes = part.get_payload(decode=True) # 获取解码后的有效载荷字节
if not payload_bytes: # 如果载荷为空
continue # 继续下一部分
if content_type == 'text/plain': # 如果是纯文本
if not plain_text_content: # 如果纯文本内容尚未设置 (优先取第一个)
try: # 尝试解码
plain_text_content = payload_bytes.decode(charset if charset else 'utf-8', errors='replace') # 解码纯文本内容
print(" 已提取纯文本内容。") # 打印提取信息
except Exception as e: # 捕获解码错误
print(f" 解码纯文本内容失败: {
e}") # 打印解码失败信息
elif content_type == 'text/html': # 如果是HTML
if not html_content: # 如果HTML内容尚未设置 (优先取第一个)
try: # 尝试解码
html_content = payload_bytes.decode(charset if charset else 'utf-8', errors='replace') # 解码HTML内容
print(" 已提取HTML内容。") # 打印提取信息
except Exception as e: # 捕获解码错误
print(f" 解码HTML内容失败: {
e}") # 打印解码失败信息
# 如果同时找到了纯文本和HTML,并且它们是 alternative 类型,通常只需要一个。
# 如果是 mixed 类型,则可能需要所有文本部分。
# 这里的逻辑是获取第一个找到的纯文本和HTML。
print("\n--- 提取结果 ---") # 打印提取结果标题
if plain_text_content: # 如果有纯文本内容
print("纯文本正文 (摘要):") # 打印纯文本正文标题
print(plain_text_content[:500] + "..." if len(plain_text_content) > 500 else plain_text_content) # 打印纯文本正文摘要
else: # 如果没有纯文本内容
print("未找到纯文本正文。") # 打印未找到信息
if html_content: # 如果有HTML内容
print("\nHTML正文 (摘要):") # 打印HTML正文标题
print(html_content[:500] + "..." if len(html_content) > 500 else html_content) # 打印HTML正文摘要
else: # 如果没有HTML内容
print("未找到HTML正文。") # 打印未找到信息
except Exception as e: # 捕获其他所有异常
print(f"提取邮件正文时发生错误: {
e}") # 打印提取错误信息
# 示例邮件 (包含 multipart/alternative,纯文本和HTML版本)
sample_raw_email_6 = b"""From: MultiPart Test <multi@example.com>
To: recipient@example.com
Subject: =?utf-8?B?5qGI5rWL5oiz5Lu2?=
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="---=_Alternative_001"
---=_Alternative_001
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Hello, this is the plain text version.
You are seeing this because your client may not support HTML.
=E8=BF=99=E6=98=AF=E7=BA=AF=E6=96=87=E6=9C=AC=E7=89=88=E6=9C=AC=E3=80=82
---=_Alternative_001
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64
PGh0bWw+PGJvZHk+PHAxPkNoZWNrIG91dCB0aGlzIDxiPkhUTUw8L2I+IHZlcnNpb24uPC9w
PjxwPuS4reWGhOWPuOW/q+S9k+eOpeS6u+aWhy48L3A+PC9ib2R5PjwvaHRtbD4=
---=_Alternative_001--
"""
# 示例邮件 (只有纯文本)
sample_raw_email_7 = b"""From: Plain Text Only <plain@example.com>
To: recipient@example.com
Subject: Just a plain text email
MIME-Version: 1.0
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
This is a simple email without any HTML part.
"""
# if __name__ == "__main__": # 当脚本直接运行时
# print("--- 演示提取邮件正文 (含纯文本和HTML) ---") # 打印演示信息
# extract_text_html_bodies(sample_raw_email_6) # 提取邮件正文 (含纯文本和HTML)
#
# print("\n" + "="*50 + "\n") # 分隔符
#
# print("--- 演示提取邮件正文 (仅纯文本) ---") # 打印演示信息
# extract_text_html_bodies(sample_raw_email_7) # 提取邮件正文 (仅纯文本)
这段代码演示了如何从multipart/alternative
类型的邮件中智能地提取纯文本和HTML正文。它使用msg.walk()
遍历邮件部分,并根据Content-Type
来识别和解码文本内容。这种策略确保了无论邮件以何种MIME结构组织,都能准确地获取到可读的正文内容,并支持处理多种字符集。
7.2.5 提取附件:文件名、内容类型、保存
邮件附件是邮件内容的重要组成部分。email
库使得识别和提取附件变得相对简单。
- 识别附件:
- 在
msg.walk()
遍历过程中,对于非multipart
的部分:- 检查
part.get_filename()
是否返回非None
值。这通常表示一个附件或内联内容。 - 或者检查
part.get_content_maintype()
是否不是'text'
,并且part.get('Content-Disposition')
的类型是'attachment'
。
- 检查
- 在
- 获取附件内容:
- 使用
part.get_payload(decode=True)
获取附件的原始二进制数据。
- 使用
- 保存附件:
- 将获取到的二进制数据写入本地文件。
- 确保文件名安全,防止路径遍历攻击。
import email # 导入email库
from email.message import Message # 导入Message类
import os # 导入os库
import base64 # 导入base64库,用于模拟附件内容
# 假设这个目录已存在,或者你会在程序开始时创建它
ATTACHMENT_DIR_FOR_EXTRACTION = "extracted_attachments" # 定义附件提取目录
if not os.path.exists(ATTACHMENT_DIR_FOR_EXTRACTION): # 如果附件提取目录不存在
os.makedirs(ATTACHMENT_DIR_FOR_EXTRACTION) # 创建附件提取目录
print(f"已创建附件提取目录: {
ATTACHMENT_DIR_FOR_EXTRACTION}") # 打印创建目录信息
def extract_attachments_from_email(raw_email_bytes: bytes):
"""
演示如何从邮件中识别并提取附件,并保存到本地文件。
参数:
raw_email_bytes (bytes): 包含附件的原始邮件字节串。
"""
print("\n--- 提取邮件附件 ---") # 打印信息
try: # 尝试解析邮件
msg = email.message_from_bytes(raw_email_bytes) # 将原始字节数据解析成一个邮件对象
print("邮件解析成功。") # 打印解析成功信息
attachments_found = [] # 初始化找到的附件列表
for part_num, part in enumerate(msg.walk()): # 遍历邮件的所有MIME部分
# 跳过多部分容器
if part.is_multipart(): # 如果是多部分容器
continue # 跳过,处理其子部分
# 检查 Content-Disposition
content_disposition = part.get('Content-Disposition') # 获取Content-Disposition
if content_disposition: # 如果Content-Disposition存在
disposition_type = content_disposition.split(';')[0].strip().lower() # 获取Content-Disposition类型
if disposition_type == 'attachment': # 如果是附件类型
filename = part.get_filename() # 获取文件名
if filename: # 如果文件名存在
# 确保文件名安全,避免路径遍历攻击等
safe_filename = os.path.basename(filename) # 只取文件名部分,去除路径
payload_bytes = part.get_payload(decode=True) # 获取解码后的有效载荷字节
if payload_bytes: # 如果载荷不为空
attachment_path = os.path.join(ATTACHMENT_DIR_FOR_EXTRACTION, safe_filename) # 构造附件本地路径
with open(attachment_path, 'wb') as f: # 以二进制写入模式打开文件
f.write(payload_bytes) # 写入附件内容
attachments_found.append(attachment_path) # 将附件路径添加到列表
print(f" 已提取附件: '{
safe_filename}' 到 {
attachment_path} (类型: {
part.get_content_type()})") # 打印提取信息
else: # 如果载荷为空
print(f" 附件 '{
filename}' 没有内容。") # 打印没有内容信息
else: # 如果文件名不存在
print(f" 发现一个类型为 'attachment' 但没有文件名的附件 (Part {
part_num}).") # 打印无文件名附件信息
# 另一种识别附件的常见方式:如果不是文本类型且有文件名
elif part.get_content_maintype() != 'text' and part.get_filename(): # 如果不是文本类型且有文件名
filename = part.get_filename() # 获取文件名
safe_filename = os.path.basename(filename) # 确保文件名安全
payload_bytes = part.get_payload(decode=True) # 获取解码后的有效载荷字节
if payload_bytes: # 如果载荷不为空
attachment_path = os.path.join(ATTACHMENT_DIR_FOR_EXTRACTION, safe_filename) # 构造附件本地路径
with open(attachment_path, 'wb') as f: # 以二进制写入模式打开文件
f.write(payload_bytes) # 写入附件内容
attachments_found.append(attachment_path) # 将附件路径添加到列表
print(f" 已提取非文本内容作为附件: '{
safe_filename}' 到 {
attachment_path} (类型: {
part.get_content_type()})") # 打印提取信息
else: # 如果载荷为空
print(f" 非文本内容 '{
filename}' 没有内容。") # 打印没有内容信息
if attachments_found: # 如果找到附件
print(f"\n成功提取 {
len(attachments_found)} 个附件。") # 打印附件数量
for path in attachments_found: # 遍历附件路径
print(f" - {
path}") # 打印附件路径
else: # 如果没有找到附件
print("\n此邮件未包含任何附件。") # 打印未包含附件信息
except Exception as e: # 捕获其他所有异常
print(f"提取附件时发生错误: {
e}") # 打印提取错误信息
# 示例邮件 (包含一个PDF附件和一个文本附件)
sample_raw_email_8 = b"""From: Attachment Demo <demo@example.com>
To: recipient@example.com
Subject: Test Email with Multiple Attachments
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="---=_Attachment_Demo_1"
---=_Attachment_Demo_1
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Hello, this email contains some attachments.
=E8=BF=99=E5=B0=81=E9=82=AE=E4=BB=B6=E5=8C=85=E5=90=AB=E9=99=84=E4=BB=B6=E3=80=82
---=_Attachment_Demo_1
Content-Type: application/pdf; name="sample.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="sample.pdf"
JVBERi0xLjQKJcOgw7HDqwoKMSAwIG9iagpbL1BERiAvVGV4dF0KPj4KZW5kb2JqCjIgMCBj
Y3Q8PC9UeXBlL1BhZ2UvUGFyZW50IDMgMCBSL1Jlc291cmNlczw8L0ZvbnQ8PC9GMTEgNSAw
IFI+Pi9Qcm9jU2V0Wy9QREYvVGV4dF1+Pi9NZWRpYUJveFswIDAgNTk1IDg0Ml0+Pi9Db250
ZW50cyA0IDAgUj4+CmVuZG9iagooJSAqKioqKioqKioqKioqKioqKiAqCiAgICAgICAgICAg
ICAgICAgICAgICAgKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKg==
---=_Attachment_Demo_1
Content-Type: text/plain; name="readme.txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="readme.txt"
VGhpcyBpcyBhIHJlYWRtZSBmaWxlLg0KV2VsY29tZSB0byB0aGUgYXR0YWNobWVudCBkZW1v
Lg==
---=_Attachment_Demo_1--
"""
# if __name__ == "__main__": # 当脚本直接运行时
# extract_attachments_from_email(sample_raw_email_8) # 提取附件
# print(f"所有提取的附件已保存到目录: {ATTACHMENT_DIR_FOR_EXTRACTION}") # 打印保存路径
这段代码演示了如何从邮件中识别并提取附件。它使用part.get('Content-Disposition')
来判断一个MIME部分是否为附件,并通过part.get_filename()
获取文件名。然后,它使用part.get_payload(decode=True)
获取附件的原始二进制数据,并将其保存到本地文件。这种方法确保了附件能够被正确地识别、提取和存储。
7.2.6 处理内联图片和引用
内联图片是HTML邮件中直接显示在正文中的图片,而不是作为单独的附件。它们通常通过Content-ID
在HTML中引用。
- 识别内联内容:
part.get('Content-Disposition')
的类型为'inline'
。part.get('Content-ID')
返回非None
值。
- 获取内容:与附件相同,使用
part.get_payload(decode=True)
。 - 关联到HTML:
- 在解析HTML正文时,查找
<img>
标签的src
属性,如果其值以cid:
开头,则表示它引用了具有相应Content-ID
的内联内容。 - 你可以将内联图片保存到临时文件,然后修改HTML内容中的
src
属性,使其指向本地文件路径,以便在本地浏览器中正确显示HTML邮件。
- 在解析HTML正文时,查找
import email # 导入email库
from email.message import Message # 导入Message类
import os # 导入os库
import re # 导入re模块,用于正则表达式
import base64 # 导入base64库
# 定义内联内容保存目录
INLINE_CONTENT_DIR = "inline_content_cache" # 定义内联内容缓存目录
if not os.path.exists(INLINE_CONTENT_DIR): # 如果内联内容缓存目录不存在
os.makedirs(INLINE_CONTENT_DIR) # 创建内联内容缓存目录
print(f"已创建内联内容缓存目录: {
INLINE_CONTENT_DIR}") # 打印创建目录信息
def process_email_with_inline_content(raw_email_bytes: bytes):
"""
演示如何处理包含内联图片等引用的HTML邮件。
它会提取HTML内容,保存内联图片,并尝试修改HTML以引用本地图片。
参数:
raw_email_bytes (bytes): 包含内联内容的原始邮件字节串。
"""
print("\n--- 处理带内联内容的邮件 ---") # 打印信息
try: # 尝试解析邮件
msg = email.message_from_bytes(raw_email_bytes) # 将原始字节数据解析成一个邮件对象
print("邮件解析成功。") # 打印解析成功信息
html_content = "" # 初始化HTML内容
inline_items = {
} # 存储内联内容的字典,键为Content-ID,值为本地路径
for part_num, part in enumerate(msg.walk()): # 遍历邮件的所有MIME部分
content_type = part.get_content_type() # 获取内容类型
content_disposition = part.get('Content-Disposition') # 获取Content-Disposition
content_id = part.get('Content-ID') # 获取Content-ID
filename = part.get_filename() # 获取文件名
# 处理 HTML 正文
if content_type == 'text/html': # 如果是HTML类型
charset = part.get_content_charset() # 获取字符集
payload_bytes = part.get_payload(decode=True) # 获取解码后的有效载荷字节
if payload_bytes: # 如果载荷不为空
try: # 尝试解码HTML
html_content = payload_bytes.decode(charset if charset else 'utf-8', errors='replace') # 解码HTML内容
print(f" 已提取HTML正文 (Part {
part_num}).") # 打印提取信息
except Exception as e: # 捕获解码错误
print(f" 解码HTML内容失败: {
e}") # 打印解码失败信息
# 处理内联内容 (通常是图片,通过 Content-ID 引用)
elif content_disposition and content_disposition.lower().startswith('inline') and content_id: # 如果是内联且有Content-ID
# 确保 Content-ID 格式是 <id@domain>,去除 <>
clean_content_id = content_id.strip('<>').lower() # 清理Content-ID,去除尖括号并转为小写
filename = part.get_filename() # 获取文件名
if not filename: # 如果没有文件名,尝试从Content-ID中生成
# 尝试从 Content-Type 推断文件扩展名
ext = content_type.split('/')[-1] # 从Content-Type获取文件扩展名
filename = f"inline_{
clean_content_id.replace('@', '_').replace('.', '_')}.{
ext}" # 生成文件名
safe_filename = os.path.basename(filename) # 确保文件名安全
payload_bytes = part.get_payload(decode=True) # 获取解码后的有效载荷字节
if payload_bytes: # 如果载荷不为空
local_path = os.path.join(INLINE_CONTENT_DIR, safe_filename) # 构造本地路径
with open(local_path, 'wb') as f: # 以二进制写入模式打开文件
f.write(payload_bytes) # 写入内联内容
inline_items[clean_content_id] = local_path # 将Content-ID和本地路径存入字典
print(f" 已提取内联内容: '{
safe_filename}' (Content-ID: {
content_id}) 到 {
local_path}") # 打印提取信息
else: # 如果载荷为空
print(f" 内联内容 '{
filename}' (Content-ID: {
content_id}) 没有内容。") # 打印没有内容信息
# 也可以处理其他附件,但这里主要聚焦内联内容
# 5. 尝试在HTML内容中替换 cid: 引用为本地文件路径
if html_content and inline_items: # 如果有HTML内容和内联项目
print("\n--- 尝试修改HTML以引用本地内联图片 ---") # 打印修改HTML信息
modified_html_content = html_content # 复制HTML内容
for cid, local_path in inline_items.items(): # 遍历内联项目
# 构造 cid: 引用模式
# 例如:<img src="cid:image1@example.com">
# (?i) 忽略大小写
# re.escape(cid) 确保 cid 中的特殊字符被正确转义
# re.sub(pattern, replacement, string)
# 替换 src="cid:..." 为 src="file://absolute/path/to/local_image.png"
# 注意:file:// 协议在某些浏览器环境中可能受限,
# 更稳定的方式是搭建本地HTTP服务器来提供这些文件。
# 此处仅为演示概念。
cid_pattern = r'src=["\']cid:' + re.escape(cid) + r'["\']' # 构造CID引用模式
# Windows路径可能需要特殊处理,斜杠方向和协议头
# file:// 的路径是 URI 格式,通常使用正斜杠
# local_file_uri = pathlib.Path(local_path).as_uri() # Python 3.4+
local_file_uri = local_path.replace('\\', '/') # 将反斜杠替换为正斜杠
# 替换 HTML 中的 cid 引用
modified_html_content = re.sub(cid_pattern, f'src="{
local_file_uri}"', modified_html_content, flags=re.IGNORECASE) # 替换HTML中的CID引用
print(f" 替换 Content-ID '{
cid}' 为本地路径 '{
local_file_uri}'。") # 打印替换信息
# 可以将修改后的HTML保存到文件,以便在浏览器中查看
html_output_path = os.path.join(INLINE_CONTENT_DIR, "modified_email.html") # 构造HTML输出路径
with open(html_output_path, 'w', encoding='utf-8') as f: # 以写入模式打开文件,编码UTF-8
f.write(modified_html_content) # 写入修改后的HTML内容
print(f"\n修改后的HTML已保存到: {
html_output_path}") # 打印保存路径
print("请用浏览器打开此HTML文件,检查内联图片是否正确显示。") # 打印提示信息
else: # 如果没有HTML内容或内联项目
print("邮件没有HTML内容或内联图片可供处理。") # 打印没有HTML内容或内联图片信息
except Exception as e: # 捕获其他所有异常
print(f"处理带内联内容的邮件时发生错误: {
e}") # 打印处理错误信息
# 示例邮件 (包含 HTML 和一个内联图片)
sample_raw_email_9 = b"""From: Inline Image Demo <image@example.com>
To: recipient@example.com
Subject: Email with an Inline Image
MIME-Version: 1.0
Content-Type: multipart/related; boundary="---=_Related_Image_001"; type="text/html"; start="<htmlpart1@example.com>"
---=_Related_Image_001
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Content-ID: <htmlpart1@example.com>
<html><body>
<h1>Hello from HTML!</h1>
<p>Here is an inline image:</p>
<img src="cid:inline.image.1@example.com">
<p>And some Chinese text: =E4=BD=A0=E5=A5=BD=EF=BC=81</p>
</body></html>
---=_Related_Image_001
Content-Type: image/jpeg; name="my_inline_photo.jpeg"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="my_inline_photo.jpeg"
Content-ID: <inline.image.1@example.com>
/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgEBAgICAwICAwQDAwMEBAQE
BQQEBAQFBQQEBAQHBwYGBgYGBgcGBwcHBwcHBwcHBw//2wBDAwsICAgICAgICAgICAgICAgICAgI
CAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg//8AAEQgAAQABAAIB
EQA/8QAFQABAQAAAAAAAAAAAAAAAAAAAAD/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAA
AAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAERAAERAAAL0AAAAAD//2Q=
---=_Related_Image_001--
"""
# if __name__ == "__main__": # 当脚本直接运行时
# process_email_with_inline_content(sample_raw_email_9) # 处理内联图片邮件
# print(f"\n所有内联内容已保存到: {INLINE_CONTENT_DIR}") # 打印保存路径
这段代码详细演示了如何处理包含内联图片等引用的HTML邮件。它首先提取HTML正文和内联内容(通过Content-ID
和Content-Disposition: inline
识别),并将内联内容保存到本地文件。然后,它展示了如何修改HTML内容,将cid:
引用替换为指向本地文件路径的src
属性,从而使得在本地查看修改后的HTML时内联图片能够正确显示。这对于构建完整的邮件查看器或渲染HTML邮件非常实用。
7.3 邮件构建:创建 MIME 消息
除了解析邮件,email
库的另一个强大功能是从头开始构建符合MIME标准的复杂邮件,这对于发送富文本邮件、带附件邮件或自动化报告邮件非常有用。
7.3.1 email.mime.text.MIMEText
:创建纯文本和HTML邮件
MIMEText
类是创建简单文本内容的邮件或MIME部分的基石。
MIMEText(_text, _subtype='plain', _charset='utf-8')
:_text
:邮件正文内容(字符串)。_subtype
:文本的子类型,可以是'plain'
(纯文本,默认)或'html'
。_charset
:文本的字符集,默认为'utf-8'
。email
库会自动处理内容的编码和Content-Transfer-Encoding
的设置。
import email # 导入email库
from email.mime.text import MIMEText # 导入MIMEText类
from email.header import Header # 导入Header类
from email.utils import formatdate # 导入formatdate函数
def create_simple_text_emails():
"""
演示如何使用 MIMEText 创建纯文本和HTML邮件。
"""
print("\n--- 创建简单的文本邮件 ---") # 打印信息
# 1. 创建纯文本邮件
print("\n1. 创建纯文本邮件...") # 打印创建信息
plain_text_msg = MIMEText('这是一封纯文本测试邮件。\nHello, world!', 'plain', 'utf-8') # 创建一个纯文本邮件对象
# 设置邮件头部 (必须是字符串,不能直接是bytes)
plain_text_msg['From'] = Header('发件人 <sender@example.com>', 'utf-8') # 设置发件人,支持中文
plain_text_msg['To'] = Header('收件人 <recipient@example.com>', 'utf-8') # 设置收件人,支持中文
plain_text_msg['Subject'] = Header('纯文本测试邮件 - Python', 'utf-8') # 设置主题,支持中文
plain_text_msg['Date'] = formatdate(localtime=True) # 设置日期,使用本地时间
print("纯文本邮件构建成功。") # 打印构建成功信息
# 可以通过 msg.as_string() 或 msg.as_bytes() 获取邮件的原始字符串或字节内容,用于发送。
# print("\n原始纯文本邮件内容:\n", plain_text_msg.as_string()) # 打印原始邮件内容
# 2. 创建 HTML 邮件
print("\n2. 创建 HTML 邮件...") # 打印创建信息
html_body = """ # 定义HTML邮件正文
<html>
<head></head>
<body>
<h1>Hello HTML Email!</h1>
<p>This is an <b>HTML</b> formatted email from Python.</p>
<p>这是一封由 <b>Python</b> 发送的 <span style="color: blue;">HTML</span> 格式邮件。</p>
</body>
</html>
"""
html_msg = MIMEText(html_body, 'html', 'utf-8') # 创建一个HTML邮件对象
# 设置邮件头部
html_msg['From'] = Header('HTML发件人 <html.sender@example.com>', 'utf-8') # 设置HTML发件人
html_msg['To'] = Header('HTML收件人 <html.recipient@example.com>', 'utf-8') # 设置HTML收件人
html_msg['Subject'] = Header('HTML 测试邮件 - Python', 'utf-8') # 设置HTML主题
html_msg['Date'] = formatdate(localtime=True) # 设置日期
print("HTML 邮件构建成功。") # 打印构建成功信息
# print("\n原始HTML邮件内容:\n", html_msg.as_string()) # 打印原始邮件内容
print("\n--- 邮件构建演示完成 ---") # 打印完成信息
# 实际发送时,可以将这些 msg 对象传递给 smtplib
# import smtplib
# with smtplib.SMTP_SSL('smtp.example.com', 465) as smtp_server:
# smtp_server.login('user', 'pass')
# smtp_server.send_message(plain_text_msg) # send_message() 直接接受 Message 对象
# smtp_server.send_message(html_msg)
这段代码演示了如何使用email.mime.text.MIMEText
类创建纯文本和HTML格式的邮件。它展示了如何指定文本内容、子类型('plain'
或'html'
)和字符集,并设置标准的邮件头部字段(From
, To
, Subject
, Date
)。email
库会自动处理内容的编码和MIME相关头部,使得邮件构建过程非常简洁。
7.3.2 email.mime.multipart.MIMEMultipart
:构建多部分邮件
MIMEMultipart
类是构建复杂多部分邮件的关键,例如包含纯文本和HTML版本的邮件,或包含附件的邮件。
MIMEMultipart(_subtype='mixed', boundary=None, _policy=None, **_params)
:_subtype
:多部分邮件的子类型,常用的是:'mixed'
(默认):用于包含不相关部分的邮件,如正文和附件。'alternative'
:用于包含相同内容的替代版本,如纯文本和HTML。客户端会显示它支持的最佳版本。'related'
:用于包含相互关联的部分,如HTML和其中引用的内联图片。
boundary
:可选,指定用于分隔各MIME部分的分隔符字符串。如果为None
,email
库会自动生成一个。
attach(payload, _params=None)
:- 将一个
Message
对象(或任何MIME部分对象,如MIMEText
,MIMEImage
)添加到当前MIMEMultipart
对象的载荷中。
- 将一个
构建 multipart/alternative
邮件 (纯文本 + HTML):
import email # 导入email库
from email.mime.multipart import MIMEMultipart # 导入MIMEMultipart类
from email.mime.text import MIMEText # 导入MIMEText类
from email.header import Header # 导入Header类
from email.utils import formatdate # 导入formatdate函数
def create_alternative_email():
"""
演示如何创建包含纯文本和HTML替代版本的邮件 (multipart/alternative)。
"""
print("\n--- 创建 multipart/alternative 邮件 (纯文本 + HTML) ---") # 打印信息
# 创建根 MIMEMultipart 对象,类型为 'alternative'
# 客户端会选择它支持的最佳版本显示 (通常是HTML)
msg = MIMEMultipart('alternative') # 创建一个多部分邮件对象,子类型为'alternative'
# 设置邮件头部
msg['From'] = Header('替代发件人 <alternative.sender@example.com>', 'utf-8') # 设置发件人
msg['To'] = Header('替代收件人 <alternative.recipient@example.com>', 'utf-8') # 设置收件人
msg['Subject'] = Header(