Django 的邮件子系统通过 EMAIL_BACKEND 配置项决定邮件走向,常见的选择包括 SMTP、控制台、文件、内存等后端。默认情况下,这个配置只接受一个字符串,意味着同一时刻似乎只能启用一种投递方式。但在实际项目中,尤其是开发调试阶段,有时既希望邮件通过 SMTP 真实发送给收件人,又想在控制台或日志里立即看到发送过程和完整内容。这种需求无法靠修改单一配置直接实现,需要自己组合两个现成后端。

为什么默认配置无法同时启用两个后端
Django 的 EMAIL_BACKEND 配置项接收一个 Python 导入路径,框架启动后加载该类并通过 send_messages 完成投递。常见类位于 django.core.mail.backends 模块。其中 smtp.EmailBackend 使用 smtplib 与邮件服务器通信,console.EmailBackend 则把邮件内容写入指定流,默认是 sys.stdout。两者都实现了 BaseEmailBackend 的 open、close、send_messages 三个核心方法。
由于 settings 里的 EMAIL_BACKEND 是单值配置,不能传列表或元组。即使把多个后端路径用逗号拼接,Django 也会尝试按完整路径导入而失败。因此同时使用两个后端不能靠修改这一项来完成,必须引入一个自定义后端作为代理。代理本身不直接处理 MIME 或网络通信,而是把同一批 EmailMessage 转交给内部的 SMTP 后端和控制台后端。
可以先想清楚组合目标。通常开发环境希望控制台输出帮助检查收件人、主题、正文和附件,而 SMTP 仍然真实投递;也可能只想在部分测试环境启用控制台,生产环境只走 SMTP。所以复合后端最好能根据 Django 配置或环境变量决定是否加载控制台后端,而不是写死两个实例。
编写复合邮件后端
新建一个后端模块,比如 myproject/mail_backends.py 或放在某个应用下。类继承 BaseEmailBackend,构造方法接收 fail_silently=False 等参数,与标准后端签名保持一致。在初始化的逻辑里创建 smtp.EmailBackend 和 console.EmailBackend 实例。注意这两个后端都有 open 和 close 方法,所以复合类也应实现对应生命周期方法,以便在批量发送前建立 SMTP 连接、结束后及时断开。
核心方法 send_messages(self, email_messages) 返回成功发送的邮件数量。这里需要遍历两个后端分别发送,返回值的含义取决于 Django 的邮件发送接口:通常返回成功投递的邮件数量。简单实现可以分别调用并返回 SMTP 后端的返回值,因为控制台几乎不会失败。但为了统一,可以对两个返回值取较小值,或者以 SMTP 结果为准,并单独捕获控制台异常写入日志。下面给出一个完整实现示例。
import sys
from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.backends import smtp, console
class CompositeEmailBackend(BaseEmailBackend):
"""
同时使用 SMTP 与控制台邮件后端的复合后端。
"""
def __init__(self, fail_silently=False, **kwargs):
super().__init__(fail_silently=fail_silently, **kwargs)
# 控制台后端默认写入当前标准输出,也可以换成日志流
self.console_backend = console.EmailBackend(
fail_silently=True,
stream=sys.stdout,
)
# SMTP 后端会读取 settings 中的 EMAIL_HOST、EMAIL_PORT 等配置
self.smtp_backend = smtp.EmailBackend(
fail_silently=fail_silently,
)
def open(self):
self.console_backend.open()
return self.smtp_backend.open()
def close(self):
self.console_backend.close()
self.smtp_backend.close()
def send_messages(self, email_messages):
if not email_messages:
return 0
console_count = 0
smtp_count = 0
try:
console_count = self.console_backend.send_messages(email_messages)
except Exception as exc:
if not self.fail_silently:
raise
# 即使控制台失败,也继续尝试 SMTP
print("控制台邮件后端发送失败:%s" % exc)
try:
smtp_count = self.smtp_backend.send_messages(email_messages)
except Exception:
if not self.fail_silently:
raise
return smtp_count
这段代码中的 console_backend 使用了 fail_silently=True,目的是让控制台异常不会掩盖真实 SMTP 错误;SMTP 则使用外层传入的 fail_silently 值,保持与原生行为一致。返回值以 SMTP 为准,因为真实投递结果对上层调用方更有意义。如果需要在两个后端之间修改消息副本,可以引入 copy.deepcopy,避免一个后端修改消息对象后影响另一个。本实现直接传入同一批消息,两个后端都只读取内容,通常不会产生冲突。
注册复合后端并动态控制控制台输出
在 settings.py 中设置 EMAIL_BACKEND 为自定义类路径,例如 myproject.mail_backends.CompositeEmailBackend。同时确保 SMTP 的常规配置完整:EMAIL_HOST、EMAIL_PORT、EMAIL_HOST_USER、EMAIL_HOST_PASSWORD、EMAIL_USE_TLS 或 EMAIL_USE_SSL。控制台后端不需要额外配置。邮件发送代码仍然使用 django.core.mail.send_mail 或 EmailMessage.send,不会感知到后端已换成复合类。
如果只想在 DEBUG 为 True 时启用控制台,可以在自定义类构造时读取 django.conf.settings.DEBUG,条件化实例化。例如当 DEBUG 为真时创建 console.EmailBackend,否则将 console_backend 设为 None,send_messages 里判断非 None 再调用。这样生产环境即使误配了复合后端,也不会把邮件内容打印到标准输出,降低敏感信息泄露风险。
import sys
from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.backends import smtp, console
class DebugAwareCompositeEmailBackend(BaseEmailBackend):
def __init__(self, fail_silently=False, **kwargs):
super().__init__(fail_silently=fail_silently, **kwargs)
self.smtp_backend = smtp.EmailBackend(
fail_silently=fail_silently,
)
if getattr(settings, "DEBUG", False):
self.console_backend = console.EmailBackend(
fail_silently=True,
stream=sys.stdout,
)
else:
self.console_backend = None
def open(self):
if self.console_backend is not None:
self.console_backend.open()
return self.smtp_backend.open()
def close(self):
if self.console_backend is not None:
self.console_backend.close()
self.smtp_backend.close()
def send_messages(self, email_messages):
if not email_messages:
return 0
if self.console_backend is not None:
try:
self.console_backend.send_messages(email_messages)
except Exception:
if not self.fail_silently:
raise
return self.smtp_backend.send_messages(email_messages)
还可以把控制台输出流从 sys.stdout 换成 logging 模块的 StreamHandler 或自定义文件流,这样邮件内容能进入统一日志格式,便于后期排查。比如 console.EmailBackend(stream=log_stream),但要注意控制台后端输出的是完整邮件字符串,可能包含收件人、正文甚至密码重置链接等敏感信息,日志文件必须做访问控制。
发送行为、重复投递与生产环境建议
同时使用两个后端时,同一封邮件会被处理两次。接收者收到的邮件只来自 SMTP;控制台只是一份可读快照,不会减少真实投递次数。如果调试目标是不实际发送但保留控制台输出,可以单独把 EMAIL_BACKEND 设为 console.EmailBackend。同时使用则必须有明确目的,例如需要将邮件内容同步到审计日志或分析系统,否则可能造成困惑,特别是开发邮件触发频率较高时控制台会被大量内容刷屏。
异常处理方面,控制台后端基本不会失败,但仍建议把控制台调用包裹在 try 块中。SMTP 后端受网络、认证、限流影响,失败时 Django 会抛出 smtplib.SMTPException 或 ConnectionError。复合后端应在 fail_silently=False 时原样抛出异常,保持与原生行为一致,调用方可以继续复用现有降级或重试逻辑。批量发送时,send_messages 的返回值通常表示成功发送的邮件数量,不同后端对不同失败情况的统计可能不一致,所以复合类最好采用 SMTP 的返回值,让上层逻辑认为真实投递结果具有最终意义。
生产环境如果要保留控制台输出,更推荐用文件后端或第三方日志系统替代控制台后端。django.core.mail.backends.file.EmailBackend 可以把每封邮件写成文件,更适合审计。对于高可用场景,还可以把复合后端与 Celery 任务结合,让邮件发送异步执行,但要注意后端实例的线程安全,open 和 close 不应在多线程共享连接,每个任务内部创建独立后端或使用锁保护。复合后端只是一个轻量代理,真正要扩展邮件能力时,可以继续嵌套缓存后端、限流后端,保持接口一致。
Django邮件后端SMTP控制台邮件后端修改时间:2026-08-26 16:58:06