Skip to main content

plain.email

Send emails from your Plain application using SMTP, console output, or the development preview backend.

Overview

You can send emails using the send_mail function for simple cases, or use the EmailMessage and EmailMultiAlternatives classes for more control. For template-based emails, the TemplateEmail class renders HTML templates automatically.

Sending a simple email

The send_mail function is the easiest way to send an email.

from plain.email import send_mail

send_mail(
    subject="Welcome!",
    message="Thanks for signing up.",
    from_email="hello@example.com",
    recipient_list=["user@example.com"],
)

To include an HTML version along with the plain text:

send_mail(
    subject="Welcome!",
    message="Thanks for signing up.",
    from_email="hello@example.com",
    recipient_list=["user@example.com"],
    html_message="<h1>Thanks for signing up!</h1>",
)

Sending HTML emails

For more control over multipart emails, use EmailMultiAlternatives.

from plain.email import EmailMultiAlternatives

email = EmailMultiAlternatives(
    subject="Your order confirmation",
    body="Your order #123 has been confirmed.",
    from_email="orders@example.com",
    to=["customer@example.com"],
)
email.attach_alternative("<h1>Order #123 Confirmed</h1>", "text/html")
email.send()

Template-based emails

The TemplateEmail class renders emails from template files. You provide a template name, and it looks for corresponding files in your templates/email/ directory:

  • email/{template}.html - HTML content (required)
  • email/{template}.txt - Plain text content (optional, falls back to stripping HTML tags)
  • email/{template}.subject.txt - Subject line (optional)
from plain.email import TemplateEmail

email = TemplateEmail(
    template="welcome",
    context={"user_name": "Alice"},
    to=["alice@example.com"],
)
email.send()

With these template files:

