Guides
Templates
Templates keep email copy out of your code. Each one is a folder with a subject and a body, rendered by Jinja2 in a sandbox, with HTML escaping on and missing data treated as an error.
Folder layout
templates/
_layouts/
base.html shared layout
welcome/
subject.txt required
body.html html body, autoescaped
body.txt optional; generated from body.html when missing
password-reset/
subject.txt
body.htmlPoint MailingKit at the folder with templates= or MAILINGKIT_TEMPLATES_DIR:
mail = MailingKit.from_env(templates="templates")
await mail.send_template("welcome", {"name": "Ada"}, to="ada@example.com")Writing a template
Welcome to {{ product }}, {{ name }}{% extends "_layouts/base.html" %}
{% block content %}
<h1>Welcome, {{ name }}</h1>
<p>Your {{ product }} workspace is ready.</p>
<p><a href="{{ login_url }}">Open your workspace</a></p>
{% endblock %}
Any Jinja2 feature works: extends, include, loops, conditions and
filters. Values are escaped in .html files, so a user called
<script> is shown as text. Plain text files are not escaped.
Subjects have their whitespace collapsed to single spaces, so data can never put a line break
into the Subject header.
Typed template data
Bind a template to a dataclass or a Pydantic model to check data before anything renders:
from dataclasses import dataclass
from mailingkit import Template
@dataclass
class CandidateSelected:
candidate_name: str
company_name: str
next_round: str
interview_url: str
CANDIDATE_SELECTED = Template("candidate-selected", CandidateSelected)
await mail.send_template(
CANDIDATE_SELECTED,
CandidateSelected(
candidate_name="Grace",
company_name="Acme",
next_round="technical interview",
interview_url="https://jobs.example.com/interview/123",
),
to="grace@example.com",
)You can also pass a plain dict. It is checked against the model first, and every problem is reported at once:
TemplateDataError: Template 'candidate-selected' data is invalid:
missing required fields: interview_url, next_round
Dataclasses catch missing and unknown fields. Pydantic models also validate types, so
{"number": "abc"} for an int field fails clearly. Pydantic is never
required: MailingKit uses it only when your model is a Pydantic model.
Untyped templates are still strict
Without a model, a template that uses a variable you did not pass raises
TemplateDataError instead of rendering an empty string. Typed templates just
catch the problem earlier and list every missing field.
Previewing
render() returns the subject and bodies without sending, which is useful for previews, admin screens and tests:
rendered = mail.render(CANDIDATE_SELECTED, data)
print(rendered.subject)
print(rendered.text)Custom Jinja2 environments
For custom filters or a different loader, build a TemplateRenderer yourself:
from mailingkit import MailingKit, TemplateRenderer
renderer = TemplateRenderer("templates", globals={"support_email": "help@example.com"})
renderer.environment.filters["money"] = lambda cents: f"${cents / 100:,.2f}"
mail = MailingKit.from_env(templates=renderer)
Passing your own environment= is also supported. In that case autoescaping and
strict undefined handling are your responsibility.
Errors
| Error | When |
|---|---|
TemplateNotFoundError | No subject.txt, or no body file. |
TemplateDataError | Data is missing, unknown, of the wrong type, or a variable is undefined. |
TemplateRenderError | A syntax error, or the sandbox blocked an unsafe operation. |