Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

deadletterbox-1.0.0.tar.gz (18.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

deadletterbox-1.0.0-py3-none-any.whl (19.0 kB view details)

Uploaded Python 3

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

Hashes for deadletterbox-1.0.0.tar.gz
Algorithm Hash digest
SHA256 92094e7b9934c991a8b2d85c31d7b0ef655ee3b99b9d2ac3a1613216fe03c7b2
MD5 2c81931bcee80b7ac797bd657cdc2737
BLAKE2b-256 8b8b36a806577d2833c2e1d364d0ed6d91c03ca47058b3bea08b12c2596e34e5

See more details on using hashes here.

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

Hashes for deadletterbox-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bb685ddab4e969e5b7db3238a9c8f3569bd673327ab1387ee1ff097a601be07f
MD5 621216914a2093d7144339daaf64b37f
BLAKE2b-256 88965ab253466cd1f08243a831127c956842c537382ceada81e641a36915bca9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page