根本原因是Django默认同步连接SMTP且本地无邮件服务或端口未开放;需确认EMAIL_BACKEND为smtp后端、使用应用专用密码、适配465/587端口,并用线程实现异步发送。

SMTP配置后邮件发不出去,send_mail 卡住或报 ConnectionRefusedError
根本原因通常是 Django 默认用同步方式连 SMTP 服务器,而本地开发环境没装邮件服务,或生产环境防火墙/端口没放开。别急着换库,先确认连接通不通:
-
python -c "import smtplib; s = smtplib.SMTP('smtp.gmail.com', 587); s.starttls(); print('OK')"—— 手动测通路,绕过 Django - Django 的
EMAIL_BACKEND必须设为'django.core.mail.backends.smtp.EmailBackend',不是默认的console后端 - Gmail/Outlook 等需要开启「应用专用密码」或 OAuth2,普通账号密码大概率被拒,错误信息通常是
SMTPAuthenticationError - 阿里云、腾讯云 ECS 默认封 25 端口,得改用 465(SSL)或 587(TLS),对应配置
EMAIL_USE_SSL或EMAIL_USE_TLS,二者不能同时为True
想发邮件不阻塞视图响应,但又不想引入 Celery
直接用线程最轻量,Django 自身不带异步邮件发送能力,send_mail 是纯同步调用。硬上 async/await 没用 —— SMTP 库大多不支持 asyncio。
- 用
threading.Thread包一层最稳妥:from threading import Thread def send_async_mail(*args, **kwargs): Thread(target=send_mail, args=args, kwargs=kwargs).start() - 注意:线程里不能依赖 request 对象或未序列化的上下文(比如 model 实例),得提前取好
str或dict数据传进去 - 别用
concurrent.futures.ThreadPoolExecutor做简单通知——线程池适合复用,单次发信反而增加调度开销 - 如果并发量真高(比如每秒几十封),线程还是不如 Celery + Redis,但那是另一个量级的问题了
send_mail 和 EmailMessage 该选哪个
send_mail 只能发纯文本,附件、HTML 内容、多收件人抄送都得换 EmailMessage。它不是“高级版”,而是“必须用”的底层接口。
-
send_mail本质就是封装了EmailMessage的简单调用,参数少,适合密码重置这类单文本通知 - 要嵌入图片、加附件、用模板渲染 HTML,必须用
EmailMessage:msg = EmailMessage( subject='订单完成', body='<h1>感谢下单</h1>', from_email='noreply@example.com', to=['user@example.com'] ) msg.content_subtype = 'html' msg.send() -
EmailMessage的to、cc、bcc都是 list,别传字符串,否则发信失败但无提示 - 模板里用
{% autoescape off %}渲染 HTML 邮件时,确保内容可信,否则 XSS 风险直接进收件箱
测试环境邮件全发到控制台,但上线后收不到
因为 EMAIL_BACKEND 在不同环境配错了。Django 不会自动切换,靠你手动区分。
立即学习“Python免费学习笔记(深入)”;
- 开发时设
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend',邮件打 stdout,不发真实网络请求 - 生产环境必须显式覆盖为 SMTP 后端,并且
EMAIL_HOST、EMAIL_PORT、EMAIL_HOST_USER、EMAIL_HOST_PASSWORD全部非空 - 别把密码写死在 settings.py 里,用环境变量:
os.getenv('EMAIL_HOST_PASSWORD', ''),否则部署时容易漏改 - 有些 SMTP 服务商(如 Mailgun)要求发信域名在白名单里,Django 的
DEFAULT_FROM_EMAIL必须和验证过的域名一致,否则静默丢弃
异步发信的坑不在代码逻辑,而在配置漂移和权限细节——一封邮件卡住,八成是 SMTP 凭据过期、端口不通,或者邮箱服务商悄悄升级了认证策略。


















