Django 6.1: conheça o MAILERS, o novo jeito de enviar e-mails
Publicado em 29/09/2026.
Testado com: Django 6.1.1 e Python 3.12.
Guia oficial de migração: https://docs.djangoproject.com/en/6.1/howto/mailers-migration/
Por anos o envio de e-mail no Django foi um backend só, espalhado por onze settings EMAIL_*. No Django 6.1 isso vira um dicionário, igual ao CACHES e ao DATABASES: o MAILERS. Neste tutorial vamos ver como era, como fica, configurar mais de um provedor, enviar escolhendo o provedor com using=, migrar um projeto existente (incluindo o caso chato do fail_silently) e testar.
Pré-requisitos
- Python 3.12, 3.13 ou 3.14.
- Django 6.1 (
pip install "django>=6.1"). Os exemplos foram rodados com o Django 6.1.1.
Como era antes
Um backend só, configurado em vários settings soltos:
# config/settings.py (antes do Django 6.1)
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.exemplo.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'no-reply@exemplo.com'
EMAIL_HOST_PASSWORD = '...'
EMAIL_USE_TLS = True
EMAIL_TIMEOUT = 10
O projeto inteiro tinha uma configuração de envio, e só uma. Para mandar e-mail transacional pelo SMTP próprio e marketing por outro provedor, você abria a segunda conexão na mão, com as credenciais no meio do código:
# loja/emails.py (antes do Django 6.1)
from django.core import mail
connection = mail.get_connection(
'path.to.custom.EmailBackend',
host='smtp.marketing.com',
username='...',
password='...',
)
mail.send_mail('Novidades', 'Corpo', 'de@exemplo.com', ['para@exemplo.com'], connection=connection)
E parte da configuração vazava para os argumentos do envio: auth_user, auth_password, fail_silently e connection.
Como fica: o setting MAILERS
# config/settings.py
from decouple import config
MAILERS = {
'default': {
'BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
'OPTIONS': {
'host': config('EMAIL_HOST', 'smtp.exemplo.com'),
'use_tls': True,
'username': config('EMAIL_USERNAME', ''),
'password': config('EMAIL_PASSWORD', ''),
'timeout': 10,
},
},
'marketing': {
'BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
'OPTIONS': {
'host': config('MARKETING_EMAIL_HOST', 'smtp.marketing.com'),
'use_tls': True,
'username': config('MARKETING_EMAIL_USERNAME', ''),
'password': config('MARKETING_EMAIL_PASSWORD', ''),
},
},
}
DEFAULT_FROM_EMAIL = 'no-reply@exemplo.com'
- Cada chave do dicionário é um alias (um "mailer"). O
'default'é o usado quando você não diz nada. BACKEND: a classe do backend. Se for omitido, o padrão é o SMTP.OPTIONS: tudo o que antes eraEMAIL_*vira uma chave aqui dentro, em minúsculas. As opções são repassadas como argumentos nomeados para o__init__do backend.- O
portpode ser omitido quando é o padrão do tipo de conexão: 587 comuse_tls, 465 comuse_ssl, 25 sem nenhum. - Um backend de terceiros (de um provedor com API própria) entra do mesmo jeito, com as opções que ele documentar, por exemplo
'BACKEND': 'example.third.party.EmailBackend'e'OPTIONS': {'region': 'africa-1'}.
O python-decouple foi usado só para tirar as credenciais do código; use o que preferir.
Projetos novos criados com startproject no 6.1 já vêm com um MAILERS que usa o backend de console (o e-mail é impresso no terminal). Projetos existentes não ganham o MAILERS sozinhos: os EMAIL_* continuam funcionando, com avisos de depreciação.
Enviando: escolha o mailer pelo alias
# loja/emails.py
import logging
from django.core import mail
from django.core.mail import EmailMessage, send_mail
logger = logging.getLogger(__name__)
def enviar_recibo(pedido):
# vai pelo mailer 'default'
send_mail(
'Recibo do seu pedido',
f'Obrigado! Seu pedido {pedido.pk} foi confirmado.',
None, # usa o DEFAULT_FROM_EMAIL
[pedido.cliente.email],
)
def enviar_novidades(emails):
# vai pelo mailer 'marketing'
send_mail(
'Novidades da semana',
'Confira os lançamentos.',
None,
emails,
using='marketing',
)
def enviar_com_anexo(destinatario, caminho_pdf):
msg = EmailMessage('Seu boleto', 'Segue o boleto em anexo.', to=[destinatario])
msg.attach_file(caminho_pdf)
msg.send(using='marketing')
def enviar_lote(mensagens):
# a instância do backend, se você precisar dela
backend = mail.mailers['marketing']
return backend.send_messages(mensagens)
send_mail(..., using='marketing')substitui oconnection=get_connection(...). Host, porta, usuário e senha não aparecem mais no ponto de envio: o código diz qual mailer quer, os settings dizem o que aquele mailer é.EmailMessage.send(using=...)funciona do mesmo jeito.mail.mailers['marketing']devolve a instância do backend configurado, email.mailers.defaultdevolve o default.- Um alias que não existe levanta
django.core.mail.MailerDoesNotExist("The mailer 'xyz' is not configured.").
De-para completo
| Antes | Django 6.1 |
|---|---|
EMAIL_BACKEND |
MAILERS['default']['BACKEND'] |
EMAIL_HOST |
'host' em OPTIONS (obrigatório no SMTP; antes o padrão era 'localhost') |
EMAIL_PORT |
'port' em OPTIONS |
EMAIL_HOST_USER |
'username' em OPTIONS (repare: não é host_user) |
EMAIL_HOST_PASSWORD |
'password' em OPTIONS |
EMAIL_USE_TLS |
'use_tls' em OPTIONS |
EMAIL_USE_SSL |
'use_ssl' em OPTIONS |
EMAIL_SSL_CERTFILE |
'ssl_certfile' em OPTIONS |
EMAIL_SSL_KEYFILE |
'ssl_keyfile' em OPTIONS |
EMAIL_TIMEOUT |
'timeout' em OPTIONS |
EMAIL_FILE_PATH |
'file_path' em OPTIONS (com o backend filebased) |
mail.get_connection() |
mail.mailers.default |
mail.get_connection('backend', ...) |
um alias novo em MAILERS + mail.mailers['alias'] |
send_mail(..., connection=conn) |
send_mail(..., using='alias') |
send_mail(..., auth_user=..., auth_password=...) |
um alias com 'username' e 'password' em OPTIONS + using= |
Exemplo real de migração, lado a lado:
# config/settings.py - antes
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'mail.example.net'
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_USER = 'user@example.net'
EMAIL_HOST_PASSWORD = 'password'
# config/settings.py - depois
MAILERS = {
'default': {
'BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
'OPTIONS': {
'host': 'mail.example.net',
'use_tls': True,
# port não é necessário: com use_tls o padrão já é 587
'username': 'user@example.net',
'password': 'password',
},
},
}
Atenção: não dá para ter os dois ao mesmo tempo. Se MAILERS estiver definido junto com algum EMAIL_* depreciado, o Django nem sobe:
django.core.exceptions.ImproperlyConfigured: Deprecated email settings are not allowed when MAILERS is defined: EMAIL_BACKEND, EMAIL_HOST.
Na transição, mail.mailers['default'] funciona com qualquer um dos dois jeitos de configurar. Dá para migrar os settings primeiro e o código depois.
O caso chato: fail_silently
O fail_silently está depreciado em send_mail(), send_mass_mail(), mail_admins(), mail_managers() e EmailMessage.send(), e não combina com using:
send_mail('a', 'b', None, ['c@d.com'], using='default', fail_silently=True)
# TypeError: 'fail_silently' is not compatible with 'using'.
Não existe um argumento substituto: decida o que você quer ignorar e trate no seu código.
# loja/emails.py (continuação)
def enviar_novidades_sem_quebrar(emails):
try:
send_mail('Novidades', 'Corpo', None, emails, using='marketing')
except mail.MailerDoesNotExist:
# o projeto não configurou o mailer 'marketing': segue a vida
pass
except OSError:
# falhas de rede e de SMTP
logger.exception('falha ao enviar e-mail de marketing')
except mail.MailerDoesNotExist: enviar se o e-mail estiver configurado, sem erro se não estiver (útil em apps reutilizáveis).except OSError: ignorar só problemas de SMTP e rede (o mesmo que ofail_silentlydo backend SMTP fazia).except Exception: ignorar tudo, por exemplo dentro de um handler de erro, para não gerar falha em cascata.- Se você usava
fail_silentlypara "ignorar e-mail digitado errado", pode simplesmente removê-lo: erros de destinatário quase nunca são detectados no momento do envio.
Se quiser reaproveitar o comportamento em vários pontos, crie um alias com a opção no próprio backend:
# config/settings.py (trecho)
MAILERS = {
'default': {...},
'admin-logging': {
'BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
'OPTIONS': {
'host': 'smtp.exemplo.com',
'fail_silently': True,
},
},
}
E aponte o AdminEmailHandler para ele, com a nova opção using (o antigo email_backend do handler também foi depreciado):
# config/settings.py (trecho)
LOGGING = {
'version': 1,
'handlers': {
'mail_admins': {
'class': 'django.utils.log.AdminEmailHandler',
'using': 'admin-logging',
},
},
}
O Django passa a avisar quando a config está errada
mail.E001: impede usar no mailer'default'um backend que não é de produção (console, locmem, file). Só roda no check de deploy.mail.W001: avisa quando você definiuMAILERS, mas esqueceu a entrada'default'.sendtestemail --using: o comando de teste ganhou a opção para escolher o alias.
python manage.py check --deploy
# ?: (mail.E001) Your MAILERS setting uses a development-only email backend in the 'default' entry (django.core.mail.backends.console.EmailBackend).
python manage.py sendtestemail voce@exemplo.com --using marketing
Ambiente de desenvolvimento
Em desenvolvimento, aponte os mailers para backends que não enviam nada:
# config/settings.py (desenvolvimento)
MAILERS = {
'default': {
'BACKEND': 'django.core.mail.backends.console.EmailBackend',
},
'marketing': {
'BACKEND': 'django.core.mail.backends.filebased.EmailBackend',
'OPTIONS': {'file_path': BASE_DIR / 'emails-marketing'},
},
}
O default imprime no terminal, e o marketing grava um arquivo .log por envio na pasta emails-marketing.
Testes
O test runner do Django substitui todos os mailers pelo backend em memória: nada é enviado de verdade e as mensagens ficam em mail.outbox, inclusive as enviadas com using=.
# loja/tests.py
from django.core import mail
from django.test import TestCase
class EmailTest(TestCase):
def test_marketing(self):
mail.send_mail('Novidades', 'corpo', None, ['cliente@exemplo.com'], using='marketing')
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'Novidades')
python manage.py test loja
Por isso mesmo, a suíte de testes pode não mostrar os avisos de depreciação da sua configuração de produção.
Encontrando o que falta migrar
Rode o projeto com os avisos de depreciação ligados:
python -W always manage.py runserver
python -W always manage.py test
Com EMAIL_* nos settings, você verá algo assim:
RemovedInDjango70Warning: The EMAIL_BACKEND setting is deprecated. Migrate to MAILERS before Django 7.0.
RemovedInDjango70Warning: The EMAIL_HOST setting is deprecated. Migrate to MAILERS before Django 7.0.
(Essa é a mensagem do Django 6.1.1. A documentação já chama a próxima versão maior de Django 2028: com a adoção do versionamento por calendário, a antiga 7.0 virou 2028.)
Antes de migrar, confira se as bibliotecas de terceiros que mandam e-mail no seu projeto já suportam MAILERS. Se alguma delas ler settings.EMAIL_HOST ou chamar get_connection('caminho.do.Backend'), você verá AttributeError: The EMAIL_... setting is not available when MAILERS is defined ou RuntimeError: get_connection(backend, ...) is not supported with MAILERS. Nesse caso, atualize a biblioteca ou mantenha os EMAIL_* até ela ser atualizada.
O prazo
- Django 6.1 (agora):
MAILERSexiste e funciona. OsEMAIL_*,get_connection(),connection=,fail_silently,auth_usereauth_passwordcontinuam valendo, com aviso de depreciação. - Django 2028: os
EMAIL_*e esses argumentos são removidos, e enviar e-mail semMAILERSpassa a levantarMailerDoesNotExist.
Resumo
- Traduza os
EMAIL_*paraMAILERS['default']. Na maioria dos projetos, é só isso. - Troque
get_connection()pormail.mailerseconnection=porusing=. - Substitua
fail_silentlyportry/exceptou por um alias com'fail_silently': True. - Rode com os avisos de depreciação ligados para achar o resto.
Documentação: