Python邮件发送服务实战:从SMTP协议到Excel批量通知

发布时间:2026/9/4 19:07:34
Python邮件发送服务实战:从SMTP协议到Excel批量通知 自动化脚本跑完只是开始把结果送到该看的人手上才算真的完成。很多做 Python 办公自动化的同学都有类似经历Excel 汇总好了、日报生成好了、临时报表也导出了但最后一步“发邮件通知业务方”还是靠手动。每天早上先打开邮箱客户端写标题、粘附件、填收件人一不留神还会漏发或发错版本。在 py100 系列练习中lv2-089 这个项目要求实现“办公自动化-邮件发送服务”正是要把这个最容易被人忽略的收尾环节变成可复用、可定时、可排查的工程能力。本文会从 SMTP 协议原理讲到邮箱授权码配置再给出纯文本邮件、HTML 邮件、附件邮件、批量发送以及读取 Excel 收件人名单的完整示例。读完你可以直接替换成自己的邮箱配置把一套可用的邮件发送服务接到日报、周报、数据监控或告警任务中。1. 邮件发送服务要解决的真实问题1.1 为什么“发邮件”值得单独做一个项目单独看一封邮件逻辑其实并不复杂填发件人、收件人、标题、正文点发送。可一旦放进办公自动化体系里“发邮件”就会暴露出不少工程问题。第一类是重复性日报、周报、月报每天或每周都要发送手工操作意味着同样的步骤要反复执行上百次。第二类是可靠性网络超时、SMTP 认证失败、附件过大、收件人地址写错任何一种异常都会让发送中断但手工操作时很难留下完整记录排查时只能靠回忆。第三类是协作问题脚本可能是数据分析师写的通知对象却分布在业务、运营、管理多个角色中谁在什么时间收到什么内容需要有一套清晰的规则而不是靠个人习惯。把“邮件发送服务”做成独立模块本质上是把上述重复、易错、难追踪的工作变成标准化代码。脚本可以定时执行发送结果可以写日志收件人名单可以统一维护附件生成和邮件发送可以被拆成两个可独立测试的阶段。这样一来人只需要处理异常情况正常流程交给程序即可。1.2 这个项目适合谁学lv2 的定位意味着读者应该已经掌握 Python 基础语法、函数封装和简单的文件读写但未必接触过网络编程和邮件协议。如果你正在做办公自动化想把“数据计算”和“消息通知”打通这个项目非常适合当作练习起点。如果你在维护一个已经跑在服务器上的 Python 定时任务希望任务执行完以后主动推送结果邮件发送也是成本最低、最容易实现的通知方式之一。不过要注意邮件发送服务并不适合用来做高并发、大规模营销群发。企业级邮件推送通常需要专门的邮件服务商、SPF/DKIM 域名校验和专业的投递反馈体系这不是 Python 标准库能够解决的问题。个人开发和中小型内部办公场景才是本文展示方案的舒适区。1.3 先给一个总体判断Python 邮件发送的核心并不在于某个高深算法而在于理清三个概念SMTP 负责传输MIME 负责组装邮件内容邮箱授权码负责安全认证。把这三件事理解到位再配合统一配置、异常捕获、日志记录就能封装出一个足够稳定的邮件发送小工具。真正的坑通常在配置和工程细节上例如收件人解析、附件编码、超时处理、敏感信息泄露这些恰恰是初学者最容易忽略的部分。2. 底层原理SMTP、MIME 和邮箱授权码2.1 邮件从发件人到收件人经历了什么日常发送邮件时用户看到的是网页端或客户端里的“发送”按钮但在技术底层邮件通过 SMTP 协议完成投递。SMTP 的全称是 Simple Mail Transfer Protocol直译过来是简单邮件传输协议它规定了邮件客户端与邮件服务器之间如何建立连接、如何认证身份、如何提交邮件数据。可以把它理解为邮局门口的寄件窗口你告诉窗口工作人员要寄给谁、寄什么内容工作人员接过邮件放入分拣系统。当 Python 代码调用 smtplib 连接发件人邮箱的 SMTP 服务器时实际做的事情就是代替人工填写寄件信息并把邮件推送出去。后续收件人的邮件服务器如何接收、如何把邮件放入收件人收件箱对发送端来说是不可见的过程也不需要开发者在发送代码里处理。理解这一点对排查问题很有帮助如果脚本报连接错误应优先检查发件人的 SMTP 服务器地址、端口和网络连通性如果脚本没有报错但收件人一直没收到则要检查服务商是否有延迟或退信。2.2 smtplib 负责发送email 负责组装Python 标准库中有两个和邮件密切相关的模块初学者经常混淆。smtplib 负责与 SMTP 服务器通信它的核心操作是连接服务器、登录、调用 sendmail 发送原始邮件文本。而 email 模块负责把“人类可读”的邮件结构转换成标准格式例如发件人、收件人、主题、正文、附件应该如何编码邮件头应该是什么格式不同类型的内容如何标记。可以这样对比email 模块负责“写出一封内容完整、格式正确的信”smtplib 负责“把这封信送到邮局并投递出去”。在代码中两者通常是配合使用的。先用 email.mime 构建邮件对象调用 as_string() 得到符合邮件规范的字符串再把它交给 smtplib 的 sendmail 方法。很多刚入门的人只查到一个例子里有 MIMEText 和 SMTP 两个类却不明白为什么邮件要经历一步“转字符串”原因就在于 email 模块产生的是结构化对象SMTP 传输的则是文本协议报文。2.3 为什么不能用邮箱登录密码直接登录在各类邮件教程中出现最多的报错之一是 SMTPAuthenticationError代码明明写对了用户名密码也没输错却仍然被服务器拒绝。大多数情况下原因是使用了邮箱的登录密码而不是 SMTP 授权码。出于安全考虑主流邮箱服务商并不允许第三方程序直接使用完整账号密码进行 SMTP 登录。用户可以开启 SMTP 服务并生成一个独立的授权码这个授权码通常拥有较低的权限只能用于发信等特定操作即便泄露也可以单独重置不会影响网页端邮箱和账号安全。163 邮箱、QQ 邮箱以及部分企业邮箱都采用类似机制。具体开启路径在不同服务商中略有差异一般在邮箱设置的安全或账户选项中能找到“开启 SMTP”和“生成授权码”的入口。不同服务商的“开启 SMTP”入口和授权码生成方式可能变动通常以服务商官网帮助文档为准。3. 环境准备与最小配置3.1 Python 环境与第三方依赖邮件发送的核心功能由 Python 标准库提供因此不需要额外安装重依赖。建议使用 Python 3.8 或更新的版本并保证环境可以正常执行 python 和 pip 命令。本文示例主要使用以下标准库smtplib建立 SMTP 连接并发送邮件email.mime构建纯文本、HTML、多部分邮件email.utils格式化发件人姓名和日期logging输出发送日志time控制批量发送的间隔如果在 Windows 环境还没安装 Python需要先到官网下载安装包在 Linux 环境则可以通过包管理器安装也可使用 pyenv 管理多个版本。安装完成后通过 python --version 确认版本号。3.2 在代码中管理邮箱配置最直接的配置方式是写在代码最上方但我不建议这种做法。邮箱授权码属于敏感信息写入源码会带来两个问题一是代码如果提交到 Git 仓库或被同事共享授权码就可能泄露二是如果后续更换邮箱或重置授权码需要改动代码容易遗漏和误改。更合理的做法是把账号、授权码、SMTP 服务器地址放到环境变量或独立的配置文件中。下面演示通过环境变量读取配置的最小方式import os SMTP_HOST os.getenv(SMTP_HOST, smtp.example.com) SMTP_PORT int(os.getenv(SMTP_PORT, 465)) SENDER_EMAIL os.getenv(SENDER_EMAIL, ) SENDER_NAME os.getenv(SENDER_NAME, 自动化通知) AUTH_CODE os.getenv(AUTH_CODE, )设置环境变量后代码里不会出现明文账号密码发送服务也更便于在不同环境之间迁移。比如测试环境连测试邮箱生产环境连正式邮箱只需要调整环境变量即可。若想把配置写入 .env 文件可以使用 python-dotenv 库但使用时需要注意 .env 文件不应提交到版本库。3.3 SMTP 端口和协议的选择常见 SMTP 服务使用两种加密方式。一种是通过 SSL 加密端口通常是 465Python 中使用 SMTP_SSL 连接另一种是使用 STARTTLS 在 25 或 587 端口上先建立普通连接再升级为加密连接Python 中先创建 SMTP 对象再调用 starttls。不同邮箱服务商支持的端口不完全一致建议优先使用 465 或 587。端口选择应以你所用邮箱服务商的 SMTP 文档为准示例代码中默认使用 SSL 模式。4. 第一封可运行的纯文本邮件要判断一个邮件发送服务是否可用先不急着写花哨的 HTML 模板和批量发送逻辑而是用最小代码发一封纯文本邮件验证链路。链路通了后续再逐步增加功能。4.1 新建 mail_demo.py 文件路径mail_demo.py 功能发送一封纯文本测试邮件验证 SMTP 配置是否正确 import smtplib import os 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)) SENDER_EMAIL os.getenv(SENDER_EMAIL, senderexample.com) SENDER_NAME os.getenv(SENDER_NAME, 自动化通知) AUTH_CODE os.getenv(AUTH_CODE, ) TO_EMAIL receiverexample.com SUBJECT Python 邮件发送测试 CONTENT 这是一封由 Python smtplib 发送的测试邮件。 def send_text_email(): # 构建邮件对象 msg MIMEText(CONTENT, plain, utf-8) msg[From] formataddr((SENDER_NAME, SENDER_EMAIL)) msg[To] TO_EMAIL msg[Subject] SUBJECT # 建立 SSL 连接 with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout10) as server: server.login(SENDER_EMAIL, AUTH_CODE) server.sendmail(SENDER_EMAIL, [TO_EMAIL], msg.as_string()) print(邮件发送成功) if __name__ __main__: send_text_email()4.2 关键代码逐段解释MIMEText 构造函数接收三个参数第一个是正文内容第二个是内容类型third 是字符编码。文本类型设置为 plainHTML 类型则设置为 html。charset 使用 utf-8可以保证中文内容正常显示。From、To、Subject 是邮件头字段。From 不是只填邮箱地址就行通常还会包含发送者姓名。formataddr 可以格式化“姓名 邮箱地址”这种结构既方便收件人阅读也符合常见邮件客户端的解析习惯。To 字段填写收件人地址sendmail 的第二个参数要求是收件人列表因此即使只有一个收件人也要写成 [TO_EMAIL] 或 [recipient1, recipient2]。with 语句会在发送完成后自动关闭连接避免程序不断占用服务器连接资源。timeout10 指定连接超时时间为 10 秒防止网络异常时脚本长时间卡住。login 方法使用邮箱账号和授权码进行身份验证sendmail 最后发送的是 msg.as_string()也就是把邮件对象转换成符合 SMTP 需要的字符串格式。4.3 运行和验证先设置环境变量例如在 Windows PowerShell 中执行$env:SMTP_HOST smtp.example.com $env:SMTP_PORT 465 $env:SENDER_EMAIL 你的邮箱地址 $env:SENDER_NAME 自动化通知 $env:AUTH_CODE 你的授权码 python mail_demo.py在 Linux 或 macOS 中可使用如下等价命令export SMTP_HOSTsmtp.example.com export SMTP_PORT465 export SENDER_EMAIL你的邮箱地址 export SENDER_NAME自动化通知 export AUTH_CODE你的授权码 python mail_demo.py如果代码正确执行并输出“邮件发送成功”可以打开收件箱确认是否收到。需要注意有些邮箱会自动把测试邮件归入垃圾箱第一次测试没看到时先检查垃圾箱再把发送方加入白名单。程序输出成功但收件箱未出现邮件优先查看服务商是否有退信通知或发送频率限制。5. 正式场景HTML 邮件、附件和批量发送纯文本邮件可以验证链路但实际办公场景中通常需要发送带格式的日报、带图表的经营分析或者把 Excel、CSV 文件作为附件发给相关人员。下面演示三个常用能力。5.1 发送 HTML 邮件报表类通知如果只有纯文字可读性会差很多。HTML 邮件允许在正文中包含表格、颜色、字体加粗等样式更符合每日数据播报的场景。 文件路径send_html_report.py 功能使用 HTML 模板块拼装日报数据发送到收件人邮箱 import smtplib import os 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)) SENDER_EMAIL os.getenv(SENDER_EMAIL) SENDER_NAME os.getenv(SENDER_NAME, 日报服务) AUTH_CODE os.getenv(AUTH_CODE) TO_EMAIL bossexample.com SUBJECT 7月20日业务日报 html_content html body h27月20日业务日报/h2 p以下数据由自动化脚本生成/p table border1 cellpadding6 cellspacing0 styleborder-collapse: collapse; tr th指标/th th当日值/th /tr tr td新增用户/td td1280/td /tr tr td订单量/td td326/td /tr tr td销售额/td td45210 元/td /tr /table /body /html def send_html_email(): # 第二个参数是 html表示按 HTML 格式渲染 msg MIMEText(html_content, html, utf-8) msg[From] formataddr((SENDER_NAME, SENDER_EMAIL)) msg[To] TO_EMAIL msg[Subject] SUBJECT with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout10) as server: server.login(SENDER_EMAIL, AUTH_CODE) server.sendmail(SENDER_EMAIL, [TO_EMAIL], msg.as_string()) print(HTML 日报发送成功) if __name__ __main__: send_html_email()HTML 版本与纯文本版本核心区别仅在 MIMEText 的第二个参数一个是 html一个是 plain。由于收件人的邮件客户端和服务器对 HTML 邮件的支持程度不一致重要通知如果只需要少量文字说明仍然建议使用纯文本如果邮件中包含较多结构化数据表格HTML 会更直观。5.2 发送带附件邮件带附件的邮件不能直接使用 MIMEText因为一个 MIME 对象只能携带一种类型的内容。需要先创建一个 MIMEMultipart 对象作为容器把正文 MIMEText 和附件二进制内容一起放进去。 文件路径send_with_attachment.py 功能发送一封带 Excel 附件的邮件 import smtplib import os from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication from email.utils import formataddr SMTP_HOST os.getenv(SMTP_HOST) SMTP_PORT int(os.getenv(SMTP_PORT, 465)) SENDER_EMAIL os.getenv(SENDER_EMAIL) SENDER_NAME os.getenv(SENDER_NAME, 报表服务) AUTH_CODE os.getenv(AUTH_CODE) TO_EMAIL financeexample.com SUBJECT 7月对账单 CONTENT 请查收附件中的对账单。 def send_email_with_attachment(attachment_path, display_nameNone): msg MIMEMultipart() msg[From] formataddr((SENDER_NAME, SENDER_EMAIL)) msg[To] TO_EMAIL msg[Subject] SUBJECT body MIMEText(CONTENT, plain, utf-8) msg.attach(body) if display_name is None: display_name os.path.basename(attachment_path) with open(attachment_path, rb) as f: part MIMEApplication(f.read()) # RFC 2231 支持中文附件名避免出现附件名乱码 part.add_header( Content-Disposition, attachment, filename(utf-8, , display_name), ) msg.attach(part) with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout10) as server: server.login(SENDER_EMAIL, AUTH_CODE) server.sendmail(SENDER_EMAIL, [TO_EMAIL], msg.as_string()) print(附件邮件发送成功) if __name__ __main__: # 使用前确保 report.xlsx 已存在于同级目录 send_email_with_attachment(report.xlsx, 7月对账单.xlsx)这里需要注意三点。第一附件要用二进制模式打开使用 MIMEApplication 可以直接把文件内容包装成 MIME 应用类型比 MIMEBase 手动设置主类型和子类型更方便。第二中文附件名需要特殊处理add_header 传入 filename(utf-8, , display_name)这样大多数邮件客户端能正确识别中文文件名。第三邮件大小需要关注SMTP 服务商通常有单封邮件附件大小限制超过限制会发送失败或触发服务商拒收。5.3 批量发送时的节流与异常处理批量发送并不是把 sendmail 放进 for 循环那么简单。连续快速发送大量邮件很容易触发服务商的频率限制导致后续邮件全部被拒收。更合理的方式是逐封发送并在每封之间设置间隔同时记录每一封的发送结果。 文件路径batch_send.py 功能向多个收件人逐封发送邮件并记录成功与失败情况 import smtplib import time import logging from email.mime.text import MIMEText from email.utils import formataddr logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, ) logger logging.getLogger(__name__) SMTP_HOST os.getenv(SMTP_HOST) SMTP_PORT int(os.getenv(SMTP_PORT, 465)) SENDER_EMAIL os.getenv(SENDER_EMAIL) SENDER_NAME os.getenv(SENDER_NAME, 通知服务) AUTH_CODE os.getenv(AUTH_CODE) RECIPIENTS [ user1example.com, user2example.com, user3example.com, ] SUBJECT 批量通知 CONTENT 这是一条需要发给多位同事的批量通知。 def send_batch_email(): success_count 0 fail_count 0 with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout10) as server: server.login(SENDER_EMAIL, AUTH_CODE) for recipient in RECIPIENTS: try: msg MIMEText(CONTENT, plain, utf-8) msg[From] formataddr((SENDER_NAME, SENDER_EMAIL)) msg[To] recipient msg[Subject] SUBJECT server.sendmail(SENDER_EMAIL, [recipient], msg.as_string()) success_count 1 logger.info(发送成功: %s, recipient) except smtplib.SMTPException as exc: fail_count 1 logger.error(发送失败: %s, 原因: %s, recipient, exc) except Exception as exc: fail_count 1 logger.error(未知异常: %s, 原因: %s, recipient, exc) finally: time.sleep(2) # 控制发送间隔避免触发频率限制 logger.info(批量发送完成成功 %s 封失败 %s 封, success_count, fail_count) if __name__ __main__: send_batch_email()从工程角度看批量发送真正重要的不是“把所有 sendmail 写完”而是异常隔离。如果一条 recipient 地址格式错误导致整个循环中断前面发送成功的邮件虽然已经投递但后面所有收件人都收不到通知这会造成非常隐蔽的事故。逐封捕获异常后程序可以继续执行并把失败信息写入日志方便之后人工补发。6. 办公自动化集成读取 Excel 收件人名单并发送在实际办公自动化里收件人往往不是硬编码在 Python 文件中而是维护在一张 Excel 表中。业务部门可以在表格里增删成员脚本每次运行时读取名单自动发送通知。这种模式把“修改发送名单”从程序发布流程中解放出来也是 Excel 办公自动化最常见的用法。6.1 Excel 名单结构假设有一份 staff_recipients.xlsx内容如下姓名 邮箱 所属组 张三 zhangsanexample.com 数据组 李四 lisiexample.com 运营组 王五 wangwuexample.com 数据组脚本需要读取这张表按“所属组”过滤需要通知的成员再给每个收件人发送一封简单日报。打开 Excel 并读取内容可以使用 openpyxl 库如果尚未安装执行下面的命令pip install openpyxl6.2 读取名单并发送通知 文件路径send_by_excel.py 功能读取 Excel 中的收件人名单然后向指定成员发送通知 import os import time import smtplib import logging from email.mime.text import MIMEText from email.utils import formataddr from openpyxl import load_workbook logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, ) logger logging.getLogger(__name__) SMTP_HOST os.getenv(SMTP_HOST) SMTP_PORT int(os.getenv(SMTP_PORT, 465)) SENDER_EMAIL os.getenv(SENDER_EMAIL) SENDER_NAME os.getenv(SENDER_NAME, 数据组通知) AUTH_CODE os.getenv(AUTH_CODE) EXCEL_FILE staff_recipients.xlsx GROUP_NAME 数据组 SUBJECT 数据组日报已生成 CONTENT 请及时登录报表系统查看最新日报如有问题请联系数据组。 def load_recipients_from_excel(file_path, group_name): 读取 Excel 中指定组成员的邮箱列表 wb load_workbook(file_path, data_onlyTrue) ws wb.active recipients [] # 从第二行开始遍历忽略表头 for row in ws.iter_rows(min_row2, values_onlyTrue): name, email, group row if group and group.strip() group_name: if email and in email: recipients.append((name, email)) else: logger.warning(邮箱格式异常: %s, email) wb.close() return recipients def send_emails(recipients): with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout10) as server: server.login(SENDER_EMAIL, AUTH_CODE) for name, email in recipients: try: msg MIMEText(CONTENT, plain, utf-8) msg[From] formataddr((SENDER_NAME, SENDER_EMAIL)) msg[To] email msg[Subject] SUBJECT server.sendmail(SENDER_EMAIL, [email], msg.as_string()) logger.info(已发送给 %s %s, name, email) except Exception as exc: logger.error(发送给 %s %s 失败: %s, name, email, exc) finally: time.sleep(1) if __name__ __main__: data_group load_recipients_from_excel(EXCEL_FILE, GROUP_NAME) logger.info(本次需要发送 %s 封邮件, len(data_group)) send_emails(data_group)这段代码有两点值得注意。第一openpyxl 的 load_workbook 支持 data_onlyTrue如果 Excel 中包含公式data_onlyTrue 可以读取公式缓存结果否则读出的是公式字符串。第二读取每行时使用 values_onlyTrue每行会转换为一个元组避免在代码中频繁操作 Cell 对象。读取完成后需要调用 wb.close() 释放文件句柄否则在 Windows 系统上多次运行时会提示文件被占用。6.3 与定时任务结合脚本本身只完成“名单读取 邮件发送”如果希望每天早上 9 点自动执行还需要一个调度器。在 Windows 上可以使用任务计划程序来定时执行 python 命令在 Linux 上可以使用 cron。具体调度配置不属于邮件发送模块本身但理解整个流程很重要定时器负责到点启动脚本脚本负责生成内容和发送结果日志负责记录是否成功。建议在脚本启动的最开始写一条日志标识本次任务开始执行方便后续排查“是没执行还是执行失败”。7. 常见问题与排查思路7.1 高频问题表问题现象可能原因排查方式解决方案报错 smtplib.SMTPAuthenticationError使用了登录密码而非授权码或授权码错误检查邮箱设置中 SMTP 服务是否开启对比授权码是否复制完整重新生成授权码并更新环境变量报错 TimeoutError 或连接超时网络不通或 SMTP 地址端口配置错误检查 SMTP_HOST 和 SMTP_PORT使用 telnet 或 ping 测试连通性更换成正确 SMTP 服务器检查防火墙限制发送成功但收件人没收到被服务商延迟或进入垃圾箱查看邮箱服务商退信通知检查垃圾箱把发件人加入白名单降低发送频率中文收件人显示乱码邮件头编码不正确检查 From 和 To 是否使用 formataddr 格式化使用 email.utils.formataddr 构造发件人字段中文附件名乱码附件文件名未加 RFC 2231 格式检查 add_header 中 filename 参数设置 filename(utf-8, , display_name)单个收件人失败导致全部中断没有对每个收件人单独捕获异常查看日志定位中断位置逐封发送并捕获异常记录失败邮箱代码运行后卡住不退出SMTP 调用未设置超时查看代码是否指定 timeout 参数在 SMTP_SSL 或 SMTP 构造时增加 timeout7.2 典型排查路径当邮件发送失败时不要直接看代码逻辑先确认异常发生在哪一层。第一层是连接层如果抛出 socket.gaierror 或 TimeoutError问题通常出在网络或 SMTP 域名解析上跟邮件内容没有关系。第二层是认证层如果抛出 SMTPAuthenticationError优先检查授权码和账号。第三层才是发送层例如 SMTPDataError、554 之类表示服务器拒绝接受数据。按这个顺序排查能节约大量时间。如果日志里没有任何输出先检查 Python 是否真的执行到了发送函数最简单的办法是在脚本入口加 print 或 logging 标记。很多定时任务悄悄失败并不是代码逻辑问题而是环境变量没有加载脚本读取到的授权码是空字符串最终在 login 阶段抛出异常。7.3 一封退信说明读什么服务商返回退信时通常会在退信内容中包含一段错误码。常见的 550 表示邮箱地址不存在或无权发送554 通常表示邮件内容或发送行为被判定为垃圾邮件。遇到 554 时除了检查内容措辞还要确认单次发送数量和频率是否合规。作为自动化脚本主题和正文应保持可识别性尽量避免使用容易触发垃圾邮件过滤机制的夸张词汇。8. 最佳实践与工程化建议8.1 把邮件发送封装成独立服务不建议在业务脚本里直接写 SMTP 连接代码。更推荐的做法是封装一个 MailSender 类把连接建立、登录、发送、关闭连接全部收拢到一个模块中。业务脚本只需要调用 sender.send_text(to, subject, content) 这样的方法。这样做的收益是如果后续发件邮箱或 SMTP 服务商发生变化只需要修改邮件模块不需要改动所有调用方。 文件路径mail_sender.py 功能一个轻量邮件发送封装支持文本和附件 import smtplib import os from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication from email.utils import formataddr class MailSender: def __init__(self, host, port, username, auth_code, sender_name): self.host host self.port port self.username username self.auth_code auth_code self.sender_name sender_name def _build_base(self, to_email, subject): msg MIMEMultipart() msg[From] formataddr((self.sender_name, self.username)) msg[To] to_email msg[Subject] subject return msg def send_text(self, to_email, subject, content): msg MIMEText(content, plain, utf-8) msg[From] formataddr((self.sender_name, self.username)) msg[To] to_email msg[Subject] subject self._send(msg, to_email) def send_with_attachment(self, to_email, subject, content, file_path): msg self._build_base(to_email, subject) body MIMEText(content, plain, utf-8) msg.attach(body) with open(file_path, rb) as f: part MIMEApplication(f.read()) part.add_header( Content-Disposition, attachment, filename(utf-8, , os.path.basename(file_path)), ) msg.attach(part) self._send(msg, to_email) def _send(self, msg, to_email): with smtplib.SMTP_SSL(self.host, self.port, timeout10) as server: server.login(self.username, self.auth_code) server.sendmail(self.username, [to_email], msg.as_string())封装类需要注意一个设计细节不要每次都读取环境变量而是在初始化时把配置传入。这样单元测试时可以传入测试服务器地址而不是依赖全局环境变量可测试性会好很多。8.2 配置集中管理并防止泄密授权码应放在环境变量中而不是硬编码。团队协作时应约定 .env 文件或密钥管理工具负责保存敏感信息普通配置则写入配置中心或项目配置文件。代码仓库中提交的示例配置只能保留变量占位符不能包含真实邮箱和授权码。一旦发现授权码可能泄露立即到邮箱服务商平台重置这是最快的止损方式。8.3 日志、重试与告警邮件服务是自动流程里的通知出口如果它本身出现问题但又没有通知渠道整个自动化链条就会“悄悄断掉”因此日志必须完整。每条发送记录至少应包含时间、收件人、主题、发送结果、异常信息。对于偶发网络超时可以增加一定次数的重试但重试之间必须间隔一段时间且重试次数不宜过多避免邮件重复发送。如果只是发送通知失败可以考虑把失败信息写入本地 CSV 文件下次运行任务时补发如果是对实时性要求极高的告警类邮件失败后应通过其他渠道提醒维护人员。8.4 合法合规使用邮件服务邮件自动化能力的边界必须清楚。只能向明确知情并授权的收件人发送邮件不能利用脚本获取或遍历邮箱地址进行未经同意的营销推广。办公自动化中批量发送内部通知时收件人名单应来自公司内部维护的通讯录或业务系统并遵循企业信息安全规范。发送前确认内容准确、附件无误尤其是涉及金额、考核、人事等敏感信息时宁可多一次人工复核也不要依赖全自动直接发送。8.5 定时任务中的常见坑定时任务执行环境通常没有图形界面不能依赖脚本默认读取当前目录下的资源文件。如果脚本中用相对路径读取 Excel 或附件而定时任务的当前工作目录与脚本目录不一致就会出现 FileNotFoundError。解决方法是脚本启动时基于自身文件路径去拼接资源目录使用 Path(file).parent 定位脚本所在目录再计算资源文件路径。这样无论从哪里启动都能找到文件。9. 本文小结与下一步学习方向这篇文章从 py100 系列 lv2-089 项目出发完整拆解了 Python 邮件发送服务涉及的核心链路SMTP 负责把邮件提交给服务器MIME 负责组装可解析的邮件内容授权码和 SSL 负责安全连接。示例代码覆盖了纯文本邮件、HTML 报表邮件、附件邮件、批量发送以及通过 openpyxl 从 Excel 中读取收件人名单的办公自动化整合场景。如果你刚刚完成基础语法学习可以把最容易出错的授权码配置和 SMTP 端口作为第一个练习目标先确保最小邮件能发出去。如果已经能正常发信再继续完善异常处理和日志记录把 sendmail 从业务代码中剥离出来做成独立模块。如果你打算把它接入真实定时任务建议顺带阅读 openpyxl 生成 Excel、logging 日志轮转、cron 或 Windows 计划任务的用法让整个办公自动化链路不仅“能跑”而且“跑得稳定”。邮件发送服务的工程价值不只体现在代码量上更体现在它把最终触达用户的一步变成了可管理、可观察、可追溯的流程。这也是办公自动化区别于一次性数据处理脚本的重要标志。建议收藏本文中的代码示例和排查表实际遇到发送失败时打开对照处理会比自己盲目尝试高效得多。