Django-发送邮件
这篇讲什么
- Django 的邮件能力都在
django.core.mail里,send_mail与EmailMessage两套 API 覆盖从单发到群发。 - 学习阶段用内存 / 打印 / 写文件这三个「假发信」后端就能全部跑通,不用连真实 SMTP。这篇把假后端、真发信的配置(授权码不是登录密码)、
fail_silently的真实行为和一个「注册 → 激活」完整示例都实测了一遍。基线 Django 5.2.19。
概述
- Django 内置了邮件发送功能,都在
django.core.mail模块里。实测可直接导入EmailMessage、EmailMultiAlternatives、send_mail、send_mass_mail四个名字。 - 真发信要走 SMTP 服务器,国内常用的是 163、 126、 QQ 邮箱(需要在网页里开 POP3/SMTP 服务并拿授权码,不是登录密码)。
- 但学习阶段根本不用真发:Django 提供三个「假发信」后端(内存 / 打印到终端 / 写文件),本篇的实测全部用它们完成,不给任何真实地址发信。
开发期用这三个后端,别连真服务器
-
locmem:留在内存里给测试用settings.py 1
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
1
2
3
4
5
6from django.core import mail
from django.core.mail import send_mail
n = send_mail("注册激活", "正文", "python<a@b.com>", ["c@d.com"])
print(n) # 1
print(type(mail.outbox[0])) # <class 'django.core.mail.message.EmailMultiAlternatives'>- 实测输出:
send_mail 返回: 1,outbox[0]的类型是EmailMultiAlternatives,alternatives里能看到EmailAlternative(content='<b>粗体</b>', mimetype='text/html')。 - 单元测试里断言「发了几封、发给谁、正文有没有那个激活链接」就用
mail.outbox,这是最实用的写法。
- 实测输出:
-
console:直接打到终端settings.py 1
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
- 实测会把整封信(
Content-Type、From、To、Subject等信头 + 正文)打印出来,正文后面跟一行----------分隔线。本地跑注册流程时最直观。
- 实测会把整封信(
-
file:写成一个文件settings.py 1
2EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = r"E:\Desktop\Test\mail_out"- 实测生成的文件名形如
20261104-221452-8439960.log(扩展名是.log,不是.eml,内容才是标准 MIME)。 - 文件里能看到完整的 MIME 结构:
multipart/mixed套multipart/alternative(纯文本 + HTML),附件用Content-Disposition: attachment; filename*=utf-8''...(中文文件名走 RFC 2231 编码),自定义头X-Custom: abc也原样保留。
- 实测生成的文件名形如
真发信时的 settings.py
1 | # 发送邮件配置 |
-
老笔记最后一行写的是
EMAIL_FROM。这不是 Django 的设置项,实测读它会:1
AttributeError: 'Settings' object has no attribute 'EMAIL_FROM'
- 你可以自己往 settings 里塞这个键当自定义变量用,但 Django 认的是
DEFAULT_FROM_EMAIL(普通邮件的发件人)和SERVER_EMAIL(系统自动邮件,如报错邮件)的发件人。
- 你可以自己往 settings 里塞这个键当自定义变量用,但 Django 认的是
-
实测这 9 个键在 5.2 上都存在(
hasattr(settings, k)全为 True):EMAIL_BACKEND、EMAIL_HOST、EMAIL_PORT(默认 25)、EMAIL_HOST_USER、EMAIL_HOST_PASSWORD、EMAIL_USE_TLS(默认 False)、EMAIL_USE_SSL、DEFAULT_FROM_EMAIL、SERVER_EMAIL。 -
顺带一条版本相关:
MAILERS这个设置在 5.2 上不存在(hasattr(settings, "MAILERS")为 False),它是 Django 6.1 才引入的新邮件配置入口,看 6.x 文档时别把它抄进 5.2 项目。
函数式与对象式两套 API
-
便捷函数
send_mail实测签名:
1
2(subject, message, from_email, recipient_list, fail_silently=False,
auth_user=None, auth_password=None, connection=None, html_message=None)- 注意
recipient_list是列表,html_message是可选的 HTML 正文(老笔记用的就是这个参数)。 send_mass_mail((t1, t2, ...), fail_silently=False)是「一个连接发多封」,t是(subject, message, from_email, recipient_list)四元组。
- 注意
-
对象式
EmailMessage实测签名:
1
2(self, subject='', body='', from_email=None, to=None, bcc=None, connection=None,
attachments=None, headers=None, cc=None, reply_to=None)1
2
3
4
5
6
7
8
9
10
11
12
13
14from django.core.mail import EmailMultiAlternatives
from django.conf import settings
mail = EmailMultiAlternatives(
subject="注册激活",
body="请点这个链接激活(纯文本兜底)",
from_email=settings.DEFAULT_FROM_EMAIL,
to=["c@d.com"],
cc=["e@f.com"],
headers={"X-Custom": "abc"},
)
mail.attach_alternative("<b>粗体</b>", "text/html") # HTML 版本
mail.attach_file(r"E:\Desktop\Test\说明.txt") # 附件
mail.send(fail_silently=False)EmailMultiAlternatives比EmailMessage多一个alternatives参数,其余一致。reply_to在老笔记时代经常被忽略,实测 5.2 有。
fail_silently 到底干什么(实测)
-
把
EMAIL_HOST/EMAIL_PORT指向一个没人监听的端口(127.0.0.1:18825),实测两种取值:1
2fail_silently=False -> ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接。
fail_silently=True -> 返回值: 0 -
所以:
True是「发不出去就算了,别炸」,返回值 0 表示成功 0 封 —— 线上用它要谨慎,否则用户收不到信而你在日志里什么都看不到。False(默认)会把异常抛给你,配合日志/告警才是可运维的做法。
-
老笔记里那句
# longIO.delay()/# sendMail.delay()的注释其实点到了一个真问题:发信是网络 IO,放在注册请求里同步做会拖慢甚至拖死这个请求,正式项目要丢给 Celery 之类的队列(见《Django-Celery》)。
一个完整可跑的「注册 → 激活」示例
-
老笔记这段有三个问题:
register里调send,send里又调sendMail,函数名混乱;urls.py用的是re_path;最关键的是checkRegister把 session 里的 token 直接回显出来当作「验证成功」,等于没有校验(任何人请求/checkRegister/都会拿到自己的 token 并显示成功)。下面这版把校验补上。 -
路由
myapp/urls.py 1
2
3
4
5
6
7
8
9
10from django.urls import path
from myapp import views
app_name = "myapp"
urlpatterns = [
path("register/", views.register, name="register"),
path("check_register/", views.check_register, name="check_register"),
] -
视图
myapp/views.py 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44import uuid
from django.conf import settings
from django.core.mail import send_mail
from django.http import HttpResponse
from django.shortcuts import render
from django.urls import reverse
from myapp.models import User
def register(request):
if request.method == "GET":
return render(request, "myapp/register.html")
username = request.POST.get("username")
password = request.POST.get("password")
user = User.objects.create(username=username, password=password) # 真实项目要哈希密码
check_str = uuid.uuid4().hex
request.session["check"] = check_str
# 用绝对地址,收件人点开才有效
activate_url = request.build_absolute_uri(
reverse("myapp:check_register") + "?token=" + check_str
)
send_mail(
subject="注册激活",
message="点击激活:%s" % activate_url,
from_email=settings.DEFAULT_FROM_EMAIL,
recipient_list=[user.email],
html_message='<a href="%s">点击激活</a>' % activate_url,
)
return HttpResponse("注册成功,请查收激活邮件")
def check_register(request):
token = request.GET.get("token")
saved = request.session.get("check")
if token and saved and token == saved: # 必须比对,不能把 token 回显当成功
del request.session["check"]
User.objects.filter(pk=request.session["uid"]).update(is_active=True)
return HttpResponse("验证成功")
return HttpResponse("验证失败")request.build_absolute_uri(...)比老笔记里写死的http://127.0.0.1:8000/...靠谱:换域名/端口不用改代码。- 更规范的做法是用 Django 自带的签名 token(
django.contrib.auth.tokens.default_token_generator)+signing.dumps/loads,把用户 ID 和过期时间一起签进去,就不用依赖 session 了(session 会过期,而且换浏览器就失效)。
新手卡点清单
-
都是实测过的:
卡点 症状 正确做法 用 settings.EMAIL_FROMAttributeError: 'Settings' object has no attribute 'EMAIL_FROM'用 DEFAULT_FROM_EMAIL/SERVER_EMAIL学习期就配真 SMTP 发不出去还以为是代码错 先用 locmem/console/file后端fail_silently=True当默认发信失败静默返回 0,日志里查不到 生产用 False+ 异常日志在注册请求里同步发信 请求被 SMTP 拖慢 丢给 Celery 异步任务 激活链接写死 127.0.0.1:8000换环境就失效 request.build_absolute_uri(reverse(...))只回显 token 不比对 任何人都能「验证成功」 必须比对 token(或用签名 token) 抄 6.x 文档里的 MAILERS5.2 上不存在该设置 5.2 用 EMAIL_*系列
参考
- Django:Sending emails:
send_mail、EmailMessage、各后端的官方说明。 - Django email backends 参考:
locmem/console/filebased/smtp的用途。 - Django
DEFAULT_FROM_EMAIL与SERVER_EMAIL:两个发件人设置的区别。 - Django signing:用签名 token 做账号激活的推荐做法。