☰
Python自动收发邮件实战:从SMTP到IMAP的完整指南
2026/9/28 14:21:20 网站建设 项目流程

去年年初我维护的一台业务服务器频繁在深夜报警,我连续一周半夜爬起来查日志、看报表、回消息,整个人都快被拖垮了。后来我下定决心把整套流程交给 Python:每日订单汇总早上八点自动发出,服务异常时的告警邮件在几十秒内送达手机,连一些格式固定的回复也交给了程序代劳。用 Python 自动收发邮件听起来像是老掉牙的话题,但真正把它做对、做稳,里面其实有不少门道。这篇文章写给所有想把重复性邮件工作交给程序的人——无论是定时发报表、批量发通知,还是从收件箱里提取附件自动入库,看完你都能直接动手。

我默认你已经装好了 Python 环境,至少是 3.8 以上的版本。如果没有装,先去官网下载安装包,把环境配好再回来看代码。下面我就按自己的实操路径,把发件、收件、踩坑、落地的完整过程展开讲。

1. 先想清楚你的自动化场景,再决定怎么写代码

很多人拿到"自动收发邮件"这个需求,第一反应就是搜代码、抄下来、跑通,结果跑了两天就放弃了。问题几乎都出在场景没想清楚:你到底要让程序做什么,做到什么程度,失败的时候谁兜底。

1.1 最常见的三种自动化邮件需求

我接触过的自动化邮件项目基本可以归为下面三类,你看自己属于哪一种,再去对照后面的章节:

需求类型典型场景核心诉求
定时发送报告每天早上把昨天的销售数据、访问量、服务器状态汇总成邮件发出去程序按时触发,内容稳定,负责人无需手工整理
批量发送通知给一批客户发送活动通知、给团队成员发送任务分配收件人列表可维护,发送过程有日志,失败能重试
自动处理入站邮件收到订单邮件后自动下载附件、收到退订邮件后自动更新名单程序能监听收件箱,解析内容,触发后续动作

第一种最容易被 Python 取代,因为它完全是"定时 + 格式化内容"的组合。第二种需要额外注意发送频率和收件人数据管理,不能拿着个人邮箱硬发几千封。第三种技术含量最高,涉及 IMAP 协议、邮件解析、附件处理,也是本文后面要重点展开的。

1.2 邮件为什么至今还是自动化的首选渠道

你可能会问,现在都用企业微信、钉钉、飞书了,邮件是不是落伍了?恰恰相反,邮件在自动化这件事上有一个不可替代的优势:它是完全开放的协议,SMTP、IMAP、POP3 都是公开标准,没有任何一家公司能垄断它。只要对方有一个合法的电子邮箱地址,你就能把消息从你的程序直接送进对方的收件箱。

相比之下,IM 工具的消息推送往往依赖第三方接口,审核、配额、回调签名都是成本;短信一条一毛钱,批量发送还要走内容审核流程。邮件免费、稳定、跨平台,再加上本就支持 HTML 富文本和附件,做业务通知和报表分发再合适不过。

1.3 先给程序的"工作范围"画一条线

这是我最想强调的一点:自动化要有边界。我见过有人把自动回复写成了一句简单的 if 关键词匹配,结果用户发来"退款投诉"也被自动回复"将在 24 小时内处理",最后客户被激怒,问题升级。正确做法是先把范围定清楚——什么操作程序可以独立完成,什么操作必须转人工。

我的经验是:只让程序处理高确定性、低风险的任务,比如"收到日报邮件就下载附件""每天晚上清点未处理工单并催办"。涉及钱、法律、投诉、复杂沟通的邮件,程序只负责标记、分类、提醒,最终决策留给人。把这个原则想明白了,后面写代码会踏实很多。

2. 发送邮件:SMTP 协议与 smtplib 的实战组合

发送是整个自动化的基础,也是大多数人第一个要跑通的功能。写代码之前,先把底层链路弄清楚。

