Skip to main content

dead-letter

dead-letter

PyPI package Python versions License: PolyForm Noncommercial

Turn .eml email exports into clean, local, LLM-ready Markdown.

dead-letter converts email exports and archives into clean Markdown with YAML front matter — threads split, signatures stripped, attachments extracted, calendars parsed. One file or ten thousand.

Use it to build a readable email archive, move messages into Markdown-based knowledge systems, or prepare email for RAG and LLM pipelines without feeding raw MIME and base64 into your context window. No account, upload, or API key required.

⚡ Try it

If you already have uv, run dead-letter without installing it globally:

uvx --python 3.12 dead-letter convert message.eml

Or install with Homebrew or pip below. Agents and MCP clients can use the shorter deterministic setup in llms-install.md.

🎯 Common use cases

  • Email → Markdown archives — turn exported .eml collections into readable, portable Markdown with structured metadata
  • RAG and LLM ingestion — normalize message text, thread structure, links, and attachment metadata before chunking or indexing
  • Agent workflows — expose conversion and diagnostics directly to Claude, Codex, and other MCP clients
  • Knowledge bases — move email into Markdown-first systems such as Obsidian, static archives, or local search pipelines
  • Digital preservation — retain human-readable content and useful message structure without depending on one mail client

✨ Features

  • Full-fidelity conversion — HTML sanitization, Gmail/Outlook thread segmentation, inline image handling, and calendar event summaries
  • CLI — point it at a file or a directory and go
  • Local web UI — dark command-center interface with drag-and-drop import, watch mode, conversion grade badges, processing history, and per-job diagnostics
  • Inbox/Cabinet workflow — drop .eml files into an Inbox, let dead-letter organize the Markdown bundles into a Cabinet
  • Install validation — dead-letter doctor checks your runtime environment
  • Conversion report — opt-in JSON report with per-file diagnostics, including attachment referenced/retained counts for automation and audit
  • MCP server — integrate with Claude Desktop, Claude Code, Codex, and other MCP clients
  • Claude plugin — one-command install in Claude Code or Cowork with four slash commands (/dead-letter:convert, /dead-letter:summarize, /dead-letter:triage, /dead-letter:cabinet)
  • Portable Agent Skill — teaches Claude Code, Codex, Copilot, Cursor, and other skill-aware agents when and how to convert .eml files
  • Python API — from dead_letter import convert and you're off

🧠 Built for LLM Pipelines

Raw .eml files are noisy input for downstream LLM and retrieval pipelines — MIME headers, multipart boundaries, duplicated HTML/plain bodies, and encoded attachments all get mixed into the text path.

dead-letter normalizes that into Markdown with YAML front matter, so message text and metadata are ready for chunking or indexing without MIME parsing or base64 cleanup. Default convert() and convert_dir() runs write a single .md per message and keep attachment names in front matter.

If you want the filesystem artifacts separated too, bundle and Cabinet workflows write message.md plus retained decoded files under attachments/. The Markdown is ready for text ingestion, while PDFs, spreadsheets, calendar files, and other retained binary attachments stay cleanly split out for whatever downstream parser you already use.

For direct LLM integration, the MCP server lets Claude Desktop, Claude Code, Codex, and other MCP clients call dead-letter's conversion tools without shelling out.

📊 Token-cost benchmarks

