Skip to main content

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)

Source distribution for deadletterbox 1.0.0
File Size Uploaded
deadletterbox-1.0.0.tar.gz 18.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for deadletterbox 1.0.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page