A small library for sending simple or template-based emails.
Project description
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)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deadletterbox-1.0.0.tar.gz.
File metadata
- Download URL: deadletterbox-1.0.0.tar.gz
- Upload date:
- Size: 18.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
92094e7b9934c991a8b2d85c31d7b0ef655ee3b99b9d2ac3a1613216fe03c7b2
|
|
| MD5 |
2c81931bcee80b7ac797bd657cdc2737
|
|
| BLAKE2b-256 |
8b8b36a806577d2833c2e1d364d0ed6d91c03ca47058b3bea08b12c2596e34e5
|
File details
Details for the file deadletterbox-1.0.0-py3-none-any.whl.
File metadata
- Download URL: deadletterbox-1.0.0-py3-none-any.whl
- Upload date:
- Size: 19.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bb685ddab4e969e5b7db3238a9c8f3569bd673327ab1387ee1ff097a601be07f
|
|
| MD5 |
621216914a2093d7144339daaf64b37f
|
|
| BLAKE2b-256 |
88965ab253466cd1f08243a831127c956842c537382ceada81e641a36915bca9
|