用smtplib+email发带附件邮件需手动构建mime结构:以mimemultipart为容器,attach mimetext正文和mimeapplication附件;附件须rb模式读取、base64编码、中文名用header编码;定时用schedule而非apscheduler,避免job重复注册;smtp配置须核对服务器地址/端口、完整邮箱账号、app密码;生产环境必须在job内加try/except分类型处理异常并重试。

用 smtplib + email 构建带附件的邮件体
直接用 smtplib.sendmail() 发纯文本没问题,但带附件必须手动构造 MIME 结构。核心是用 email.mime.multipart.MIMEMultipart() 作为容器,再把正文(MIMEText)和附件(MIMEBase 或 MIMEApplication) attach 进去。
常见错误:把附件内容直接写进邮件体、没调用 encoders.encode_base64()、附件名含中文没用 Header 编码——这三类都会导致收件方看到乱码或无法下载。
- 附件路径必须存在且 Python 有读取权限,推荐先用
os.path.isfile(attachment_path)校验 - 二进制附件(如 PDF、Excel)务必用
rb模式打开,并传给MIMEApplication(..., _subtype='octet-stream') - 中文附件名要包装成:
Header(filename, 'utf-8').encode(),然后赋给add_header('Content-Disposition', ...)
用 schedule 实现轻量级定时,别碰 APScheduler 初期
schedule 足够应付每天固定时间发一封、每周一上午发一类报表等场景;它不依赖数据库、不带 Web 接口、代码一眼能看全,出问题也容易 debug。而 APScheduler 默认用内存存储 job,重启就丢;配 SQLite 又要多管 schema 和锁,对单机脚本属于过度设计。
典型误用:在循环里反复调用 schedule.every().day.at("09:00").do(send_email) —— 这会不断叠加新 job,不是覆盖。正确做法是只调用一次,然后用 while True: schedule.run_pending(); time.sleep(60) 驱动。
- 时间字符串必须是 24 小时制,
"9:00"无效,得写"09:00"或"21:30" - 如果发送逻辑耗时较长(比如附件生成要 20 秒),建议在
do()里开线程,否则会阻塞整个 scheduler 的 tick - 加个日志:在 job 函数开头写
logging.info(f"Sending report at {datetime.now()}"),方便确认是否准时触发
SMTP 登录失败?重点查这三项配置
90% 的“认证失败”不是密码错,而是这三个地方没对上:
- SMTP 服务器地址和端口不匹配:Gmail 用
smtp.gmail.com:587(STARTTLS),Outlook 用smtp-mail.outlook.com:587,国内企业邮箱常是smtp.exmail.qq.com:465(SSL)——端口错了直接连不上 - 账号不是“登录邮箱”,而是“发信邮箱”:有些企业邮箱要求用完整域名格式,比如
user@company.com,填user就报535 5.7.8 Error - 没开“SMTP 服务”或“客户端专用密码”:Gmail/Outlook 现在强制用 App Password,不能用主密码;腾讯企业邮箱需在管理后台开启 SMTP 并生成授权码
测试连接最简方式:telnet smtp.gmail.com 587(Linux/macOS)或 Test-NetConnection smtp.gmail.com -Port 587(PowerShell),通了再进 Python 层。
生产环境必须加异常捕获和重试机制
邮件发送不是原子操作:网络抖动、SMTP 临时拒绝、附件生成失败、磁盘满……任何一个环节崩了,整个定时任务就静默失效。没有 try/except 包裹,等于裸奔。
不要只 catch Exception,至少区分三类:
-
smtplib.SMTPAuthenticationError:立刻告警,说明凭据失效,人工介入 -
smtplib.SMTPServerDisconnected或OSError:网络问题,可自动重试 2 次,间隔 10 秒 - 附件打开失败(
FileNotFoundError)、MIME 构造异常(ValueError):记录错误路径,跳过本次,不重试
真正容易被忽略的是:重试逻辑必须放在 job 函数内部,而不是 schedule 外层——因为 schedule 不管你函数里干了什么,它只负责调用一次。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











