资讯动态

用Python实现邮件自动收发:SMTP/IMAP协议实战指南

发布时间:2026/9/8 14:53:31 来源:尧图企业网站定制
最近有个做数据分析的朋友跟我吐槽说每天早上一到公司就要先手动把前一天的统计报表下载下来整理成 Excel 再发给整个团队一来一回差不多耗掉半小时。我听完只回了一句这事其实在你睡觉的时候就能自动干完。Python 自动收发邮件听起来有点像某个程序员自嗨的小技巧但实际上它是个人效率自动化和团队协作里性价比极高的一块。整套逻辑不依赖任何付费服务用 Python 自己就能跑起来而且发送用标准库 smtplib接收用 imaplib不需要额外装多少第三方包适合刚学完基础语法、想拿真实需求练手的人也适合运维、运营、数据分析师解决日常繁琐重复的收发任务。很多人一听到自动收发邮件就以为要碰什么黑科技其实不是。它完全走的是邮箱服务商已经开放好的标准协议——想发邮件就通过 SMTP 协议把信交给对方服务器想收邮件就通过 IMAP 或 POP3 协议把服务器上的邮件拉回来。只要在代码里把服务器地址、端口、账号、授权码配好剩下的就是纯逻辑的事。这篇文章我会从环境准备讲到发送、接收、定时任务、异常排查全程用可复制的 Python 代码和数据说话尽量把我踩过的坑也一并写出来让你少走弯路。1. 先搞清楚自动收发邮件的底层逻辑1.1 发信是 SMTP收信是 IMAP/POP3本质都是对话邮件系统不是一个人对着另一个人直接喊话而是用户代理也就是我们的代码客户端和邮件服务器之间的对话。你发一封邮件时其实是把自己的电脑当成一个客户端连接上收件人邮箱服务商的 SMTP 服务器然后用约定好的文本命令把信投递进去。整个过程有点像你去邮局寄包裹你不直接走进收件人家而是把包裹交给邮局邮局再负责配送。Python 里的 smtplib 干的事就是把这种文本对话封装成了一个个方法。你在代码里写的server.login(user, auth_code)底层是要验证身份server.send_message(msg)底层就是告诉服务器我有封信需要投递。反过来收信时客户端连接的是 IMAP 或 POP3 服务器登录后可以列出邮件夹里的邮件、下载邮件原文、还可以标记已读或删除。IMAP 和 POP3 的区别主要在于POP3 是把信拿走的协议邮件通常会从服务器下载到本地后删除IMAP 则是服务器始终保留客户端同步操作的协议你看到的已读、文件夹这些状态都会同步到服务器。1.2 为什么这个对话值得用 Python 自动化如果只是偶尔发一两封邮件手动操作当然没问题。但一旦出现下面这些场景手动的成本就会直线上升每天定时给不同人发送同一份格式的日报或报表收到某个固定格式邮件后自动下载附件并归档系统监控有异常时自动把告警发送到运维群对应邮箱大批量发送内容相似但收件人不同的通知或者像我的情况脚本跑完数据后自动把结果和图表打包发给老板。这些需求有一个共同点人是不可靠的定时器是可靠的。人会忘记、会生病、会离职但服务器上的 cron 任务不会。Python 自动收发邮件就是把这些重复性工作交给代码把时间省下来去做更有价值的事情。而且它很白帽完全是在邮箱服务商允许的协议和接口范围内操作不涉及任何绕过限制的行为只要频率合理、内容合规就是非常健康的自动化做法。1.3 适合谁学别一开始就奔着群发营销去如果你是 Python 入门者这个项目能同时练到网络编程、字符串处理、文件流、异常处理、定时任务是一个典型的一鱼多吃项目。如果你是运维或后端开发自动收发电邮可以作为监控告警链路的接入口。如果你是数据岗这个技能也能帮你把表哥表姐的活儿自动化。要特别提醒一点不要把自动发信和垃圾邮件划等号。用 Python 给自己或团队发运营类邮件没问题但如果拿它做无差别群发营销轻则被邮箱服务商标记为垃圾账号重则封号。后续代码里的频率控制、内容规范我都会提到这也是自动化能长久跑下去的前提。2. 环境准备Python、编辑器、邮箱授权码2.1 先解决 Python 环境问题很多新手卡在第一步——不是不会写代码而是电脑上根本没有能跑的 Python 环境。这里按下不表主要是我的经验不管是 Windows、macOS 还是 Linux先确认python --version能正常返回版本号。如果提示找不到命令就去 Python 官网下载对应系统的安装包安装时务必把 Add Python to PATH 这个勾选项选中否则后面在终端里运行脚本会非常痛苦。如果你用的是 VS Code装好 Python 扩展后在左下角或命令面板里选择解释器确认是刚装好的那个版本就行。如果你用 PyCharm新建项目时直接指定 Base interpreter 为系统 Python也行。不管用哪个编辑器我建议给这个项目单独建一个虚拟环境避免和系统全局的包互相污染。执行下面这段就行mkdir auto_email cd auto_email python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate自动收发邮件本身用到的是标准库不一定需要额外 pip 安装什么包。但为了后续调试方便我会在后面的实操里用到一个可选的轻量包python-dotenv用来读取本地配置文件里的授权码。这一点先不急着装等真用到再说。2.2 邮箱端的设置IMAP/SMTP 开关和授权码这步是很多人第一次自动发信失败的头号原因代码没写错但邮箱服务器不让你登录。原因是你不能用邮箱的登录密码去连接 SMTP/IMAP 服务而需要专门的授权码。授权码相当于邮箱服务商给你开出的第二把钥匙专门给第三方客户端使用。登录邮箱网页版后通常在设置 - 账户或客户端设置里能找到开启 SMTP/IMAP 服务的开关。开启后服务商会生成一串授权码把它复制保存好这段码就是代码 login 时要用的密码。不同邮箱服务商的服务器地址和端口我整理成了参考表服务商SMTP 服务器SMTP SSL 端口IMAP 服务器IMAP SSL 端口常见场景 Asmtp.qq.com465imap.qq.com993常见场景 Bsmtp.163.com465imap.163.com993常见场景 Csmtp.exmail.qq.com465imap.exmail.qq.com993常见场景 Dsmtp.gmail.com465imap.gmail.com993使用前建议去对应邮箱的官方帮助页确认一下有些服务商还支持 587 端口配合 STARTTLS。这里需要理解一个关键点465 端口是 SSL 加密而 587 端口本身不强制加密需要通过 STARTTLS 命令把连接升级到加密通道。Python 里分别对应SMTP_SSL和SMTP.starttls()选错端口就会报连接或握手错误。2.3 别把授权码硬编码进源码写代码时最忌讳的事就是把授权码直接写在.py文件里一旦代码被推到公开仓库相当于把邮箱钥匙送给了全世界。我的习惯是新建一个.env文件放在本地不提交到版本库然后用python-dotenv把里面的配置读取进来pip install python-dotenv.env文件内容如下SMTP_SERVERsmtp.qq.com SMTP_PORT465 MAIL_USERyournameexample.com MAIL_AUTH_CODEyour_auth_code_here IMAP_SERVERimap.qq.com IMAP_PORT993这样真正跑代码时变量就和代码分离了。就算你换电脑、换邮箱也不需要动逻辑代码只改.env就行。这也是后面所有代码样例统一采用的配置方式。3. 发送邮件从纯文本到带附件的完整实现3.1 5 分钟跑通第一封自动发送邮件我先从一个最核心的骨架开始。这一步的目地是验证整个链路通不通能不能连接服务器、能不能登录、能不能发出去一封最简单的信。import os import smtplib from email.message import EmailMessage from dotenv import load_dotenv load_dotenv() SMTP_SERVER os.getenv(SMTP_SERVER) SMTP_PORT int(os.getenv(SMTP_PORT, 465)) MAIL_USER os.getenv(MAIL_USER) MAIL_AUTH_CODE os.getenv(MAIL_AUTH_CODE) msg EmailMessage() msg[From] MAIL_USER msg[To] receiverexample.com msg[Subject] 这是一封Python自动发送的邮件 msg.set_content(恭喜如果你的终端没有报错说明SMTP链路已经通了。) try: with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout30) as server: server.login(MAIL_USER, MAIL_AUTH_CODE) server.send_message(msg) print(发送成功) except Exception as e: print(f发送失败: {e})这段代码的关键点有两个。第一用SMTP_SSL而不是SMTP因为在 465 端口下 SSL 是从第一步就开始握手的如果你写成SMTP再starttls()会得到报错。第二用EmailMessage构建邮件内容是 Python 3.6 官方推荐的做法它比老式的MIMEText更省事比如set_content会自动处理纯文本正文。信发出去后去收件箱看一眼。如果进入垃圾箱而不是收件箱不用太慌第一次发送的新账号容易被服务商误判你手动点一下这不是垃圾邮件加上后续正常发送频率一般会慢慢恢复正常。3.2 带 HTML 正文和附件的标准发送模板纯文本够用但实际中我们经常要发包含表格、图表的 HTML 邮件或者挂上报表附件。这里我给一个可以直接套用的完整模板import os import smtplib from email.message import EmailMessage from email.header import Header from email.utils import formataddr from dotenv import load_dotenv load_dotenv() def send_email(to_list, subject, html_content, attach_pathsNone): SMTP_SERVER os.getenv(SMTP_SERVER) SMTP_PORT int(os.getenv(SMTP_PORT, 465)) MAIL_USER os.getenv(MAIL_USER) MAIL_AUTH_CODE os.getenv(MAIL_AUTH_CODE) msg EmailMessage() # 发件人显示名称可以自定义避免裸邮箱看着像群发机器人 msg[From] formataddr((str(Header(数据机器人, utf-8)), MAIL_USER)) msg[To] ,.join(to_list) msg[Subject] subject # 允许正文是HTML格式注意配合模板不要插入外部不可信内容 msg.add_alternative(html_content, subtypehtml) # 添加附件 for path in attach_paths or []: filename os.path.basename(path) with open(path, rb) as fp: # maintype 和 subtype 根据实际文件类型填写 msg.add_attachment( fp.read(), maintypeapplication, subtypeoctet-stream, filenamefilename, ) with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout30) as server: server.login(MAIL_USER, MAIL_AUTH_CODE) server.send_message(msg)使用这个模板时的注意事项msg.add_alternative(html_content, subtypehtml)的奥妙在于它会把纯文本和 HTML 内容组合成 MIME 的 alternative 类型。收件人的邮件客户端如果支持 HTML 就显示 HTML不支持就显示纯文本。实际中你也可以先set_content(纯文本兜底)再加add_alternative这样兼容性最好。附件超过邮箱服务商限制常见 25MB 或 50MB会被服务器拒绝建议先压缩成 zip 或用网盘链接替代。文件名如果存在中文直接用filename7月报表.xlsx在多数场景没问题但如果对方邮箱解析异常可以显式用Header编码文件名。3.3 多人发送、抄送、密送的准确处理自动发送中另一个高频需求是同时发给一批人。直接把多个地址用逗号拼进msg[To]是能工作的但有更规范的处理方式msg[To] aexample.com, bexample.com msg[Cc] cexample.com msg[Bcc] dexample.com with smtplib.SMTP_SSL(...) as server: server.login(MAIL_USER, MAIL_AUTH_CODE) msg[To] , .join(to_list) if cc_list: msg[Cc] , .join(cc_list) if bcc_list: msg[Bcc] , .join(bcc_list) server.send_message(msg)这个点有必要讲透Cc是抄送所有收件人都可以看到抄送对象Bcc是密送收件人看不到这些地址。如果你批量给一群客户发定制内容千万不能用To把所有人都暴露出来要用独立循环逐一发送设置好每封邮件的收件人字段。这也是大家在群发邮件时容易被忽略的隐私问题。3.4 发送环节的一些细节与风险控制我这几年在发信环节遇到过形形色色的问题挑几个最有代表性的讲一下。邮箱字段注入要当心。如果你发的邮件里收件人或主题来自用户输入哪怕只是内部系统表单也要先清洗\r和\n这两个字符。因为 SMTP 协议里换行符是头字段的结束标记一旦有恶意者把换行符塞进主题就可能拼出额外的邮件头。内部用还好但如果做的是工具平台这就是一个必须防的攻击点。简单处理方式是做一次替换subject subject.replace(\r, ).replace(\n, )发送频率也需要控制。每天同一账号大量发信会瞬间触发服务商的流控返回类似发送过于频繁或直接拒绝登录的错误。我的经验是单次任务不要超过几十封宁可分成多个时间段发送也不要一口气轰炸。发送后记得检查返回值有些服务商不是即时拒绝而是接收了但实际进垃圾箱。如果你在用脚本给客户发重要通知可以在邮件正文和标题里避免堆砌免费促销等高频垃圾词否则很容易被服务商的风控误伤。日志也值得一记。无论是成功还是失败至少要把时间、收件人、主题、异常类型写到日志文件里。否则出问题时你只能靠猜根本定位不了是哪个收件人、哪个环节挂了。4. 接收邮件拉取未读、解析正文、下载附件4.1 收信协议选择为什么我推荐 IMAP 而不是 POP3很多人学到这里会犯的选择困难症是POP3 和 IMAP 到底用哪个直接说结论在绝大多数自动化收信场景下IMAP 更合适。特性IMAPPOP3服务器邮件保留默认保留部分实现下载后删除或标记删除多设备同步状态全同步不同设备各自为政邮件夹操作支持服务器端目录不支持已读/未读状态可同步可操作基本不可控Python 标准库imaplibpoplib原因很好理解如果你的脚本用 POP3 把邮件拉下来但服务器端的原信被删除了那么你手机上就再也看不到这封邮件了。而 IMAP 是服务端为主的协议脚本读取后如果不主动删除邮件依然原封不动地留在服务器上手机上也能正常看到。我个人的习惯是脚本只读取和归档从不主动删除邮件避免误删造成不可逆损失。4.2 用 imaplib 拉取符合条件的邮件在实际收信之前先给一段拉取最近 1 天未读邮件的基础代码。注意 IMAP 搜索条件要用服务端的日期格式而且返回的是邮件编号列表不是正文本身import os import imaplib import email from email.header import decode_header from datetime import date, timedelta from dotenv import load_dotenv load_dotenv() IMAP_SERVER os.getenv(IMAP_SERVER) IMAP_PORT int(os.getenv(IMAP_PORT, 993)) MAIL_USER os.getenv(MAIL_USER) MAIL_AUTH_CODE os.getenv(MAIL_AUTH_CODE) def decode_mime_header(value): if value is None: return parts decode_header(value) result [] for text, charset in parts: if isinstance(text, bytes): text text.decode(charset or utf-8, errorsreplace) result.append(str(text)) return .join(result) since_date (date.today() - timedelta(days1)).strftime(%d-%b-%Y) with imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT) as conn: conn.login(MAIL_USER, MAIL_AUTH_CODE) conn.select(INBOX) # 搜索最近一天且未读的邮件 status, data conn.search(None, (UNSEEN SINCE {}).format(since_date)) if status ! OK: raise RuntimeError(搜索邮件失败) msg_ids data[0].split() print(f共拉取到 {len(msg_ids)} 封邮件) for num in reversed(msg_ids): # 从新到旧处理 status, msg_data conn.fetch(num, (RFC822)) if status ! OK: continue raw_email msg_data[0][1] msg email.message_from_bytes(raw_email) subject decode_mime_header(msg.get(Subject)) sender decode_mime_header(msg.get(From)) print(f主题: {subject} | 发件人: {sender})这里有一个常常搞晕新手的坑imaplib返回的数据格式[(b..., bRFC822...), b)]因为邮件正文里可能包含换行、非 ASCII 字符服务器会分片返回取msg_data[0][1]才是完整的原始邮件字节流。如果网络不好或邮件超大分片可能不止一段稳妥的做法是把所有分片拼起来再解析。4.3 解析正文和附件不要直接用正则去抠字符串邮件解析最忌讳的做法是把原始邮件当作普通字符串用正则去匹配正文。因为 MIME 结构下正文和附件被多层嵌套的 boundary 分隔用正则极易匹配错。正确姿势是用 Python 官方email库遍历邮件的 MIME 树。def extract_content_and_attachments(msg, download_dirattachments): content_text attachments [] if msg.is_multipart(): for part in msg.walk(): content_disposition str(part.get(Content-Disposition, )) filename part.get_filename() if filename: filename decode_mime_header(filename) if content_disposition and attachment in content_disposition: # 附件分段逐个保存 data part.get_payload(decodeTrue) attachments.append((filename, data)) elif part.get_content_type() text/plain: payload part.get_payload(decodeTrue) charset part.get_content_charset() or utf-8 content_text payload.decode(charset, errorsreplace) else: # 非 multipart 的单段邮件 payload msg.get_payload(decodeTrue) charset msg.get_content_charset() or utf-8 if msg.get_content_type() text/plain: content_text payload.decode(charset, errorsreplace) for filename, data in attachments: os.makedirs(download_dir, exist_okTrue) path os.path.join(download_dir, filename) with open(path, wb) as f: f.write(data) print(f已保存附件: {path}) return content_text, attachments需要特别说明的是Content-Disposition的判断。邮件里不只是附件这一种附加内容还可能有内嵌图片比如 HTML 邮件里引用的小图。正常的附件头里会带attachment而内嵌图片是inline。如果你不加判断把inline的图片也当成附件下载收件箱里的营销邮件会给你下载一堆 logo 和图。上面的代码已经处理了这一点只对真正标成 attachment 的部件进行保存。解析时还要注意当邮件正文是 HTML 但没有text/plain版本时下载下来的正文会是空的。如果你想提取 HTML 里的可见文字需要额外用BeautifulSoup之类的工具取文本或者退而求其次直接保存 HTML 原文。对于自动化归档场景通常把 HTML 原样保存作为快照就够用了。4.4 一个可落地的例子自动下载指定发件人的日报附件把前面的知识串起来做一个能真正落地的脚本。功能是每天定时登录邮箱下载指定发件人最近一天邮件里的所有 Excel 附件并按发件人和日期保存到本地。def download_attachments_from_sender(sender_email_keyword, directorydaily_reports): since_date (date.today() - timedelta(days1)).strftime(%d-%b-%Y) with imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT) as conn: conn.login(MAIL_USER, MAIL_AUTH_CODE) conn.select(INBOX) # 条件可以组合FROM 表示发件人SINCE 表示日期范围 search_criteria f(FROM {sender_email_keyword} SINCE {since_date}) status, data conn.search(None, search_criteria) if status ! OK: return msg_ids data[0].split() total 0 for num in msg_ids: status, msg_data conn.fetch(num, (RFC822)) if status ! OK: continue raw b for part_tuple in msg_data: if isinstance(part_tuple, tuple): raw part_tuple[1] msg email.message_from_bytes(raw) subject decode_mime_header(msg.get(Subject)) print(f处理邮件: {subject}) extract_content_and_attachments(msg, directory) total 1 print(f共处理 {total} 封邮件)这里有一点容易踩坑如果你搜索条件里同时带FROM和SINCE语法必须写成(FROM xxx SINCE date)而不是用 JSON 那样的花括号。IMAP 搜索语法还有UNSEEN、SUBJECT、BODY、BEFORE、ON、TO、CC等条件可以组合成非常精细的筛选。如果你需要处理几百上千封历史邮件建议先用小范围测试搜索条件确认筛选结果没问题后再放开全部邮件的处理防止误归档。5. 把自动化接到真实场景定时任务和可靠性设计5.1 定时运行方案Linux cron 和 Windows 任务计划脚本能跑通只是第一步真正让自动化发挥价值的是让它定时自己跑。根据项目部署的操作系统不同有两种最主流的方案。在 Linux 服务器上用crontab -e打开任务表加一行# 每天9点整执行发日报脚本 0 9 * * * cd /opt/auto_email /opt/auto_email/venv/bin/python send_report.py logs/send_report.log 21注意我在命令里写的是虚拟环境里 Python 的绝对路径而不是直接写python。原因在于 crontab 执行时的 PATH 环境和终端不一样直接写python可能找不到解释器或者找到系统自带的旧版本。日志重定向也很关键21能把错误输出和标准输出合并追加到日志文件方便事后翻查。在 Windows 上我常用的做法是任务计划程序新建任务触发器选每天 9:00操作里启动程序填 Python 解释器路径参数填脚本路径起始于填脚本所在目录。这里同样建议填绝对路径不要依赖相对路径否则脚本里基于相对路径的文件操作容易找错目录。如果你的脚本需要长期运行而不是每天跑一次也可以在自己的代码里加一个循环加time.sleep的方式实现简单调度但这样会占用一个常驻进程异常退出后除非配合守护工具否则很不稳定。相比之下 cron 和系统任务计划更简单可靠。5.2 邮件发送失败的重试机制邮件发送依赖网络和服务商瞬时抖动在所难免。一个成熟的自动发送模块应该具备合理的重试能力但重试不是简单地疯狂重复那样只会加重服务商负载甚至把账号搞封。常用的重试策略是指数退避第一次失败后等几秒再试第二次多等一会儿一般五到十次后放弃并告警。示例代码如下import time import smtplib def send_with_retry(msg, max_retries5): for attempt in range(max_retries): try: with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout30) as server: server.login(MAIL_USER, MAIL_AUTH_CODE) server.send_message(msg) return True except (smtplib.SMTPServerDisconnected, TimeoutError, ConnectionError): wait 2 ** attempt print(f第 {attempt 1} 次尝试失败{wait} 秒后重试) time.sleep(wait) return False但要注意并不是所有异常都值得重试。比如SMTPAuthenticationError表示授权码错了你重试一万次也不会成功还会加剧封号风险。所以正确做法是只对超时、断连、连接重置这类瞬时异常做指数退避重试对认证错误、收件人格式错误这类业务错误直接抛出或记录。重试之后再失败就要考虑把问题暴露出来。我通常的做法是设置一个二次告警通道如果邮件发送模块自身也失败了就通过企业微信机器人或短信平台发一条告警让值班人员人工介入。因为如果自动化发不了的邮件无人知晓那比不发更危险——你以为大家收到了实际上没有人收到。5.3 日志、配置与依赖管理这个项目不复杂但保持整洁同样重要。日志我会用 Python 自带的logging模块输出到文件和控制台关键内容包括发送结果、拉取数量、异常堆栈。日志不仅方便事后排查也能在邮箱服务商反馈有异常登录时帮你快速定位是不是自己的脚本在哪个机器上跑偏了。配置方面前文已经提到了.env文件。依赖管理虽然这个项目标准库占大头但如果你后续加入了requests比如发送 HTTP webhook、pandas读取报表数据记得用pip freeze requirements.txt把版本锁定下来。否则过半年在新机器上部署时会发现别的版本行为已经悄悄变了。6. 常见问题与排查技巧实录6.1 我遇到的典型报错和处理方式这个项目能跑通不难但期间会遇到一些很经典的报错。我把它们整理成一张速查表大家可以按图索骥异常或现象常见原因处理方案smtplib.SMTPAuthenticationError授权码不对、未开启 SMTP 服务、账号输入错误重新登录网页邮箱生成授权码用新码测试ssl.SSLError: WRONG_VERSION_NUMBER端口和协议不匹配比如用 SSL 连接了 587 端口或普通 SMTP 连了 465判断你用的是 SMTPS465还是 STARTTLS587ConnectionRefusedError端口不通可能是本地防火墙或服务商禁用了该端口用telnet smtp.xxx.com 465测试连通性socket.timeout网络不稳定或服务器响应慢把 timeout 调大一些或者检查 DNS 是否正常imaplib.IMAP4.error: bLOGIN failedIMAP 授权码独立于 SMTP可能未开启 IMAP 服务到邮箱设置里确认 IMAP 已开启并生成对应授权码发送成功但对方一直没收到被对方服务商拦截为垃圾邮件或发件人 ID 未配置 SPF/DKIM检查垃圾箱在域名邮箱里配置 SPF、DKIM 记录收到的邮件主题乱码邮件头编码格式不正确统一用 Header 对非 ASCII 文本编码fetch 出来的邮件正文不完整大邮件被分片传输只取了第一片遍历 data 拼接所有 tuple 中的字节再解析搜索邮件一直返回空结果日期格式错误或搜索条件里引号位置不对IMAP 日期格式必须是01-Jan-2024确认条件写法6.2 高效调试的思路先手工再脚本遇到问题时我强烈建议先用最简单的方式确认服务器可用再回来查脚本。对于 SMTP可以在没有 Python 的情况下用openssl s_client验证服务器端口是否能正常完成 TLS 握手openssl s_client -connect smtp.example.com:465 -quiet如果这个命令能顺利连上并提示协议信息说明服务器和端口没问题问题大概率在代码层。对于 IMAP也可以在 Python 交互式环境里逐条执行conn.login、conn.select、conn.search这样能直观看见每一步服务器的响应码比一上来就写完整脚本要容易定位得多。邮箱服务商的网页端也经常提供最近登录/发信记录当你被拒绝登录时去网页端查看是否有异常提示或风控验证能更快发现是不是账号被临时限制。如果确认授权码没有问题可能就是触发了风控等待一段时间或去网页端重新验证一下身份就好。6.3 几条很少有人讲但很实用的避坑技巧第一所有的脚本第一版跑通前先拿自己的小号或者一个不重要的邮箱当测试目标。不要直接给老板或客户发测试邮件万一把垃圾邮件写错了体验非常糟糕。我通常会在脚本里配置一个环境变量TEST_MODE设置为True时把所有收件人强制替换成测试邮箱等确认无误后再关掉。第二附件保存时不要直接用原始文件名拼接路径。一方面原始文件名可能包含奇怪的路径分隔符或非法字符另一方面同名文件会互相覆盖。我的做法是在文件名前加时间戳或邮件编号前缀避免误覆盖。第三对于 HTML 邮件尽量不要把用户输入的文本不经过转义就拼进 HTML 模板。否则遇到用户输入了script标签或特殊符号邮件的显示效果会出问题甚至可能触发收件方安全网关的警告。内部邮件可能还无所谓但如果是正式的客户通知这会引起不必要的麻烦。7. 从收发邮件延伸到更大一点的自动化场景自动收发邮件本身是一个入口。它最大的价值不是发一封报表这么简单而是能当做一个事件引擎让邮件系统和其他业务系统联动起来。比如你可以在服务器上部署一个常驻进程用 IMAP 每隔几分钟轮询一次收件箱。当收到标题以【自动指令】开头的邮件时脚本解析后续内容触发预先定义好的一系列操作把邮件里的附件存储到网盘、运行数据分析脚本、把结果写成报告并回复给发件人。这个模式本质上是邮箱即命令入口如果你经常在外面无法访问内部系统这样的邮件机器人能帮你完成简单的远程任务。还可以把邮件作为通知出口接上监控系统。我的经验是真正好用的监控通知需要遵循分级原则普通任务完成发一封邮件即可任务失败要立刻发告警严重故障则要同时触发多渠道通知。如果所有场景都只发一封普通邮件收件人很快就会把自动化邮件当成狼来了失去关注度。做这些扩展时第一优先级的永远是合规和账号安全。邮箱服务商对自动登录、发信频率都有各自的策略不清楚之前先查阅官方文档。频率过高容易被封内容不规范容易被标记垃圾邮件这些前面都反复提到了。不要因为自动化而把账号置于风险之中。8. 我自己的几点体会做自动收发邮件这个功能技术难点其实不高最考验人的是对邮件协议细节的理解和对工程稳定性的安排。把脚本跑通一次只是第一步能够让它稳定运行半年不出问题才是真正值得骄傲的事情。我个人最推荐的实践路径是先从一个足够小的真实需求出发。哪怕就是每天早上把我昨晚生成的日报发给领导这样简单的需求也远比空写十遍示例代码有效。因为真实需求会逼你面对收件人写错、附件超限、周末不要打扰对方、自己和同事换邮箱导致授权码变更等一连串真实的问题这些问题的解决方案才是这个项目的精华所在。最后再分享一个小技巧如果你不清楚自己的代码是哪个环节出了问题先把邮箱服务商客服页面和官方文档的客户端设置说明找出来逐行核对一遍服务器地址、端口、授权码八成以上的问题都出在那张配置表上。搞定了这些Python 自动收发邮件这个技能基本就是你的囊中之物了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价