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.