发50封邮件,会开多少条SMTP连接?有人在新版Django上认真数了一遍:旧配置下是50条,换成新的MAILERS配置后,还是50条。
![]()
Django 6.1在2026年8月进入稳定版,MAILERS不是实验特性。它要替代的是EMAIL_BACKEND,以及跟着它的大约十来个EMAIL_*配置项。直觉上,2026年从零搭的配置系统总该顺手把连接方式也理一理,于是有人在迁移前先做了测量。
结论是:连接数一条没少。只有放弃文档里的写法、手动复用连接对象,数字才会降下来——而这个技巧在MAILERS出现之前就一样管用。
MAILERS到底是什么
6.1之前,一个项目只有一个全局邮件后端,用EMAIL_BACKEND指定,主机、端口、凭据、TLS都是平铺的配置项。MAILERS把它们变成一个按名字索引的字典,每个条目有自己的BACKEND类和OPTIONS:
- 用send_mail()时加using="default"选择邮件器
- 或者用mail.mailers["default"]直接拿后端实例
- 旧的EMAIL_BACKEND等配置仍可用,但发布说明写明已弃用,将在未来版本被替换
测试环境是一个临时的Django 6.1项目(需要Python 3.12或更新版本,系统默认的3.11直接装不上),配一个基于aiosmtpd的本地SMTP服务器,记录每一次EHLO和DATA命令,这样拿到的是真实连接数,而不是靠耗时猜。
发现一:MAILERS本身不减少连接
50封邮件用三种方式各发三轮,对着本地服务器跑。前两种方式的结果无法区分。读Django源码能解释原因:mail.mailers[alias]会调用create_connection(),每次访问都新建一个后端实例,和旧的get_connection()一样。
MAILERS是配置格式,不是连接池。代码在循环里调send_mail(using="default"),每封邮件都会新建一条TCP连接、一次EHLO、一次STARTTLS握手,和以前完全一致。
唯一能砍掉连接数的做法,是把连接对象取一次、传给每一条消息,也就是get_connection() as connection的老写法,在Django里已经用了十多年。MAILERS没让它更容易,也没让它更难,只是换了个访问入口。
每请求发一封邮件的项目,这条发现不影响什么。批量发几百条通知的任务,只迁移配置、不动发送代码,什么也省不下来——而这恰恰容易误判,因为新特性的卖点就是“现代配置系统”。
发现二:同一个错误配置,一处是警告,一处是致命错误
把一个选项拼错(tiemout写成timeout之外的样子),用两种方式传给SMTP后端。直接构造实例的老写法,只给出警告:直接创建EmailBackend实例已弃用,请改用mail.mailers;后端__init__()不支持'tiemout';Django 7.0中BaseEmailBackend将对未知关键字参数抛出TypeError。后端照样被构造出来,拼写错误被静默忽略,对象正常工作。
同样的拼写错误放进MAILERS["default"]["OPTIONS"],结果是InvalidMailer:MAILERS['default']未知选项'tiemout'。没有对象,没有回退,什么也发不出去。
这是实打实的改进,也是迁移最强的理由:配置错的邮件器在启动时就失败,而不是悄悄丢掉一个选项、用你没选的默认值把邮件发出去。它同时是个坑——如果你把现有的EMAIL_HOST_*配置块手工抄进OPTIONS,任何与后端构造参数名对不上的键,现在会让整个邮件器失效,而不是被忽略。
发现三:using和它替代的参数不能共存
using的存在就是为了替代send_mail()的connection、fail_silently、auth_user和auth_password参数。两者同时传,立刻抛错,不是警告:
- using加fail_silently:TypeError,'fail_silently'与'using'不兼容
- using加connection:TypeError,'connection'与'using'不兼容
- using加auth_user:TypeError,'auth_user'和'auth_password'与'using'不兼容,提示改为在MAILERS的OPTIONS里设置username和password
直截了当的迁移没问题,删掉旧参数、把值搬进OPTIONS就行。麻烦在于代码库在上游某处按条件构造connection或fail_silently参数:只在一个调用点加上using、不审计所有调用方,第一次走到两个参数同时存在的代码路径,原本能跑的调用就变成崩溃。
发现四:新的安全检查只盯着已经迁移的人
Django 6.1新增部署检查mail.E001:如果默认邮件器是console、dummy、基于文件或内存的后端,manage.py check --deploy会失败;MAILERS里完全没有default条目时,mail.W001给出警告。两条都用真实文本验证过,确实会触发。
然后拿一个从没碰过MAILERS的项目跑同样的检查:EMAIL_BACKEND仍指向console后端,其余配置看起来像生产环境(DEBUG = False、装了安全中间件、有真实的SECRET_KEY)。结果只报了两条安全警告,关于邮件的一条都没有。
这个检查只读settings.MAILERS。仍在使用弃用配置的项目,得不到任何防护,而把console后端误发到生产,正是这个新检查要抓的错误。安全网只覆盖已经做完迁移的人;最容易犯这个错的人——多年没动过邮件配置的人——什么也没多拿到。
发现五:文档说的弃用警告大多不触发
发布说明称弃用配置“会继续工作,但会发出弃用警告”。用一个纯EMAIL_BACKEND配置、没有任何MAILERS的项目,在warnings.simplefilter("always")生效的情况下调用send_mail(),记录到的警告数是0。
同一个项目里直接调get_connection()确实会警告:RemovedInDjango70Warning,get_connection()已弃用。差别在django/utils/deprecation.py:Django的警告辅助函数会刻意跳过源自Django自身代码的调用,防止一个弃用特性建立在另一个之上时警告层层级联。send_mail()内部调用get_connection(),这次调用算“在Django内部”,什么也不产生。
只有你自己的代码调用get_connection()或直接构造后端时才会收到警告。绝大多数Django项目只调send_mail(),也就是说绝大多数用旧配置的项目升级到6.1时,一条警告都看不到,与发布说明给人的印象不符。
发布说明有一条说法完全站得住:没有定义MAILERS时,mail.mailers["default"]会正确回退到用旧版EMAIL_HOST/EMAIL_PORT配置构造后端。把这两个指向一个假主机,返回的对象带着正确的主机和端口。
测试路上踩的坑
第一版测试服务器用handle_connect钩子统计连接,aiosmtpd的处理器API里没有这个方法;它静默接受了方法名,然后从不调用,于是每次运行都报告零连接,不管Django做了什么。改用统计handle_EHLO调用,因为每个SMTP客户端每条连接恰好发一次EHLO。这一版第一次跑,所有发送都失败在503 Error: send HELO first。真正的bug是自己写的:aiosmtpd的“新式”EHLO钩子要求处理器自己设置session.host_name,内置处理器会自动做,自定义的必须显式写。少了这一行,服务器从不认为连接打过招呼,尽管它确实打了。
迁移时该做什么
还在Django 6.1上用EMAIL_BACKEND的,别指望升级本身会警告你什么;也别指望只迁移配置、不改发送代码就能改变SMTP连接数。两个真实收益是:OPTIONS在启动时的严格校验,能抓住旧配置静默吞掉的拼写错误;以及新的mail.E001部署检查,值得拥有,但只在你已经定义MAILERS["default"]之后才保护你。
如果生产配置模块还意外带着console或dummy后端,在旧配置下,check --deploy不会告诉你。迁移之后、而不是之前,拿真实的生产配置跑一遍python manage.py check --deploy;升级前在代码库里搜get_connection(和直接构造后端的地方,因为行为改变——而不只是配置改变——就发生在这两类调用点上。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.