Metadata-Version: 2.4
Name: deadletterbox
Version: 1.0.0
Summary: A small library for sending simple or template-based emails.
Author-email: Leon Denard <ltdenard@denard.me>
License-Expression: BSD-3-Clause
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Communications :: Email
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Jinja2>=3.0
Requires-Dist: pandas>=1.5
Dynamic: license-file

# 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

```bash
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`               |

```bash
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

```bash
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>`.

```bash
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`](#generic-report-emails-with-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()))"`.

```bash
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:

```bash
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

```python
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.

```python
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.

```python
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:

```python
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.

```python
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:

```python
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`:

```python
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)
```
