Backend

Emails

Transactional emails from templates, localized, queued on Celery and delivered by SMTP or any provider through django-anymail.

pleate_backend.emails renders transactional emails from Django templates, in the recipient's language, and sends them through Celery. Every other app uses it: verification links, password resets, security notices, notifications, newsletter confirmations, contact messages, waitlist invites.

Sending

python
from pleate_backend.emails import send_email

send_email(
    "order_shipped",                         # template name
    to="ada@plate.dev",                      # or a list
    context={"name": "Ada", "action_url": "https://plate.app/orders/1042", "action_label": "Track order"},
    language="it",                           # default: LANGUAGE_CODE
    reply_to=["support@plate.app"],
)

With PLEATE["EMAILS"]["ASYNC"] on (the default) the email is queued as the pleate_backend.emails.send task, retried up to five times with backoff on network errors. If the queue cannot be reached it is sent inline, so a broker outage never loses a password reset. Context values must be JSON serializable. render_email(template, context, language) returns (subject, text, html) for previews and tests.

Templates

A template is three files in pleate/emails/: the subject, the plain text body and, optionally, the HTML body. Every template gets site_name and frontend_url. HTML ones extend pleate/emails/base.html, fill {% block content %}, and get a button when the context has action_url and action_label; {% block footer %} replaces the line at the bottom.

templates/pleate/emails/
order_shipped.subject.txt
order_shipped.txt
order_shipped.html
order_shipped.subject.txt
{% load i18n %}{% blocktranslate %}Your {{ site_name }} order is on its way{% endblocktranslate %}
order_shipped.html
{% extends "pleate/emails/base.html" %}
{% load i18n %}
{% block content %}
  <p>{% blocktranslate %}Hi {{ name }},{% endblocktranslate %}</p>
  <p>{% translate "Your order left our kitchen this morning." %}</p>
{% endblock %}

Built in templates

TemplateSent byContext
verify_emailSignup, resendname, action_url, action_label
password_resetForgot password, staff resetname, action_url, action_label, minutes
email_changeEmail change (to the new address)name, new_email, action_url, action_label
security_noticePassword, email, 2FA and OAuth changesname, message
notificationnotify() on the email channeltitle, body, action_url, action_label
newsletter_confirmNewsletter double opt inaction_url, action_label
contact_messageContact form, to the teamname, email, subject, message
waitlist_inviteStaff invite from the waitlistaction_url, action_label

Overriding

Templates are looked up through Django's loaders, so a file with the same path in your project wins. Put your project templates first:

settings.py
TEMPLATES[0]["DIRS"] = [BASE_DIR / "templates"]
text
templates/pleate/emails/base.html            # your layout, logo and colors for every email
templates/pleate/emails/verify_email.html    # just this one

Delivery

Delivery uses Django 6.1 mailers. SMTP is the default; Mailpit catches everything in development. For an email API install the anymail extra and swap the backend:

settings.py
# SMTP (Mailpit in development)
MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {"host": "localhost", "port": 1025},
    }
}

# Resend, Postmark, SES, Mailgun... through django-anymail
MAILERS = {"default": {"BACKEND": "anymail.backends.resend.EmailBackend"}}
ANYMAIL = {"RESEND_API_KEY": env("RESEND_API_KEY")}

DEFAULT_FROM_EMAIL = "Plate <hello@plate.app>"

Settings

KeyDefaultMeaning
PLEATE["EMAILS"]["ASYNC"]TrueQueue on Celery. False sends inline (tests, no worker).
DEFAULT_FROM_EMAIL"no-reply@localhost"Sender of every email.
PLEATE["SITE_NAME"]"pleate"Name used in subjects and the layout.
PLEATE["FRONTEND_URL"]"http://localhost:3000"Base of every link.