非常游戏网
如何在Python中实现异步的邮件发送_利用aiosmtplib库提升邮件推送效率

如何在Python中实现异步的邮件发送_利用aiosmtplib库提升邮件推送效率

2026-05-29日常编程210404

smtplib.sendmail()不能直接用于异步邮件发送,因为它是阻塞式调用,会等待TCP连接、认证、数据传输等全过程,拖垮事件循环;在FastAPI等异步框架中直接使用会导致协程饿死、响应延迟突增。

为什么不能直接用 smtplib.sendmail() 做异步邮件发送

smtplib.SMTP.sendmail() 是阻塞式调用,每次发信都会等待 TCP 连接建立、HELO/EHLO 交换、认证、DATA 传输、QUIT 关闭——整个过程可能耗时 300ms~2s,取决于网络和邮件服务器响应。在 FastAPI 或 Quart 等异步框架里直接调用它,会拖垮整个事件循环,让其他协程饿死。

常见错误现象包括:

  • RuntimeWarning: coroutine 'sendmail' was never awaited
  • 接口响应延迟突增,尤其批量发信时 CPU 不高但吞吐骤降
  • 使用 asyncio.to_thread() 包裹后看似“异步”,实则只是把阻塞操作扔进线程池,没解决连接复用和并发控制问题

aiosmtplib.send() 的核心参数怎么配才不翻车

aiosmtplib.send() 本身是协程函数,但默认行为仍可能踩坑:

  • hostnameport 必须显式指定,不能依赖 SMTP 默认端口逻辑(比如设成 port=587 却忘了开 start_tls=True
  • usernamepassword 要用真实邮箱密码或应用专用密码(Gmail/Outlook 已禁用“允许不够安全的应用”)
  • use_tlsstart_tls 别混淆:use_tls=True 表示连接即走 TLS(对应 465 端口),start_tls=True 表示先明文连接再升级(对应 587 端口)
  • 没有内置连接池,高频发信必须自己管理 aiosmtplib.SMTP 实例,否则每次 send() 都新建连接,反而更慢

示例正确写法:

import aiosmtplib
from email.message import EmailMessage

smtp = aiosmtplib.SMTP( hostname="smtp.gmail.com", port=587, use_tls=False, start_tls=True, # 注意这里 ) await smtp.connect() await smtp.login("user@gmail.com", "app_password_here")

如何复用 SMTP 连接并安全并发发信

单个 aiosmtplib.SMTP 实例不是线程安全的,也不支持多协程并发调用 send()。强行并发会触发 RuntimeError: Another operation is in progress

正确做法是:

Python 3.14.3

微软官方的 Python 扩展,是 VS Code 安装量最高的扩展(209M+)。集成 IntelliSense(通过 Pylance)、调试(通过 Python Debugger)、代码检查、格式化、重构和单元测试等功能。支持 Jupyter Notebook、虚拟环境管理和多 Python 版本切换。

  • 创建连接池:用 asyncio.Semaphore 控制并发数(建议 ≤ 5,避免被 Gmail 限流)
  • 复用连接:一个连接实例由多个协程按需 acquire/release,而非每次新建
  • 设置超时:给 send()timeout=15,防止某封信卡住整个池

关键结构示意:

_semaphore = asyncio.Semaphore(3)  # 限制最多 3 个并发发信

async def send_one(email: EmailMessage): async with _semaphore: await smtp.send_message(email, recipients=["to@example.com"])

发信失败时怎么区分是临时故障还是永久错误

aiosmtplib 抛出的异常类型很关键:

  • aiosmtplib.errors.SMTPRecipientsRefused:收件人地址无效(永久错误,应清理名单)
  • aiosmtplib.errors.SMTPDataError:邮件内容被拒(如含恶意链接、超大附件)
  • aiosmtplib.errors.SMTPTimeoutErrorOSError:网络抖动或服务器不可达(临时错误,可重试 1~2 次)
  • aiosmtplib.errors.SMTPAuthenticationError:账号密码错或应用密码过期(需人工介入)

重试逻辑别无脑套 asyncio.sleep(1),Gmail 对频繁重试会直接封 IP 段;建议用指数退避 + jitter,且只对超时类错误重试。

连接复用本身不解决 DNS 解析失败、证书过期、SMTP 服务器主动断连等问题——这些都得靠日志捕获具体 exception.args 内容,而不是只看异常类型。