deadletterbox
A small library for sending email over SMTP -- plain text, HTML, inline
images, attachments, and Jinja2 templates -- usable as a Python module or
as a deadletterbox CLI command.
Install
pip install -e .
Credentials
Both the CLI and Mailer fall back to the same environment variables, so
you don't have to pass secrets as arguments:
| Env var | CLI flag | Mailer argument |
Default |
|---|---|---|---|
DEADLETTERBOX_USERNAME |
--username |
username |
(none) |
DEADLETTERBOX_PASSWORD |
--password |
password |
(none) |
DEADLETTERBOX_HOST |
--host |
host |
smtp.gmail.com |
DEADLETTERBOX_PORT |
--port |
port |
587 |
export DEADLETTERBOX_USERNAME="me@example.com"
export DEADLETTERBOX_PASSWORD="app-password"
With those set, Mailer() needs no arguments -- pass username=/password=
(etc.) explicitly only when you want to override the environment.
CLI usage
The CLI has two subcommands: send for plain text/HTML mail, and
template for Jinja2-rendered mail.
Simple text email
deadletterbox send \
--subject "Build finished" \
--from-email me@example.com \
--to alice@example.com --to bob@example.com \
--text "The nightly build passed."
HTML email with an attachment and an inline image
--to, --cc, --bcc, --attachment, and --inline-image are all
repeatable. An inline image's Content-ID is the exact path you pass to
--inline-image, so reference it in your HTML as cid:<that same path>.
deadletterbox send \
--subject "Weekly report" \
--from-email me@example.com \
--to alice@example.com \
--cc manager@example.com \
--html '<h1>Weekly Report</h1><img src="cid:./assets/logo.png"><p>See attached CSV.</p>' \
--inline-image ./assets/logo.png \
--attachment ./reports/weekly.csv
Email rendered from a Jinja2 template with dynamic data
Template variables can be inline JSON (--template-vars) or a JSON file
(--template-vars-file); use one of --template-path, --template-string,
or --template-name (a template loaded by name, either from
--template-dir or, if omitted, from deadletterbox's own bundled templates).
This example targets the bundled report_summary.html template (see
ReportBuilder below) directly
from the CLI. The palette block is the "dark" theme's CSS variables --
you can swap in "light", "blue", or "green" by pasting the output of
python -c "import json; from deadletterbox import get_palette; print(json.dumps(get_palette('blue').css_variables()))".
cat > vars.json <<'JSON'
{
"title": "Nightly Scan Results",
"subtitle": "2026-07-12",
"palette": {
"bg": "#050e2f", "surface": "#0a1940", "surface-alt": "#0d2470",
"header-start": "#081a59", "header-end": "#0d2470",
"accent": "#ff8200", "accent-end": "#e67400", "accent-contrast": "#ffffff",
"text": "#c8d6e5", "text-strong": "#ffffff", "text-muted": "#6c7a8d",
"border": "#142a6e", "row-hover": "#0d2470"
},
"sections": [
{
"heading": "Category Totals",
"columns": ["category", "count"],
"rows": [
{"category": "critical-hosts", "count": 5},
{"category": "info-hosts", "count": 12}
]
}
],
"report_url": "https://example.com/reports/nightly.csv",
"report_label": "nightly.csv",
"footer_text": "Automated Report | Security Team",
"signature_html": "Security Team<br>security@example.com"
}
JSON
deadletterbox template \
--subject "Nightly Scan Results" \
--from-email me@example.com \
--to security-team@example.com \
--template-name report_summary.html \
--template-vars-file vars.json \
--attachment ./reports/nightly.csv
If you're calling deadletterbox from Python instead, ReportBuilder builds
this same vars.json shape for you -- see below.
Or with a template given directly as a string:
deadletterbox template \
--subject "Quick note" \
--from-email me@example.com \
--to alice@example.com \
--template-string 'Hi {{ name }}, your quota is {{ "{:,}".format(quota) }}.' \
--template-vars '{"name": "Alice", "quota": 15000}'
Library usage
Simple text or HTML email
from deadletterbox import Mailer
# Picks up DEADLETTERBOX_USERNAME/DEADLETTERBOX_PASSWORD (and DEADLETTERBOX_HOST/
# DEADLETTERBOX_PORT) from the environment -- see Credentials above.
mailer = Mailer()
mailer.send_simple(
subject="Build finished",
from_email="me@example.com",
to=["alice@example.com", "bob@example.com"],
text="The nightly build passed.",
)
Inline images and attachments
Pass plain file paths and deadletterbox wraps them for you, or construct
Attachment/InlineImage yourself for control over the filename or
Content-ID.
from deadletterbox import Mailer, InlineImage
mailer = Mailer()
logo = InlineImage("./logo.png", content_id="logo")
mailer.send_simple(
subject="Weekly report",
from_email="me@example.com",
to=["alice@example.com"],
html='<h1>Weekly Report</h1><img src="cid:logo"><p>See attached CSV.</p>',
inline_images=[logo],
attachments=["./weekly.csv"],
)
HTML body rendered from a template with dynamic data
send_with_template takes template_path= for your own file,
template_string= for an inline template, or template_name= to load a
template by name -- including deadletterbox's bundled report_summary.html.
This example renders that bundled template directly with a raw variables
dict; ReportBuilder (below) builds this same dict for you.
from deadletterbox import Mailer, get_palette
mailer = Mailer()
mailer.send_with_template(
subject="Nightly Scan Results",
from_email="me@example.com",
to=["security-team@example.com"],
template_name="report_summary.html",
template_vars={
"title": "Nightly Scan Results",
"subtitle": "2026-07-12",
"palette": get_palette("dark").css_variables(),
"sections": [
{
"heading": "Category Totals",
"columns": ["category", "count"],
"rows": [
{"category": "critical-hosts", "count": 5},
{"category": "info-hosts", "count": 12},
],
},
],
"footer_text": "Automated Report | Security Team",
},
attachments=["./reports/nightly.csv"],
)
You can also render a template without sending, using TemplateRenderer
directly:
from deadletterbox import TemplateRenderer
renderer = TemplateRenderer()
html = renderer.render_string(
"Hi {{ name }}, your quota is {{ '{:,}'.format(quota) }}.",
name="Alice",
quota=15000,
)
Generic report emails with ReportBuilder
ReportBuilder renders deadletterbox's bundled report_summary.html
template -- a themeable layout with a title, any number of dynamically
generated tables, an optional download link, and a signature. Table data
can be a list of dicts, a dict of dicts, nested data, or a pandas
DataFrame; nothing about the table shape is hardcoded in the template.
from deadletterbox import Mailer, ReportBuilder
builder = ReportBuilder(
title="Nightly Scan Results",
subtitle="2026-07-12",
palette="blue", # "light", "dark", "blue", or "green"
report_url="https://example.com/reports/nightly.csv",
report_label="nightly.csv",
footer_text="Automated Report | Security Team",
)
builder.add_table(
heading="Category Totals",
data=[
{"category": "critical-hosts", "count": 5},
{"category": "info-hosts", "count": 12},
],
)
# Nested data is flattened into columns automatically.
builder.add_table(
heading="Findings by Host",
data=[
{"host": "example.com", "stats": {"critical": 3, "info": 1}},
{"host": "other.com", "stats": {"critical": 0, "info": 2}},
],
)
builder.set_signature(lines=["Security Team", "security@example.com"])
mailer = Mailer()
mailer.send_report(
builder,
subject="Nightly Scan Results",
from_email="me@example.com",
to=["security-team@example.com"],
attachments=["./reports/nightly.csv"],
)
You can also pass a pandas DataFrame directly to add_table, or build
your own Palette instance if the bundled themes don't fit:
import pandas as pd
from deadletterbox import Palette
frame = pd.DataFrame({"domain": ["example.com"], "count": [42]})
builder.add_table(data=frame, heading="From a DataFrame")
custom_palette = Palette("brand", {
"bg": "#101010", "surface": "#1a1a1a", "surface-alt": "#242424",
"header-start": "#202020", "header-end": "#2c2c2c",
"accent": "#00c2a8", "accent-end": "#00a68f", "accent-contrast": "#ffffff",
"text": "#e6e6e6", "text-strong": "#ffffff", "text-muted": "#999999",
"border": "#333333", "row-hover": "#242424",
})
builder2 = ReportBuilder(title="Custom themed report", palette=custom_palette)
Lower-level: building an EmailMessage directly
For full control (e.g. building the message without sending it, or sending
it yourself), construct an EmailMessage and call Mailer.send:
from deadletterbox import Mailer, EmailMessage, Attachment, InlineImage
message = EmailMessage(
subject="Weekly report",
from_email="me@example.com",
to=["alice@example.com"],
html='<p>See attached.</p><img src="cid:chart">',
)
message.add_inline_image(InlineImage("./assets/chart.png", content_id="chart"))
message.add_attachment(Attachment("./reports/weekly.csv", filename="weekly-report.csv"))
mailer = Mailer()
mailer.send(message)
Release files for deadletterbox 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| deadletterbox-1.0.0.tar.gz | 18.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| deadletterbox-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.4 kB
Release files / deadletterbox-1.0.0.tar.gz
| Download URL | deadletterbox-1.0.0.tar.gz |
|---|---|
| Size | 18.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
92094e7b9934c991a8b2d85c31d7b0ef655ee3b99b9d2ac3a1613216fe03c7b2
|
|
BLAKE2b-256 checksum How to use checksums |
8b8b36a806577d2833c2e1d364d0ed6d91c03ca47058b3bea08b12c2596e34e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|
Release files / deadletterbox-1.0.0-py3-none-any.whl
| Download URL | deadletterbox-1.0.0-py3-none-any.whl |
|---|---|
| Size | 19.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bb685ddab4e969e5b7db3238a9c8f3569bd673327ab1387ee1ff097a601be07f
|
|
BLAKE2b-256 checksum How to use checksums |
88965ab253466cd1f08243a831127c956842c537382ceada81e641a36915bca9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|