2.1 发一封邮件,数据到底走了一条什么路

你可以把邮件想象成纸质信:你的 Python 程序是写信的人,SMTP 服务器是最近的邮局,对方的收件服务器是对方小区的收发室,收件人的邮箱客户端是最终的收件箱。

程序通过smtplib与发件人的 SMTP 服务器建立一个加密连接,提交"发件人、收件人、标题、正文、附件"这封信的完整内容。发件服务器校验你的身份没问题后,会把这封信转给收件人域名对应的收件服务器。双方服务器之间经过一系列投递、反垃圾检查,最终落到收件人的邮箱里。

这个链路里,程序直接打交道的是发件服务器。所以你需要知道的是你自己的邮件服务商提供的 SMTP 地址和端口,而不是对方服务器的地址。端口和加密方式也值得注意,SSL 加密通常用 465 端口,STARTTLS 通常用 587 端口。QQ 邮箱和 163 邮箱都支持这两种方式,我习惯用 465,连接逻辑最简单。

2.2 准备账号:授权码不是你的登录密码

这里有一个百分之八十的新手都会踩的坑:用邮箱的登录密码去调用 SMTP。直接后果就是报错SMTPAuthenticationError。

主流的免费邮箱出于安全原因,不允许第三方代码直接用账号密码做 SMTP 登录,需要在邮箱设置里开通 SMTP 服务,并生成一个独立授权码。以 QQ 邮箱为例,进入设置,选择账户,找到 POP3/IMAP/SMTP 服务这一栏,开启 SMTP 服务和 IMAP 服务,按提示发送短信验证,然后就能拿到一串授权码。163 邮箱的操作路径类似,协议开通后同样能生成独立密码。Gmail 则需要开启两步验证后创建一个应用专用密码。

拿到授权码后有一个很关键的习惯:不要把授权码直接硬编码在源代码里。哪怕你只是自己用,也建议放在环境变量里,或者放在一个不提交到版本库的配置文件里。我自己的做法是写在.env文件里,用python-dotenv加载,这样既方便本机调试,也不会因为代码传到公开仓库泄露密钥。

2.3 用 smtplib 发送第一封纯文本邮件

环境准备好了,直接看代码。下面是我最常用来测试连通性的最小脚本:

import os import smtplib from email.mime.text import MIMEText from email.utils import formataddr SMTP_HOST = os.getenv("SMTP_HOST", "smtp.qq.com") SMTP_PORT = int(os.getenv("SMTP_PORT", "465")) USERNAME = os.getenv("SMTP_USER") AUTH_CODE = os.getenv("SMTP_AUTH_CODE") TO_ADDR = os.getenv("TO_ADDR") msg = MIMEText("这是一封 Python 自动发送的测试邮件。", "plain", "utf-8") msg["From"] = formataddr(("自动化小助手", USERNAME)) msg["To"] = TO_ADDR msg["Subject"] = "SMTP 连通性测试" with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server: server.login(USERNAME, AUTH_CODE) server.sendmail(USERNAME, [TO_ADDR], msg.as_string()) print("邮件发送成功")

这段脚本里几个细节值得展开。

MIMEText的第一个参数是正文,第二个参数plain表示纯文本,第三个参数显式指定 UTF-8 编码。这里务必带上utf-8,不然中文正文在部分服务商的服务器上会出现编码问题。

formataddr用来构造"名称 <邮箱地址>"格式的发件人信息,这样收件人看到的是你定义的中文名,而不是一串光秃秃的邮箱地址。

SMTP_SSL会直接在 TCP 连接上启动 SSL 加密,连接 465 端口时就是这么用的。由于我们用的是上下文管理器with,发送完成后会自动关闭连接,不需要手动调用quit()。

server.sendmail(USERNAME, [TO_ADDR], msg.as_string())的第二个参数是收件人列表,这就是批量发送的入口——你可以传入一个包含多个地址的列表。

