Day12:Python实现邮件自动发送
一、为什么要做这个小工具?
在Day10,我们用pandas汇总了Excel。
在Day11,我们用docxtpl批量生成了Word报告。
假如这些报告,我们要通过邮件发送出去,Python能不能帮上忙,不让我们一份一份地、手动添加附件、手动发送出去呢?
当然可以。
我们干的就是把重复劳动这四个字从字典里彻底删掉。
今天,我们就用Python自动登录我们的邮箱,把那些报告一份份精准地发出去。
实际的工作中也确实有一些跟邮件有关的工作。
比如每天早上9点,自动把日报模板发给所有组员。
又或者每周五下午5点,自动把汇总好的周报发给leader。
二、先发一封邮件
这个版本我们先把核心流程大打通,那就是先发送一封存文本的邮件出去,看能不能发送成功,接收到。
2.1. 关键的第一步
第一步是本问最最最重要的一步,也是所有新手都会卡住的地方。那就是获取邮箱授权码。
出于安全考虑,几乎所有邮箱(QQ、163、Gmail)都不允许我们直接用登录密码通过Python脚本来发邮件。
我们必须去邮箱的设置里面,开启一个叫SMTP的服务,然后生成一个专门给程序用的应用密码(或者交授权码)。
以QQ邮箱为例,流程如下:
登录QQ邮箱,进入账号与安全:

进入安全设置,页面拖到最后:

点击开启服务(POP3/IMAP/SMTP/Exchange/CardDAV 服务):

这串16位的码,就是我们的授权码。把他复制下来,这就是我们等下要用的密码,不是我们的QQ登录密码。
2.2. 敲代码
import smtplib
from email.message import EmailMessage
import sslSMTP_SERVER = "smtp.qq.com"
SENDER_EMAIL = "1234@qq.com"
SENDER_PASS = "这里换成我们刚申请的16位授权码"
RECEIVER_EMAIL = "1234@qq.com"msg = EmailMessage()
msg["Subject"] = "来自懒惰蜗牛的问候!"
msg["From"] = SENDER_EMAIL
msg["To"] = RECEIVER_EMAIL
msg.set_content("这是一封由懒惰蜗牛Python脚本自动发送的邮件!")print("正在连接到服务器...")
context = ssl.create_default_context()
with smtplib.SMTP_SSL(SMTP_SERVER, 465, context=context) as smtp:smtp.login(SENDER_EMAIL, SENDER_PASS)print("登录成功!准备发送...")smtp.send_message(msg)print("邮件发送成功!")smtp.quit()
如果你是用的163邮箱,服务器就是smtp.163.com,其他逻辑一样的。
2.3. 运行结果
邮箱里面收到了邮件:

2.4. 代码讲解
import smtplib导入Python内置的邮件模块,专门负责连接邮件服务器(SMTP)并发送邮件。
from email.message…是导入Python内置的类似信纸的模块,专门负责构建邮件的内容,比如标题、正文等。
ssl是Python标准库的一部分,主要提供对SSL/TLS协议的支持,用来加密网络通信。
SMTP_SERVER定义的是接收邮件的域名地址。
SENDER_EMAIL是发送人的邮箱地址,RECEIVER_EMAIL是接收人的邮箱地址。
这两个可以一样,就是用自己的邮箱给自己发送邮件。
SENDER_PASS是我们刚申请的授权码。
msg = EmailMessage()是实例化了一个msg对象,这个对象就是用来操作邮件内容的。
比如msg[“Subject”] 就是在设置邮件的标题,msg[“From”]在设置邮件的发送人邮箱地址。
像Subject、From、To这些特定的字符串不是Python定的,而是一个叫IETF的国际组织定的。
他们会出官方说明书RFC,指定标准的邮件里面包含什么东西,然后大家就按照这个规则来玩。
ssl.create_default_context()是创建一个默认安全配置的SSL上下文对象。
如果你没有计算机基础,不知道SSL这东西,暂时可以这么理解 ,用了SSL,我们和邮箱服务器之间的通信就会变成密文,就像两个人用密码本传纸条,就算被别人截住了信息,他拿去也看不懂。安全。
又碰到了with … as smtp:就是自动管理连接的,中间的smtplib.SMTP_SSL(SMTP_SERVER, 465, context=context)就是在我们跟邮箱服务器之间创建一个安全的连接。
465是端口,访问一个具体的应用,我们通过对方的域名(会映射到IP)能找到这台服务器,但是这个服务器上可能跑着很多的应用。
我们得通过端口号来确定,访问的具体是哪个应用。
我们怎么知道是465这个端口呢?

其实一般情况下,市面上的邮箱服务器都会约定是这些端口。
接着往下看,连接建立好了,那服务器怎么知道我们是不是他的合法用户呢?
这就需要smtp.login把发送者邮箱地址和之前的授权码发给服务器,让他验证下,如果验证过了,那就登录成功了。
登陆成功,就剩下把我们准备好的邮件内容发过去了,smtp.send_message(msg)就是在发送邮件。
三、带上我们的附件
纯文本邮件太单调了。
我们的目标是发送上一篇生成的Word报告。
要发送附件,我们的信纸EmailMessage就要用更高级的写法了。
3.1. 敲代码
import smtplib
import ssl
from email.message import EmailMessageSMTP_SERVER = "smtp.qq.com"
SENDER_EMAIL = "1234@qq.com"
SENDER_PASS = "这里换成我们刚申请的16位授权码"
RECEIVER_EMAIL = "1234@qq.com"msg = EmailMessage()
msg["Subject"] = "【重要】您的X月绩效通知书"
msg["From"] = SENDER_EMAIL
msg["To"] = RECEIVER_EMAIL
msg.set_content("您好,附件为您的5月绩效通知书,请查收。")attachment_path = "绩效通知-张三-销售A组.docx"with open(attachment_path, "rb") as f:file_data = f.read()file_name = f.namemsg.add_attachment(file_data,maintype="application",subtype="octet-stream",filename=file_name)print("正在连接并发送带附件的邮件...")
context = ssl.create_default_context()
with smtplib.SMTP_SSL(SMTP_SERVER, 465, context=context) as smtp:smtp.login(SENDER_EMAIL, SENDER_PASS)smtp.send_message(msg)print("带附件的邮件发送成功!")smtp.quit()
3.2. 运行效果