dead-letter's value isn't fewer tokens than every alternative — it's fidelity per token: the cheapest representation that keeps the email intact. Measured across a synthetic corpus of HTML threads, attachments, and newsletters (tokenizer o200k_base, medians):

  • ~88% fewer tokens than the raw .eml — a single email with a PDF attachment is ~126k tokens raw vs ~180 converted.
  • The only representation that keeps the email whole — thread structure, per-message sender attribution, links, and attachment metadata all survive. Naive text extraction is cheaper precisely because it drops them (0/2 attachments retained vs dead-letter's 2/2).

The benchmark is honest about where it loses: naive extraction is fewer tokens when you don't mind throwing away attachments, links, and thread structure. Full method, the complete table (including those rows), tokenizer disclosure, and a one-command reproduce are in benchmarks/.

📦 Install

With Homebrew on Apple silicon macOS:

brew tap BigCactusLabs/tap
brew install dead-letter

The Homebrew formula installs the core CLI only: dead-letter convert and dead-letter doctor. It intentionally does not bundle the optional web UI or MCP server dependency stacks.

With pip:

pip install dead-letter            # core + CLI
pip install dead-letter[cli]       # + watchfiles (used by backend/UI watch mode)
pip install dead-letter[ui]        # + web UI, API server, and watch mode
pip install dead-letter[mcp]       # + MCP server

Use pipx for isolated UI or MCP installs:

pipx install 'dead-letter[ui]'    # installs dead-letter and dead-letter-ui
pipx install 'dead-letter[mcp]'   # installs dead-letter and dead-letter-mcp

Or run individual entrypoints without a persistent install using uvx:

uvx --python 3.12 dead-letter convert message.eml
uvx --python 3.12 --from 'dead-letter[mcp]' dead-letter-mcp

From source:

git clone https://github.com/BigCactusLabs/dead-letter.git
cd dead-letter
uv sync --extra dev     # all extras
uv sync --extra ui      # UI only
uv sync --extra mcp     # MCP only

Agent Skill (any host)

Install the portable Agent Skill into whichever agent you use:

gh skill install BigCactusLabs/dead-letter dead-letter --agent claude-code
gh skill install BigCactusLabs/dead-letter dead-letter --agent codex
gh skill install BigCactusLabs/dead-letter dead-letter --agent github-copilot

Needs gh 2.90 or newer. The skill is independent of the Claude plugin. For other hosts, manual installation, and discovery metadata, see Agent Discovery.

🚀 Quick Start

CLI — convert a single file:

dead-letter convert message.eml

Convert a whole directory:

dead-letter convert inbox/ --output out/

Generate a JSON conversion report alongside the output:

dead-letter convert inbox/ --output out/ --report

With --output, the report is written to that output directory as .dead-letter-report.json. Without --output, file conversions write the report next to the source message and directory conversions write it to the input directory root.

Check your runtime environment:

dead-letter doctor

Directory conversion scans recursively for .eml files, matches the suffix case-insensitively, skips symlinked files whose resolved targets escape the requested input tree, and deduplicates in-tree symlink aliases that resolve to the same message file.

Web UI — start the local server:

dead-letter-ui --host 127.0.0.1 --port 8765

Open http://127.0.0.1:8765 — on first launch, a setup prompt suggests default Inbox and Cabinet folders. Configure or skip to start converting. Import .eml files with drag and drop or the file picker. Single-file imports use file mode, while multi-file drops create one directory-mode batch job. Mixed drops ask for confirmation before skipping non-.eml files. The backend enforces a 100 MB per-file import limit for both single and batch uploads.

From a source checkout, prefix with uv run:

uv run dead-letter convert message.eml
uv run --extra ui dead-letter-ui --host 127.0.0.1 --port 8765

🐍 Python API

from dead_letter import convert

result = convert("message.eml")
print(result.subject, result.sender)
print(result.output)  # path to the generated .md

With options:

from dead_letter import convert, ConvertOptions

result = convert("message.eml", options=ConvertOptions(
    strip_signatures=True,
    strip_quoted_headers=True,
))

Strip signature images (logos, social icons) and tracking pixels:

result = convert("message.eml", options=ConvertOptions(
    strip_signature_images=True,
    strip_tracking_pixels=True,
))

When enabled, these filters remove matched images from rendered Markdown and omit stripped inline signature/tracking assets from bundle attachment output.

Bundle conversion (Markdown + attachments + source in one directory):

from dead_letter import convert_to_bundle

bundle = convert_to_bundle("message.eml", bundle_root="cabinet/", source_handling="copy")
print(bundle.markdown)     # cabinet/message/message.md
print(bundle.attachments)  # retained extracted files under cabinet/message/attachments/

source_handling="copy" preserves the original .eml in place. If omitted, convert_to_bundle() defaults to source_handling="move" and moves the source message into the bundle.

Retained extracted attachment filenames are normalized to safe basenames before they are written under attachments/.

Quality diagnostics include referenced/retained attachment counts when a message has attachments eligible for retention, so dropped artifacts are machine-detectable. See Quality Diagnostics.

Batch:

from dead_letter import convert_dir

for r in convert_dir("inbox/", output="out/"):
    print(f"{'✓' if r.success else '✗'} {r.source.name}")

🔌 MCP Server

dead-letter ships an MCP server so LLM clients can convert .eml files directly without shelling out.

Launch it directly with uvx:

uvx --python 3.12 --from 'dead-letter[mcp]' dead-letter-mcp

Or install the MCP extra first:

pip install 'dead-letter[mcp]'
dead-letter-mcp

From a source checkout:

uv run --extra mcp dead-letter-mcp

Claude Desktop (recommended: one-click bundle):

Download the .mcpb file from the latest release, then install it one of three ways:

  • double-click the downloaded file
  • drag it onto the Claude Desktop window
  • Settings > Extensions > Advanced settings > Install Extension..., then pick the file

Claude Desktop lists the extension and the four tools below appear in chat. Before installing, verify the download against the .sha256 sidecar published alongside it:

# macOS
shasum -a 256 -c dead-letter-mcp-X.Y.Z.mcpb.sha256

# Windows (PowerShell)
certutil -hashfile dead-letter-mcp-X.Y.Z.mcpb SHA256

The bundle installs nothing else on your machine beyond what Claude Desktop already manages; it needs network access on first launch to fetch the package from PyPI. Claude Desktop is macOS and Windows only — on Linux, use the manual config below or uvx directly.

Claude Desktop (manual claude_desktop_config.json — alternative) — launch the published package directly with uvx:

{
  "mcpServers": {
    "dead-letter": {
      "command": "uvx",
      "args": ["--python", "3.12", "--from", "dead-letter[mcp]", "dead-letter-mcp"]
    }
  }
}

Claude Code or Cowork (recommended — Claude plugin):

/plugin marketplace add BigCactusLabs/bigcactuslabs-plugins
/plugin install dead-letter

The plugin bundles the MCP server (via uvx, no pip install needed — just uv on PATH) and adds four slash commands: /dead-letter:convert, /dead-letter:summarize, /dead-letter:triage, /dead-letter:cabinet. Email content handled through the plugin is treated as untrusted data, not instructions, so tool-use, credential, and exfiltration requests embedded in messages are not followed. Source under plugin/.

The marketplace pins each published plugin tag and commit. Release automation updates that pointer only after the bundled MCP server's exact PyPI version is live, so Claude Code and Cowork resolve the same reproducible release.

Claude Code (manual MCP add — alternative):

claude mcp add dead-letter -- uvx --python 3.12 --from 'dead-letter[mcp]' dead-letter-mcp

Codex:

codex mcp add dead-letter -- uvx --python 3.12 --from 'dead-letter[mcp]' dead-letter-mcp
codex mcp list

The codex mcp add command registers the local dead-letter MCP server, and codex mcp list verifies that it's available.

Tools

Tool Required arguments Returns
convert_eml eml_path Markdown text. Also writes a file when output_path is given.
convert_eml_to_bundle eml_path, bundle_root JSON with bundle_path, markdown_path, attachment_paths. Copy-only: the original .eml is never moved or deleted.
convert_directory directory, output_directory JSON summary. Capped at 50 .eml files per call.
get_diagnostics eml_path Quality and structure JSON. Writes nothing permanent.

All four take a preset (default, clean, verbose, raw) and per-flag overrides. Full contract, including the MCP-only constraints and the error-text table: docs/reference/v4-runtime-contracts.md.

🗂 Project Structure

src/dead_letter/
├── core/           # conversion pipeline (MIME, HTML, threads, rendering)
├── backend/        # CLI, API server, job runner, watch mode, MCP server
└── frontend/       # static web UI (Alpine.js ES modules + vanilla fetch)
tests/
├── core/           # conversion pipeline tests with .eml fixtures
├── backend/        # API, job, and watch tests
├── plugin/         # Claude plugin manifest, skill, and command tests
└── frontend/       # JS unit tests

🧪 Testing

uv run pytest -q tests/core        # conversion pipeline
uv run pytest -q tests/backend     # API and job runner
uv run pytest -q tests/plugin      # Claude plugin manifest, skill, and command surfaces
node --test tests/frontend/*.test.js     # frontend

CI runs all four on PRs and on pushes to main or feat/** branches with the same commands, plus npx --yes @anthropic-ai/claude-code@2.1.145 plugin validate plugin/ and node --check src/dead_letter/frontend/static/app.js.

📚 Docs

🔧 Tools We Love

  • MarkEdit — TextEdit for Markdown, native macOS, ~4 MB. Opens dead-letter output like it was always meant to live there.
  • mo — local Markdown viewer that renders files in the browser with live reload. Point it at your Cabinet and read converted mail like a feed.

⚠️ Known Limitations

  • Local-only — no remote server, no auth
  • In-memory job registry (state resets on restart)
  • Single-user, single-machine

License

PolyForm Noncommercial 1.0.0 — free for personal, educational, and nonprofit use. Commercial use requires a separate license from Big Cactus Labs.

Metadata

Release files for dead-letter 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dead-letter 0.3.1
File Size Uploaded
dead_letter-0.3.1.tar.gz 158.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dead-letter 0.3.1
File Interpreter ABI Platform
dead_letter-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 324.5 kB

Release files / dead_letter-0.3.1.tar.gz

Download URL dead_letter-0.3.1.tar.gz
Size 158.4 kB
Tags Source
SHA-256 checksum
How to use checksums
b901643b7f39761b35af8b585e276aa89b7e34693e8c314d0dadb19bd7ec96b5
BLAKE2b-256 checksum
How to use checksums
e662eb91520390ed72980c2eeb6114a38b86b71104bf0e71a6f5451c59e6eebe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release files / dead_letter-0.3.1-py3-none-any.whl

Download URL dead_letter-0.3.1-py3-none-any.whl
Size 166.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
79ec9d39b63d1ed08134fdad07fcd9cf723fa46f5bb3b42edc2c5f4a468512f3
BLAKE2b-256 checksum
How to use checksums
9568162ec3a2ad1ae74092a1506647a44cf45b2bf798ceb52559fb0ab5126242
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 19, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.8

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.0

2 release files

This release

0.3.1 This release

2 release files

0.3.0

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

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