2.4 升级版:发送 HTML 邮件和带附件的邮件

运营活动通知、定期报表这类场景,纯文本不够用,需要 HTML 排版甚至携带附件。这时候要用到MIMEMultipart:

import os import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication from email.utils import formataddr SMTP_HOST = os.getenv("SMTP_HOST", "smtp.qq.com") SMTP_PORT = int(os.getenv("SMTP_PORT", "465")) USERNAME = os.getenv("SMTP_USER") AUTH_CODE = os.getenv("SMTP_AUTH_CODE") TO_ADDR = os.getenv("TO_ADDR") msg = MIMEMultipart("related") msg["From"] = formataddr(("运维报表系统", USERNAME)) msg["To"] = TO_ADDR msg["Subject"] = "8 月 15 日服务器监控报表" html_body = """ <html> <body> <p>您好:</p> <p>以下是今日监控报表摘要,完整数据请查收附件。</p> <table border="1" cellpadding="6" style="border-collapse: collapse;"> <tr><th>指标</th><th>当前值</th><th>状态</th></tr> <tr><td>CPU 使用率</td><td>42%</td><td>正常</td></tr> <tr><td>内存使用率</td><td>71%</td><td>正常</td></tr> <tr><td>磁盘空间</td><td>83%</td><td>注意</td></tr> <tr><td>在线用户数</td><td>1520</td><td>正常</td></tr> </table> <p>报告生成时间:2025-08-15 08:00:00</p> </body> </html> """ msg.attach(MIMEText(html_body, "html", "utf-8")) # 附加一个 CSV 文件 with open("report_20250815.csv", "rb") as f: attachment = MIMEApplication(f.read()) attachment.add_header("Content-Disposition", "attachment", filename="report_20250815.csv") msg.attach(attachment) with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server: server.login(USERNAME, AUTH_CODE) server.sendmail(USERNAME, [TO_ADDR], msg.as_string()) print("报表邮件已发送")

这里有一个合入经验:正文和附件都通过attach挂到MIMEMultipart对象上,正文用 HTML MIME 类型,附件用MIMEApplication把文件字节流包起来。Content-Disposition的attachment会告诉收件方"这是一个附件",filename指定了客户端显示的文件名。对于 CSV、Excel、PDF 这类文件,MIMEApplication都能直接兼容。

另外提醒一点:HTML 邮件不要做成重图、复杂 CSS 的网页。很多邮件客户端的渲染引擎比较老,对外部样式、JavaScript 的支持很差。内联样式是相对稳妥的方案,越简单越不容易乱版。

2.5 发送后的确认与错误处理

邮件发送不是调用成功就等于投递成功,这一点新手要尽早建立认知。sendmail返回时只代表发件服务器把信收下了,后面的路由、投递、反垃圾过滤,程序都看不到。真正能确认成功与否的只有两类反馈:一类是发件服务器的同步异常,另一类是收件人服务器返回的退信。

代码层面需要捕获的常见异常包括:

  • SMTPAuthenticationError:账号或授权码错误,多半是授权码配置问题。
  • SMTPRecipientsRefused:收件人列表里有非法地址,服务器直接拒绝。
  • smtplib.SMTPServerDisconnected:连接被服务器断开,通常是发送太频繁触发风控。

我在生产环境里的做法是给发送函数加一个简单的重试包装:

import logging import smtplib import time logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") def send_with_retry(msg, retries=3, delay=5): for attempt in range(1, retries + 1): try: with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server: server.login(USERNAME, AUTH_CODE) server.sendmail(USERNAME, [TO_ADDR], msg.as_string()) logging.info("邮件发送成功,尝试次数:%d", attempt) return True except (smtplib.SMTPServerDisconnected, OSError) as e: logging.warning("发送失败(第 %d 次):%s", attempt, e) if attempt < retries: time.sleep(delay) return False

