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)
Metadata
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 | Provenance | Actions |
|---|---|---|---|---|
| deadletterbox-1.0.0.tar.gz | 18.4 kB | Without provenance | Details for deadletterbox-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| deadletterbox-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for deadletterbox-1.0.0-py3-none-any.whl |
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
|
Security for deadletterbox 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.