dead-letter 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dead_letter-0.1.0/LICENSE +21 -0
- dead_letter-0.1.0/PKG-INFO +239 -0
- dead_letter-0.1.0/README.md +197 -0
- dead_letter-0.1.0/pyproject.toml +68 -0
- dead_letter-0.1.0/setup.cfg +4 -0
- dead_letter-0.1.0/src/dead_letter/__init__.py +21 -0
- dead_letter-0.1.0/src/dead_letter/backend/__init__.py +1 -0
- dead_letter-0.1.0/src/dead_letter/backend/api.py +654 -0
- dead_letter-0.1.0/src/dead_letter/backend/cli.py +206 -0
- dead_letter-0.1.0/src/dead_letter/backend/doctor.py +170 -0
- dead_letter-0.1.0/src/dead_letter/backend/filesystem.py +104 -0
- dead_letter-0.1.0/src/dead_letter/backend/jobs.py +589 -0
- dead_letter-0.1.0/src/dead_letter/backend/schemas.py +215 -0
- dead_letter-0.1.0/src/dead_letter/backend/settings.py +88 -0
- dead_letter-0.1.0/src/dead_letter/backend/ui_server.py +60 -0
- dead_letter-0.1.0/src/dead_letter/backend/watch.py +273 -0
- dead_letter-0.1.0/src/dead_letter/core/__init__.py +13 -0
- dead_letter-0.1.0/src/dead_letter/core/_pipeline.py +629 -0
- dead_letter-0.1.0/src/dead_letter/core/attachments.py +119 -0
- dead_letter-0.1.0/src/dead_letter/core/calendar.py +48 -0
- dead_letter-0.1.0/src/dead_letter/core/conversation.py +21 -0
- dead_letter-0.1.0/src/dead_letter/core/header_parser.py +41 -0
- dead_letter-0.1.0/src/dead_letter/core/html.py +59 -0
- dead_letter-0.1.0/src/dead_letter/core/html_conversation.py +176 -0
- dead_letter-0.1.0/src/dead_letter/core/image_filter.py +185 -0
- dead_letter-0.1.0/src/dead_letter/core/mime.py +109 -0
- dead_letter-0.1.0/src/dead_letter/core/mime_selection.py +58 -0
- dead_letter-0.1.0/src/dead_letter/core/quotes.py +66 -0
- dead_letter-0.1.0/src/dead_letter/core/render.py +66 -0
- dead_letter-0.1.0/src/dead_letter/core/report.py +127 -0
- dead_letter-0.1.0/src/dead_letter/core/sanitize.py +67 -0
- dead_letter-0.1.0/src/dead_letter/core/slugs.py +23 -0
- dead_letter-0.1.0/src/dead_letter/core/text_conversation.py +118 -0
- dead_letter-0.1.0/src/dead_letter/core/threads.py +28 -0
- dead_letter-0.1.0/src/dead_letter/core/types.py +267 -0
- dead_letter-0.1.0/src/dead_letter/core/zone_cleanup.py +48 -0
- dead_letter-0.1.0/src/dead_letter/frontend/index.html +573 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/app.js +564 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/favicon-192x192.png +0 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/favicon-32x32.png +0 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/favicon.ico +0 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/lib/helpers.js +121 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/stores/history.js +39 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/stores/job.js +500 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/stores/settings.js +109 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/stores/watch.js +334 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/styles.css +1656 -0
- dead_letter-0.1.0/src/dead_letter/frontend/static/vendor/alpine.esm.js +3407 -0
- dead_letter-0.1.0/src/dead_letter/frontend/style-guide.html +628 -0
- dead_letter-0.1.0/src/dead_letter.egg-info/PKG-INFO +239 -0
- dead_letter-0.1.0/src/dead_letter.egg-info/SOURCES.txt +53 -0
- dead_letter-0.1.0/src/dead_letter.egg-info/dependency_links.txt +1 -0
- dead_letter-0.1.0/src/dead_letter.egg-info/entry_points.txt +3 -0
- dead_letter-0.1.0/src/dead_letter.egg-info/requires.txt +23 -0
- dead_letter-0.1.0/src/dead_letter.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Big Cactus Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dead-letter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert .eml email exports to Markdown with YAML front matter
|
|
5
|
+
Author: Big Cactus Labs
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/BigCactusLabs/dead-letter
|
|
8
|
+
Project-URL: Repository, https://github.com/BigCactusLabs/dead-letter
|
|
9
|
+
Project-URL: Issues, https://github.com/BigCactusLabs/dead-letter/issues
|
|
10
|
+
Keywords: eml,email,markdown,converter,archive
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Communications :: Email
|
|
17
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: icalendar>=6.1.1
|
|
22
|
+
Requires-Dist: mail-parser>=4.1.4
|
|
23
|
+
Requires-Dist: mail-parser-reply>=1.35
|
|
24
|
+
Requires-Dist: nh3>=0.2.20
|
|
25
|
+
Requires-Dist: html-to-markdown>=2.9.1
|
|
26
|
+
Requires-Dist: selectolax>=0.3.28
|
|
27
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
28
|
+
Provides-Extra: cli
|
|
29
|
+
Requires-Dist: watchfiles>=1.0.0; extra == "cli"
|
|
30
|
+
Provides-Extra: ui
|
|
31
|
+
Requires-Dist: dead-letter[cli]; extra == "ui"
|
|
32
|
+
Requires-Dist: fastapi>=0.111.0; extra == "ui"
|
|
33
|
+
Requires-Dist: httpx>=0.28.0; extra == "ui"
|
|
34
|
+
Requires-Dist: python-multipart>=0.0.18; extra == "ui"
|
|
35
|
+
Requires-Dist: uvicorn[standard]>=0.30.0; extra == "ui"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: dead-letter[ui]; extra == "dev"
|
|
38
|
+
Requires-Dist: anyio>=4.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest>=9.0.0; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
<p align="center">
|
|
44
|
+
<img src="docs/brand/production/readme-logo.png" width="128" alt="dead-letter">
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
# dead-letter
|
|
48
|
+
|
|
49
|
+
[](https://pypi.org/project/dead-letter/)
|
|
50
|
+
[](LICENSE)
|
|
51
|
+
|
|
52
|
+
**Your `.eml` files deserve a second life.**
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
56
|
+
## โจ Features
|
|
57
|
+
|
|
58
|
+
- **Full-fidelity conversion** โ HTML sanitization, Gmail/Outlook thread segmentation, inline image handling, and calendar event summaries
|
|
59
|
+
- **CLI** โ point it at a file or a directory and go
|
|
60
|
+
- **Local web UI** โ dark command-center interface with drag-and-drop import, watch mode, conversion grade badges, processing history, and per-job diagnostics
|
|
61
|
+
- **Inbox/Cabinet workflow** โ drop `.eml` files into an Inbox, let dead-letter organize the Markdown bundles into a Cabinet
|
|
62
|
+
- **Install validation** โ `dead-letter doctor` checks your runtime environment
|
|
63
|
+
- **Conversion report** โ opt-in JSON report with per-file diagnostics for automation and audit
|
|
64
|
+
- **Python API** โ `from dead_letter import convert` and you're off
|
|
65
|
+
|
|
66
|
+
## ๐ง Built for LLM Pipelines
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
## ๐ฆ Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install dead-letter # core + CLI
|
|
78
|
+
pip install dead-letter[cli] # + watch mode (watchfiles)
|
|
79
|
+
pip install dead-letter[ui] # + web UI, API server, and watch mode
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or use [pipx](https://pipx.pypa.io/) for an isolated install:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pipx install 'dead-letter[ui]' # installs dead-letter and dead-letter-ui commands
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
From source:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
git clone https://github.com/BigCactusLabs/dead-letter.git
|
|
92
|
+
cd dead-letter
|
|
93
|
+
uv sync --extra dev
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## ๐ Quick Start
|
|
97
|
+
|
|
98
|
+
**CLI** โ convert a single file:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
dead-letter convert message.eml
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Convert a whole directory:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
dead-letter convert inbox/ --output out/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Generate a JSON conversion report alongside the output:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
dead-letter convert inbox/ --output out/ --report
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
With `--output`, the report is written to that output directory as
|
|
117
|
+
`.dead-letter-report.json`. Without `--output`, file conversions write the
|
|
118
|
+
report next to the source message and directory conversions write it to the
|
|
119
|
+
input directory root.
|
|
120
|
+
|
|
121
|
+
Check your runtime environment:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
dead-letter doctor
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Directory conversion scans recursively for `.eml` files, matches the suffix
|
|
128
|
+
case-insensitively, and skips symlinked files whose resolved targets escape the
|
|
129
|
+
requested input tree.
|
|
130
|
+
|
|
131
|
+
**Web UI** โ start the local server:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
dead-letter-ui --host 127.0.0.1 --port 8765
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
From a source checkout, prefix with `uv run`:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
uv run dead-letter convert message.eml
|
|
143
|
+
uv run dead-letter-ui --host 127.0.0.1 --port 8765
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## ๐ Python API
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from dead_letter import convert
|
|
150
|
+
|
|
151
|
+
result = convert("message.eml")
|
|
152
|
+
print(result.subject, result.sender)
|
|
153
|
+
print(result.output) # path to the generated .md
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
With options:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from dead_letter import convert, ConvertOptions
|
|
160
|
+
|
|
161
|
+
result = convert("message.eml", options=ConvertOptions(
|
|
162
|
+
strip_signatures=True,
|
|
163
|
+
strip_quoted_headers=True,
|
|
164
|
+
))
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Strip signature images (logos, social icons) and tracking pixels:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
result = convert("message.eml", options=ConvertOptions(
|
|
171
|
+
strip_signature_images=True,
|
|
172
|
+
strip_tracking_pixels=True,
|
|
173
|
+
))
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Bundle conversion (Markdown + attachments + source in one directory):
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from dead_letter import convert_to_bundle
|
|
180
|
+
|
|
181
|
+
bundle = convert_to_bundle("message.eml", bundle_root="cabinet/")
|
|
182
|
+
print(bundle.markdown) # cabinet/message/message.md
|
|
183
|
+
print(bundle.attachments) # [cabinet/message/attachments/logo.png, ...]
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Extracted attachment filenames are normalized to safe basenames before they are
|
|
187
|
+
written under `attachments/`.
|
|
188
|
+
|
|
189
|
+
Batch:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from dead_letter import convert_dir
|
|
193
|
+
|
|
194
|
+
for r in convert_dir("inbox/", output="out/"):
|
|
195
|
+
print(f"{'โ' if r.success else 'โ'} {r.source.name}")
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## ๐ Project Structure
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
src/dead_letter/
|
|
202
|
+
โโโ core/ # conversion pipeline (MIME, HTML, threads, rendering)
|
|
203
|
+
โโโ backend/ # CLI, API server, job runner, watch mode
|
|
204
|
+
โโโ frontend/ # static web UI (htmx + Alpine.js)
|
|
205
|
+
tests/
|
|
206
|
+
โโโ core/ # conversion pipeline tests with .eml fixtures
|
|
207
|
+
โโโ backend/ # API, job, and watch tests
|
|
208
|
+
โโโ frontend/ # JS unit tests
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## ๐งช Testing
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
uv run pytest tests/core # conversion pipeline
|
|
215
|
+
uv run pytest tests/backend # API and job runner
|
|
216
|
+
node --test tests/frontend/*.test.js # frontend
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
CI runs all three on every push and PR.
|
|
220
|
+
|
|
221
|
+
## ๐ Docs
|
|
222
|
+
|
|
223
|
+
- [Docs Index](docs/README.md) โ public docs landing page
|
|
224
|
+
- [Runtime Contracts](docs/reference/v4-runtime-contracts.md) โ full API and core behavior spec
|
|
225
|
+
- [Frontend State Model](docs/reference/frontend-state-model.md)
|
|
226
|
+
- [Quality Diagnostics](docs/reference/quality-diagnostics.md)
|
|
227
|
+
- [Brand & Style Guide](docs/brand/style-guide.md)
|
|
228
|
+
- [Changelog](CHANGELOG.md)
|
|
229
|
+
- [Contributing](CONTRIBUTING.md)
|
|
230
|
+
|
|
231
|
+
## โ ๏ธ Known Limitations (v0.1)
|
|
232
|
+
|
|
233
|
+
- Local-only โ no remote server, no auth
|
|
234
|
+
- In-memory job registry (state resets on restart)
|
|
235
|
+
- Single-user, single-machine
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/brand/production/readme-logo.png" width="128" alt="dead-letter">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# dead-letter
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/dead-letter/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
**Your `.eml` files deserve a second life.**
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
## โจ Features
|
|
15
|
+
|
|
16
|
+
- **Full-fidelity conversion** โ HTML sanitization, Gmail/Outlook thread segmentation, inline image handling, and calendar event summaries
|
|
17
|
+
- **CLI** โ point it at a file or a directory and go
|
|
18
|
+
- **Local web UI** โ dark command-center interface with drag-and-drop import, watch mode, conversion grade badges, processing history, and per-job diagnostics
|
|
19
|
+
- **Inbox/Cabinet workflow** โ drop `.eml` files into an Inbox, let dead-letter organize the Markdown bundles into a Cabinet
|
|
20
|
+
- **Install validation** โ `dead-letter doctor` checks your runtime environment
|
|
21
|
+
- **Conversion report** โ opt-in JSON report with per-file diagnostics for automation and audit
|
|
22
|
+
- **Python API** โ `from dead_letter import convert` and you're off
|
|
23
|
+
|
|
24
|
+
## ๐ง Built for LLM Pipelines
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
## ๐ฆ Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install dead-letter # core + CLI
|
|
36
|
+
pip install dead-letter[cli] # + watch mode (watchfiles)
|
|
37
|
+
pip install dead-letter[ui] # + web UI, API server, and watch mode
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or use [pipx](https://pipx.pypa.io/) for an isolated install:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pipx install 'dead-letter[ui]' # installs dead-letter and dead-letter-ui commands
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
From source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/BigCactusLabs/dead-letter.git
|
|
50
|
+
cd dead-letter
|
|
51
|
+
uv sync --extra dev
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## ๐ Quick Start
|
|
55
|
+
|
|
56
|
+
**CLI** โ convert a single file:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
dead-letter convert message.eml
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Convert a whole directory:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
dead-letter convert inbox/ --output out/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Generate a JSON conversion report alongside the output:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
dead-letter convert inbox/ --output out/ --report
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
With `--output`, the report is written to that output directory as
|
|
75
|
+
`.dead-letter-report.json`. Without `--output`, file conversions write the
|
|
76
|
+
report next to the source message and directory conversions write it to the
|
|
77
|
+
input directory root.
|
|
78
|
+
|
|
79
|
+
Check your runtime environment:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
dead-letter doctor
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Directory conversion scans recursively for `.eml` files, matches the suffix
|
|
86
|
+
case-insensitively, and skips symlinked files whose resolved targets escape the
|
|
87
|
+
requested input tree.
|
|
88
|
+
|
|
89
|
+
**Web UI** โ start the local server:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
dead-letter-ui --host 127.0.0.1 --port 8765
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
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.
|
|
96
|
+
|
|
97
|
+
From a source checkout, prefix with `uv run`:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
uv run dead-letter convert message.eml
|
|
101
|
+
uv run dead-letter-ui --host 127.0.0.1 --port 8765
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## ๐ Python API
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from dead_letter import convert
|
|
108
|
+
|
|
109
|
+
result = convert("message.eml")
|
|
110
|
+
print(result.subject, result.sender)
|
|
111
|
+
print(result.output) # path to the generated .md
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
With options:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from dead_letter import convert, ConvertOptions
|
|
118
|
+
|
|
119
|
+
result = convert("message.eml", options=ConvertOptions(
|
|
120
|
+
strip_signatures=True,
|
|
121
|
+
strip_quoted_headers=True,
|
|
122
|
+
))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Strip signature images (logos, social icons) and tracking pixels:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
result = convert("message.eml", options=ConvertOptions(
|
|
129
|
+
strip_signature_images=True,
|
|
130
|
+
strip_tracking_pixels=True,
|
|
131
|
+
))
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Bundle conversion (Markdown + attachments + source in one directory):
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from dead_letter import convert_to_bundle
|
|
138
|
+
|
|
139
|
+
bundle = convert_to_bundle("message.eml", bundle_root="cabinet/")
|
|
140
|
+
print(bundle.markdown) # cabinet/message/message.md
|
|
141
|
+
print(bundle.attachments) # [cabinet/message/attachments/logo.png, ...]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Extracted attachment filenames are normalized to safe basenames before they are
|
|
145
|
+
written under `attachments/`.
|
|
146
|
+
|
|
147
|
+
Batch:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from dead_letter import convert_dir
|
|
151
|
+
|
|
152
|
+
for r in convert_dir("inbox/", output="out/"):
|
|
153
|
+
print(f"{'โ' if r.success else 'โ'} {r.source.name}")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## ๐ Project Structure
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
src/dead_letter/
|
|
160
|
+
โโโ core/ # conversion pipeline (MIME, HTML, threads, rendering)
|
|
161
|
+
โโโ backend/ # CLI, API server, job runner, watch mode
|
|
162
|
+
โโโ frontend/ # static web UI (htmx + Alpine.js)
|
|
163
|
+
tests/
|
|
164
|
+
โโโ core/ # conversion pipeline tests with .eml fixtures
|
|
165
|
+
โโโ backend/ # API, job, and watch tests
|
|
166
|
+
โโโ frontend/ # JS unit tests
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## ๐งช Testing
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
uv run pytest tests/core # conversion pipeline
|
|
173
|
+
uv run pytest tests/backend # API and job runner
|
|
174
|
+
node --test tests/frontend/*.test.js # frontend
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
CI runs all three on every push and PR.
|
|
178
|
+
|
|
179
|
+
## ๐ Docs
|
|
180
|
+
|
|
181
|
+
- [Docs Index](docs/README.md) โ public docs landing page
|
|
182
|
+
- [Runtime Contracts](docs/reference/v4-runtime-contracts.md) โ full API and core behavior spec
|
|
183
|
+
- [Frontend State Model](docs/reference/frontend-state-model.md)
|
|
184
|
+
- [Quality Diagnostics](docs/reference/quality-diagnostics.md)
|
|
185
|
+
- [Brand & Style Guide](docs/brand/style-guide.md)
|
|
186
|
+
- [Changelog](CHANGELOG.md)
|
|
187
|
+
- [Contributing](CONTRIBUTING.md)
|
|
188
|
+
|
|
189
|
+
## โ ๏ธ Known Limitations (v0.1)
|
|
190
|
+
|
|
191
|
+
- Local-only โ no remote server, no auth
|
|
192
|
+
- In-memory job registry (state resets on restart)
|
|
193
|
+
- Single-user, single-machine
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dead-letter"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Convert .eml email exports to Markdown with YAML front matter"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [{ name = "Big Cactus Labs" }]
|
|
13
|
+
keywords = ["eml", "email", "markdown", "converter", "archive"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Environment :: Web Environment",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Topic :: Communications :: Email",
|
|
21
|
+
"Topic :: Text Processing :: Markup :: Markdown",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"icalendar>=6.1.1",
|
|
25
|
+
"mail-parser>=4.1.4",
|
|
26
|
+
"mail-parser-reply>=1.35",
|
|
27
|
+
"nh3>=0.2.20",
|
|
28
|
+
"html-to-markdown>=2.9.1",
|
|
29
|
+
"selectolax>=0.3.28",
|
|
30
|
+
"pyyaml>=6.0.2",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
cli = ["watchfiles>=1.0.0"]
|
|
35
|
+
ui = [
|
|
36
|
+
"dead-letter[cli]",
|
|
37
|
+
"fastapi>=0.111.0",
|
|
38
|
+
"httpx>=0.28.0",
|
|
39
|
+
"python-multipart>=0.0.18",
|
|
40
|
+
"uvicorn[standard]>=0.30.0",
|
|
41
|
+
]
|
|
42
|
+
dev = [
|
|
43
|
+
"dead-letter[ui]",
|
|
44
|
+
"anyio>=4.0.0",
|
|
45
|
+
"pytest>=9.0.0",
|
|
46
|
+
"pytest-cov>=5.0.0",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://github.com/BigCactusLabs/dead-letter"
|
|
51
|
+
Repository = "https://github.com/BigCactusLabs/dead-letter"
|
|
52
|
+
Issues = "https://github.com/BigCactusLabs/dead-letter/issues"
|
|
53
|
+
|
|
54
|
+
[project.scripts]
|
|
55
|
+
dead-letter = "dead_letter.backend.cli:main"
|
|
56
|
+
dead-letter-ui = "dead_letter.backend.ui_server:main"
|
|
57
|
+
|
|
58
|
+
[tool.setuptools]
|
|
59
|
+
package-dir = {"" = "src"}
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.packages.find]
|
|
62
|
+
where = ["src"]
|
|
63
|
+
|
|
64
|
+
[tool.setuptools.package-data]
|
|
65
|
+
"dead_letter" = ["frontend/**/*"]
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
addopts = "--import-mode=importlib"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""dead-letter: Convert .eml email exports to Markdown with YAML front matter."""
|
|
2
|
+
|
|
3
|
+
from dead_letter.core import (
|
|
4
|
+
BundleResult,
|
|
5
|
+
ConvertOptions,
|
|
6
|
+
ConvertResult,
|
|
7
|
+
convert,
|
|
8
|
+
convert_dir,
|
|
9
|
+
convert_to_bundle,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"__version__",
|
|
14
|
+
"BundleResult",
|
|
15
|
+
"ConvertOptions",
|
|
16
|
+
"ConvertResult",
|
|
17
|
+
"convert",
|
|
18
|
+
"convert_dir",
|
|
19
|
+
"convert_to_bundle",
|
|
20
|
+
]
|
|
21
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Backend package for dead-letter CLI and API."""
|