Python 自动发邮件实战:每天 8 点把报表自动发到邮箱
摘要: 每天早上到公司第一件事:打开前一天的报表、写邮件、发老板?这件事完全可以交给 Python:本文用 标准库 smtplib + email 组装带 Excel 附件的邮件,用 schedule 做定时,每天 8 点自动投递到老板邮箱。更关键的是,本文提供一个 "零成本调试法"------用本地调试 SMTP 服务器把发信链路完整验证通,不用真邮箱、不用真实授权码就能把代码写对。全文代码均已实测通过。
标题
- [Python 自动发邮件实战:每天 8 点把报表自动发到邮箱](#Python 自动发邮件实战:每天 8 点把报表自动发到邮箱)
-
- 一、效果预览与整体思路
- 二、准备工作:拿到邮箱"授权码"
- [三、组装邮件:EmailMessage 三步走](#三、组装邮件:EmailMessage 三步走)
- [四、发送邮件:SMTP_SSL 十行搞定](#四、发送邮件:SMTP_SSL 十行搞定)
- [五、零成本调试:本地 SMTP 服务器大法](#五、零成本调试:本地 SMTP 服务器大法)
- [六、定时调度:schedule 让脚本每天自动跑](#六、定时调度:schedule 让脚本每天自动跑)
- 七、完整代码
- 八、运行结果
- 九、生产环境部署建议与常见坑
- 壁纸分享
- 十、总结
一、效果预览与整体思路
最终效果:脚本挂在后台上,每天 08:00 自动把《9 月销售汇总报表.xlsx》(系列第 1 篇的产出)作为附件发给老板,正文是一段简短的 HTML 说明。
整体结构是一张三层积木:
① 组装邮件 build_message() → ② 发送邮件 send_mail() → ③ 定时调度 run_scheduler()
EmailMessage:正文+附件 smtplib:SMTP_SSL schedule:每天 8 点触发
每一层都能单独测试------这是本篇最重要的工程思想:先保证"一封信能发出去",再保证"到点会触发"。
二、准备工作:拿到邮箱"授权码"
用代码发邮件,登录用的不是邮箱密码,而是"授权码"(第三方客户端专用密码):
- QQ 邮箱 :设置 → 账号 → 开启 SMTP 服务 → 生成授权码;SMTP 服务器
smtp.qq.com,SSL 端口465; - 163 邮箱 :设置 → POP3/SMTP/IMAP → 开启并生成授权码;服务器
smtp.163.com,SSL 端口465。
企业邮箱找 IT 要 SMTP 地址和端口即可,逻辑完全一样。
安全提醒 :授权码相当于发信密码,不要硬编码进会外传的代码,更不要提交到 Git。本文为演示写在配置区,正式项目请用环境变量
os.environ["MAIL_AUTH_CODE"]。
三、组装邮件:EmailMessage 三步走
推荐用新式的 email.message.EmailMessage(标准库),比老式的 MIMEMultipart 一堆类拼装清爽得多:
python
from email.message import EmailMessage
from pathlib import Path
def build_message(subject: str, html_body: str,
attachment: Path | None = None) -> EmailMessage:
"""组装一封"正文 + 附件"的邮件对象(与发送解耦,方便单独测试)"""
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = SENDER
msg["To"] = ", ".join(RECEIVERS)
msg.set_content(html_body, subtype="html") # 支持 HTML 正文
if attachment is not None and attachment.exists():
# filename 直接传中文,EmailMessage 会自动按标准编码,不会乱码
msg.add_attachment(attachment.read_bytes(),
maintype="application", subtype="xlsx",
filename=attachment.name)
return msg
四个值得记住的点:
set_content(body, subtype="html"):邮件正文天然支持 HTML,加个颜色、加个签名链接,比纯文本体面得多;add_attachment()传中文文件名不乱码 :它会自动按 RFC 2231 标准编码;老式MIMEApplication里那套add_header("Content-Disposition", ..., filename=("gbk", "", name))的黑魔法可以扔了;- 组装与发送分离 :
build_message只造信不发信,单元测试友好; - 附件存在才附 :报表没生成时脚本不会崩,只是少个附件或跳过(后面
job()里有检查)。
小科普:邮件里的中文主题在传输时会被编码成形如
=?utf-8?b?44CQ...?=的样子,这是 RFC 2047 标准编码,收件端(Outlook/Foxmail/手机邮箱)都会自动还原成中文,不是乱码。
四、发送邮件:SMTP_SSL 十行搞定
python
import smtplib
def send_mail(msg: EmailMessage, host: str = SMTP_HOST, port: int = SMTP_PORT,
debug_server: str | None = None):
"""发送邮件;debug_server 形如 'localhost:1025',走本地调试服务器"""
if debug_server:
d_host, d_port = debug_server.rsplit(":", 1)
with smtplib.SMTP(d_host, int(d_port), timeout=10) as smtp:
smtp.send_message(msg)
print(f"[调试] 邮件已投递到本地调试服务器 {debug_server}")
return
with smtplib.SMTP_SSL(host, port, timeout=15) as smtp: # 465 端口走 SSL
smtp.login(SENDER, AUTH_CODE)
smtp.send_message(msg)
print(f"邮件已发送:{msg['Subject']} -> {msg['To']}")
注意 465 端口要用 SMTP_SSL (连接即加密);如果用 587 端口,则是 SMTP() + smtp.starttls()。两者别混。
五、零成本调试:本地 SMTP 服务器大法
学这篇最大的障碍通常是:没有授权码/不好意思用真邮箱反复测试。解法是在本机跑一个调试 SMTP 服务器,它不真发信,只把收到的邮件打印出来------
bash
pip install aiosmtpd
python -m aiosmtpd -n -l localhost:1025
保持这个窗口开着,另开一个窗口运行脚本时加上 --debug-server localhost:1025(见第七节完整代码),邮件就"发"给了本地服务器,终端会完整打印出协议交互和邮件内容:
text
---------- MESSAGE FOLLOWS ----------
Subject: =?utf-8?b?44CQ5rWL6K+V44CRUHl0aG9uIOiHquWKqOaKpeihqOmCruS7tg==?=
From: your_account@qq.com
To: boss@example.com
Content-Type: multipart/mixed; ...
Content-Disposition: attachment;
filename*0*=utf-8''9%E6%9C%88%E9%94%80%E5%94%AE%E6%B1%87%E6%80%BB%E6%8A%A5;
filename*1*=%E8%A1%A8.xlsx
从这里可以直接确认:主题编码正确、附件文件名 9月销售汇总报表.xlsx 编码正确、正文是 HTML。链路验证通过后,把配置换成真实邮箱,一次性切换成功率极高。
六、定时调度:schedule 让脚本每天自动跑
定时用 schedule(轻量,几行上手):
bash
pip install schedule
python
import schedule
import time
def job(debug_server: str | None = None):
"""到点要做的事:组装并发送报表邮件"""
if not ATTACHMENT.exists():
print(f"[跳过] 找不到报表文件:{ATTACHMENT}")
return
body = ("<p>早上好,昨日销售报表见附件。</p>"
"<p style='color:#888'>本邮件由 Python 脚本自动发送</p>")
send_mail(build_message("【自动】9 月销售汇总报表", body, ATTACHMENT),
debug_server=debug_server)
def run_scheduler(test_seconds: int | None = None,
debug_server: str | None = None):
if test_seconds:
# 测试模式:每 test_seconds 秒触发一次,验证调度链路
schedule.every(test_seconds).seconds.do(job, debug_server=debug_server)
print(f"[测试] 每 {test_seconds} 秒触发一次,Ctrl+C 退出")
else:
schedule.every().day.at("08:00").do(job, debug_server=debug_server)
print("定时任务已注册:每天 08:00 发送报表,Ctrl+C 退出")
while True:
try:
schedule.run_pending()
except Exception as e:
# 单次任务失败(网络抖动等)不能把整个调度循环搞挂
print(f"[任务异常] {e}")
time.sleep(1)
三个实战细节:
schedule.every().day.at("08:00"):几点几分触发一目了然,还支持.monday、.hour、.seconds等各种粒度;do(job, debug_server=...)可以带参数 ,同一份job()既能正式运行也能接调试服务器;- 调度循环里的 try/except 是保命符:某天网络抖动发信失败,抛了异常,若不接住,整个调度循环直接死掉,你第二天才发现老板没收到报表。单次失败只打日志,循环永续。
七、完整代码
保存为 mail_report.py:
python
# -*- coding: utf-8 -*-
"""
自动发邮件实战:每天 8 点把销售报表自动发到邮箱
依赖:pip install schedule
本地联调(可选):pip install aiosmtpd
用法:
python mail_report.py --test-mail # 通过本地调试服务器发一封测试邮件
python mail_report.py --test-schedule # 定时器按"每 3 秒"触发,验证调度逻辑
python mail_report.py # 正式模式:每天 08:00 发送(需先改配置)
"""
import argparse
import smtplib
import time
from email.message import EmailMessage
from pathlib import Path
import schedule
# ============ 邮箱配置(正式使用前改成你自己的)============
SMTP_HOST = "smtp.qq.com" # QQ 邮箱 SMTP;163 邮箱为 smtp.163.com
SMTP_PORT = 465 # SSL 端口
SENDER = "your_account@qq.com"
AUTH_CODE = "你的授权码" # 不是登录密码!在邮箱设置里生成
RECEIVERS = ["boss@example.com"] # 支持多个收件人
ATTACHMENT = Path(__file__).parent / "9月销售汇总报表.xlsx" # 要发的报表
def build_message(subject: str, html_body: str,
attachment: Path | None = None) -> EmailMessage:
"""组装一封"正文 + 附件"的邮件对象(与发送解耦,方便单独测试)"""
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = SENDER
msg["To"] = ", ".join(RECEIVERS)
msg.set_content(html_body, subtype="html") # 支持 HTML 正文
if attachment is not None and attachment.exists():
msg.add_attachment(attachment.read_bytes(),
maintype="application", subtype="xlsx",
filename=attachment.name)
return msg
def send_mail(msg: EmailMessage, host: str = SMTP_HOST, port: int = SMTP_PORT,
debug_server: str | None = None):
"""发送邮件;debug_server 形如 'localhost:1025',走本地调试服务器"""
if debug_server:
d_host, d_port = debug_server.rsplit(":", 1)
with smtplib.SMTP(d_host, int(d_port), timeout=10) as smtp:
smtp.send_message(msg)
print(f"[调试] 邮件已投递到本地调试服务器 {debug_server}")
return
with smtplib.SMTP_SSL(host, port, timeout=15) as smtp: # 465 端口走 SSL
smtp.login(SENDER, AUTH_CODE)
smtp.send_message(msg)
print(f"邮件已发送:{msg['Subject']} -> {msg['To']}")
# ============ 定时任务 ============
def job(debug_server: str | None = None):
"""到点要做的事:组装并发送报表邮件"""
if not ATTACHMENT.exists():
print(f"[跳过] 找不到报表文件:{ATTACHMENT}")
return
body = ("<p>早上好,昨日销售报表见附件。</p>"
"<p style='color:#888'>本邮件由 Python 脚本自动发送</p>")
send_mail(build_message("【自动】9 月销售汇总报表", body, ATTACHMENT),
debug_server=debug_server)
def run_scheduler(test_seconds: int | None = None,
debug_server: str | None = None):
if test_seconds:
schedule.every(test_seconds).seconds.do(job, debug_server=debug_server)
print(f"[测试] 每 {test_seconds} 秒触发一次,Ctrl+C 退出")
else:
schedule.every().day.at("08:00").do(job, debug_server=debug_server)
print("定时任务已注册:每天 08:00 发送报表,Ctrl+C 退出")
while True:
try:
schedule.run_pending()
except Exception as e:
print(f"[任务异常] {e}")
time.sleep(1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="定时发送销售报表邮件")
parser.add_argument("--test-mail", action="store_true",
help="通过本地调试服务器发一封测试邮件")
parser.add_argument("--test-schedule", action="store_true",
help="调度器按秒触发,快速验证定时逻辑")
parser.add_argument("--debug-server", default="localhost:1025",
help="本地调试 SMTP 地址(默认 localhost:1025)")
args = parser.parse_args()
if args.test_mail:
send_mail(build_message("【测试】Python 自动报表邮件",
"<p>这是一封测试邮件</p>", ATTACHMENT),
debug_server=args.debug_server)
elif args.test_schedule:
run_scheduler(test_seconds=3, debug_server=args.debug_server)
else:
run_scheduler()
八、运行结果
第一步,调试服务器先跑起来(窗口 A):
bash
pip install aiosmtpd
python -m aiosmtpd -n -l localhost:1025
第二步,发一封测试邮件(窗口 B):
text
$ python mail_report.py --test-mail
[调试] 邮件已投递到本地调试服务器 localhost:1025
窗口 A 立刻打印出收到的完整邮件(主题、HTML 正文、附件俱在)------发信链路验证通过。
第三步,验证定时调度(每 3 秒触发一次,实测):
text
$ python mail_report.py --test-schedule
[测试] 每 3 秒触发一次,Ctrl+C 退出
[调试] 邮件已投递到本地调试服务器 localhost:1025
[调试] 邮件已投递到本地调试服务器 localhost:1025
每 3 秒准时投递一封,调度链路也验证通过。
第四步,正式上线 :把配置区改成真实邮箱和授权码,直接 python mail_report.py,注册的任务是"每天 08:00",到点自动发信。
九、生产环境部署建议与常见坑
部署方式二选一:
- 方案 A:脚本内建调度 (本文做法)。电脑/服务器上挂着
python mail_report.py不退出即可;Windows 可用"任务计划程序"设为开机自启,Linux 用systemd或nohup守护; - 方案 B:系统级调度 。脚本改成"跑一次就退出"(去掉 while 循环),交给 Windows 任务计划程序每天 8:00 触发,或 Linux crontab:
0 8 * * * python /path/mail_report.py。系统级调度更稳(不怕脚本意外退出),推荐生产使用。
常见坑:
| 现象 | 原因 | 解法 |
|---|---|---|
(535, b'Error: authentication failed') |
用了登录密码而非授权码 | 去邮箱设置重新生成授权码 |
| 连接超时 | 端口/加密方式不匹配 | 465 用 SMTP_SSL;587 用 SMTP+starttls |
| 收件人收到乱码主题 | 手动拼邮件头 | 用 EmailMessage,让它自动编码 |
| 附件名乱码 | 老式 API 手写 filename 头 | add_attachment(..., filename=中文名) |
| 定时脚本"失踪" | 循环里异常没接住,进程已死 | 见第六节的 try/except |
壁纸分享

十、总结
复盘本篇的三层积木和两个法宝:
- 组装 :
EmailMessage+set_content(html)+add_attachment(中文名); - 发送 :
SMTP_SSL+login(授权码)+send_message; - 调度 :
schedule.every().day.at("08:00")+ 异常保护循环; - 法宝一:本地调试服务器(aiosmtpd),零成本把发信链路调通;
- 法宝二:按秒测试模式,不用等到早上 8 点才知道定时灵不灵。
在这个基础上再叠加第 1 篇的"自动合并 Excel",你就得到了一条完整的无人值守流水线:凌晨自动合并 → 早上 8 点自动发。
如果本文对你有帮助,点赞 + 收藏 + 关注三连支持一下~ 你想让脚本定时发什么?评论区聊聊。下一篇我们写《Python 批量处理 Word/PDF 实战:python-docx + PyMuPDF》。