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.
Files changed (55) hide show
  1. dead_letter-0.1.0/LICENSE +21 -0
  2. dead_letter-0.1.0/PKG-INFO +239 -0
  3. dead_letter-0.1.0/README.md +197 -0
  4. dead_letter-0.1.0/pyproject.toml +68 -0
  5. dead_letter-0.1.0/setup.cfg +4 -0
  6. dead_letter-0.1.0/src/dead_letter/__init__.py +21 -0
  7. dead_letter-0.1.0/src/dead_letter/backend/__init__.py +1 -0
  8. dead_letter-0.1.0/src/dead_letter/backend/api.py +654 -0
  9. dead_letter-0.1.0/src/dead_letter/backend/cli.py +206 -0
  10. dead_letter-0.1.0/src/dead_letter/backend/doctor.py +170 -0
  11. dead_letter-0.1.0/src/dead_letter/backend/filesystem.py +104 -0
  12. dead_letter-0.1.0/src/dead_letter/backend/jobs.py +589 -0
  13. dead_letter-0.1.0/src/dead_letter/backend/schemas.py +215 -0
  14. dead_letter-0.1.0/src/dead_letter/backend/settings.py +88 -0
  15. dead_letter-0.1.0/src/dead_letter/backend/ui_server.py +60 -0
  16. dead_letter-0.1.0/src/dead_letter/backend/watch.py +273 -0
  17. dead_letter-0.1.0/src/dead_letter/core/__init__.py +13 -0
  18. dead_letter-0.1.0/src/dead_letter/core/_pipeline.py +629 -0
  19. dead_letter-0.1.0/src/dead_letter/core/attachments.py +119 -0
  20. dead_letter-0.1.0/src/dead_letter/core/calendar.py +48 -0
  21. dead_letter-0.1.0/src/dead_letter/core/conversation.py +21 -0
  22. dead_letter-0.1.0/src/dead_letter/core/header_parser.py +41 -0
  23. dead_letter-0.1.0/src/dead_letter/core/html.py +59 -0
  24. dead_letter-0.1.0/src/dead_letter/core/html_conversation.py +176 -0
  25. dead_letter-0.1.0/src/dead_letter/core/image_filter.py +185 -0
  26. dead_letter-0.1.0/src/dead_letter/core/mime.py +109 -0
  27. dead_letter-0.1.0/src/dead_letter/core/mime_selection.py +58 -0
  28. dead_letter-0.1.0/src/dead_letter/core/quotes.py +66 -0
  29. dead_letter-0.1.0/src/dead_letter/core/render.py +66 -0
  30. dead_letter-0.1.0/src/dead_letter/core/report.py +127 -0
  31. dead_letter-0.1.0/src/dead_letter/core/sanitize.py +67 -0
  32. dead_letter-0.1.0/src/dead_letter/core/slugs.py +23 -0
  33. dead_letter-0.1.0/src/dead_letter/core/text_conversation.py +118 -0
  34. dead_letter-0.1.0/src/dead_letter/core/threads.py +28 -0
  35. dead_letter-0.1.0/src/dead_letter/core/types.py +267 -0
  36. dead_letter-0.1.0/src/dead_letter/core/zone_cleanup.py +48 -0
  37. dead_letter-0.1.0/src/dead_letter/frontend/index.html +573 -0
  38. dead_letter-0.1.0/src/dead_letter/frontend/static/app.js +564 -0
  39. dead_letter-0.1.0/src/dead_letter/frontend/static/favicon-192x192.png +0 -0
  40. dead_letter-0.1.0/src/dead_letter/frontend/static/favicon-32x32.png +0 -0
  41. dead_letter-0.1.0/src/dead_letter/frontend/static/favicon.ico +0 -0
  42. dead_letter-0.1.0/src/dead_letter/frontend/static/lib/helpers.js +121 -0
  43. dead_letter-0.1.0/src/dead_letter/frontend/static/stores/history.js +39 -0
  44. dead_letter-0.1.0/src/dead_letter/frontend/static/stores/job.js +500 -0
  45. dead_letter-0.1.0/src/dead_letter/frontend/static/stores/settings.js +109 -0
  46. dead_letter-0.1.0/src/dead_letter/frontend/static/stores/watch.js +334 -0
  47. dead_letter-0.1.0/src/dead_letter/frontend/static/styles.css +1656 -0
  48. dead_letter-0.1.0/src/dead_letter/frontend/static/vendor/alpine.esm.js +3407 -0
  49. dead_letter-0.1.0/src/dead_letter/frontend/style-guide.html +628 -0
  50. dead_letter-0.1.0/src/dead_letter.egg-info/PKG-INFO +239 -0
  51. dead_letter-0.1.0/src/dead_letter.egg-info/SOURCES.txt +53 -0
  52. dead_letter-0.1.0/src/dead_letter.egg-info/dependency_links.txt +1 -0
  53. dead_letter-0.1.0/src/dead_letter.egg-info/entry_points.txt +3 -0
  54. dead_letter-0.1.0/src/dead_letter.egg-info/requires.txt +23 -0
  55. 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
+ [![PyPI](https://img.shields.io/pypi/v/dead-letter)](https://pypi.org/project/dead-letter/)
50
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/dead-letter)](https://pypi.org/project/dead-letter/)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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."""