3.3. 代码讲解
跟第一版的代码相比,其实就新增了附件的那块内容。
attachment_path = "绩效通知-张三-销售A组.docx"指定了附件文件。
通过with open(…, “rb”) as f:打开文件,这里的"rb"是Read Binary的缩写,意思是读取的时候是二进制模式。
因为所有非纯文本文件(图片、Word、Excel、PDF)都必须用rb模式读取。
file_data通过read拿到文件数据。
file_name就是文件名。
add_attachment是邮件内容对象的方法,要把文件数据给他,还要指定主类型、子类型及文件名(这个文件名是邮件中附件显示的文件名)。
其实是EmailMessage模块帮我做了很多的事情,然后暴露出一个非常简单的方法出来给我们调用。
我们只要把文件塞给add_attachment就行了。
其中主类型、子类型指的是文件的MIME类型。
举几个常见的例子:
| 文件类型 | maintype | subtype | 说明 |
|---|---|---|---|
| 普通二进制文件(不知道具体类型时用) | application | octet-stream | 万能类型,表示这是一个二进制文件,但我不知道具体是啥 |
| PDF文件 | application | 表示这是个PDF文档 | |
| 图片JPG | image | jpeg | 表示这是个JPG格式的图片 |
| 图片PNG | image | png | 表示这是个PNG格式的图片 |
| 文本文件 | text | plain | 表示这是个纯文本文件 |
| HTML文件 | text | html | 表示这是个HTML格式的文本 |
| ZIP压缩包 | application | zip | 表示这是个ZIP文件 |
| Word文档 | application | msword 或 docx(更准确的是 vnd.openxmlformats…) | 早期是 msword,新版Office有专门的 subtype |
这些类型也不是Python定的,是互联网标准组织定义的。
指定这个类型就是为了告诉对方我这个文件是什么文件。假如对方有对应文件类型的显示图标,就能直观的显示出来。
又或者对方能够直接调用相应的编辑器直接打开对应类型的文件。
邮件发送阶段的代码跟上一版是一致的。
四、终极版
现在,我们要把Day10(Excel)、Day11(Word)和今天 (Email)串联起来,做一个全自动的绩效通知群发器。
我们的绩效数据.xlsx文件里应该有姓名,部门,业绩,评级,邮箱这几列。

模版还是使用Day11中的。
4.1. 敲代码
import smtplib
import ssl
import pandas as pd
from docxtpl import DocxTemplate
from email.message import EmailMessage
import timeSMTP_SERVER = "smtp.163.com"
SENDER_EMAIL = "123456@163.com"
SENDER_PASS = "你的16位授权码"TEMPLATE_PATH = "绩效模版.docx"
DATA_PATH = "绩效数据.xlsx"df = pd.read_excel(DATA_PATH)
print(f"--- 加载数据成功,共 {len(df)} 条记录 ---")context = ssl.create_default_context()
with smtplib.SMTP_SSL(SMTP_SERVER, 465, context=context) as smtp:smtp.login(SENDER_EMAIL, SENDER_PASS)print("--- 邮箱登录成功,准备开始群发 ---")for index, row in df.iterrows():doc = DocxTemplate(TEMPLATE_PATH)context = row.to_dict()doc.render(context)report_filename = f"report-{row['姓名']}.docx"doc.save(report_filename)msg = EmailMessage()msg["Subject"] = f"【绩效通知】{row['姓名']} - 5月绩效"msg["From"] = SENDER_EMAILmsg["To"] = row['邮箱']msg.set_content(f"您好,{row['姓名']}:\n\n附件为您的5月绩效通知书,请查收。\n\nHR部门")with open(report_filename, "rb") as f:msg.add_attachment(f.read(),maintype="application",subtype="octet-stream",filename=f"5月绩效通知-{row['姓名']}.docx")smtp.send_message(msg)print(f"已成功发送给: {row['姓名']} ({row['邮箱']})")time.sleep(1)
print("\n--- 全部任务完成!---")
运行代码之前记得先把用到的模块安装一下:
pip install pandas
pip install docxtpl
pip install openpyxl
4.2. 运行结果

4.3. 代码讲解
整合代码中没有引入新的知识点。
唯一一点就是使用time.sleep(1)让程序暂停了一会, 主要是避免服务器那边把我们当成攻击他的。
其他的都只是调整了逻辑结构。
在发送QQ邮箱时可能会出现smtplib.SMTPResponseException: (-1, b’\x00\x00\x00’),如果出现这种错误就换成163的邮箱进行测试。163的邮箱授权码跟QQ邮箱的获取流程差不多。
五、小结
Day10、Day11加上今天的文章,我们已经从Excel汇总,到Word 生成,再到Email群发。
也算是把工作场景中关于文件、邮件的操作理了一遍。
当然这些都只是点到为止,工作场景的情况肯定更加复杂,这就需要我们慢慢去细化需求,才能写出更加完善的脚本工具。