<!-- templates/email/welcome.html -->
<h1>Welcome, {{ user_name }}!</h1>
<p>We're glad you're here.</p>
{# templates/email/welcome.subject.txt #}
Welcome to our app, {{ user_name }}!

You can subclass TemplateEmail to customize the template context by overriding get_template_context().

Attachments

You can attach files to any email message.

from plain.email import EmailMessage

email = EmailMessage(
    subject="Your report",
    body="Please find your report attached.",
    to=["user@example.com"],
)

# Attach content directly
email.attach("report.csv", csv_content, "text/csv")

# Or attach a file from disk
email.attach_file("/path/to/report.pdf")

email.send()

Settings

Setting Default Env var
EMAIL_BACKEND Required PLAIN_EMAIL_BACKEND
EMAIL_DEFAULT_FROM Required PLAIN_EMAIL_DEFAULT_FROM
EMAIL_DEFAULT_REPLY_TO None PLAIN_EMAIL_DEFAULT_REPLY_TO
EMAIL_HOST "localhost" PLAIN_EMAIL_HOST
EMAIL_PORT 587 PLAIN_EMAIL_PORT
EMAIL_HOST_USER "" PLAIN_EMAIL_HOST_USER
EMAIL_HOST_PASSWORD "" PLAIN_EMAIL_HOST_PASSWORD
EMAIL_USE_TLS True PLAIN_EMAIL_USE_TLS
EMAIL_USE_SSL False PLAIN_EMAIL_USE_SSL
EMAIL_TIMEOUT None PLAIN_EMAIL_TIMEOUT
EMAIL_SSL_CERTFILE None PLAIN_EMAIL_SSL_CERTFILE
EMAIL_SSL_KEYFILE None PLAIN_EMAIL_SSL_KEYFILE
EMAIL_USE_LOCALTIME False PLAIN_EMAIL_USE_LOCALTIME

See default_settings.py for more details.

Email backends

The EMAIL_BACKEND setting controls how emails are sent. Plain includes four backends.

SMTP backend

The default backend sends emails via SMTP.

EMAIL_BACKEND = "plain.email.backends.smtp.EmailBackend"

Console backend

Prints emails to the console instead of sending them. Useful during development.

EMAIL_BACKEND = "plain.email.backends.console.EmailBackend"

Preview backend

Captures each sent message as a .eml file in .plain/emails/ for inspection during development. Nothing is delivered to an SMTP server.

EMAIL_BACKEND = "plain.email.backends.preview.EmailBackend"

Or via env var: PLAIN_EMAIL_BACKEND=plain.email.backends.preview.EmailBackend.

When plain.toolbar is installed, the toolbar gains an Email panel that lists recent captured messages and renders their HTML bodies inline. You can also open any .eml file directly in Mail.app.

In-memory backend

Captures sent messages in a list instead of delivering them — intended for tests. See Testing for the outbox built on it.

EMAIL_BACKEND = "plain.email.backends.locmem.EmailBackend"

Testing

plain.email ships a test lifecycle that routes EMAIL_BACKEND to the in-memory backend for the duration of a test run. Import outbox to read the captured messages:

from plain.email import send_mail
from plain.email.testing import outbox


def test_sends_email():
    send_mail("Subject", "Body", "from@example.com", ["person@example.com"])

    assert len(outbox) == 1
    assert outbox[0].to == ["person@example.com"]

The outbox is emptied before each test, so a test sees only the mail it sent, and the original EMAIL_BACKEND is put back when the run ends. There's nothing to configure: the package registers this with plain.testing, the test runner.

Each item in the outbox is the message that was sent, so you can assert on its subject, body, to and the rest.

FAQs

How do I send to multiple recipients efficiently?

Use send_mass_mail to send multiple messages over a single connection:

from plain.email import send_mass_mail

messages = (
    ("Subject 1", "Body 1", "from@example.com", ["to1@example.com"]),
    ("Subject 2", "Body 2", "from@example.com", ["to2@example.com"]),
)
send_mass_mail(messages)

How do I reuse a connection for multiple emails?

Use the backend as a context manager:

from plain.email import get_connection, EmailMessage

with get_connection() as connection:
    for user in users:
        email = EmailMessage(
            subject="Hello",
            body="Hi there!",
            to=[user.email],
            connection=connection,
        )
        email.send()

How do I add custom headers?

Pass a headers dict to any email class:

email = EmailMessage(
    subject="Hello",
    body="Content",
    to=["user@example.com"],
    headers={"X-Custom-Header": "value", "Reply-To": "reply@example.com"},
)

How do I create a custom email backend?

Subclass BaseEmailBackend and implement the send_messages method:

from plain.email.backends.base import BaseEmailBackend


class MyBackend(BaseEmailBackend):
    def send_messages(self, email_messages):
        # Your sending logic here
        return len(email_messages)

Installation

Install the plain.email package from PyPI:

uv add plain.email

Add plain.email to your INSTALLED_PACKAGES and configure the required settings:

# settings.py
INSTALLED_PACKAGES = [
    # ...
    "plain.email",
]

EMAIL_BACKEND = "plain.email.backends.smtp.EmailBackend"
EMAIL_DEFAULT_FROM = "noreply@example.com"

# For SMTP (adjust for your mail provider)
EMAIL_HOST = "smtp.example.com"
EMAIL_PORT = 587
EMAIL_HOST_USER = "your-username"
EMAIL_HOST_PASSWORD = "your-password"
EMAIL_USE_TLS = True

For local development, use the console backend to see emails in your terminal:

EMAIL_BACKEND = "plain.email.backends.console.EmailBackend"

Metadata

Release files for plain.email 0.23.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for plain.email 0.23.0
File Size Uploaded
plain_email-0.23.0.tar.gz 24.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for plain.email 0.23.0
File Interpreter ABI Platform
plain_email-0.23.0-py3-none-any.whl Python 3 none any Details

Total release size: 53.8 kB

Release files / plain_email-0.23.0.tar.gz

Download URL plain_email-0.23.0.tar.gz
Size 24.4 kB
Tags Source
SHA-256 checksum
How to use checksums
26814c2351c9aeff3275fa1364054155d437ea90579d02bfe58651923e625c1c
BLAKE2b-256 checksum
How to use checksums
d0c9de0f6a856de6f40aa7fa710d4cb396e9d2933bed55a53e282f0c022764af
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / plain_email-0.23.0-py3-none-any.whl

Download URL plain_email-0.23.0-py3-none-any.whl
Size 29.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a464fd8468584f48a886fbac1e31fa8672bd1755e70edb2d88f4306bd82de7ee
BLAKE2b-256 checksum
How to use checksums
5e7268e261cf29753039f231a2088c61d02e6e34a9f01c8a9363583dd3965075
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page