办公自动化里的“邮件发送服务”,说实话没有想象中那么玄。核心就三件事:把要“发出去的内容”拼好,交给 SMTP 服务器,再由 SMTP 服务器投递到收件人的邮箱。对 Python 开发者来说,标准库里的smtplib和email就已经覆盖了大部分办公场景:自动发日报、系统告警、批量通知、报表分发,都可以不用花一分钱搞定。
如果你正在折腾 Python 办公自动化,或者想把“发邮件”这个动作从手动操作里去掉,这篇内容很适合收藏。我会从最简的纯文本邮件开始,一路写到 HTML 邮件、附件发送、代码封装、HTTP 接口、批量任务、异常重试,再给一张常见问题排查表。过程中使用的都是标准库和少量第三方库,不需要自己维护复杂的邮件服务器。
这次没有用很重的一键包,而是把一个容易被忽略的问题摆在前面:邮箱服务商提供的不是“登录密码”,而是 SMTP 授权码。授权码怎么拿、代码里怎么配、哪些错误源于配置不对,都会在下面讲清楚。
1. 邮件发送服务核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目用途 | 用 Python 编写办公自动化邮件发送服务,覆盖文本邮件、HTML 邮件、附件和批量通知 |
| 核心协议 | SMTP / SMTP_SSL / STARTTLS |
| 开发语言 | Python 3.8+ |
| 主要依赖 | 标准库smtplib、email,可选Flask、openpyxl、APScheduler |
| 配置方式 | 环境变量或独立配置文件 |
| 服务形态 | 可函数调用、可命令行执行、可封装 HTTP API |
| 批量任务 | 支持读取 Excel / CSV 收件人列表并循环发送 |
| 定时运行 | 可用系统计划任务或 APScheduler 触发 |
| 显存 / GPU 占用 | 不使用 GPU |
| 运行成本 | 可部署在任何能运行 Python 的服务器上 |
表格里没有写具体邮箱服务商的端口,是因为不同服务商的加密方式不同。最常用的是465 端口 + SSL和587 端口 + STARTTLS,后面的环境准备章节会展开区分。
2. 适用场景与使用边界
邮件发送服务适合的办公场景非常明确。比较典型的一类是“周期性任务”:每天把数据库查询结果、运营报表或自动化测试报告发送给固定人群;另一类是“事件触发任务”:程序出现异常时给运维发告警邮件,或者用户提交工单后自动发送受理回执。把邮件发送功能封装好之后,任何 Python 程序都可以用三五行代码接入,这是它在自动化体系里最实用的地方。
不适合的场景也很清楚:如果需要发送营销类邮件、大附件批量群发或高频率通知,免费邮箱服务商通常会有严格的频率限制,发多了会被拦截。这时候更适合接入专门的企业邮件服务或第三方邮件 API。另外,“回复复杂邮件、管理收件箱、解析对方回复内容”这类交互需求也不是smtplib的强项,应该交给更大的平台,或者使用 IMAP 协议做进一步处理。
使用边界方面要特别注意合规。用 Python 发邮件给别人之前,必须确认对方知情并同意接收,邮件底部最好加上“如需退订请联系管理员”之类的说明。涉及内部报表时要注意收件人名单是否属于敏感信息;不要把大量收件人的真实邮箱地址放在“收件人”一栏导致互见,内部通知尽量使用 BCC 密送方式。
3. 环境准备与配置
3.1 基础环境
开发机上需要有 Python 3.8 或更高版本。Windows 用户建议直接去 python.org 下载安装包,安装时勾选“Add Python to PATH”;Linux 用户则先确认系统自带版本,如果偏低,可以用apt install python3或者源码编译。
不建议直接在系统 Python 里安装第三方依赖。先建一个独立虚拟环境,可以避免后续多个项目互相干扰。如果你之前没有用过虚拟环境,下面的命令可以当作模板:
# Windows python -m venv venv venv\Scripts\activate # macOS / Linux python3 -m venv venv source venv/bin/activate pip install --upgrade pip3.2 需要的依赖包
标准库部分不需要额外安装,它们是 Python 安装包里自带的:
smtplib:负责连接 SMTP 服务器并执行发送。email.mime:负责构造符合 MIME 标准的邮件主体。email.utils:负责生成合法的邮件头和格式化地址。
后面的章节如果要把邮件发送能力暴露成 HTTP 接口,还需要安装一个轻量 Web 框架,例如 Flask:
pip install flask openpyxlopenpyxl用于读取 Excel 收件人列表。收件人数据如果保存在 CSV 里,甚至不需要这个库,直接用 Python 内置的csv模块就够了。
3.3 邮箱服务商 SMTP 参数
主流邮箱服务商普遍支持 SMTP 发信,但一般不允许直接用“账号密码”登录。你需要在邮箱设置里开启 SMTP 服务,并生成一个专门的“授权码”,代码里用这个授权码替代登录密码。
配置前要准备五类信息:
| 参数 | 含义 | 示例 |
|---|---|---|
| SMTP_HOST | 邮件服务商提供的服务器地址 | smtp.qq.com、smtp.163.com |
| SMTP_PORT | 发送端口 | 465 或 587 |
| SMTP_USER | 邮箱账号 | yourname@example.com |
| SMTP_PASS | 授权码,不是邮箱登录密码 | 服务商设置页生成 |
| USE_SSL | 是否使用 SMTP_SSL | 以服务商文档为准 |
常见的端口规则是:465端口通常要求全程 SSL,587端口通常先普通连接,再用starttls()升级为加密连接。有的服务商两种方式都支持,有的只开放其中一个。写代码时最好把参数抽成配置,不要硬编码在发信函数里。
3.4 授权码获取流程
以个人邮箱为例,绝大多数服务商的做法是:登录网页端邮箱 → 设置 → 账户 → 开启 SMTP 服务 → 验证手机号 → 生成授权码。授权码通常是一段 16 位左右的字符串,生成后立刻保存,因为部分平台只显示一次。需要注意的是,授权码已经等同于邮箱的“发信权限”,不适合写在公开代码仓库里。
如果你们公司有自己的企业邮局,那么 SMTP_HOST 通常是内网地址或企业邮箱域名,具体参数需要问邮件系统管理员。这里强调一点:优先让管理员提供“专门用来发系统通知的邮箱账号”,而不是拿某个员工的个人邮箱去跑自动任务,否则离职或改密码会导致所有自动化通知中断。
4. 最小可运行的纯文本邮件
先实现一个能跑通的最小示例。代码不依赖于任何第三方库,只负责把一封纯文本邮件通过SMTP_SSL发送出去。项目里新建文件send_text_mail.py:
import os import smtplib from email.mime.text import MIMEText from email.utils import formataddr SMTP_HOST = os.getenv("SMTP_HOST", "smtp.example.com") SMTP_PORT = int(os.getenv("SMTP_PORT", "465")) SMTP_USER = os.getenv("SMTP_USER", "user@example.com") SMTP_PASS = os.getenv("SMTP_PASS", "your-auth-code") def send_text_mail(to_email: str, subject: str, content: str) -> None: msg = MIMEText(content, "plain", "utf-8") msg["From"] = formataddr(("通知机器人", SMTP_USER)) msg["To"] = formataddr(("收件人", to_email)) msg["Subject"] = subject with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=10) as server: server.login(SMTP_USER, SMTP_PASS) server.sendmail(SMTP_USER, [to_email], msg.as_string()) if __name__ == "__main__": send_text_mail( to_email="receiver@example.com", subject="Python 邮件发送测试", content="这是一封来自 Python 办公自动化脚本的测试邮件。" ) print("邮件发送完成")这段代码有几个地方需要你按实际环境替换:
- 环境变量里的
SMTP_HOST、SMTP_PORT、SMTP_USER、SMTP_PASS。 receiver@example.com换成自己的测试收件箱。
建议第一次测试不要直接用真实客户邮箱,而是发给自己另一个邮箱,或者在同一邮箱的收件箱中确认能正常收到。发送成功后,终端会打印“邮件发送完成”,收件箱里应该能收到标题为“Python 邮件发送测试”的邮件。
如果你使用的服务商只开放587端口,那么发送部分要改成这样:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as server: server.starttls() server.login(SMTP_USER, SMTP_PASS) server.sendmail(SMTP_USER, [to_email], msg.as_string())这两种写法的差别只在于是否在建立连接后执行starttls()。其他逻辑不变。如果你不确定服务商支持哪种方式,先查官方文档,再用最小脚本分别测试。
4.1 如何验证发送成功
运行脚本之后,不要只看终端输出。最好的验证方式是:
- 在收件箱找到该邮件。
- 检查是否进入了垃圾箱,如果进入了垃圾箱,说明正文或发件域名可能触发了垃圾邮件规则。
- 查看完整邮件头信息,确认发送 IP、发送时间与 SMTP 服务器记录一致。
如果终端报错,常见的有三种:连接超时、登录失败、被拒收。出现“SMTPAuthenticationError”基本可以判断是授权码或服务器地址不正确;出现“Connection refused”则说明端口错误,或者服务器不允许当前网络访问。
4.2 运行参数抽取为配置项
办公脚本长期使用后,会发现一个问题:邮箱账号可能变、收件人列表可能变、邮件正文模板可能变。如果全部写在代码里,每次改动都要找人改代码,非常不方便。所以建议把配置抽到环境变量,或者独立文件里。在项目根目录放一个.env不一定是标准做法,但确实比较直观。我们可以用更朴素的config.py:
SMTP_CONFIG = { "host": os.getenv("SMTP_HOST", "smtp.example.com"), "port": int(os.getenv("SMTP_PORT", "465")), "user": os.getenv("SMTP_USER", "user@example.com"), "password": os.getenv("SMTP_PASS", "your-auth-code"), } DEFAULT_FROM_NAME = "办公自动化通知" DEFAULT_SENDER = SMTP_CONFIG["user"]这样主业务代码调用时只获取配置对象,不会把敏感变量散落到处都是。
5. 升级:HTML 邮件与附件
纯文本邮件能满足最基本的通知需求,但在办公场景里,日报、图表、Excel 附件往往是刚需。Python 的email.mime模块可以轻松构造一封同时包含 HTML 正文和多个文件的邮件。
5.1 发送 HTML 邮件
HTML 邮件的正文可以包含表格、颜色、图片链接,适合用来展示运营数据摘要。将上面示例里创建MIMEText的plain改成html,即可把内容按 HTML 渲染:
import smtplib from email.mime.text import MIMEText from email.utils import formataddr html_content = """ <html> <body> <h3>业务日报</h3> <table border="1" cellpadding="6" style="border-collapse:collapse;"> <tr><th>指标</th><th>今日值</th></tr> <tr><td>订单数</td><td>1024</td></tr> <tr><td>新增用户</td><td>256</td></tr> </table> <p>详情请查看附件中的 Excel 报表。</p> </body> </html> """ msg = MIMEText(html_content, "html", "utf-8") msg["From"] = formataddr(("日报机器人", "sender@example.com")) msg["To"] = formataddr(("接收人", "receiver@example.com")) msg["Subject"] = "07 月 25 日业务日报"需要同时支持纯文本和 HTML 时,通常的做法是构造MIMEMultipart("alternative"),先添加纯文本版本,再添加 HTML 版本。这样老旧邮件客户端可以显示纯文本,新客户端可以显示 HTML。不过办公内网场景一般不会太在意这一点,多数时候直接发 HTML 就够了。
5.2 添加附件
发送带附件的邮件时,邮件对象要使用MIMEMultipart,而不是直接创建MIMEText。下面演示发送一封包含 Excel 报表附件的邮件:
import os import smtplib from email import encoders from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr SMTP_HOST = "smtp.example.com" SMTP_PORT = 465 SMTP_USER = "sender@example.com" SMTP_PASS = "your-auth-code" def send_mail_with_attachment(to_email: str, subject: str, html: str, attachment_path: str): msg = MIMEMultipart() msg["From"] = formataddr(("报表机器人", SMTP_USER)) msg["To"] = formataddr(("收件人", to_email)) msg["Subject"] = subject msg.attach(MIMEText(html, "html", "utf-8")) if attachment_path and os.path.isfile(attachment_path): with open(attachment_path, "rb") as fp: part = MIMEBase("application", "octet-stream") part.set_payload(fp.read()) encoders.encode_base64(part) filename = os.path.basename(attachment_path) part.add_header( "Content-Disposition", "attachment", filename=("utf-8", "", filename), ) msg.attach(part) with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=15) as server: server.login(SMTP_USER, SMTP_PASS) server.sendmail(SMTP_USER, [to_email], msg.as_string()) if __name__ == "__main__": send_mail_with_attachment( to_email="receiver@example.com", subject="今日业务日报", html="<p>今日日报详见附件,请查收。</p>", attachment_path="./report_20250725.xlsx", )附件部分的核心是MIMEBase。先读取二进制文件,再做 Base64 编码,这样邮件数据才能安全传输。中文文件名需要通过filename=("utf-8", "", filename)方式处理,否则部分客户端打开附件时会出现乱码。
如果你的收件人一次需要多个附件,可以在一个循环里读取多个文件并多次msg.attach(part)。要注意一点:附件总大小过大会导致发送变慢,也容易被邮箱服务商拦截。办公场景通常建议附件不超过 10MB,超大文件应该走企业网盘并发送分享链接。
5.3 在邮件正文里嵌入图片
HTML 邮件如果需要嵌入图片而不是引用外链图片,可以使用MIMEMultipart("related")容器,再把图片作为独立MIMEImage对象添加,并通过Content-ID引用:
from email.mime.image import MIMEImage msg = MIMEMultipart("related") msg.attach(MIMEText('<img src="cid:chart001">', "html", "utf-8")) with open("chart.png", "rb") as fp: image = MIMEImage(fp.read()) image.add_header("Content-ID", "<chart001>") msg.attach(image)使用cid:方式嵌入的图片不会依赖外部网络地址,适合邮件接收者对隐私比较敏感的办公场景。不过这种邮件整体体积也会变大,不建议嵌太多大图。
6. 把发送代码封装成可复用服务
上面几节代码已经能发纯文本、HTML 和附件邮件,但直接调用还是不够工程化。一个办公自动化系统里,发邮件动作可能在很多地方被触发,比如定时任务跑完要发报表、程序 catch 到异常要发告警、用户点击按钮后要发确认邮件。如果每个模块各自构造smtplib连接,代码会非常冗余,而且在连接管理上容易出问题。
建议写一个独立模块mail_service.py,把公共逻辑收拢成类:
import os import smtplib import time from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr from typing import List, Optional class MailService: def __init__(self, host: str, port: int, user: str, password: str, use_ssl: bool = True): self.host = host self.port = port self.user = user self.password = password self.use_ssl = use_ssl def _build_message(self, to_list: List[str], subject: str, html: str = "", text: str = "") -> str: msg = MIMEMultipart("alternative") msg["From"] = formataddr(("系统通知", self.user)) msg["To"] = ", ".join(to_list) msg["Subject"] = subject if text: msg.attach(MIMEText(text, "plain", "utf-8")) if html: msg.attach(MIMEText(html, "html", "utf-8")) return msg.as_string() def _get_server(self): if self.use_ssl: return smtplib.SMTP_SSL(self.host, self.port, timeout=15) server = smtplib.SMTP(self.host, self.port, timeout=15) server.starttls() return server def send(self, to_list: List[str], subject: str, html: str = "", text: str = "") -> None: message = self._build_message(to_list, subject, html, text) server = self._get_server() try: server.login(self.user, self.password) server.sendmail(self.user, to_list, message) finally: server.quit()这里为什么要用属性把 SMTP 连接参数放到实例里?因为一个系统里可能有多套邮件账号:业务通知用一个账号、运维告警用另一个账号。通过MailService("smtp1.example.com", ...)和MailService("smtp2.example.com", ...)可以轻松创建多个发送实例,不会互相干扰。
调用的时候也很简单:
service = MailService( host="smtp.example.com", port=465, user="sender@example.com", password="auth-code", ) service.send( to_list=["manager@example.com"], subject="系统部署完成", html="<p>新版本已于 <b>10:30</b> 部署完成。</p>", )这时再配合上一节提到的附件逻辑,就可以把附件参数、图片参数逐步扩展到build_message里。先保住“能发出来”,再做复杂度扩张,这是封装邮件服务时比较稳妥的演进方式。
7. REST API 与批量任务
7.1 为什么需要暴露为 HTTP 接口
很多办公系统不是 Python 单体程序。比如你有一个用 Java 写的工单系统,业务人员点击按钮后希望自动给客户发邮件,但团队不想在 Java 项目里再引入一套邮件依赖。这时候如果单独维护一个“邮件发送服务”,提供 HTTP 接口,其他语言都可以通过简单 POST 请求调用。
这里用 Flask 写一个最小可用的 REST 接口。安装依赖:
pip install flask然后创建mail_api.py:
from flask import Flask, request, jsonify from mail_service import MailService app = Flask(__name__) service = MailService( host="smtp.example.com", port=465, user="sender@example.com", password="your-auth-code", ) @app.post("/api/mail/send") def send_mail(): payload = request.get_json(force=True) to_list = payload.get("to_list") subject = payload.get("subject") html = payload.get("html", "") text = payload.get("text", "") if not to_list or not subject: return jsonify({"code": 400, "message": "to_list 和 subject 不能为空"}), 400 try: service.send(to_list=to_list, subject=subject, html=html, text=text) return jsonify({"code": 0, "message": "发送成功"}) except Exception as exc: return jsonify({"code": 500, "message": str(exc)}), 500 if __name__ == "__main__": app.run(host="127.0.0.1", port=8000)启动服务:
python mail_api.py用 curl 测试:
curl -X POST http://127.0.0.1:8000/api/mail/send \ -H "Content-Type: application/json" \ -d '{ "to_list": ["receiver@example.com"], "subject": "接口测试邮件", "text": "如果收到这封邮件,说明邮件发送服务接口已跑通。" }'返回 JSON 里code为 0 时,说明发送成功。另一个语言调用时,只需要构造等价的 HTTP POST 请求即可。
这个接口还有一个很重要的现实问题:没有鉴权。如果服务监听在公网地址上,任何人都可以请求你的服务器发垃圾邮件,这会让你的 IP 被邮箱服务商加入黑名单。生产环境至少要加一个简单的 token 校验:
import os API_TOKEN = os.getenv("API_TOKEN", "change-me") @app.before_request def check_token(): if request.headers.get("X-Token") != API_TOKEN: return jsonify({"code": 401, "message": "unauthorized"}), 401启动时设置环境变量API_TOKEN,调用方在请求头里携带X-Token。如果只是内网服务,也建议限制监听地址为127.0.0.1或内网 IP,并用防火墙控制访问来源。
7.2 批量任务:从 Excel 读取收件人并发送
批量发送时最忌讳的是把收件人放到 Excel 里,又用人工复制粘贴方式去发信。自动化批量发送应该由脚本统一读取名单、循环调用发送服务。
准备一份recipients.xlsx,列结构可以类似这样:
| 姓名 | 邮箱 | 部门 |
|---|---|---|
| 张三 | zhangsan@example.com | 运营部 |
| 李四 | lisi@example.com | 技术部 |
批量发送脚本示例:
import openpyxl import time from mail_service import MailService service = MailService( host="smtp.example.com", port=465, user="sender@example.com", password="your-auth-code", ) workbook = openpyxl.load_workbook("recipients.xlsx") sheet = workbook.active for index, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): name, email, department = row[0], row[1], row[2] if not email: continue content = f"{name}你好,{department}第三季度考核结果已发布,请登录内部系统查看。" try: service.send( to_list=[email], subject="季度考核结果通知", text=content, ) print(f"[{index}] 发送成功: {email}") except Exception as exc: print(f"[{index}] 发送失败: {email}, error: {exc}") time.sleep(2)这段代码每发完一封就休息 2 秒,是为了避免触发邮箱服务商的频率限制。有些开发者为了追求速度会去掉sleep,结果发到第几十封时被服务商临时限制,导致后面全部失败。所以批量场景里,频率控制比发送速度重要得多。
不过需要注意,真实办公项目中一次给几百人发个性化内容,更适合先走邮件服务商提供的“批量模板”能力,而不是用个人 SMTP 逐封发送。个人邮箱 SMTP 通常不适合大批量营销,只适合内部人员数量可控的通知场景。
7.3 批量任务加日志和失败重试
批量发送最怕失败后没有办法追踪。简单做法是把每次发送结果写入本地日志文件:
import csv import time result_rows = [] for index, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): name, email = row[0], row[1] try: service.send(to_list=[email], subject="内部通知", text=f"{name} 您好,见信好。") result_rows.append([name, email, "success", ""]) except Exception as exc: result_rows.append([name, email, "failed", str(exc)]) finally: time.sleep(2) with open("send_result.csv", "w", newline="", encoding="utf-8-sig") as fp: writer = csv.writer(fp) writer.writerow(["姓名", "邮箱", "状态", "错误信息"]) writer.writerows(result_rows)把结果写成csv后,哪怕中途脚本被中断,也可以根据本地日志筛选出失败名单,重跑时只处理失败项。如果需要自动重试,可以在捕获异常后对单个邮箱尝试 2 到 3 次,每次间隔递增,比如第一次失败后等 5 秒,第二次失败后等 15 秒。要避免失败后无脑立刻重试,否则只是反复撞同一堵墙。
8. 定时发送与稳定性设计
办公自动化的很多邮件并不是“手动触发”,而是在固定时间自动触发。比如每天早上 9 点发日报、每周一早上发周报、每月 1 号发上月汇总。实现定时任务有两种最常用的思路,任选一种都行。
8.1 使用系统计划任务
在 Windows 上可以用“任务计划程序”定时执行 Python 脚本,在 Linux 上使用 crontab:
# 每天早上 09:00 执行发送任务 0 9 * * * cd /path/to/project && /usr/bin/python3 send_report.py >> send_log.log 2>&1这种方案的好处是进程不需要常驻内存,运行完就退出,节省资源。缺点是任务数量多了以后,管理分散在多个 cron 条目或计划任务里,依赖关系不直观。
8.2 使用 APScheduler 定时调度
Python 服务内部可以用APScheduler执行定时逻辑。安装依赖:
pip install apscheduler写一个常驻进程的应用:
from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.cron import CronTrigger from services.report_service import send_daily_report scheduler = BlockingScheduler() scheduler.add_job( send_daily_report, trigger=CronTrigger(hour=9, minute=0), id="daily_report", replace_existing=True, ) if __name__ == "__main__": scheduler.start()APScheduler 适合需要在同一进程内维护任务状态、或者任务之间有关联的场景,比如先查数据库再发邮件,一个函数里就能串起来。
8.3 稳定性设计思路
要让邮件发送服务在无人值守时也能稳定运行,有几个点值得按优先级处理:
第一,发送逻辑要设置 SMTP 连接超时。否则 SMTP 服务器无响应时,脚本可能卡住很久。
第二,单次任务处理多条邮件时,建议把任务拆成“读取数据”和“发送邮件”两个阶段。先完整读取收件人列表和报表数据,再进入发送循环,避免在读取阶段因邮件发送异常导致数据读取中断。
第三,重试机制要分级。网络闪断可以马上重试;认证失败不要重试,因为大概率是配置问题;服务商明确返回限频错误时,要等待一段时间后再继续。
第四,增加“任务执行结果”日志,每次发送任务完成后,输出一行包含任务名、成功条数、失败条数、耗时的结构化日志。告警时才能快速知道是哪个环节出了问题。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| SMTPAuthenticationError | 密码、授权码错误,或邮箱未开启 SMTP | 检查配置的授权码和账号是否一致 | 重新生成并配置 SMTP 授权码 |
| Connection refused / timeout | 端口错误或网络不通 | telnet 测试端口连通性 | 换 465/587 端口,确认服务商限制 |
| starttls 报错 | 服务器不支持 STARTTLS,或端口选错 | 查看服务商 SMTP 文档 | 改用 SMTP_SSL 方式 |
| 邮件发送到垃圾箱 | 正文含敏感词、发信频率过高、域名信誉低 | 用另一邮箱检查垃圾箱 | 优化正文措辞,降频,检查邮箱域名 |
| 收不到邮件 | 投递延迟或服务商拦截 | 查看发件方日志与退信 | 确认退信内容,检查收件地址 |
| 附件中文名乱码 | 没有按 RFC 2231 处理附件名 | 查看邮件头 Content-Disposition | 使用filename=("utf-8", "", filename) |
| 批量发送到一半失败 | 触发频率限制,或某收件地址无效 | 查看本地失败日志 | 增加 sleep,重试失败列表 |
| 接口返回 500 | 授权码失效、收件人列表为空、附件路径错误 | 查看服务端 traceback 日志 | 根据异常信息修改参数 |
这几种问题里,出现频率最高的是“能连上但登录失败”。如果检查过授权码没有复制错,可以再去邮箱服务商的设置页确认是否已经开启 SMTP 服务,以及账户是否处于安全限制状态。频繁尝试登录失败也可能触发服务商的安全保护,导致暂时无法登录。
大批量发送后收件人收不到邮件,则需要先找退信。大部分邮箱服务商发送失败后会把退信信息发到发件邮箱的“已删除”或“收件箱”里,里面通常会给出类似 550、554 的 SMTP 错误码。550 一般代表收件人不存在或对方拒收;554 多为发件方策略拒绝,可以隔一段时间再试,或者降低发送速度。
10. 最佳实践与使用建议
10.1 配置管理
把邮箱账号、授权码、SMTP 地址、收件人默认名单都放到代码外部。最简单的做法是使用环境变量,生产环境再引入配置中心。避免把真实授权码直接提交到 Git 仓库。
Windows 临时设置环境变量:
set SMTP_HOST=smtp.example.com set SMTP_PORT=465 set SMTP_USER=sender@example.com set SMTP_PASS=your-auth-code python send_report.pyLinux / macOS 临时设置环境变量:
export SMTP_HOST="smtp.example.com" export SMTP_PORT="465" export SMTP_USER="sender@example.com" export SMTP_PASS="your-auth-code" python send_report.py10.2 开发阶段使用假邮箱服务
刚开始开发时,建议先用本地调试工具mailhog、Mailpit或者smtp4dev这类假 SMTP 服务测试。它们的原理是在本地起一个 SMTP 服务端,邮件不会真实发送到外网,而是在网页里展示收到的邮件内容。这样调试邮件模板、附件和收件人列表时不会产生真实发送行为,更不会误发给真实用户。
安装这一类工具后,把SMTP_HOST设置为127.0.0.1,端口设为工具默认端口,即可让程序把邮件发到本地调试界面。确认模板和逻辑没有问题时,再切回企业邮箱 SMTP。
10.3 设置 BCC 密送
如果邮件要同时发给多人,而每个人不需要看到其他收件人地址,可以使用 BCC。在原来代码基础上增加:
msg["Bcc"] = ", ".join(bcc_list) # 发送时收件人地址应包含 BCC 地址 server.sendmail(SMTP_USER, to_list + bcc_list, msg.as_string())这里需要理解msg["To"]和sendmail的收件人参数不一致是正常的。邮件头里的地址给收件人查看,sendmail的地址才是 SMTP 实际投递对象。直接把大量邮箱放到msg["To"],会泄露收件人信息,办公通知场景建议尽量使用 BCC。
10.4 合规与授权提醒
编写邮件发送服务前,需要确保自己拥有发送对象的联系方式并取得合理授权。不要在未授权的情况下采集、批量发送邮件,也不要用来做垃圾广告。内部系统在发送前还应该再次检查收件人名单,避免因名单误配导致信息泄密。使用企业邮箱账号配置自动发送时,也要遵守公司邮件使用规范,不要用系统账号发送与工作无关的内容。
如果有“退订”需求,不要再发不带退订入口的批量邮件。面向外部用户的邮件服务通常需要完整的订阅管理和退订流程,这不是一个简单的smtplib脚本能替代的。
10.5 预留“测试模式”
在邮件服务里增加开关TEST_MODE。测试模式下,不管收件人列表填什么,实际都只发送到预先设置的白名单邮箱。开启这个开关后,开发调试时就不怕误发邮件给真实用户。
TEST_MODE = True TEST_RECEIVER = "qa@example.com" if TEST_MODE: actual_to_list = [TEST_RECEIVER] else: actual_to_list = payload_to_list邮件标题可以统一加上前缀【测试】,方便识别。上线前关闭测试模式,然后做一轮真实发信验证即可。
10.6 观察日志和运行状态
一个没有日志的邮件服务等于随时会出问题但无法定位的服务。推荐在每次发送时记录:任务 ID、收件人数、成功数、失败数、耗时、异常摘要。为了保险,把最近一次任务的状态写到一个文件里,再配合其他监控系统检查。这样“邮件服务是不是还在正常工作”这个问题,就有了可观测的判断依据。
办公自动化里的邮件发送服务做到这个程度,已经可以支撑大多数内部通知和报表分发需求了。如果你手头正缺这样的模块,建议先把纯文本最小示例跑通,再按实际场景逐步加入 HTML 模板和附件;第一次批量发送前先做 5 封以内的测试发送,再扩大到完整名单。碰到报错不用慌,绝大多数问题都出在授权码和端口配置这两步。