Backend
Emails
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
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.
order_shipped.subject.txt
order_shipped.txt
order_shipped.html{% load i18n %}{% blocktranslate %}Your {{ site_name }} order is on its way{% endblocktranslate %}{% 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
| Template | Sent by | Context |
|---|---|---|
| verify_email | Signup, resend | name, action_url, action_label |
| password_reset | Forgot password, staff reset | name, action_url, action_label, minutes |
| email_change | Email change (to the new address) | name, new_email, action_url, action_label |
| security_notice | Password, email, 2FA and OAuth changes | name, message |
| notification | notify() on the email channel | title, body, action_url, action_label |
| newsletter_confirm | Newsletter double opt in | action_url, action_label |
| contact_message | Contact form, to the team | name, email, subject, message |
| waitlist_invite | Staff invite from the waitlist | action_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:
TEMPLATES[0]["DIRS"] = [BASE_DIR / "templates"]templates/pleate/emails/base.html # your layout, logo and colors for every email
templates/pleate/emails/verify_email.html # just this oneDelivery
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:
# 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
| Key | Default | Meaning |
|---|---|---|
| PLEATE["EMAILS"]["ASYNC"] | True | Queue 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. |