Email Services and Message Handling
Email services in this codebase are centered around a flexible backend system that separates message construction from the actual delivery mechanism. The implementation is currently transitioning to a centralized MAILERS configuration system, managed by the MailersHandler class.
The Mailers System
The MailersHandler (accessible via django.core.mail.mailers) is the primary interface for obtaining email backend instances. It uses the MAILERS dictionary in your settings to manage different email configurations (aliases).
Configuration Structure
A mailer configuration defines which backend to use and any specific options required by that backend.
# Example settings.py configuration
MAILERS = {
"default": {
"BACKEND": "django.core.mail.backends.smtp.EmailBackend",
"OPTIONS": {
"host": "smtp.example.com",
"port": 587,
"use_tls": True,
},
},
"logging": {
"BACKEND": "django.core.mail.backends.filebased.EmailBackend",
"OPTIONS": {"file_path": "/var/log/emails"},
},
}
The MailersHandler retrieves these configurations and instantiates the appropriate BaseEmailBackend subclass.
Email Backends
All email backends inherit from BaseEmailBackend in django/core/mail/backends/base.py. This base class defines the standard interface: open(), close(), and send_messages(email_messages).
SMTP Backend
The django.core.mail.backends.smtp.EmailBackend is the standard production backend. It manages a persistent connection to an SMTP server.
- Key Options:
host,port,username,password,use_tls,use_ssl,timeout. - Exclusivity:
use_sslanduse_tlsare mutually exclusive. If both are set toTrue, the backend raises anInvalidMailererror. - Connection Management: It uses
smtplib.SMTP(orSMTP_SSL) and supports automatic login if credentials are provided.
Development and Testing Backends
For non-production environments, several specialized backends are available:
- Console Backend (
django.core.mail.backends.console.EmailBackend): Writes email content to a stream, defaulting tosys.stdout. - File-based Backend (
django.core.mail.backends.filebased.EmailBackend): Writes emails to files in a specified directory. It creates a unique filename for each session using a timestamp. - Locmem Backend (
django.core.mail.backends.locmem.EmailBackend): Used primarily for testing. Instead of sending emails, it appendsEmailMessageobjects to a list atdjango.core.mail.outbox.
Constructing Messages
Messages are constructed using the EmailMessage class or its subclass EmailMultiAlternatives.
EmailMessage
The EmailMessage class in django/core/mail/message.py is a container for all email data.
Important Requirement: The recipient fields (to, cc, bcc, reply_to) must be lists or tuples. Passing a string will result in a TypeError.
from django.core.mail import EmailMessage
msg = EmailMessage(
subject="Order Confirmation",
body="Thank you for your order.",
from_email="sales@example.com",
to=["customer@example.com"],
cc=["manager@example.com"],
)
Multi-part Alternatives
To send emails with both plain text and HTML versions, use EmailMultiAlternatives. This class allows you to attach "alternatives" to the main body.
from django.core.mail import EmailMultiAlternatives
subject, from_email, to = "Newsletter", "news@example.com", "user@example.com"
text_content = "This is an important message."
html_content = "<p>This is an <strong>important</strong> message.</p>"
msg = EmailMultiAlternatives(subject, text_content, from_email, [to])
msg.attach_alternative(html_content, "text/html")
msg.send()
Attachments
You can add attachments using attach() or attach_file().
attach(filename, content, mimetype): Adds an attachment with raw content.attach_file(path): Reads a file from the filesystem and attaches it, automatically guessing the mimetype.
msg.attach_file("path/to/report.pdf")
msg.attach("data.csv", "column1,column2\nval1,val2", "text/csv")
Sending Messages
The preferred way to send a message is calling the .send() method on the message instance.
Using Mailer Aliases
You can specify which configured mailer to use by passing the using argument to the send() method. This refers to the keys defined in your MAILERS setting.
msg = EmailMessage(...)
# Sends using the 'logging' configuration from settings.MAILERS
msg.send(using="logging")
Batch Sending and Connections
For performance, you can send multiple messages over a single connection by using a backend as a context manager. This prevents the overhead of opening and closing a network connection for every individual email.
from django.core.mail import mailers
messages = [msg1, msg2, msg3]
with mailers["default"] as connection:
connection.send_messages(messages)
The BaseEmailBackend implementation of __enter__ calls open(), and __exit__ ensures close() is called, even if an exception occurs during sending.
Testing Email Logic
When using the locmem backend, you can verify that emails were "sent" by inspecting django.core.mail.outbox.
from django.core import mail
# Assuming MAILERS['default'] uses the locmem backend
msg = mail.EmailMessage("Subject", "Body", "from@example.com", ["to@example.com"])
msg.send()
# Verify the email was sent
assert len(mail.outbox) == 1
assert mail.outbox[0].subject == "Subject"
The Locmem backend performs a copy.deepcopy() of the message before adding it to the outbox, ensuring that the state of the message in the outbox reflects its state at the time of sending.