Skip to main content

Convert .eml email exports to Markdown with YAML front matter

Project description

dead-letter

dead-letter

PyPI License: PolyForm Noncommercial

Your .eml files deserve a second life.

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

✨ 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 validationdead-letter doctor checks your runtime environment
  • Conversion report — opt-in JSON report with per-file diagnostics 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, /summarize, /triage, /cabinet)
  • Python APIfrom 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.

📦 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

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

🚀 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/")
print(bundle.markdown)     # cabinet/message/message.md
print(bundle.attachments)  # retained extracted files under cabinet/message/attachments/

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

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.

Install and launch:

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

From a source checkout:

uv run --extra mcp dead-letter-mcp

Claude Desktop — add to claude_desktop_config.json:

{
  "mcpServers": {
    "dead-letter": {
      "command": "uv",
      "args": ["--directory", "/path/to/dead-letter", "run", "--extra", "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. Source under plugin/.

Claude Code (manual MCP add — alternative):

claude mcp add dead-letter -- uv run --extra mcp dead-letter-mcp

Codex:

codex mcp add dead-letter -- uv run --extra 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.

🗂 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
└── 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 every push and PR with the same commands (plus claude plugin validate plugin/).

📚 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.

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

dead_letter-0.2.1.tar.gz (147.7 kB view details)

Uploaded Source

Built Distribution

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

dead_letter-0.2.1-py3-none-any.whl (158.9 kB view details)

Uploaded Python 3

File details

Details for the file dead_letter-0.2.1.tar.gz.

File metadata

  • Download URL: dead_letter-0.2.1.tar.gz
  • Upload date:
  • Size: 147.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for dead_letter-0.2.1.tar.gz
Algorithm Hash digest
SHA256 9026167a1c6d452d03105716658eda6b88a944625802bee75c200f8703d4452c
MD5 7f7b70e9f18828cf7c16125b07caf063
BLAKE2b-256 ba972ff8474357540295a5a2b540746380b80095741abb9ac27ca2548cc6211c

See more details on using hashes here.

Provenance

The following attestation bundles were made for dead_letter-0.2.1.tar.gz:

Publisher: release.yml on BigCactusLabs/dead-letter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dead_letter-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: dead_letter-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 158.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for dead_letter-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e4fa0ad86b8be41a816794ec0831f60cfa1e2686c06ebfad97b4f650ffab2a60
MD5 81de9c782bbed76262dbcf4b2ea54f9d
BLAKE2b-256 18d1206d6b2dd4ebabf67ac8409e53cc8fa3ad1fa2119bd11d1a11591042ff8b

See more details on using hashes here.

Provenance

The following attestation bundles were made for dead_letter-0.2.1-py3-none-any.whl:

Publisher: release.yml on BigCactusLabs/dead-letter

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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