【Python】邮件处��?2

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邮件��?
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(
评论
添加红包

请填写红包祝福语或标��?

��?

红包个数最小为10��?

��?

红包金额最��?5��?

当前余额3.43��? 前往充��? >
需支付��?10.00��?
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红��?

打赏作��?

宅男很神��?

你的鼓励将是我创作的最大动��?

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付��?¥1
获取��?
扫码支付

您的余额不足,请更换扫码支付��?充��?

打赏作��?

实付��?
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明��?

1.余额是钱包充值的虚拟货币,按��?1:1的比例进行支付金额的抵扣��?
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程��?

余额充��?