{
  "markdown": "<!-- mcp-name: io.github.BigCactusLabs/dead-letter -->\n\n<p align=\"center\">\n  <img src=\"docs/brand/production/readme-logo.png\" width=\"128\" alt=\"dead-letter\">\n</p>\n\n# dead-letter\n\n[![PyPI package](https://img.shields.io/pypi/v/dead-letter?label=PyPI%20package&cacheSeconds=300)](https://pypi.org/project/dead-letter/)\n[![Python versions](https://img.shields.io/pypi/pyversions/dead-letter?label=Python&cacheSeconds=300)](https://pypi.org/project/dead-letter/)\n[![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm%20Noncommercial-purple.svg)](LICENSE)\n\n**Your `.eml` files deserve a second life.**\n\ndead-letter converts email exports into clean Markdown with YAML front matter — threads split, signatures stripped, attachments extracted, calendars parsed. One file or ten thousand.\n\n## ✨ Features\n\n- **Full-fidelity conversion** — HTML sanitization, Gmail/Outlook thread segmentation, inline image handling, and calendar event summaries\n- **CLI** — point it at a file or a directory and go\n- **Local web UI** — dark command-center interface with drag-and-drop import, watch mode, conversion grade badges, processing history, and per-job diagnostics\n- **Inbox/Cabinet workflow** — drop `.eml` files into an Inbox, let dead-letter organize the Markdown bundles into a Cabinet\n- **Install validation** — `dead-letter doctor` checks your runtime environment\n- **Conversion report** — opt-in JSON report with per-file diagnostics, including attachment referenced/retained counts for automation and audit\n- **MCP server** — integrate with Claude Desktop, Claude Code, Codex, and other MCP clients\n- **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`)\n- **Python API** — `from dead_letter import convert` and you're off\n\n## 🧠 Built for LLM Pipelines\n\nRaw `.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.\n\ndead-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.\n\nIf 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.\n\nFor 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.\n\n### 📊 Token-cost benchmarks\n\ndead-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):\n\n- **~88% fewer tokens than the raw `.eml`** — a single email with a PDF attachment is ~126k tokens raw vs ~180 converted.\n- **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).\n\nThe 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/`](benchmarks/).\n\n## 📦 Install\n\nWith Homebrew on Apple silicon macOS:\n\n```bash\nbrew tap BigCactusLabs/tap\nbrew install dead-letter\n```\n\nThe Homebrew formula installs the core CLI only: `dead-letter convert` and\n`dead-letter doctor`. It intentionally does not bundle the optional web UI or\nMCP server dependency stacks.\n\nWith pip:\n\n```bash\npip install dead-letter            # core + CLI\npip install dead-letter[cli]       # + watchfiles (used by backend/UI watch mode)\npip install dead-letter[ui]        # + web UI, API server, and watch mode\npip install dead-letter[mcp]       # + MCP server\n```\n\nUse [pipx](https://pipx.pypa.io/) for isolated UI or MCP installs:\n\n```bash\npipx install 'dead-letter[ui]'    # installs dead-letter and dead-letter-ui\npipx install 'dead-letter[mcp]'   # installs dead-letter and dead-letter-mcp\n```\n\nFrom source:\n\n```bash\ngit clone https://github.com/BigCactusLabs/dead-letter.git\ncd dead-letter\nuv sync --extra dev     # all extras\nuv sync --extra ui      # UI only\nuv sync --extra mcp     # MCP only\n```\n\n## 🚀 Quick Start\n\n**CLI** — convert a single file:\n\n```bash\ndead-letter convert message.eml\n```\n\nConvert a whole directory:\n\n```bash\ndead-letter convert inbox/ --output out/\n```\n\nGenerate a JSON conversion report alongside the output:\n\n```bash\ndead-letter convert inbox/ --output out/ --report\n```\n\nWith `--output`, the report is written to that output directory as\n`.dead-letter-report.json`. Without `--output`, file conversions write the\nreport next to the source message and directory conversions write it to the\ninput directory root.\n\nCheck your runtime environment:\n\n```bash\ndead-letter doctor\n```\n\nDirectory conversion scans recursively for `.eml` files, matches the suffix\ncase-insensitively, skips symlinked files whose resolved targets escape the\nrequested input tree, and deduplicates in-tree symlink aliases that resolve to\nthe same message file.\n\n**Web UI** — start the local server:\n\n```bash\ndead-letter-ui --host 127.0.0.1 --port 8765\n```\n\nOpen `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.\nThe backend enforces a 100 MB per-file import limit for both single and batch\nuploads.\n\nFrom a source checkout, prefix with `uv run`:\n\n```bash\nuv run dead-letter convert message.eml\nuv run --extra ui dead-letter-ui --host 127.0.0.1 --port 8765\n```\n\n## 🐍 Python API\n\n```python\nfrom dead_letter import convert\n\nresult = convert(\"message.eml\")\nprint(result.subject, result.sender)\nprint(result.output)  # path to the generated .md\n```\n\nWith options:\n\n```python\nfrom dead_letter import convert, ConvertOptions\n\nresult = convert(\"message.eml\", options=ConvertOptions(\n    strip_signatures=True,\n    strip_quoted_headers=True,\n))\n```\n\nStrip signature images (logos, social icons) and tracking pixels:\n\n```python\nresult = convert(\"message.eml\", options=ConvertOptions(\n    strip_signature_images=True,\n    strip_tracking_pixels=True,\n))\n```\n\nWhen enabled, these filters remove matched images from rendered Markdown and omit\nstripped inline signature/tracking assets from bundle attachment output.\n\nBundle conversion (Markdown + attachments + source in one directory):\n\n```python\nfrom dead_letter import convert_to_bundle\n\nbundle = convert_to_bundle(\"message.eml\", bundle_root=\"cabinet/\", source_handling=\"copy\")\nprint(bundle.markdown)     # cabinet/message/message.md\nprint(bundle.attachments)  # retained extracted files under cabinet/message/attachments/\n```\n\n`source_handling=\"copy\"` preserves the original `.eml` in place. If omitted,\n`convert_to_bundle()` defaults to `source_handling=\"move\"` and moves the source\nmessage into the bundle.\n\nRetained extracted attachment filenames are normalized to safe basenames before\nthey are written under `attachments/`.\n\nQuality diagnostics include referenced/retained attachment counts when a message\nhas attachments eligible for retention, so dropped artifacts are\nmachine-detectable. See [Quality Diagnostics](docs/reference/quality-diagnostics.md).\n\nBatch:\n\n```python\nfrom dead_letter import convert_dir\n\nfor r in convert_dir(\"inbox/\", output=\"out/\"):\n    print(f\"{'✓' if r.success else '✗'} {r.source.name}\")\n```\n\n## 🔌 MCP Server\n\ndead-letter ships an [MCP](https://modelcontextprotocol.io/) server so LLM clients can convert `.eml` files directly without shelling out.\n\nInstall and launch:\n\n```bash\npip install dead-letter[mcp]\ndead-letter-mcp\n```\n\nFrom a source checkout:\n\n```bash\nuv run --extra mcp dead-letter-mcp\n```\n\n**Claude Desktop** — add to `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"dead-letter\": {\n      \"command\": \"uv\",\n      \"args\": [\"--directory\", \"/path/to/dead-letter\", \"run\", \"--extra\", \"mcp\", \"dead-letter-mcp\"]\n    }\n  }\n}\n```\n\n**Claude Code or Cowork (recommended — Claude plugin):**\n\n```\n/plugin marketplace add BigCactusLabs/bigcactuslabs-plugins\n/plugin install dead-letter\n```\n\nThe 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/`](plugin/).\n\nThe marketplace pins each published plugin tag and commit. Release automation\nupdates that pointer only after the bundled MCP server's exact PyPI version is\nlive, so Claude Code and Cowork resolve the same reproducible release.\n\n**Claude Code (manual MCP add — alternative):**\n\n```bash\nclaude mcp add dead-letter -- uv run --extra mcp dead-letter-mcp\n```\n\n**Codex:**\n\n```bash\ncodex mcp add dead-letter -- uv run --extra mcp dead-letter-mcp\ncodex mcp list\n```\n\nThe `codex mcp add` command registers the local `dead-letter` MCP server, and `codex mcp list` verifies that it's available.\n\n### Tools\n\n| Tool | Required arguments | Returns |\n| --- | --- | --- |\n| `convert_eml` | `eml_path` | Markdown text. Also writes a file when `output_path` is given. |\n| `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. |\n| `convert_directory` | `directory`, `output_directory` | JSON summary. Capped at 50 `.eml` files per call. |\n| `get_diagnostics` | `eml_path` | Quality and structure JSON. Writes nothing permanent. |\n\nAll 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`](docs/reference/v4-runtime-contracts.md#mcp-server-dead_letterbackendmcp_server).\n\n## 🗂 Project Structure\n\n```\nsrc/dead_letter/\n├── core/           # conversion pipeline (MIME, HTML, threads, rendering)\n├── backend/        # CLI, API server, job runner, watch mode, MCP server\n└── frontend/       # static web UI (Alpine.js ES modules + vanilla fetch)\ntests/\n├── core/           # conversion pipeline tests with .eml fixtures\n├── backend/        # API, job, and watch tests\n├── plugin/         # Claude plugin manifest, skill, and command tests\n└── frontend/       # JS unit tests\n```\n\n## 🧪 Testing\n\n```bash\nuv run pytest -q tests/core        # conversion pipeline\nuv run pytest -q tests/backend     # API and job runner\nuv run pytest -q tests/plugin      # Claude plugin manifest, skill, and command surfaces\nnode --test tests/frontend/*.test.js     # frontend\n```\n\nCI runs all four on PRs and on pushes to `main` or `feat/**` branches with the\nsame commands, plus\n`npx --yes @anthropic-ai/claude-code@2.1.145 plugin validate plugin/` and\n`node --check src/dead_letter/frontend/static/app.js`.\n\n## 📚 Docs\n\n- [Docs Index](docs/README.md) — public docs landing page\n- [Runtime Contracts](docs/reference/v4-runtime-contracts.md) — full API and core behavior spec\n- [Frontend State Model](docs/reference/frontend-state-model.md)\n- [Quality Diagnostics](docs/reference/quality-diagnostics.md)\n- [Brand & Style Guide](docs/brand/style-guide.md)\n- [Changelog](CHANGELOG.md)\n- [Contributing](CONTRIBUTING.md)\n- [Agent Guide](AGENTS.md) — operational guide for AI coding agents working in this repo\n\n## 🔧 Tools We Love\n\n- **[MarkEdit](https://github.com/MarkEdit-app/MarkEdit)** — TextEdit for Markdown, native macOS, ~4 MB. Opens dead-letter output like it was always meant to live there.\n- **[mo](https://github.com/k1LoW/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.\n\n## ⚠️ Known Limitations\n\n- Local-only — no remote server, no auth\n- In-memory job registry (state resets on restart)\n- Single-user, single-machine\n\n## License\n\n[PolyForm Noncommercial 1.0.0](LICENSE) — free for personal, educational, and nonprofit use. Commercial use requires a separate license from [Big Cactus Labs](https://github.com/BigCactusLabs).\n",
  "bytes": 13107,
  "sha": "92fae30ccd095696d1b8c902dd5710f7e6b1e0b2b15c9f32b4787ba465fa05b7",
  "repo_slug": "bigcactuslabs/dead-letter",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_bigcactuslabs_dead_letter_a27e0dc1/readme"
}