注意,重试只适合网络抖动和短时不可用的场景,认证失败和收件人拒绝这类确定性错误不要重试,重试只会浪费资源和加重封禁风险。

3. 接收邮件:IMAP 协议与读取路径

发送解决了"往外的路",接收解决的是"往内的路"。自动处理入站邮件,比如下载对账单、解析客户反馈、自动归档附件,都需要从收件箱把邮件读回来。

3.1 为什么我推荐用 IMAP 而不是 POP3

接收邮件有两个主流协议,POP3 和 IMAP,把它们搞混会导致后面代码写歪。

POP3 的设计思路是"下载后删除":客户端把邮件从服务器上拉下来,然后邮件就从服务器消失了。这在只有一台电脑、一个客户端的年代没问题,但对自动化程序来说是大坑——程序读走一封邮件,你在手机上的邮件客户端就发现它不见了。

IMAP 的设计思路是"服务端同步":邮件始终存放在服务器上,客户端只相当于一个查看窗口,可以标记已读、未读、移动文件夹,但邮件本身不会消失。多个设备看到的是同一份数据,这也意味着程序读走邮件后,你的手机里还能看到它,只是状态从"未读"变成了"已读"。

自动化场景还有另一个关键好处:IMAP 支持持久性的 UID 编号,程序可以记住"上次处理到哪封邮件",下次只处理编号更大的新邮件。这在下一章会展开讲,是自动化落地时最实用的一个能力。

3.2 登录邮箱并扫描收件箱

在调用之前需要先确认在邮箱服务商后台开通了 IMAP 服务,获取授权码的方式和 SMTP 一样。QQ 邮箱开通后在设置里还能看到 IMAP/SMTP 服务状态,顺手确认一下就好。

与 SSH 服务器的套路一致,IMAP 的默认端口是 993,也需要走 SSL:

import imaplib import email from email.header import decode_header IMAP_HOST = "imap.qq.com" # 163 邮箱对应 imap.163.com IMAP_PORT = 993 USERNAME = os.getenv("SMTP_USER") AUTH_CODE = os.getenv("SMTP_AUTH_CODE") server = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) server.login(USERNAME, AUTH_CODE) server.select("INBOX") # 搜索所有未读邮件 typ, data = server.search(None, "UNSEEN") mail_ids = data[0].split() print(f"未读邮件数量:{len(mail_ids)}")

server.select("INBOX")表示进入收件箱文件夹。IMAP 的文件夹模型比 POP3 复杂,除了 INBOX 还有已发送、已删除、草稿箱等内置目录,企业邮箱还可能有自定义目录。在动手处理之前先server.list()看一眼有哪些目录,能帮你避免在错误的文件夹上来回折腾。

search(None, "UNSEEN")返回的是服务端过滤后的邮件编号列表。IMAP 的搜索条件很丰富,SINCE "01-Aug-2025"按日期过滤,FROM "boss@example.com"按发件人过滤,SUBJECT "月度对账"按标题过滤。多个条件可以组合,比如search(None, 'UNSEEN', 'FROM', 'boss@example.com')。

3.3 解析邮件正文:邮件是一个树形结构

很多初学者在拿到data之后,会对里面的二进制内容直接decode("utf-8"),期望一下拿到正文,结果发现要么是空字符串,要么是一堆乱码。原因在于 MIME 邮件是一棵嵌套树,正文可能被拆成了纯文本、HTML、附件等多个部分。

正确的解析姿势是拿到邮件对象后,用msg.walk()遍历所有 MIME 节点:

