不能直接用send_mail()发附件,因其仅支持纯文本或简单HTML,不支持多部分MIME结构;需用EmailMultiAlternatives添加HTML备选内容和bytes附件,并通过线程、Celery等真正异步发送。

为什么不能直接用 send_mail() 发附件?
Django 自带的 send_mail() 和 send_mass_mail() 只支持纯文本或简单 HTML 内容,不提供构建多部分 MIME 邮件(如含附件 + HTML 正文)的接口。强行拼接会导致附件损坏、HTML 渲染失败,或被邮箱服务商标记为垃圾邮件。
- 它底层调用的是
Django.core.mail.EmailMessage,但默认不启用 multipart/related 或 multipart/mixed 结构 - 附件需通过
attach()显式添加,且必须在设置content_subtype='html'后仍保持正文与附件的 MIME 层级正确 - 同步发送会阻塞请求线程,尤其当附件较大或 SMTP 延迟高时,Web 请求可能超时
用 EmailMultiAlternatives 构建带附件的 HTML 邮件
EmailMultiAlternatives 是 Django 提供的、专用于多格式邮件(如同时提供 text/plain 和 text/html)并支持附件的类。它比手动构造 MIMEMultipart 更安全,也兼容 Django 的邮件后端配置(如 EMAIL_BACKEND)。
将 Markdown 文章转换为微信公众号兼容的纯 HTML 格式。当用户要求"转成微信格式"、"生成公众号文章 HTML"、"排版到微信草稿箱"、"微信粘贴格式"时触发。输出带内联样式的纯 HTML,微信编辑器可直接渲染,无需额外适配。支持标题、段落、列表、粗体、行内代码、表格、引用块、代码块、配图的完整转换。
- 先实例化
EmailMultiAlternatives,传入主题、空正文(因为 HTML 是备选内容)、发件人和收件人 - 调用
attach_alternative(html_content, "text/html")添加 HTML 版本 - 调用
attach(filename, content, mimetype)添加附件;content必须是 bytes(不是 str),常见错误是传入字符串导致乱码或截断 - 若附件来自文件,推荐用
open(path, "rb").read();若来自内存(如生成的 PDF),确保是bytes类型
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
<p>html_content = render_to_string("email/welcome.html", {"name": "Alice"})
msg = EmailMultiAlternatives(
subject="欢迎注册",
body="", # 纯文本备用内容,可为空
from_email="noreply@example.com",
to=["user@example.com"],
)
msg.attach_alternative(html_content, "text/html")
msg.attach("report.pdf", pdf_bytes, "application/pdf")
msg.send()</p>
如何真正异步发送?别只加个 @asyncio.coroutine
Django 的邮件后端(如 smtp)默认是同步阻塞的。仅用 async def 包裹 msg.send() 没用——它不会自动变成非阻塞调用。
- 正确做法是把发送逻辑移到线程或进程里:
asyncio.to_thread()(Python 3.9+)或concurrent.futures.ThreadPoolExecutor - 如果用 Celery,直接将邮件任务声明为
@shared_task,并在视图中用send_email_task.delay(...)触发 - 注意:异步任务中不能依赖 request 对象或未序列化的 Django model 实例;所有数据需提前序列化(如传
user_id而非user对象) - 附件内容不能是文件句柄或临时路径(线程/worker 可能无权限访问),必须是完整
bytes或 base64 编码后传入
附件过大或 HTML 引用本地资源时的典型问题
- Gmail 等邮箱会拒绝超过 25MB 的附件;更大的文件应转为下载链接,并在 HTML 中用
@#@#@#@#@#@#@#@#@#@0; 替代
- HTML 中的
<img src="static/logo.png"> 在邮件客户端里基本不显示——CSS 和图片需内联或转为 CID 引用;推荐用 django-inline-css + premailer 处理样式,图片用 msg.attach() 添加并设 headers={"Content-ID": "<logo>"}</logo>,再在 HTML 中写 <img src="cid:logo">
- 使用
render_to_string 时,确保模板中不调用数据库查询或耗时函数;否则异步任务本身又变慢了
@#@#@#@#@#@#@#@#@#@0; 替代 <img src="static/logo.png"> 在邮件客户端里基本不显示——CSS 和图片需内联或转为 CID 引用;推荐用 django-inline-css + premailer 处理样式,图片用 msg.attach() 添加并设 headers={"Content-ID": "<logo>"}</logo>,再在 HTML 中写 <img src="cid:logo"> render_to_string 时,确保模板中不调用数据库查询或耗时函数;否则异步任务本身又变慢了 附件内容是否已转为 bytes,HTML 是否完成内联处理,以及异步执行环境是否隔离了请求上下文——这三点漏掉任一个,都会让邮件发不出或发错。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










