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() 本身是协程函数,但默认行为仍可能踩坑:
-
hostname和port必须显式指定,不能依赖 SMTP 默认端口逻辑(比如设成port=587却忘了开start_tls=True) -
username和password要用真实邮箱密码或应用专用密码(Gmail/Outlook 已禁用“允许不够安全的应用”) -
use_tls和start_tls别混淆:use_tls=True表示连接即走 TLS(对应 465 端口),start_tls=True表示先明文连接再升级(对应 587 端口) - 没有内置连接池,高频发信必须自己管理
aiosmtplib.SMTP实例,否则每次send()都新建连接,反而更慢
示例正确写法:
import aiosmtplib from email.message import EmailMessagesmtp = 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.SMTPTimeoutError或OSError:网络抖动或服务器不可达(临时错误,可重试 1~2 次) -
aiosmtplib.errors.SMTPAuthenticationError:账号密码错或应用密码过期(需人工介入)
重试逻辑别无脑套 asyncio.sleep(1),Gmail 对频繁重试会直接封 IP 段;建议用指数退避 + jitter,且只对超时类错误重试。
连接复用本身不解决 DNS 解析失败、证书过期、SMTP 服务器主动断连等问题——这些都得靠日志捕获具体 exception.args 内容,而不是只看异常类型。