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 <[email protected]>
To: Recipient <[email protected]>
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 <[email protected]>
To: HTML Recipient <[email protected]>
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?= <[email protected]>
To: [email protected]
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 <[email protected]>
To: [email protected]
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 <[email protected]>
To: Complex Recipient <[email protected]>
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: <[email protected]>
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 <[email protected]>
To: [email protected]
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 <[email protected]>
To: [email protected]
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 <[email protected]>
To: [email protected]
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:[email protected]">
# (?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 <[email protected]>
To: [email protected]
Subject: Email with an Inline Image
MIME-Version: 1.0
Content-Type: multipart/related; boundary="---=_Related_Image_001"; type="text/html"; start="<[email protected]>"
---=_Related_Image_001
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Content-ID: <[email protected]>
<html><body>
<h1>Hello from HTML!</h1>
<p>Here is an inline image:</p>
<img src="cid:[email protected]">
<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: <[email protected]>
/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('发件��? <[email protected]>', 'utf-8') # 设置发件人,支持中文
plain_text_msg['To'] = Header('收件��? <[email protected]>', '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发件��? <[email protected]>', 'utf-8') # 设置HTML发件��?
html_msg['To'] = Header('HTML收件��? <[email protected]>', '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('替代发件��? <[email protected]>', 'utf-8') # 设置发件��?
msg['To'] = Header('替代收件��? <[email protected]>', 'utf-8') # 设置收件��?
msg['Subject'] = Header('纯文本与HTML替代邮件 - Python', 'utf-8') # 设置主题
msg['Date'] = formatdate(