Skip to main content

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_ssl and use_tls are mutually exclusive. If both are set to True, the backend raises an InvalidMailer error.
  • Connection Management: It uses smtplib.SMTP (or SMTP_SSL) and supports automatic login if credentials are provided.

Development and Testing Backends​

For non-production environments, several specialized backends are available:

  1. Console Backend (django.core.mail.backends.console.EmailBackend): Writes email content to a stream, defaulting to sys.stdout.
  2. 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.
  3. Locmem Backend (django.core.mail.backends.locmem.EmailBackend): Used primarily for testing. Instead of sending emails, it appends EmailMessage objects to a list at django.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.