def parse_msg(raw_email): msg = email.message_from_bytes(raw_email) subject_parts = decode_header(msg.get("Subject", "")) subject = "" for part, charset in subject_parts: if isinstance(part, bytes): subject += part.decode(charset or "utf-8", errors="replace") else: subject += part text_content = "" html_content = "" attachments = [] for part in msg.walk(): content_type = part.get_content_type() content_disposition = str(part.get("Content-Disposition", "")) if "attachment" in content_disposition: filename = part.get_filename() if filename: filename_parts = decode_header(filename) filename = "".join( p.decode(c or "utf-8") if isinstance(p, bytes) else p for p, c in filename_parts ) attachments.append({ "filename": filename, "data": part.get_payload(decode=True) }) elif content_type == "text/plain": text_content = decode_payload(part) elif content_type == "text/html": html_content = decode_payload(part) return { "subject": subject, "from": msg.get("From", ""), "date": msg.get("Date", ""), "text": text_content, "html": html_content, "attachments": attachments } def decode_payload(part): payload = part.get_payload(decode=True) if payload is None: return "" charset = part.get_content_charset() or "utf-8" return payload.decode(charset, errors="replace")

这段代码里有两个地方特别容易写错。

第一个是get_payload(decode=True)与get_content_charset()搭配使用。传输过程中的邮件正文大多是 Base64 编码,decode=True会把编码后的内容解成原始字节流,然后你再用正确的字符集把它解码成字符串。如果字符集取错了,中文就会乱码。

第二个是Content-Disposition的判断。有的邮件客户端发送的附件没有标准attachment标识,用inline内嵌图片,这种也要识别。内嵌图片在 HTML 邮件里常见,逻辑上不算业务附件,你可以根据自己的需求决定是保存还是丢弃。

3.4 自动下载邮件附件的完整片段

解析完成后,下载附件其实就是把字节流写到本地磁盘上:

def save_attachments(parsed, save_dir="downloads"): os.makedirs(save_dir, exist_ok=True) for att in parsed["attachments"]: # 做一层文件名清洗,避免路径穿越 safe_name = os.path.basename(att["filename"]) file_path = os.path.join(save_dir, safe_name) with open(file_path, "wb") as f: f.write(att["data"]) print(f"已保存附件:{file_path}")

这里我特意加了os.path.basename,原因是攻击者可能通过构造文件名路径穿越,把文件写到服务器的任意目录。自动化程序处理的是外部邮件,安全上多留一个心眼总没错。另一个细节是不要信任附件里的可执行文件,程序只负责把它落盘,任何后续执行动作都不该由邮件内容自动触发。

4. 踩坑实录:授权码、编码和正文读取的三个教训

这一节是我在自己项目的真实故障里总结出来的,每一条都踩过、修过、验证过,记录下来希望你能跳过这些坑。

4.1 一直报 SMTPAuthenticationError,问题出在授权码

当时我一个新同事搭发送脚本,始终报SMTPAuthenticationError,我远程一看,代码里的变量叫SMTP_PASSWORD,传入的却是邮箱的登录密码。他把 QQ 邮箱的登录密码填进了授权码的位置,当然过不了认证。

授权码的正确使用方式我在前面写过:去邮箱设置里开启 SMTP/IMAP 服务,生成一把独立的授权码,然后再写进配置文件。如果你已经生成过授权码,又改了邮箱登录密码,授权码一般是不会变的,但有些服务商在检测到异地登录后会自动禁用旧的授权码,这时旧授权码就会失效,需要重新生成。

另外排查这个报错时注意三个小细节:

  • 授权码复制的时候别把空格带进去。
  • 账号要写成完整的邮箱地址,不能只写用户名。
  • 环境变量里如果存了授权码,确认没有被 shell 里的引号吞掉。

4.2 中文主题乱码:问题的根源是 RFC 2047 编码

给邮件设置中文主题时,不少人会直接写msg["Subject"] = "每日报表",然后对方收到的主题显示一串=?utf-8?B?XXXX?=或者干脆乱码。

根本原因是邮件头不像正文,它默认是纯 ASCII 的。非 ASCII 文本必须经过 RFC 2047 编码,转成=?utf-8?B?Base64?=这种形式,邮件客户端接收到之后再解回中文。Python 的 email 库在大部分情况下会自动帮你做这件事,但你手动构造时最好显式使用email.header.Header:

