Skip to main content

Convert .eml email exports to Markdown with YAML front matter

Project description

dead-letter

dead-letter

PyPI License: MIT

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
  • 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 decoded files under attachments/. The Markdown is ready for text ingestion, while PDFs, spreadsheets, calendar files, and other binary attachments stay cleanly split out for whatever downstream parser you already use.

📦 Install

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

Or use pipx for an isolated install:

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

From source:

git clone https://github.com/BigCactusLabs/dead-letter.git
cd dead-letter
uv sync --extra dev

🚀 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, and skips symlinked files whose resolved targets escape the requested input tree.

Web UI — start the local server:

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

Open http://127.0.0.1:8765, save your Inbox and Cabinet folders in Settings, then 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.

From a source checkout, prefix with uv run:

uv run dead-letter convert message.eml
uv run 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,
))

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)  # [cabinet/message/attachments/logo.png, ...]

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}")

🗂 Project Structure

src/dead_letter/
├── core/           # conversion pipeline (MIME, HTML, threads, rendering)
├── backend/        # CLI, API server, job runner, watch mode
└── frontend/       # static web UI (htmx + Alpine.js)
tests/
├── core/           # conversion pipeline tests with .eml fixtures
├── backend/        # API, job, and watch tests
└── frontend/       # JS unit tests

🧪 Testing

uv run pytest tests/core           # conversion pipeline
uv run pytest tests/backend        # API and job runner
node --test tests/frontend/*.test.js     # frontend

CI runs all three on every push and PR.

📚 Docs

⚠️ Known Limitations (v0.1)

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

License

MIT

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.1.0.tar.gz (129.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.1.0-py3-none-any.whl (140.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: dead_letter-0.1.0.tar.gz
  • Upload date:
  • Size: 129.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for dead_letter-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4104cc6721fbe982a2a2fa4055151a34aba98462c4b32ba9988aee3b495bb186
MD5 ab7f22d64f84f6757fe31687afb6e264
BLAKE2b-256 d213f1e69501a852ad6aeee7a4108ef8010a09131e8d48b83d0c0f47abd6534a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: dead_letter-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 140.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for dead_letter-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3eb2c7da8ef13e72feaa7c9db5bf1fb9499d73fb2227e52f6bb8e4caa1377908
MD5 8e6a34e3e1713f0624ed64fb90433009
BLAKE2b-256 068e10688179d667d1b53619baccd64c67c2a9c6f1df968f8ef85e9a479d5966

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