Jinja2 sandboxed email templates with autoescape and custom filters: working versions (tested 2026-10-15)
A Jinja2 environment with FileSystemLoader, per-extension autoescape, StrictUndefined, custom filters returning Markup, and a sandbox, tested offline against temporary templates.
passed6 passed · 0 failed2026-10-15 14:14 UTC- Status
- passed
- Last passed
- 2026-10-15 14:14 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions3
jinja2@3.1.6markupsafe@3.0.4pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38748988915
The code
Exactly the files that passed. Your agent can pull them with get_recipe("jinja2-markupsafe-email-templates") (install), or see them on GitHub.
requirements.txt
jinja2==3.1.6 markupsafe==3.0.4 pytest==9.1.1
email_templates.py
from pathlib import Path
from jinja2 import FileSystemLoader, StrictUndefined, select_autoescape
from jinja2.sandbox import SandboxedEnvironment
from markupsafe import Markup
def money(cents: int) -> str:
return f"${cents / 100:,.2f}"
def link(url: str, label: str) -> Markup:
# Markup.format escapes its arguments; plain str.format would not.
return Markup('<a href="{}">{}</a>').format(url, label)
def make_env(template_dir: Path) -> SandboxedEnvironment:
env = SandboxedEnvironment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(["html", "xml"]),
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
)
env.filters["money"] = money
env.filters["link"] = link
return env
tests/test_email_templates.py
import pytest
from jinja2 import Environment, TemplateError, TemplateNotFound
from jinja2.exceptions import UndefinedError
from email_templates import make_env
def write(tmp_path, name, body):
(tmp_path / name).write_text(body)
def test_html_autoescapes_user_input(tmp_path):
write(tmp_path, "welcome.html", "<p>Hi {{ name }}</p>")
env = make_env(tmp_path)
out = env.get_template("welcome.html").render(name="<script>x</script>")
assert out == "<p>Hi <script>x</script></p>"
def test_txt_is_not_escaped(tmp_path):
write(tmp_path, "welcome.txt", "Hi {{ name }}")
env = make_env(tmp_path)
assert env.get_template("welcome.txt").render(name="A & B") == "Hi A & B"
def test_filters_return_markup_so_html_is_not_double_escaped(tmp_path):
write(tmp_path, "receipt.html", "<p>Total {{ cents | money }}</p>\n{{ url | link(label) }}")
env = make_env(tmp_path)
out = env.get_template("receipt.html").render(
cents=12345, url="https://x.test/?a=1&b=2", label="<b>Ann</b>"
)
assert "Total $123.45" in out
assert '<a href="https://x.test/?a=1&b=2"><b>Ann</b></a>' in out
def test_strict_undefined_fails_loudly_on_typo(tmp_path):
write(tmp_path, "welcome.html", "Hi {{ nmae }}")
env = make_env(tmp_path)
with pytest.raises(UndefinedError):
env.get_template("welcome.html").render(name="Ann")
def test_sandbox_blocks_dunder_access(tmp_path):
write(tmp_path, "evil.txt", "{{ ''.__class__.__mro__ }}")
env = make_env(tmp_path)
# The sandbox returns an Undefined for unsafe attributes; StrictUndefined turns that into an error.
with pytest.raises(TemplateError):
env.get_template("evil.txt").render()
# A plain Environment happily walks the object graph.
assert "str" in Environment().from_string("{{ ''.__class__.__mro__ }}").render()
def test_loader_rejects_path_traversal(tmp_path):
env = make_env(tmp_path)
with pytest.raises(TemplateNotFound):
env.get_template("../secrets.txt")
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.