在日常开发中,发送邮件是一个常见的需求,比如用户注册验证、密码重置、系统通知等场景,Python提供了强大的标准库来处理邮件发送。
一、SMTP协议详解
SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)是用于发送电子邮件的标准协议。
基本概念
SMTP 是 TCP/IP 协议族的一部分,专门负责:
- 发送邮件:从邮件客户端发送到邮件服务器,或在邮件服务器之间传输
- 路由邮件:确定邮件的传输路径
- 交付邮件:将邮件送达目标邮件服务器
工作原理
发送方客户端 → SMTP服务器A → SMTP服务器B → 接收方客户端
邮件传输流程:
- 连接建立:客户端连接到 SMTP 服务器
- 身份验证:验证发件人身份
- 邮件传输:发送邮件内容
- 服务器转发:SMTP 服务器将邮件转发到目标服务器
- 邮件存储:目标服务器存储邮件供接收方获取
安全机制
SSL/TLS 加密:
- SSL:在连接建立时就加密
- TLS:在连接建立后升级为加密连接
身份验证:
- 用户名/密码验证
- OAuth2 验证(更安全)
二、开启 QQ 邮箱的 SMTP 服务
常用邮箱SMTP配置
# 常用邮箱SMTP配置
EMAIL_CONFIGS = {
"gmail": {
"smtp_server": "smtp.gmail.com",
"smtp_port": 587
},
"qq": {
"smtp_server": "smtp.qq.com",
"smtp_port": 587
},
"163": {
"smtp_server": "smtp.163.com",
"smtp_port": 25
},
"126": {
"smtp_server": "smtp.126.com",
"smtp_port": 25
},
"outlook": {
"smtp_server": "smtp-mail.outlook.com",
"smtp_port": 587
}
}
网页地址
qq邮箱:https://mail.qq.com/
163邮箱:https://mail.163.com/
其中qq邮箱和163邮箱在日常生活中比较常用,这里以qq邮箱为例。
开启 QQ 邮箱的 SMTP 服务
- 登录 QQ 邮箱;
- 进入“设置” > “账户”;
- 在“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务”中找到“开启服务”;
- 开启 IMAP/SMTP服务;
- 根据提示生成 授权码(不是QQ密码,而是用于第三方登录的专用密码)。
⚠️ 注意:SMTP 服务默认关闭,必须手动开启,并获取授权码。
其他的邮箱方法也类似。
SMTP服务类型
163邮箱有IMAP/SMTP服务和POP3/SMTP服务两种类型,IMAP/SMTP服务和POP3/SMTP服务的区别在于,IMAP支持多设备同步和服务器存储,而POP3更适合本地存储和单一设备使用
主要区别:
(1)协议功能不同
IMAP(Internet Message Access Protocol)和POP3(Post Office Protocol version 3)都是用于接收邮件的协议,而SMTP(Simple Mail Transfer Protocol)则是用于发送邮件的协议。因此,IMAP/SMTP和POP3/SMTP的区别主要在于接收邮件部分。
(2)邮件存储方式不同
IMAP支持将邮件存储在远程服务器上,用户可以通过多个设备同步查看邮件内容,而不会将邮件从服务器删除。这种方式适合需要跨设备访问邮件的用户。
POP3则通常会将邮件从服务器下载到本地设备,并在下载后可选择是否从服务器删除邮件。因此,POP3更适合邮件主要在单一设备上使用的场景。
(3)适用场景不同
IMAP适合需要多设备同步、灵活管理邮件的用户,例如在手机、电脑和平板之间切换使用邮箱的场景。POP3则更适合希望将邮件集中存储在本地设备、节省服务器空间的用户,例如仅在电脑上查看邮件的场景。
三、代码实现
得益于电子邮件系统的互通性,不同服务提供商的邮箱之间可以互相通信,也就是说可以用163邮箱发送邮件给qq邮箱,下面示例用163邮箱发送邮件给qq邮箱。
import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
def send_email_task(subject, content):
"""
发送邮件的任务函数
Args:
subject: 邮件主题
content: 邮件内容
"""
# 邮件配置信息(请根据实际情况修改)
smtp_server = "smtp.163.com" # SMTP服务器地址
smtp_port = 25 # SMTP端口
sender_email = "xxx@163.com" # 发件人邮箱
sender_password = "xxx" # 发件人邮箱授权码
receiver_email = "xxx@qq.com" # 收件人邮箱地址
try:
# 创建邮件对象
message = MIMEMultipart()
message["From"] = Header("邮件系统", 'utf-8') # 设置邮件发送者
message["To"] = Header(receiver_email, 'utf-8') # 设置邮件接受者
message["Subject"] = Header(subject, 'utf-8') # 设置邮件主题
# 添加邮件内容
message.attach(MIMEText(content, 'plain', 'utf-8'))
# 连接SMTP服务器并发送邮件
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls() # 启用TLS加密
server.login(sender_email, sender_password)
server.sendmail(sender_email, receiver_email, message.as_string())
server.quit()
print(f"[邮件发送] 邮件已成功发送至 {receiver_email},时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
except Exception as e:
print(f"[邮件发送] 邮件发送失败: {str(e)}")
if __name__ == "__main__":
email_content = f"这是一封邮件,发送时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
send_email_task("邮件提醒", email_content)
TLS 加密详解
代码中server.starttls()可能存在疑问,下面做下解析。
TLS(Transport Layer Security,传输层安全协议)是一种加密通信协议,用于在网络通信中提供安全性和数据完整性。
TLS 是 SSL(Secure Sockets Layer)的继任者,主要用于:
- 加密数据传输:防止数据被窃听
- 身份验证:确认通信双方的身份
- 数据完整性:确保数据在传输过程中未被篡改
改用SSL加密协议:
server = smtplib.SMTP_SSL(mail_host, 465)
'plain' 的作用
在代码 message.attach(MIMEText(content, 'plain', 'utf-8')) 中,'plain' 参数指定了邮件正文的 MIME 类型。
'plain' 表示邮件正文内容是纯文本格式(Plain Text),它有以下特点:
1. 纯文本特性
- 不支持任何格式化(如粗体、斜体、颜色等)
- 不支持图片、链接样式等复杂元素
- 所有内容都以普通文本形式显示
2. 兼容性
- 几乎所有邮件客户端都支持纯文本邮件
- 占用空间小,传输速度快
- 不会被邮件客户端误判为垃圾邮件
改用HTML 格式:
# HTML格式(html)
html_content = """
<html>
<body>
<h2>HTML邮件</h2>
<p>这是一封<b>HTML格式</b>的邮件</p>
</body>
</html>
"""
message.attach(MIMEText(html_content, 'html', 'utf-8'))
纯文本显示:
这是一封纯文本邮件
欢迎使用
HTML显示:
HTML邮件
这是一封**HTML格式**的邮件
何时使用 'plain'
- 发送简单通知或提醒
- 需要确保邮件在所有设备上都能正常显示
- 发送代码、日志等技术内容
- 对邮件格式要求不高
何时使用 'html'
- 需要丰富的文本格式
- 包含链接、图片等元素
- 制作精美的邮件模板
- 需要更好的视觉效果
完整示例
# 纯文本邮件
message.attach(MIMEText("这是一封纯文本邮件\n欢迎使用", 'plain', 'utf-8'))
# HTML邮件
message.attach(MIMEText("<h1>HTML邮件</h1><p>欢迎使用</p>", 'html', 'utf-8'))
# 同时支持纯文本和HTML(推荐)
text_part = MIMEText("这是一封纯文本邮件", 'plain', 'utf-8')
html_part = MIMEText("<h1>HTML邮件</h1><p>欢迎使用</p>", 'html', 'utf-8')
message.attach(text_part)
message.attach(html_part)
问题处理
上面的方式处理,代码执行可能存在From
标头格式不符合标准的问题。优化后代码:
import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.utils import formataddr
# 常用邮箱SMTP配置
EMAIL_CONFIGS = {
"gmail": {
"smtp_server": "smtp.gmail.com",
"smtp_port": 587
},
"qq": {
"smtp_server": "smtp.qq.com",
"smtp_port": 587
},
"163": {
"smtp_server": "smtp.163.com",
"smtp_port": 25
},
"126": {
"smtp_server": "smtp.126.com",
"smtp_port": 25
},
"outlook": {
"smtp_server": "smtp-mail.outlook.com",
"smtp_port": 587
}
}
def send_email_task(subject, content):
"""
发送邮件的任务函数
Args:
subject: 邮件主题
content: 邮件内容
"""
# 邮件配置信息(请根据实际情况修改)
smtp_server = EMAIL_CONFIGS["qq"]["smtp_server"] # SMTP服务器地址
smtp_port = EMAIL_CONFIGS["qq"]["smtp_port"] # SMTP端口
sender_email = "xxx@qq.com" # 发件人邮箱
sender_password = "xxx" # 发件人邮箱授权码
receiver_email = "xxx@163.com" # 收件人邮箱地址
try:
# 创建邮件对象
message = MIMEMultipart()
message['From'] = formataddr(("邮件系统", sender_email)) # 设置邮件发送者
message["To"] = Header(receiver_email, 'utf-8') # 设置邮件接受者
message["Subject"] = Header(subject, 'utf-8') # 设置邮件主题
# 添加邮件内容
message.attach(MIMEText(content, 'plain', 'utf-8'))
# 连接SMTP服务器并发送邮件
server = smtplib.SMTP(smtp_server, smtp_port)
# server.set_debuglevel(1) # 启用调试模式,显示详细通信过程
server.starttls() # 启用TLS加密
server.login(sender_email, sender_password)
server.sendmail(sender_email, receiver_email, message.as_string())
server.quit()
print(f"[邮件发送] 邮件已成功发送至 {receiver_email},时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
except Exception as e:
print(f"[邮件发送] 邮件发送失败: {str(e)}")
if __name__ == "__main__":
email_content = f"这是一封邮件,发送时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
send_email_task("邮件提醒", email_content)
(1)解决From
标头格式问题
完整的 From
标头格式应该是 昵称编码部分 + 空格 + <邮箱地址>
。之前的From
标头只有编码后的昵称部分,缺少了实际的 <邮箱地址>
部分。
不只设置昵称,要同时设置昵称和邮箱地址。确保邮件的 From
标头包含完整的格式:昵称 <邮箱地址>
,即使昵称部分是编码的。
# message["From"] = Header("邮件系统", 'utf-8')
message['From'] = formataddr(("邮件系统", sender_email))
(2)增加调试模式
另外,打开这段注释的代码,可以改成调试模式,增加更过日志输出。
# server.set_debuglevel(1) # 启用调试模式,显示详细通信过程
(3)SMTP服务器地址及端口改成可选的常量
常用邮箱SMTP配置,放入EMAIL_CONFIGS常量中供自行选择,便于替换
定时任务版本
使用简单的threading.Timer定时器来实现定时发送邮件:
import threading
import datetime
import time
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header
from email.utils import formataddr
# 常用邮箱SMTP配置
EMAIL_CONFIGS = {
"gmail": {
"smtp_server": "smtp.gmail.com",
"smtp_port": 587
},
"qq": {
"smtp_server": "smtp.qq.com",
"smtp_port": 587
},
"163": {
"smtp_server": "smtp.163.com",
"smtp_port": 25
},
"126": {
"smtp_server": "smtp.126.com",
"smtp_port": 25
},
"outlook": {
"smtp_server": "smtp-mail.outlook.com",
"smtp_port": 587
}
}
class SimpleTimer:
"""
简单的定时任务工具类,基于 threading.Timer 实现
使用实例方法
"""
def __init__(self):
self.running_tasks = {} # 用于跟踪正在运行的任务
def _run_task(self, func, interval, task_key, args, kwargs):
"""
内部运行任务函数
Args:
func: 要执行的函数
interval: 执行间隔(秒)
task_key: 任务唯一标识
args: 传递给函数的位置参数
kwargs: 传递给函数的关键字参数
"""
if not self.running_tasks.get(task_key, False):
return
func(*args, **kwargs)
# 重新启动定时器实现循环执行(仅当任务仍在运行时)
if self.running_tasks.get(task_key, False):
timer = threading.Timer(interval, self._run_task, [func, interval, task_key, args, kwargs])
timer.start()
# 更新任务对应的timer,以便清理引用
self.running_tasks[task_key] = timer
def start_task(self, func, interval, *args, **kwargs):
"""
启动定时任务
Args:
func: 要执行的函数
interval: 执行间隔(秒)
*args: 传递给函数的位置参数
**kwargs: 传递给函数的关键字参数
Returns:
task_key: 任务标识符,可用于停止任务
"""
# 生成任务唯一标识
task_key = (id(func), args, tuple(sorted(kwargs.items())))
timer = threading.Timer(interval, self._run_task, [func, interval, task_key, args, kwargs])
self.running_tasks[task_key] = timer
timer.start()
print(f"定时任务已启动,将在 {interval} 秒后执行")
return task_key
def stop_task(self, task_key):
"""
停止指定的定时任务并清理引用
Args:
task_key: 要停止的任务标识符
"""
if task_key in self.running_tasks:
timer = self.running_tasks[task_key]
if timer:
timer.cancel()
# 清理引用以帮助垃圾回收
timer.function = None
timer.args = None
timer.kwargs = None
# 标记任务为停止状态
self.running_tasks[task_key] = False
print("定时任务已停止")
def stop_all_tasks(self):
"""
停止所有定时任务
"""
for task_key in list(self.running_tasks.keys()):
self.stop_task(task_key)
self.running_tasks.clear()
print("所有定时任务已停止")
def send_email_task(subject, content):
"""
发送邮件的任务函数
Args:
subject: 邮件主题
content: 邮件内容
"""
# 邮件配置信息(请根据实际情况修改)
smtp_server = EMAIL_CONFIGS["163"]["smtp_server"] # SMTP服务器地址
smtp_port = EMAIL_CONFIGS["163"]["smtp_port"] # SMTP端口
sender_email = "xxx@qq.com" # 发件人邮箱
sender_password = "xxx" # 发件人邮箱授权码
receiver_email = "xxx@163.com" # 收件人邮箱地址
try:
# 创建邮件对象
message = MIMEMultipart()
message['From'] = formataddr(("定时邮件系统", sender_email)) # 设置邮件发送者
message["To"] = Header(receiver_email, 'utf-8') # 设置邮件接受者
message["Subject"] = Header(subject, 'utf-8') # 设置邮件主题
# 添加邮件内容
message.attach(MIMEText(content, 'plain', 'utf-8'))
# 连接SMTP服务器并发送邮件
server = smtplib.SMTP(smtp_server, smtp_port)
# server.set_debuglevel(1) # 启用调试模式,显示详细通信过程
server.starttls() # 启用TLS加密
server.login(sender_email, sender_password)
server.sendmail(sender_email, receiver_email, message.as_string())
server.quit()
print(f"[邮件发送] 邮件已成功发送至 {receiver_email},时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
except Exception as e:
print(f"[邮件发送] 邮件发送失败: {str(e)}")
def main():
"""
使用 SimpleTimer 工具类的示例
"""
print("简单定时任务工具类示例启动...")
# 创建定时器实例
timer_scheduler = SimpleTimer()
# 启动定时邮件发送任务,每2分钟发送一次
email_content = f"这是一封定时邮件,发送时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
task_key = timer_scheduler.start_task(
send_email_task,
5, # 2分钟 = 120秒
"定时邮件提醒",
email_content
)
# 主程序可以继续做其他事情
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("程序退出")
# 停止所有定时任务
timer_scheduler.stop_all_tasks()
print("所有定时任务已停止,程序结束")
if __name__ == "__main__":
main()