Convert .eml email exports to Markdown with YAML front matter
Project description
dead-letter
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
.emlfiles into an Inbox, let dead-letter organize the Markdown bundles into a Cabinet - Install validation —
dead-letter doctorchecks your runtime environment - Conversion report — opt-in JSON report with per-file diagnostics for automation and audit
- Python API —
from dead_letter import convertand 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 — 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.
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
- Docs Index — public docs landing page
- Runtime Contracts — full API and core behavior spec
- Frontend State Model
- Quality Diagnostics
- Brand & Style Guide
- Changelog
- Contributing
⚠️ Known Limitations (v0.1)
- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dead_letter-0.1.1.tar.gz.
File metadata
- Download URL: dead_letter-0.1.1.tar.gz
- Upload date:
- Size: 132.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fbcaf7c95ffcb63f84770a6c1fc017a83a31aac642141e12daa6d4f369e7e3f6
|
|
| MD5 |
b4571a497162270cce5352414a8cf035
|
|
| BLAKE2b-256 |
bed5d61caadb2b4f270275362e63bc1eb72dc839f2d04297cd3fe046f64b144a
|
Provenance
The following attestation bundles were made for dead_letter-0.1.1.tar.gz:
Publisher:
release.yml on BigCactusLabs/dead-letter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dead_letter-0.1.1.tar.gz -
Subject digest:
fbcaf7c95ffcb63f84770a6c1fc017a83a31aac642141e12daa6d4f369e7e3f6 - Sigstore transparency entry: 1186503524
- Sigstore integration time:
-
Permalink:
BigCactusLabs/dead-letter@3caa4d994a6e6ded3387ab4d1da283a57acae589 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/BigCactusLabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3caa4d994a6e6ded3387ab4d1da283a57acae589 -
Trigger Event:
release
-
Statement type:
File details
Details for the file dead_letter-0.1.1-py3-none-any.whl.
File metadata
- Download URL: dead_letter-0.1.1-py3-none-any.whl
- Upload date:
- Size: 143.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
161c1fa50d8653c4e7553543b6add748c3ed6964fba7338a8355d35fe015c4de
|
|
| MD5 |
083b0f30ecac66ca5fce8bd2ead001ff
|
|
| BLAKE2b-256 |
0f044bbd64a4cfa15699be55a283bb2bb63d09a12d5da29f69cf9b1584c98b3b
|
Provenance
The following attestation bundles were made for dead_letter-0.1.1-py3-none-any.whl:
Publisher:
release.yml on BigCactusLabs/dead-letter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dead_letter-0.1.1-py3-none-any.whl -
Subject digest:
161c1fa50d8653c4e7553543b6add748c3ed6964fba7338a8355d35fe015c4de - Sigstore transparency entry: 1186503532
- Sigstore integration time:
-
Permalink:
BigCactusLabs/dead-letter@3caa4d994a6e6ded3387ab4d1da283a57acae589 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/BigCactusLabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3caa4d994a6e6ded3387ab4d1da283a57acae589 -
Trigger Event:
release
-
Statement type: