登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  python教程

Python imaplib 怎样用 UID 获取邮件而不依赖序号

来源:17golang原创

时间:2026-10-30 15:03:59 413浏览 收藏

用 Python imaplib 按 UID 获取邮件,关键是让搜索和获取两端都使用 uid():先调用 mail.uid("search", None, "ALL") 得到 UID,再把这些 UID 交给 mail.uid("fetch", uid, ...)。普通 search 返回的序号不能直接当 UID 使用。

官方地址:https://docs.python.org/3/library/imaplib.html

我会先改邮件的身份记录

如果要把一个收件箱交给后台批处理,我更愿意先解决“这次和下次是否指向同一封邮件”,再考虑增加并发。以下是一个设计场景,不是实际事故复盘:任务记住第 12 封,连接断开后再取第 12 封;期间有邮件被永久删除,原来的相对位置就可能变化。

消息序号适合表达当前邮箱中的位置,持久任务却需要更稳定的身份。对我来说,把序号写进长期任务表,是最先要去掉的耦合。UID 也并非全球永久唯一,它有明确的邮箱和有效期范围。

把邮件身份放回邮箱范围内

我会把本地处理键设计为 (account, mailbox, uidvalidity, uid)。account 是应用自己的账号标识;mailbox 是选中的文件夹;UIDVALIDITY 表明这一组 UID 的有效范围。重新选中邮箱后,若 UIDVALIDITY 与本地记录不同,就不能继续把旧 UID 对应到新邮件上。

账号、邮箱、UIDVALIDITY、UID与本地记录的范围关系
静态结构图:UID 的解释依赖账号、邮箱和 UIDVALIDITY,不能只存一个数字。

同一个范围中,已删除邮件的 UID 不会给另一封邮件复用,但邮件本身可以消失。因此,“UID 稳定”不能推出“取信必有正文”。这一点会影响重试和完成状态的设计。

把搜索和获取都改成 UID 命令

下面示例只选 INBOX、只读取最多十封邮件,不写本地检查点。凭据由环境变量提供,服务端须允许对应的认证方式;需要 OAuth 的服务应替换认证部分。TLS 显式使用默认校验上下文。代码未连接真实邮箱,运行结果取决于服务端状态。

import imaplib
import os
import re
import ssl
from email import policy
from email.parser import BytesParser


def require_ok(result, action):
    # 先处理协议状态,避免把 NO 或 BAD 当成正常数据。
    status, data = result
    if status != "OK":
        raise RuntimeError(f"{action} 失败:{status}")
    return data


host = os.environ["IMAP_HOST"]
user = os.environ["IMAP_USER"]
password = os.environ["IMAP_PASSWORD"]
context = ssl.create_default_context()

# with 离开时注销;示例不删除邮件,也不执行 EXPUNGE。
with imaplib.IMAP4_SSL(host, ssl_context=context, timeout=30) as mail:
    require_ok(mail.login(user, password), "登录")
    require_ok(mail.select("INBOX", readonly=True), "选中邮箱")
    _, validity_data = mail.response("UIDVALIDITY")
    if not validity_data or not validity_data[0]:
        raise RuntimeError("未取得 UIDVALIDITY,停止建立持久身份")
    validity = int(validity_data[0])

    # UID SEARCH 的结果才能直接交给 UID FETCH。
    found = require_ok(mail.uid("search", None, "ALL"), "搜索")
    uids = found[0].split() if found and found[0] else []
    for uid in uids[:10]:
        data = require_ok(
            mail.uid("fetch", uid, "(UID BODY.PEEK[])"), "获取邮件"
        )
        payload = None
        for item in data or []:
            # imaplib 的 literal 通常表现为元数据与字节组成的元组。
            if not isinstance(item, tuple) or len(item) != 2:
                continue
            meta, raw = item
            match = re.search(rb"\bUID\s+(\d+)\b", meta)
            if match and match.group(1) == uid and isinstance(raw, bytes):
                payload = raw
                break
        if payload is None:
            # 搜索后可能已被其他客户端删除,不误记为处理成功。
            print("无可用正文 UID=", uid.decode("ascii"))
            continue
        message = BytesParser(policy=policy.default).parsebytes(payload)
        identity = (user, "INBOX", validity, int(uid))
        print(identity, message.get_content_type())
        # 真实业务应在处理结果持久化成功后,再提交这一身份的完成记录。

这里没有把响应开头的数字当 UID,而是取回并匹配 UID 元数据。BODY.PEEK[] 表示获取完整邮件而不因该读取隐式设置已读标志;只读选择也避免示例对邮箱进行写操作。

UID搜索、UID获取与响应元数据和邮件字节的对应关系
静态结构图:搜索得到 UID 集合,获取响应里的 UID 元数据用于匹配邮件字节;图不是执行记录。

批处理的代价与完成标准

我喜欢这个改动的一点,是身份问题与处理速度可以分开讨论。它没有自动带来吞吐提升:示例每封一次获取,往返开销明显;完整正文也可能包含大附件。生产任务可以限制搜索范围、只取所需字段,或分批获取,但批量响应应按返回 UID 建立映射,不能按请求列表的下标拼接。

需要观察的情况我会怎样处理
UIDVALIDITY 改变停止复用旧身份,重新建立当前邮箱索引
获取返回 OK 却没有对应正文单独记录消失或缺失情况,不当作正文处理成功
网络或协议错误保留未完成项,重连后重新确认邮箱范围
业务结果已经持久化再提交该复合身份的完成状态

这些是上线时应观察的信号,不是本文已经测得的结果。若任务可能乱序完成,我不会只用“最大 UID”覆盖所有中间状态,否则较小 UID 的失败项可能被跳过。更稳妥的是记录每项结果,或者仅推进连续完成的批次检查点。

仍然容易混淆的三个问题

UID FETCH 的响应里为什么还有序号?

FETCH 响应仍可能以消息序号开头。请求使用 UID,并不意味着响应里每一个数字都成了 UID;应辨认 UID 属性。

UID 可以跨文件夹直接复用吗?

不可以。移动或复制到另一个文件夹后,不能沿用原文件夹的 UID 解释新位置;要按目标邮箱重新取得身份。

UID 能替代 Message-ID 吗?

它们解决的问题不同。UID 用于服务端邮箱中的定位,Message-ID 是邮件头字段。本文的处理键针对单个账号的邮箱同步,不声称实现了跨账号邮件去重。

参考:Python imaplib UID 接口;IMAP 消息标识与 UIDVALIDITY;FETCH 与 BODY.PEEK 语义。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>