ProvenPin Install

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 &lt;script&gt;x&lt;/script&gt;</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&amp;b=2">&lt;b&gt;Ann&lt;/b&gt;</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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.