from email.header import Header msg["Subject"] = Header("每日报表", "utf-8")

这样做的好处是编码过程完全可控,不依赖具体版本的自动行为。我在解析收到的邮件时也遵守同样的规则,用decode_header反向解码。如果发现某个主题解析出来是乱码,多半就是编码字符集没取对,或者对方发送端用了非标准编码,这时候可以尝试降级用GB2312或GBK再解一次。

4.3 读取邮件正文永远为空?因为你没有遍历 MIME 树

我见过一个很典型的 bug:解析邮件时直接用msg.get_payload(),发现纯文本邮件能正常读出来,但 HTML 邮件读出来是 list 对象,代码直接崩了。这就是没搞懂 MIME 树结构的表现。

同一个"RFC 2822"格式的邮件,可能包含multipart/alternative节点,下面挂着text/plain和text/html两个节点;也可能包含multipart/mixed节点,下面挂正文和附件。get_payload()返回的可能是字符串,也可能是节点列表——你需要遍历msg.walk(),根据每个节点的content_type决定如何处理。

再提醒一次,解析出正文后不要立刻认为万事大吉。有些邮件既有纯文本又有 HTML,推荐的做法是"优先取纯文本,纯文本不存在再取 HTML",这样可以保证在纯文本环境(比如终端工具、极简客户端)里也能看到内容。

4.4 免费邮箱的发送频率限制,这个坑最容易被忽略

免费邮箱的 SMTP 服务都有隐性频率限制。QQ 免费邮箱单次登录后连续发送大量邮件,或者短时间内高频调用 SMTP 接口,很容易触发风控,轻则退信,重则临时禁用 SMTP 功能。

我建议控制在每分钟不超过 5 至 10 封。如果业务真的要群发几百上千封,请换专业邮件发送服务,而不是拿着自己的个人邮箱硬发。一个简单判断:如果你在短时间内发出去的邮件大量进了垃圾箱,不用怀疑,就是频率和内容质量触发了反垃圾策略。

5. 让邮件跑起来:定时任务与自动处理的可靠性设计

发送和接收都跑通了,自动化才算开始。真正让人省心的是把整个流程挂起来定时执行,并保证不丢邮件、不重复处理。

5.1 三种定时方案怎么选

Python 生态里定时任务方案不少,我实际使用过这三个,对比放在下面:

方案适用场景优点缺点
schedule库轻量脚本,进程内定时代码直观,几行就能跑不支持 cron 表达式,服务重启后任务丢失
APScheduler中等规模自动化支持 cron 触发、持久化任务、多执行器配置稍重,概念较多
系统 crontab / 任务计划程序服务器运维级任务与操作系统绑定,最稳定调试不便,日志分散

我个人推荐从APScheduler开始学,它既能满足"每天早上 8 点发报表"这种 cron 场景,又能在 Python 程序里统一管理异常和日志。schedule胜在简单,但真要落地生产环境,很多细节还是要自己补。

5.2 用 APScheduler 实现"每天早上八点发报表"

安装依赖:

pip install apscheduler

然后写一个简单的入口脚本:

from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") def send_daily_report(): # 复用第 2 节的 send_with_retry 函数 send_with_retry(build_report_msg()) logging.info("每日报表任务执行完成") scheduler = BlockingScheduler() scheduler.add_job( send_daily_report, trigger=CronTrigger(hour=8, minute=0), id="daily_report", replace_existing=True, ) scheduler.start()

CronTrigger(hour=8, minute=0)就是每个自然日 8 点整执行一次。如果希望在每个工作日执行,可以加day_of_week="mon-fri"。这个任务进程必须常驻运行,比如用nohup或 systemd 托管,不要让它在某个终端里跑着就跑没了。

有一个运维层面的细节:定时任务不是配置完就一劳永逸。你要给它加一个成功/失败通知,最简单的方式是在send_daily_report里执行成功记录日志,失败时再往自己的另一个邮箱发一条告警——就当是"监控着监控"。

