引言
在自动化运维和日常办公中,电子邮件依然是最正式、最广泛使用的通知和交互手段。无论是定时发送报表、监控报警,还是自动处理用户请求,邮件自动化都是程序员工具箱中的必备技能。
在Python领域,大多数开发者对smtplib发送邮件较为熟悉,但在邮件接收与处理(尤其是IMAP协议)方面却往往力不从心,通常需要编写大量底层代码来处理邮件解析、附件下载和搜索条件。本篇文章将深入浅出,不仅讲解如何使用smtplib发送带附件的复杂邮件 ,更将重点引入强大的第三方库------imap_tools,手把手教你如何优雅地读取和处理收件箱邮件,实现真正意义上的邮件自动化闭环。
Day 55-56:夯实基础 ------ 使用smtplib发送带附件的邮件
SMTP(Simple Mail Transfer Protocol)是推送邮件的协议。Python的smtplib模块封装了其底层细节,而email库则负责构建符合MIME标准的邮件内容。
1. 核心概念:MIMEMultipart
要发送带附件的邮件,我们不能使用简单的MIMEText对象。因为邮件包含独立的两部分:正文和附件。这时需要用到MIMEMultipart,它是一个容器,可以将不同的MIME部分组合在一起。
一个标准的带附件邮件结构如下:
- MIMEMultipart (混合类型)
- MIMEText(纯文本或HTML正文)
- MIMEBase / MIMEApplication(附件)
2. 实战:发送带Excel附件的HTML邮件
下面的示例演示了如何连接QQ邮箱的SMTP服务器,发送一封包含HTML样式的正文,并附带一个Excel文件的邮件。
python
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
from email.utils import formataddr
# 邮件配置
SMTP_SERVER = 'smtp.qq.com'
SMTP_PORT = 587 # TLS端口
SENDER_EMAIL = 'your_qq@qq.com'
SENDER_PWD = '你的授权码' # QQ邮箱需使用授权码,非登录密码
def send_email_with_attachment(receiver, subject, html_content, attachment_path):
"""
发送带附件的HTML邮件
"""
# 创建一个混合类型的邮件对象
msg = MIMEMultipart()
msg['From'] = formataddr(('自动化发送者', SENDER_EMAIL))
msg['To'] = receiver
msg['Subject'] = subject
# 1. 添加HTML正文
msg.attach(MIMEText(html_content, 'html', 'utf-8'))
# 2. 处理附件
try:
with open(attachment_path, 'rb') as f:
# 创建附件对象 (MIMEBase)
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
# 编码为Base64
encoders.encode_base64(part)
# 添加头部信息,声明此为附件
filename = attachment_path.split('/')[-1] # 获取文件名
part.add_header(
'Content-Disposition',
f'attachment; filename="{filename}"'
)
msg.attach(part)
except FileNotFoundError:
print(f"附件 {attachment_path} 未找到,邮件将无附件发送。")
# 3. 发送邮件
try:
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
server.starttls() # 启用TLS加密
server.login(SENDER_EMAIL, SENDER_PWD)
server.sendmail(SENDER_EMAIL, receiver, msg.as_string())
server.quit()
print(f"邮件发送成功至 {receiver}")
except Exception as e:
print(f"发送失败: {e}")
# 使用示例
if __name__ == '__main__':
html_body = """
<h2>月度数据报告</h2>
<p>您好,</p>
<p>本月销售数据请见附件。</p>
<p><strong>请注意:</strong> 此邮件由系统自动发送,请勿回复。</p>
"""
send_email_with_attachment(
receiver='target@example.com',
subject='【自动化】2024年5月销售报表',
html_content=html_body,
attachment_path='./report.xlsx' # 假设当前目录有该文件
)
注意事项:
- 使用
.starttls()时端口通常为587;若使用SSL,则端口为465并改用smtplib.SMTP_SSL。- 对于Gmail、QQ等邮箱,务必使用授权码而非邮箱密码,以确保安全性。
Day 57-58:进阶突破 ------ 使用imap_tools智能处理收件箱
发送邮件只是自动化的一半。如何自动读取邮件、下载附件或根据规则自动回复,才是真正的难点。标准库imaplib虽然功能强大,但使用起来较为繁琐。这里推荐使用基于imaplib封装的高阶库------imap_tools,它提供了简洁的查询语法和对象化的邮件属性。
1. 安装与基础连接
首先,安装这个轻量级且无外部依赖的库:
bash
pip install imap-tools
基础连接使用上下文管理器(with语句),确保连接自动关闭:
python
from imap_tools import MailBox
# 连接IMAP服务器(QQ邮箱IMAP地址:imap.qq.com)
with MailBox('imap.qq.com').login('your_qq@qq.com', '你的授权码', initial_folder='INBOX') as mailbox:
print("登录成功,当前文件夹:收件箱")
# 后续操作...
2. 强大的查询语法(AND/OR/NOT)
imap_tools的核心优势在于其查询构造器。它模仿了SQLAlchemy的语法,告别了复杂的IMAP搜索字符串。
基本查询示例:
python
from imap_tools import MailBox, AND, OR, NOT
with MailBox('imap.qq.com').login('your_qq@qq.com', '授权码', 'INBOX') as mailbox:
# 1. 查询未读邮件
unread_emails = mailbox.fetch(AND(seen=False))
# 2. 查询特定发件人且包含附件的邮件
specific_emails = mailbox.fetch(AND(from_='boss@company.com', subject='报告'))
# 3. 复杂查询:来自某个域,或者是紧急邮件
complex_query = OR(
from_='@important.com',
subject='URGENT'
)
for msg in mailbox.fetch(complex_query):
print(msg.subject)
# 4. 日期范围查询:近3天的邮件
from datetime import date, timedelta
three_days_ago = date.today() - timedelta(days=3)
recent_mails = mailbox.fetch(AND(date_gte=three_days_ago))
3. 解析邮件并下载附件
MailMessage对象提供了直观的属性来访问邮件内容。以下是一个自动下载所有未读邮件附件的脚本:
python
import os
from imap_tools import MailBox, AND
DOWNLOAD_DIR = './attachments'
def download_attachments_from_inbox():
# 确保下载目录存在
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
with MailBox('imap.qq.com').login('your_qq@qq.com', '授权码', 'INBOX') as mailbox:
# 获取所有未读邮件
# fetch 返回一个生成器,支持分页和排序,这里设置 reverse=True 让最新的邮件先出现
for msg in mailbox.fetch(AND(seen=False), reverse=True, limit=50): # 限制处理50封,防止过载
print(f"处理邮件: {msg.subject} 来自: {msg.from_}")
# 检查是否有附件
if msg.attachments:
for att in msg.attachments:
# att 是 MailAttachment 对象
filename = att.filename
# 可以按文件类型过滤
if filename.endswith(('.xlsx', '.pdf', '.docx')):
file_path = os.path.join(DOWNLOAD_DIR, filename)
# att.payload 是附件的二进制数据 (bytes)
with open(file_path, 'wb') as f:
f.write(att.payload)
print(f" 已下载附件: {filename}")
# 处理完后,可以将邮件标记为已读(默认fetch时mark_seen=True),或移动到其他文件夹
# 将邮件移动到 "Processed" 文件夹
mailbox.move([msg.uid], 'PROCESSED')
else:
# 无附件邮件,直接标记为已读,也可以不做任何操作
# 如果不希望在fetch时自动标记已读,可以在fetch参数中设置 mark_seen=False
pass
if __name__ == '__main__':
download_attachments_from_inbox()
代码解析:
msg.attachments返回一个由MailAttachment对象组成的列表,每个对象包含filename、payload(二进制)、content_type等属性。mailbox.move([msg.uid], 'PROCESSED')演示了如何将处理完的邮件移动到指定文件夹,实现收件箱归零。fetch方法的limit参数可以有效防止一次性处理大量邮件导致的性能问题。
4. 邮件自动回复机器人雏形
结合发送和接收,我们可以构建一个简单的自动应答机器人:当收到特定主题的邮件时,自动回复预设内容。
python
import smtplib
from email.mime.text import MIMEText
from imap_tools import MailBox, AND
# 发送函数 (简化版)
def send_reply(to_addr, original_subject):
smtp_server = smtplib.SMTP('smtp.qq.com', 587)
smtp_server.starttls()
smtp_server.login('your_qq@qq.com', '授权码')
msg = MIMEText('感谢您的来信,我们已经收到您的请求,会尽快处理。', 'plain', 'utf-8')
msg['From'] = 'your_qq@qq.com'
msg['To'] = to_addr
msg['Subject'] = f'RE: {original_subject} [自动回复]'
smtp_server.send_message(msg)
smtp_server.quit()
# 主监控逻辑
def auto_responder():
with MailBox('imap.qq.com').login('your_qq@qq.com', '授权码', 'INBOX') as inbox:
# 查找未读的、主题包含"咨询"的邮件
for msg in inbox.fetch(AND(seen=False, subject='咨询')):
# 发送自动回复
send_reply(msg.from_, msg.subject)
print(f"已回复: {msg.from_}")
# 标记为已读并添加自定义标志
inbox.flag([msg.uid], ['ANSWERED'], True)
综合实战:监控邮箱并自动备份附件
假设你有一个需求:每天定时检查收件箱,将当天收到的所有邮件的Excel附件下载下来,并将下载记录发送给管理员。
完整流程:
- 使用
imap_tools连接IMAP,查询当天日期范围内的邮件。 - 遍历邮件,下载
.xlsx附件。 - 所有附件下载完成后,调用
smtplib发送一封汇总邮件给管理员。
这部分代码融合了上述所有知识点,是邮件自动化的典型应用场景。通过结合操作系统的定时任务(如Linux Crontab或Windows任务计划程序),即可实现无人值守的自动化处理。
常见问题与避坑指南
- 授权码 vs 密码 :几乎所有主流邮箱(QQ、163、Gmail)在开启SMTP/IMAP服务后,都需要使用生成的授权码作为密码登录,直接使用网页登录密码会报错。
- IMAP文件夹名称 :默认收件箱是
INBOX。其他文件夹(如"已发送"、"归档")的名称可能因邮箱服务商而异,例如QQ邮箱的"已发送"可能是&XfJT0ZAB4这种编码形式。建议先使用mailbox.folder.list()打印所有文件夹名称。 - 附件编码 :
att.payload返回的是原始二进制数据,直接写入文件即可。如果遇到附件名乱码,可以使用att.filename,该属性通常已经被imap-tools处理过。 - 性能考量 :如果需要处理海量邮件,务必使用
fetch的limit和bulk参数。设置bulk=True可以减少与服务器的交互次数,但会占用更多内存。