在日常办公中,Excel 表格整理、通知发送、报表汇总、定时提醒这类重复劳动,其实都可以交给 Python 脚本完成。最近我在整理一批办公自动化脚本时,反复使用到“邮件发送服务”这个能力,踩过授权码配置、附件中文名、批量发送限流等不少坑,最终沉淀了一套可复用的方案。这篇文章会围绕 Python 发送邮件展开,从 SMTP 原理讲到具体代码,再延伸出 HTML 邮件、附件邮件、Excel 批量发送、脚本参数传递和定时执行等场景,适合 Python 基础薄弱但想快速落地自动化脚本的办公人员,也适合需要把脚本工程化的开发者。你可以边看边复制代码跑通,遇到报错再对照文末的排查清单。
1. 办公自动化场景下的邮件发送到底是什么
很多第一次接触办公自动化的同学会把“邮件发送服务”想得很复杂,以为需要自己搭建一套邮件服务器。其实大多数场景下,我们只是需要让 Python 脚本替我们把邮件交给现有的邮箱服务商,再由对方完成投递。画一条简单的数据流可以是:
Python 脚本 -> smtplib 库 -> 你的邮箱账号 SMTP 服务 -> 收件人邮箱服务器 -> 收件人收件箱这里有几个容易混淆的概念先梳理清楚。
SMTP全称是 Simple Mail Transfer Protocol,翻译过来是简单邮件传输协议,它专门负责邮件的“投递”。发送方把邮件交给 SMTP 服务器,服务器之间相互转发,最终把邮件放进收件人邮箱。Python 内置的smtplib就是对 SMTP 协议的封装,所以我们不需要额外安装第三方库就能发送邮件。
MIME全称是 Multipurpose Internet Mail Extensions,中文叫多用途互联网邮件扩展。它解决的问题是:邮件里不只是纯文字,还会有 HTML 内容、图片、附件、多部分内容组合,MIME 定义了这些内容如何标记和编码。Python 标准库中的email.mime系列模块就是用来帮助我们构造这种邮件内容的。
授权码是邮箱服务商为了安全而提供的独立密码。比如 QQ 邮箱、网易邮箱,开启 SMTP 服务后一般会要求用授权码而不是账号密码登录,避免密码泄露后造成更大风险。这也是新手最容易出错的地方,后面环境准备部分会详细说明。
在办公自动化中,邮件发送通常承担以下几类工作:
- 把程序处理好的 Excel 报表定期发送给负责人;
- 批量给员工发送工资条、通知、邀请函;
- 系统异常或任务完成时,发送提醒给运维值班人员;
- 根据用户提交的表单数据,自动发送确认邮件;
- 与 Windows 任务计划或 Linux crontab 配合,形成定时报告服务。
从底层来看,只要掌握了“连接 SMTP 服务 -> 构造邮件内容 -> 调用发送接口”这条主线,再把账号信息、收件人信息做成可配置项,就能覆盖上面绝大多数场景。
2. 邮件发送服务的 Python 技术选型与核心思路
Python 发送邮件主要有两条路线:一条是使用标准库smtplib+email,另一条是使用云厂商的邮件 API 或第三方封装库,比如yagmail、django.core.mail等。
对于办公自动化来说,我更建议优先掌握标准库方案,原因有三个:
- 不需要额外安装依赖,Python 环境装好就能跑;
- 逻辑清晰,便于排查问题;
- 云厂商和第三方库本质上也是对 SMTP 的封装,理解底层后看其他封装会很快。
yagmail虽然可以少写几行代码,但如果你需要处理复杂的 HTML 模板、多个附件、批量发送时的异常捕获,标准库其实更可控。以后如果公司有专门的邮件 API,再根据官方文档接入即可。
2.1 smtplib 的两种连接方式
smtplib通常有两种连接方式。
方式一:SSL 连接,直接使用加密通道,常见端口是 465。代码中一般写成:
import smtplib server = smtplib.SMTP_SSL("smtp.qq.com", 465) server.login("sender@qq.com", "授权码") server.sendmail("sender@qq.com", ["receiver@example.com"], "内容") server.quit()方式二:使用 STARTTLS 扩展,服务器先在普通端口 587 上建立连接,再升级为加密连接。代码中一般写成:
import smtplib server = smtplib.SMTP("smtp.qq.com", 587) server.starttls() server.login("sender@qq.com", "授权码") server.sendmail("sender@qq.com", ["receiver@example.com"], "内容") server.quit()国内主流邮箱对 465 和 587 的兼容情况略有不同,QQ 邮箱和网易邮箱两种方式基本都支持。为了保证程序稳定,建议优先使用 465 + SMTP_SSL 方式,并显式设置超时时间,例如SMTP_SSL(host, port, timeout=30),避免网络异常导致脚本卡住。
2.2 email.mime 的内容组织
邮件内容并不是简单拼字符串,而是一棵 MIME 树。举个例子:
- 一个邮件可能有一个
multipart/mixed根节点; - 下面包含一个
alternative节点,里面再放纯文本版本和 HTML 版本; - 同级的可能还有多个附件节点。
在代码层面,典型的对象关系是:
MIMEMultipart ├── MIMEText("纯文本内容", "plain", "utf-8") ├── MIMEText("<html>...</html>", "html", "utf-8") └── MIMEApplication(附件字节流)这种分层关系决定了我们向邮件对象attach的顺序和方式。如果只需要发送一封纯文本邮件,也可以直接用一层MIMEText;但如果需要同时支持“纯文本 + HTML + 附件”,就必须使用MIMEMultipart。
3. 环境准备与邮箱服务配置
在编写代码之前,先把运行环境准备好。下面的版本只是一个常见参考,实际项目中需要以你自己环境为准。
操作系统:Windows 10/11 或 Linux(CentOS/Ubuntu 均可) Python:3.8 及以上 编辑器:PyCharm / VS Code 均可 第三方库:推荐安装 openpyxl,用于批量读取 Excel 依赖安装命令:pip install openpyxl标准库smtplib、email、os、sys不需要额外安装,直接使用即可。
3.1 准备一个可发送邮件的邮箱账号
这里以 QQ 邮箱为例,流程如下:
- 登录 QQ 邮箱网页版;
- 点击顶部“设置”,进入“账户”页;
- 找到“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV 服务”;
- 开启“SMTP 服务”,部分账号开启时可以用手机短信验证;
- 开启成功后,会得到一串授权码,记录下来。
网易 126/163 邮箱开启方式类似,在“设置 -> POP3/SMTP/IMAP”中开启服务并获取授权码。
注意,授权码和登录密码是两回事,同时在代码中不要直接硬编码授权码。本文为了演示方便,会在示例中用环境变量或配置项的方式读取。
如果你用的是企业邮箱或公司自建邮箱,需要和 IT 同事确认 SMTP 服务器地址、端口、是否需要加密以及是否使用单独口令登录。
3.2 确定 SMTP 服务器地址
不同邮箱服务商的 SMTP 服务器地址不完全相同,常见的有以下这些:
| 邮箱服务商 | SMTP 服务器 | SSL 端口 | STARTTLS 端口 |
|---|---|---|---|
| QQ 邮箱 | smtp.qq.com | 465 | 587 |
| 网易 163 | smtp.163.com | 465 | 587 |
| 网易 126 | smtp.126.com | 465 | 587 |
| 新浪 | smtp.sina.com.cn | 465 | 587 |
| 阿里企业邮 | smtp.qiye.aliyun.com | 465 | 587 |
上面这些地址和端口是长期稳定的通用信息,具体以你的邮箱服务商官方文档为准。如果临时使用某些海外邮箱服务,也建议查阅官方最新说明,不要盲目照搬端口号。
4. 第一个 Python 邮件发送脚本:入门必跑通
我们从最简单的一封纯文本邮件开始。目标是把“内网服务器重启完成”这种通知发送给运维人员,这是办公自动化中最常见的小需求。
4.1 编写最简单发送脚本
创建一个文件send_text_email.py,内容如下:
# -*- coding: utf-8 -*- import os import smtplib from email.mime.text import MIMEText from email.utils import formataddr def send_text_email(): smtp_host = os.getenv("SMTP_HOST", "smtp.qq.com") smtp_port = int(os.getenv("SMTP_PORT", "465")) sender_email = os.getenv("SENDER_EMAIL", "your_sender@qq.com") auth_code = os.getenv("AUTH_CODE", "your_auth_code") receiver_email = "receiver@example.com" subject = "【自动化通知】服务器重启完成" content = """ 服务器192.168.1.10已重启完成。 系统当前运行状态正常,请及时查看监控大盘。 发送时间:由Python脚本自动发送 """ msg = MIMEText(content, "plain", "utf-8") msg["From"] = formataddr(["自动化通知中心", sender_email]) msg["To"] = formataddr(["值班人员", receiver_email]) msg["Subject"] = subject server = None try: server = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=30) server.login(sender_email, auth_code) server.sendmail(sender_email, [receiver_email], msg.as_string()) print("邮件发送成功") except Exception as e: print("邮件发送失败:", e) finally: if server: server.quit() if __name__ == "__main__": send_text_email()运行前需要先设置环境变量,或者直接把脚本中的your_sender@qq.com和your_auth_code改成真实值。在 Windows PowerShell 中,临时设置环境变量可以这样:
$env:SENDER_EMAIL = "你的QQ邮箱" $env:AUTH_CODE = "你的授权码" python send_text_email.py在 Linux 或 macOS 中可以这样:
export SENDER_EMAIL="你的QQ邮箱" export AUTH_CODE="你的授权码" python send_text_email.py4.2 核心代码解释
这段代码虽然少,但包含几个关键点:
MIMEText(content, "plain", "utf-8")表示创建一个纯文本邮件内容,并指定编码为 UTF-8。这里不要省略编码,否则发送中文时可能出现乱码。
formataddr的作用是构造带显示名称的发件人和收件人格式,让收件人看到“自动化通知中心 your_sender@qq.com ”而不是一串裸邮箱地址。这种方式在企业内部通知中很常用,显得更正式。
server.sendmail(sender_email, [receiver_email], msg.as_string())是需要重点理解的一行。msg.as_string()会把 MIME 邮件对象序列化成一个可传输的字符串,这个字符串包含了所有邮件头、空行和正文。sendmail方法接收到的是这种完整的协议文本,而不仅仅是正文。
这里有一个容易被坑的点:sendmail的第一个参数表示信封发件人,第二个参数是一个列表,表示信封收件人。如果要把同一封邮件发送给多个收件人,第二个参数列表应包含多个邮箱地址,例如:
server.sendmail(sender_email, ["a@example.com", "b@example.com"], msg.as_string())同时需要把msg["To"]设置成对应的展示地址,它们一个负责协议投递,一个负责用户界面展示,可以并不完全一致,但在大多数场景下建议保持统一。
4.3 验证结果
运行脚本后,如果控制台输出“邮件发送成功”,说明邮件已经交给 QQ 邮箱的 SMTP 服务。这时打开收件箱或垃圾箱查看即可。如果没有任何输出,就要检查是否忘记运行login,或授权码是否填写正确。
5. 进阶实战:HTML 邮件与附件邮件
纯文本邮件看起来不够美观。办公自动化中经常需要发送“带样式的日报”“公司活动通知”,此时可以把正文写成 HTML。再进一步,如果发送的是 Excel 报表、PDF 文件,就需要添加附件。
5.1 发送 HTML 邮件
HTML 邮件并不是简单地在Subject后面塞一个<h1>标签,而是需要把 MIME 类型指定为text/html,并且最好同时提供纯文本版本,这样在某些只支持纯文本的邮件客户端中也能正常显示。
下面是一个发送 HTML 邮件的完整示例:
# -*- coding: utf-8 -*- import os import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr def build_html_content(user_name, report_date): return f""" <html> <body style="font-family: Arial, sans-serif; color: #333;"> <h2>数据日报 - {report_date}</h2> <p>{user_name},您好:</p> <p>以下是今日业务数据摘要:</p> <table border="1" cellpadding="8" cellspacing="0" style="border-collapse: collapse;"> <tr style="background-color: #f5f5f5;"> <th>指标名称</th> <th>今日值</th> <th>昨日值</th> <th>环比变化</th> </tr> <tr> <td>新增用户数</td> <td>1532</td> <td>1410</td> <td style="color: green;">+8.65%</td> </tr> <tr> <td>订单数</td> <td>980</td> <td>1020</td> <td style="color: red;">-3.92%</td> </tr> <tr> <td>总收入</td> <td>238600</td> <td>215300</td> <td style="color: green;">+10.82%</td> </tr> </table> <p>详细数据请查看附件报表。</p> <p>本邮件由 Python 办公自动化脚本自动生成。</p> </body> </html> """ def send_html_email(): sender_email = os.getenv("SENDER_EMAIL", "your_sender@qq.com") auth_code = os.getenv("AUTH_CODE", "your_auth_code") receiver_email = "receiver@example.com" msg = MIMEMultipart("alternative") msg["From"] = formataddr(["数据报表中心", sender_email]) msg["To"] = receiver_email msg["Subject"] = "数据日报通知" plain_text = "请使用支持HTML的邮件客户端查看此邮件。" html_content = build_html_content("张明", "2025-01-10") msg.attach(MIMEText(plain_text, "plain", "utf-8")) msg.attach(MIMEText(html_content, "html", "utf-8")) server = None try: server = smtplib.SMTP_SSL("smtp.qq.com", 465, timeout=30) server.login(sender_email, auth_code) server.sendmail(sender_email, [receiver_email], msg.as_string()) print("HTML邮件发送成功") except Exception as e: print("HTML邮件发送失败:", e) finally: if server: server.quit() if __name__ == "__main__": send_html_email()注意MIMEMultipart("alternative")的含义:它表示邮件的多个部分是“可替换”的,收件客户端会自动选择支持程度最高的版本。通常把纯文本放在 HTML 前面,因为某些客户端在选择版本时认为最后一个是最优版本。如果只需要 HTML,可以简化为MIMEText(html_content, "html", "utf-8"),模板中的 HTML 不能过于复杂,尽量使用内联 CSS,因为很多邮箱客户端会过滤邮件中的<style>标签和外部 CSS。
5.2 发送带 Excel 附件的邮件
附件其实是邮件中很常见但容易踩坑的部分。很多初学者在写附件文件名时,中文名称容易变成乱码,或者附件变成了奇怪的长地址编码。这里推荐用email.mime.application.MIMEApplication读取文件二进制内容,再显式设置Content-Disposition。
来看一个发送 Excel 附件的完整示例:
# -*- coding: utf-8 -*- 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 def send_mail_with_attachment(): sender_email = os.getenv("SENDER_EMAIL", "your_sender@qq.com") auth_code = os.getenv("AUTH_CODE", "your_auth_code") receiver_email = "receiver@example.com" subject = "月度报表-2025年1月" body = "您好,请查收本月度报表,有问题请随时反馈。" attachment_path = "report_2025_01.xlsx" attachment_name = "2025年1月业务报表.xlsx" msg = MIMEMultipart() msg["From"] = formataddr(["报表自动化", sender_email]) msg["To"] = receiver_email msg["Subject"] = subject msg.attach(MIMEText(body, "plain", "utf-8")) # 读取附件并添加到邮件对象 with open(attachment_path, "rb") as f: part = MIMEApplication(f.read()) part.add_header( "Content-Disposition", "attachment", filename=("utf-8", "", attachment_name) ) msg.attach(part) server = None try: server = smtplib.SMTP_SSL("smtp.qq.com", 465, timeout=30) server.login(sender_email, auth_code) server.sendmail(sender_email, [receiver_email], msg.as_string()) print("附件邮件发送成功") except Exception as e: print("附件邮件发送失败:", e) finally: if server: server.quit() if __name__ == "__main__": send_mail_with_attachment()filename=("utf-8", "", attachment_name)是防止中文文件名乱码的关键写法。它告诉邮件模块,这个文件名需要按 UTF-8 编码。如果直接写filename=attachment_name,在一些客户端中可能出现乱码。
MIMEApplication适合传输任意二进制文件,因此不需要手动指定maintype和subtype。只要文件存在、大小在服务商限制范围内,就可以作为附件发送。常见的 QQ 邮箱单个附件大小限制在 50MB 左右,超过它会收到服务商退信。
如果要同时发送多个附件,直接循环读取多个文件,然后逐个msg.attach(part)即可,不需要额外创建多个MIMEMultipart节点。
6. 办公自动化扩展:读取 Excel 批量发送邮件
在办公自动化中,最经典的场景是“从 Excel 读取人员名单,批量发送通知或工资条”。这个场景正好可以结合热词里的 Excel 办公自动化。下面用一个简单样例说明整体思路。
6.1 准备 Excel 数据表
假设我们有一个staff_list.xlsx文件,内容如下:
| 姓名 | 邮箱 | 部门 | 本月工资 | 绩效 |
|---|---|---|---|---|
| 张明 | zhangming@example.com | 技术部 | 8500 | 1200 |
| 李丽 | lili@example.com | 市场部 | 7600 | 900 |
| 王强 | wangqiang@example.com | 运营部 | 7200 | 1000 |
这里要注意,真实工资条属于敏感信息,发送前务必确认收件人邮箱准确,建议先用自己的邮箱做测试。
6.2 编写批量发送脚本
为了演示方便,下面的代码假设 Excel 的列顺序为:姓名、邮箱、部门、工资、绩效。实际使用时可以根据自己的表头调整。
# -*- coding: utf-8 -*- import os import time import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr import openpyxl def load_staff_from_excel(file_path): wb = openpyxl.load_workbook(file_path, data_only=True) ws = wb.active staff_list = [] for row in ws.iter_rows(min_row=2, values_only=True): name, email, department, salary, bonus = row if name and email: staff_list.append({ "name": str(name).strip(), "email": str(email).strip(), "department": str(department).strip(), "salary": salary, "bonus": bonus, }) return staff_list def send_salary_email(staff): sender_email = os.getenv("SENDER_EMAIL", "your_sender@qq.com") auth_code = os.getenv("AUTH_CODE", "your_auth_code") msg = MIMEMultipart() msg["From"] = formataddr(["人力资源部", sender_email]) msg["To"] = staff["email"] msg["Subject"] = f"{staff['name']},您好,您的当月工资单" html_content = f""" <html> <body> <p>{staff['name']},您好:</p> <p>以下是您本月工资信息,请核对。</p> <table border="1" cellpadding="6"> <tr><td>部门</td><td>{staff['department']}</td></tr> <tr><td>姓名</td><td>{staff['name']}</td></tr> <tr><td>基本工资</td><td>{staff['salary']}</td></tr> <tr><td>绩效奖金</td><td>{staff['bonus']}</td></tr> </table> <p>本邮件由系统自动发送,如有疑问请联系人力资源部。</p> </body> </html> """ msg.attach(MIMEText(html_content, "html", "utf-8")) server = None try: server = smtplib.SMTP_SSL("smtp.qq.com", 465, timeout=30) server.login(sender_email, auth_code) server.sendmail(sender_email, [staff["email"]], msg.as_string()) print(f"{staff['name']} 邮件发送成功") return True except Exception as e: print(f"{staff['name']} 邮件发送失败: {e}") return False finally: if server: server.quit() def batch_send(): file_path = "staff_list.xlsx" staff_list = load_staff_from_excel(file_path) print(f"共读取到 {len(staff_list)} 位员工信息") success_count = 0 fail_count = 0 for staff in staff_list: result = send_salary_email(staff) if result: success_count += 1 else: fail_count += 1 # 每次发送间隔1秒,避免触发服务商限流 time.sleep(1) print(f"发送完成,成功 {success_count} 封,失败 {fail_count} 封") if __name__ == "__main__": batch_send()6.3 批量发送注意事项
代码里每次循环都创建了新的 SMTP 连接,这种方式逻辑简单,但效率不高。如果人数很多,比如几百上千人,建议把server.login提到循环外,只建立一次连接,然后循环发送,最后统一quit。但这样做也有风险:如果某个收件人地址格式错误导致发送异常,需要确保临时连接没有被中断。
更稳妥的做法是把下面的逻辑封装为两个函数:connect_server()负责连接和登录,send_one(server, staff)负责发送单封邮件。发送出错时,在外层重新连接。
另外,批量群发时不要让每封邮件的主题、正文完全一样,否则很容易被服务商判定为垃圾邮件。即使内容相同,也可以适当加入员工姓名、工号等个性化信息。还要控制发送频率,避免在短时间内发起大量发送请求。对于企业大批量群发场景,更推荐使用邮件服务商的批量发送接口,而不是普通个人邮箱的 SMTP。
7. 脚本参数传递与定时发送
邮件脚本写完后,如果每次要改收件人、主题或正文,都需要打开 Python 文件修改,显然不方便。更好的方式是通过命令行参数控制脚本行为。这也对应到了很多 Python 学习者搜索过的“python给另一个py脚本传递参数”的场景。
7.1 使用 sys.argv 传递简单参数
Python 中最基础的参数来源是sys.argv。它表示“运行脚本时从命令行传入的参数列表”,其中第一个元素是脚本文件名本身。看一个示例:
# -*- coding: utf-8 -*- import sys def main(): print("脚本路径:", sys.argv[0]) print("参数数量:", len(sys.argv) - 1) for i, arg in enumerate(sys.argv[1:], start=1): print(f"参数{i}: {arg}") if __name__ == "__main__": main()如果执行:
python demo_args.py 张三 2025-01-10输出会类似:
脚本路径: demo_args.py 参数数量: 2 参数1: 张三 参数2: 2025-01-10把所有参数用空格拼在脚本后面,这就是最基本的传参方式。但问题在于,如果参数很多,或参数是可选的,sys.argv的位置参数会非常难维护。比如第三位到底是收件人还是主题,别人看代码时很难猜。
7.2 使用 argparse 优雅处理参数
更推荐使用 Python 标准库argparse,它支持定义“参数名”、默认值、类型和帮助信息。下面是将邮件发送脚本改造为支持命令行参数的例子:
# -*- coding: utf-8 -*- import argparse import os import smtplib from email.mime.text import MIMEText from email.utils import formataddr def send_email(to_addr, subject, content): sender_email = os.getenv("SENDER_EMAIL", "your_sender@qq.com") auth_code = os.getenv("AUTH_CODE", "your_auth_code") msg = MIMEText(content, "plain", "utf-8") msg["From"] = formataddr(["通知中心", sender_email]) msg["To"] = to_addr msg["Subject"] = subject server = None try: server = smtplib.SMTP_SSL("smtp.qq.com", 465, timeout=30) server.login(sender_email, auth_code) server.sendmail(sender_email, [to_addr], msg.as_string()) print("发送成功") finally: if server: server.quit() if __name__ == "__main__": parser = argparse.ArgumentParser(description="命令行邮件发送脚本") parser.add_argument("--to", required=True, help="收件人邮箱") parser.add_argument("--subject", required=True, help="邮件主题") parser.add_argument("--content", default="", help="邮件正文") args = parser.parse_args() send_email(args.to, args.subject, args.content)执行方式如下:
python send_mail_cli.py --to receiver@example.com --subject "测试邮件" --content "你好,这是一封命令行发送的邮件"这样脚本就可以被其他程序调用。比如在另一个main.py中,可以通过subprocess调用当前脚本,实现“一个 Python 脚本给另一个 Python 脚本传参”的效果:
import subprocess command = [ "python", "send_mail_cli.py", "--to", "receiver@example.com", "--subject", "Python自动化", "--content", "由另一个脚本触发发送" ] result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8") print("标准输出:", result.stdout) print("错误输出:", result.stderr)这种模式适合场景:业务系统已经用 Python 写好主流程,邮件模块独立维护,主流程只通过参数触发邮件发送,避免文件内部 import 过多、职责混乱。
7.3 定时发送思路
定时发送并不是 Python 脚本自身长期休眠等待,更常见的做法是委托操作系统调度。
在 Windows 上,可以使用“任务计划程序”。把要执行的命令填写为:
python C:\scripts\send_daily_report.py --to boss@example.com然后设置每天 09:00 触发一次。
在 Linux 上,可以使用 crontab:
0 9 * * * cd /opt/scripts && /usr/bin/python send_daily_report.py --to boss@example.com >> send_mail.log 2>&1这条规则表示每天 9 点执行脚本,并把日志写入send_mail.log。通过追加日志,我们可以定位发送失败或异常退出等问题。
如果你希望在 Python 进程内部做定时调度,可以使用schedule或APScheduler,但需要安装第三方库。对于纯办公自动化场景,我仍建议优先使用系统级定时任务,因为它即使脚本崩溃也会由系统记录执行状态,而且重启后会继续按照计划执行。
8. 高频报错与排查思路
邮件发送脚本的报错信息一般都比较明确,但新手经常因为不了解错误码含义而不知道从哪里下手。下面整理了几种常见问题的现象、原因和解决思路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
抛出SMTPAuthenticationError,提示 535 | 授权码错误或账号未开启 SMTP 服务 | 核对授权码是否复制完整,重新开启 SMTP 服务并生成新授权码 |
| 登录成功但提示 554 或“发送频率过高” | 短时间内发送大量相同邮件,触发了服务商反垃圾策略 | 降低发送频率,增加延时,将批量发送改成服务商提供的批量接口 |
抛出SMTPServerDisconnected | 服务器连接被主动断开 | 检查网络和端口是否能连通,尝试换 465/587 端口,增加超时时间 |
| 邮件能收到但进了垃圾箱 | 内容缺乏个性化、附件异常或域名信誉低 | 正文使用正式称呼,避免过多链接和敏感词,标题不要全大写 |
| 中文标题或附件名乱码 | 邮件头编码设置不当 | 在MIMEText中指定utf-8,附件名使用元组编码方式 |
| 收不到邮件也没有报错 | 检查收件人地址、垃圾箱、服务商退信通知 | 先给自己发一封,排除代码问题后再发给真实收件人 |
timeout超时 | 网络到服务商 SMTP 服务器不通 | 使用telnet smtp.qq.com 465测试端口连通性,检查公司防火墙 |
Excel 中的邮箱显示为None或nan | Excel 该列为空或包含非文本格式 | 读取时判断None,并使用str().strip()清理空格,非字符串改用字符串类型 |
| 报告发送成功但邮件内容乱码 | 字符集没有统一 | 代码开头加# -*- coding: utf-8 -*-,所有MIMEText都设置charset="utf-8" |
如果遇到错误,第一件事不是去猜代码逻辑,而是把错误信息完整复制到搜索引擎或文末自查。常见的错误码如 535、550、554,分别对应认证失败、接收方拒绝、发送策略限制。看到错误码后,再结合自己的操作场景基本就能快速定位。
9. 把邮件发送脚本工程化的最佳实践
脚本能跑通只是第一步。在实际办公自动化系统中,这段代码往往要在服务器上长期运行。下面这些实践可以让脚本更稳定、更安全、更容易维护。
9.1 敏感信息放到环境变量或配置中心
千万不要把邮箱账号和授权码明文写在代码里,尤其是代码需要提交到 Git 仓库时。授权码一旦泄露,别人就能利用你的邮箱发送垃圾邮件,轻则账号被封,重则影响公司域名信誉。更稳妥的方式是放到环境变量或.env配置文件中。
推荐在项目根目录创建.env文件:
SMTP_HOST=smtp.qq.com SMTP_PORT=465 SENDER_EMAIL=your_sender@qq.com AUTH_CODE=your_auth_code然后使用第三方库python-dotenv加载配置:
from dotenv import load_dotenv load_dotenv()如果不希望引入第三方库,也可以在启动脚本前手动设置环境变量,或者把配置放在config.py,并在 Git 提交规则中忽略该文件。无论如何,都要遵循最小权限原则,授权码权限只需开放 SMTP 服务,不需要开放其他邮箱管理权限。
9.2 设计可复用的邮件发送模块
把邮件发送封装成一个独立的模块,比每次都写一大段SMTP_SSL代码要清晰得多。建议抽象出下面的函数签名:
def send_email( to_addr, subject, body, attach_paths=None, is_html=False, cc=None, bcc=None, ): ...参数解释:
to_addr:收件人地址;subject:邮件主题;body:邮件正文,可以是纯文本或 HTML;attach_paths:附件路径列表,默认为空;is_html:正文是否为 HTML;cc:抄送地址列表;bcc:密送地址列表,使用密送时收件人之间互不可见,适合批量通知场景。
这样设计以后,无论是发送普通通知,还是发送报表附件,调用方只需要一行代码。附件处理、登录、异常重试都在模块内部处理。
9.3 增加日志和重试机制
生产环境要求代码是可观测的,不能只靠print输出。建议使用 Python 标准库logging,按日期输出日志。这里的实践是:单封邮件发送失败时,先把失败信息写入日志;如果连续失败多次再发送告警邮件,而不是无限循环重试导致账号被拖黑。
重试时要注意“退避策略”,即每次重试间隔递增。第一次失败后等待 5 秒,第二次等待 30 秒,第三次等待 120 秒。重试超过三次后停止,并记录最终失败原因。
9.4 做好邮件合规与反垃圾处理
很多初学者会遇到邮件发送成功但收件人长时间收不到的问题,多半是垃圾箱或服务商风控。除了降低发送频率,还需要注意:
- 提升正文质量,不要使用明显营销措辞;
- 在邮件中写清楚发件人身份和联系渠道;
- 退订或错发处理要谨慎;
- 企业场景尽量使用企业邮箱域名,而不是个人 QQ 邮箱群发给陌生人;
- 对大量不同内容的邮件,填写规范的发件人名称和回复地址。
如果公司有自己的邮件服务器,并且要求发件方配置 SPF、DKIM、DMARC 记录,需要联系管理员确认。这些 DNS 记录决定了收件方服务器是否信任我们发出的邮件。拥有自己的独立域名后,通过邮件服务商完成这些认证,能明显降低邮件被扔进垃圾箱的概率。
10. 小结与后续扩展方向
本文提到的内容可以用一句话概括:用 Python 标准库编写邮件发送脚本,再结合 Excel 和命令行参数,将其扩展成办公自动化里真正可用的邮件发送服务。你至少应该已经理解并能够上手操作这些内容:
- 明白 SMTP、MIME、授权码的基本概念;
- 能写出针对纯文本、HTML、附件邮件的发送代码;
- 能通过 openpyxl 读取 Excel 并按行批量发送个性化邮件;
- 能通过
sys.argv和argparse让脚本接收外部参数; - 能借助系统定时任务定时执行邮件脚本;
- 遇到 535、554、超时、中文乱码等问题时知道从哪些方向排查。
如果你正在整理自己的办公自动化脚本,建议先跑通“发给自己一封文本邮件”这个最小闭环,然后逐步加入 HTML 模板、Excel 附件、批量发送和日志模块。每加一个功能只改动一个变量或参数,这样出现问题时能更快定位。
后续还可以继续扩展的方向包括:将正文内容写入 Markdown 再转换为 HTML 模板、用 Python 操作 Redis 做失败任务队列、对接企业微信或钉钉机器人通知、从数据库查询数据后再生成邮件报表。掌握了这一套邮件发送能力,你会发现很多行政、运营、数据团队的人工重复工作都可以自动化完成。希望这篇记录对你快速落地有所帮助,实践过程遇到其他报错时,也欢迎按错误码现象搜索比对,再回到脚本里逐段加日志排查。