5.3 处理"邮件重复读取":记录已处理 UID

这是我做了很久之后才意识到的问题,也是我认为最值得分享的实战经验之一。

假设你写了一段程序,每 5 分钟扫描一次未读邮件,读到新邮件就解析、下载附件、存数据库。表面看没问题,但一旦程序在处理到一半的时候崩溃,重启后 IMAP 服务器上那封邮件仍然带着"未读"标记,程序会再处理一次。如果这个动作是"发一封回复邮件"或者"扣一次库存余额",后果就很严重了。

解决办法不是依赖"未读"标记,而是记录每封邮件在 IMAP 服务器上的唯一编号 UID。大致逻辑:

import imaplib server = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT) server.login(USERNAME, AUTH_CODE) server.select("INBOX") # 取出所有存在的 UID,和本地已处理集合做差集 typ, data = server.uid("search", None, "ALL") uids = data[0].split() processed_uids = load_processed_uids() # 从本地文件或数据库读取 to_process = [uid for uid in uids if uid not in processed_uids] for uid in to_process: typ, msg_data = server.uid("fetch", uid, "(RFC822)") parsed = parse_msg(msg_data[0][1]) process_parsed(parsed) mark_processed(uid) # 先落库,再标记已读 # 全部处理完后再统一把已读标记写上 for uid in to_process: server.uid("store", uid, "+FLAGS", "\\Seen")

顺序是关键:先记录"已处理",再标记"已读"。因为标记已读本身也可能失败,如果把标记已读放在前面,一旦失败,程序重启后就会重复处理。把"已处理"落库放在前面,即使后续步骤失败,重启后也会跳过这封邮件。

处理记录存在哪?小项目用一个文本文件就够了,每行一个 UID;大项目就放到 Redis 或数据库里,还能顺带记录处理时间,方便排查链路。

5.4 日志、异常、重试,自动化邮件的基本素养

最后想强调的是:自动化脚本必须能"自我解释"。你自己手工发邮件出了问题,至少知道哪一步出了问题;程序自动化发了几百封之后出了问题,如果没有任何日志,排查起来就是灾难。

我建议每个邮件任务至少记录以下几类信息:

  • 每次发送任务的触发时间、任务 ID。
  • 收件人列表的长度和摘要。
  • 发送成功、失败、重试的次数。
  • 接收任务扫描到的新邮件 UID 列表和处理结果。
  • 任何异常信息的完整堆栈。

日志可以打在本地文件里,也可以同时推到日志平台。我的习惯是本地mail_automation.log加上一个简单的摘要统计,每天打开看一眼就知道,而不是等用户来投诉了才去翻信息。

另外一个容易忽略的环节是时区。你的服务器可能在东八区,但收件方的服务器设置可能不同。发送定时报表时,最好在邮件正文里显式标注时间所属时区,避免收到的人误以为程序把时间算错了。

写在最后的小建议

我之所以说"把背景场景想清楚再写代码",是因为最近两年我越来越觉得,自动收发邮件这件事,难点从来不在 Python 语法,而在稳定性和边界。你写的每一行代码都要回答三个问题:如果今天这台服务器网络抽风了,程序会怎样?如果昨天程序崩了,今天的数据还完整吗?如果一封邮件触发了一个不该有的动作,后果谁来承担?

我的答案很简单:用日志回答前两个,用范围约束回答第三个。你从最简单的"每天早上八点发一封报表邮件"开始,跑上一周,确认日志稳定、无重复、无乱码,再逐步加入 HTML 美化、附件、收件解析、自动回复,一步都不要跨太大。

做了两年多的邮件自动化,我最真实的体会是,它不能替你判断消息是否重要,但可以替你省下大量搬运消息的时间。当你半夜真的不用再爬起来的时候,你就会理解这个项目值得做。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询