ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python发邮件教程:基于smtplib构建办公自动化邮件服务

Python发邮件教程:基于smtplib构建办公自动化邮件服务 Python 办公自动化里的邮件发送服务是解决重复通知、报表分发、状态告警时最直接的手段之一。很多人一开始觉得发邮件很简单写完邮件点一下发送就行。可真到要每天给几十个收件人发不同附件或者数据任务凌晨跑完后需要自动通知相关人员手动操作完全扛不住。这个主题解决的是用 Python 脚本代替人工发送把发件账号、收件人、正文、附件变成可配置的输入让一台普通电脑或服务器定时把邮件发出去。适合有一定 Python 基础、想优化重复工作的人也适合正在搭建小型自动化流程的运维、数据分析、行政、项目经理。最值得关注的不是 smtplib 的某个 API而是如何从一次能跑的脚本变成长期不翻车的自动化服务。下面按我实际使用的顺序拆开讲先判断场景再准备发送链路然后从最小脚本开始逐步升级成可维护的服务最后给出排查思路。1. 先判断场景不是所有邮件都该放进自动发送脚本邮件发送脚本能做很多事但放在办公自动化里要优先解决那些规则固定、频率固定、重复劳动明显的请求。先把这个判断做清楚后面才不会被需求来回打断。1.1 适合自动发送的邮件类型我一般会先看三个条件内容是否可编程生成收件人是否能从表格或系统里拿到发送时间是否固定。三个条件都满足就可以考虑自动化。日常里典型的场景包括周期报表每天把 MySQL、Excel、业务后台的统计结果整成一个附件定时发给指定人。任务通知数据清洗、文件处理、批量任务完成后发一封“成功”或“失败”的邮件。定时提醒比如上午 9 点提醒当天待办或者下班前推送未完成事项。系统告警服务异常或磁盘快满时让脚本主动发告警邮件。这类邮件有几个共同点每次发送的收件人集合稳定正文结构类似附件来自固定目录或固定任务结果丢失一封会造成麻烦。因此更适合用代码控制而不是让员工每天重复建邮件、拖附件。1.2 不适合用普通发信脚本硬撑的场景办公自动化不等于群发营销。如果你需要给几千个外部用户发推广邮件或者发到随意收集的邮箱列表问题通常不在 Python 脚本本身而在邮件服务商的发送策略、退订管理、内容合规和域名信誉。这时候硬写一个 smtplib 脚本是危险的容易被对方邮件服务器拒绝还会拖累企业邮箱的公信力。另外内部发信场景也有限制大部分邮箱对单日发信数量有上限连续大量发送同一个主题会被判定为异常流量。即使收件人都是同事也要控制发送频率。数据量比较大时应该把任务拆成多批在时间上隔开并且提前测试发送成功率。所以开始写代码前先给需求定个性是给少量已知同事发通知还是做大规模触达。前者很适合直接写脚本后者应先向管理员确认邮件系统的发送能力或接入专业邮件服务。2. 动手前确认四件事SMTP、端口、授权和运行环境邮件发送脚本的底层协议就是 SMTP理解这一层有助于排错。不要一上来复制代码先确认几个固定参数。2.1 发件邮箱的 SMTP 地址和端口怎么确认现在企业邮箱、个人邮箱基本都提供 SMTP 服务只是默认不开放需要到邮箱设置里手工开启。常见的端口是 465 和 587端口方式适用情况465SSL 加密直接用smtplib.SMTP_SSL很多邮箱的默认推荐587STARTTLS 升级加密先建普通连接再加密部分企业邮箱或服务商使用25明文 SMTP通常被运营商屏蔽不建议在办公系统里用特殊内网或专用服务你需要去发件邮箱的设置页面查 SMTP 服务器地址例如企业邮箱一般会提供smtp.company.com或类似地址。不要靠猜。有一次我在一个项目里看到企业邮箱的 SMTP 地址写错成普通个人邮箱的地址结果连续报错查了很久才发现是服务器地址不对。地址、端口、是否要求加密这三项要以邮箱服务商提供的文档为准。2.2 授权码才是脚本该用的登录凭据大部分邮箱开启 SMTP 后要求使用“授权码”而不是邮箱登录密码。这是安全设计。如果你的代码里一直在用账号密码登录失败先确认是否已开启 SMTP 并生成授权码。我建议把授权码放到环境变量或独立配置文件中不要直接写进.py文件更不要提交到 Git 仓库。之前见过有同事把企业邮箱口令写在代码里再连同整个项目推到代码托管平台。当天就被安全提醒还好发现及时。普通办公项目也要有这层意识。在本地可以先设置环境变量export MAIL_AUTH_CODE你的授权码 export MAIL_SENDERsendercompany.comPython 代码里用os.getenv读取import os auth_code os.getenv(MAIL_AUTH_CODE)如果是 Windows可以在系统环境变量里设置也可以写一个.env文件然后用 python-dotenv 读取。选择哪种方案无所谓原则是账号口令和代码分离。3. 从一次能跑的最小脚本开始不要一开始就封装类初次接触这个概念的人很容易直接找封装好的邮件库或者一上来就写一个带队列的后台任务。我建议反过来先用标准库把一封文本邮件发出去整个链路通了再谈复杂功能。3.1 第一版发送纯文本邮件Python 发邮件主要用smtplib负责网络传输用email.mime.text.MIMEText构造邮件内容。这是一个最小示例import os import smtplib from email.mime.text import MIMEText sender os.getenv(MAIL_SENDER) receiver receiverexample.com auth_code os.getenv(MAIL_AUTH_CODE) msg MIMEText(这是一封 Python 发送的测试邮件。, plain, utf-8) msg[From] sender msg[To] receiver msg[Subject] 测试邮件 with smtplib.SMTP_SSL(smtp.example.com, 465, timeout10) as server: server.login(sender, auth_code) server.sendmail(sender, [receiver], msg.as_string())这里几个点要解释MIMEText(正文, plain, utf-8)表示纯文本邮件指定 UTF-8 避免中文乱码。msg[From]和msg[To]是邮件头信息不能为空。SMTP_SSL直接走 465 端口。如果你的服务商要求 587 端口就改成smtplib.SMTP加server.starttls()。with语句会自动关闭连接比手动调用quit()更省心。第一次跑的时候我建议先发给自己。如果只是验证代码不要直接发给部门群。收件人填自己的备用邮箱看到邮件后再做下一步。3.2 第二版发送带格式的 HTML 邮件纯文本在办公通知里也能用但稍微正式一点的通知比如把关键信息做成标题、加个简单表格显示效果会更好。写法是把MIMEText的第二个参数从plain改成htmlhtml_content html body h3每日订单统计已完成/h3 p今天共处理订单 b128/b 笔异常 b3/b 笔。/p /body /html msg MIMEText(html_content, html, utf-8)HTML 邮件适合少量结构化内容。但注意不要写非常大的网页很多邮件客户端会屏蔽外部图片和复杂 CSS。字体大小、间距、链接也要保持克制真正要落地时建议先用邮件客户端的预览功能检查一遍。如果收件人使用不同终端比如手机、Outlook、各类网页邮箱同一个 HTML 邮件的显示效果可能不同。最稳妥的做法是尽量用纯文本把关键信息写清楚HTML 只负责加粗、标题和表格这些基础结构。3.3 第三版在邮件里带上附件办公自动化里最常出现在附件的文件是 Excel、PDF、CSV。用MIMEMultipart可以把正文和附件组合到一封邮件中。import os import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication sender os.getenv(MAIL_SENDER) receiver receiverexample.com auth_code os.getenv(MAIL_AUTH_CODE) msg MIMEMultipart() msg[From] sender msg[To] receiver msg[Subject] 今日报表 body MIMEText(报表详见附件。, plain, utf-8) msg.attach(body) with open(report.xlsx, rb) as f: attachment MIMEApplication(f.read()) attachment.add_header( Content-Disposition, attachment, filenamereport.xlsx ) msg.attach(attachment) with smtplib.SMTP_SSL(smtp.example.com, 465, timeout10) as server: server.login(sender, auth_code) server.sendmail(sender, [receiver], msg.as_string())附件路径不要写死。实际场景中文件可能是另一个任务自动生成的可以先检查文件是否存在文件大小为 0 时直接发告警而不是发一个空附件引起同事误会。这一版跑通后最小发送能力已经具备。4. 单封邮件能发成功之后再升级到可维护的服务很多人止步于“能发送”这是不够的。办公自动化的关键是稳定、可复用、出问题能查。所以第二步我建议把脚本拆成配置项、函数、任务入口三个部分。4.1 把发件账号、收件人列表和正文模板分开账号信息放环境变量收件人和文件名经常变化可以放到配置文件或 Excel 里正文模板如果总变可以单独维护 HTML 文件。不要为一次变化就去改动发信代码。举一个简单例子用config.ini维护发件服务器信息[smtp] server smtp.example.com port 465 sender senderexample.com授权码仍然从环境变量读取。这样别人拿到项目代码也只能看到服务器地址和发件人看不到真实口令。收件人列表用 CSV 管理比较常见比如name,email,remark 张三,zhangsanexample.com,昨天提交工单 李四,lisiexample.com,本周待办读取这份 CSV 的代码并不复杂import csv recipients [] with open(recipients.csv, encodingutf-8-sig) as f: for row in csv.DictReader(f): recipients.append(row)注意utf-8-sig可以处理 Excel 导出的带 BOM 文件。如果直接读 UTF-8有时第一列名称会多出\ufeff字符导致匹配字段失败。这个问题看起来像数据问题实际是文本编码问题我在项目里踩过。4.2 封装成函数调用方只关心收件人、主题和正文不建议把smtplib的细节写在业务脚本的每一处。可以封装一个send_mail函数import os import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication def send_mail(receiver, subject, html_body, attachmentsNone): sender os.getenv(MAIL_SENDER) auth_code os.getenv(MAIL_AUTH_CODE) server_addr os.getenv(MAIL_SERVER, smtp.example.com) server_port int(os.getenv(MAIL_PORT, 465)) msg MIMEMultipart() msg[From] sender msg[To] receiver msg[Subject] subject msg.attach(MIMEText(html_body, html, utf-8)) for path in attachments or []: with open(path, rb) as f: attachment MIMEApplication(f.read()) filename path.split(/)[-1].split(\\)[-1] attachment.add_header( Content-Disposition, attachment, filenamefilename ) msg.attach(attachment) with smtplib.SMTP_SSL(server_addr, server_port, timeout10) as server: server.login(sender, auth_code) server.sendmail(sender, [receiver], msg.as_string())这个函数满足大部分轻量需求。如果你有多个不同的业务要用可以把它放进独立的mail_helper.py在发日报、发告警、发通知时分别调用而不是各写一套。如果你向收到的文件是中文字符名还要额外处理filename编码否则容易显示乱码。4.3 批量发送不等于简单循环要加日志和失败记录最简单的批量是用 for 循环调用上面的函数success_count 0 failed_list [] for person in recipients: try: send_mail(person[email], 日报, 请查收, [report.xlsx]) success_count 1 except Exception as exc: failed_list.append({email: person[email], error: str(exc)})但真实批量发送时不能只靠控制台打印。建议至少保留两样东西日志文件每次发送的时间、收件人、状态。失败清单发送失败的收件人和错误类型方便手动补发。只打印到屏幕的问题很直接脚本在服务器上跑任务结束后屏幕输出早就刷掉了。有一次定时任务失败我打开终端发现命令历史里根本看不到错误只能回头补日志。从那以后我的办公脚本都会把日志写到独立文件。用 Python 的logging模块可以快速实现import logging logging.basicConfig( filenamemail_sender.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) logging.info(send to %s, person[email]) logging.error(send failed %s: %s, person[email], exc)5.
返回列表