Correspondence, reconstructed.
Deadletter turns scattered email threads and attachments into structured, browsable, shareable archives. It is a self-hosted Next.js application for people who need a durable, readable record of correspondence without adopting a mail server, CMS, or general-purpose file manager.
Archive content remains on the server filesystem: one Markdown file per email or document, with original attachments kept alongside it. Deadletter validates that structure, presents entries as a chronological timeline, and streams files through constrained, path-safe routes. A bundled coding-agent skill provides the intended workflow for turning messy user-supplied exports into that canonical structure.
Email exports are difficult to navigate: messages are buried in PDFs, quoted history repeats, and attachments become separated from their context. Deadletter provides a deliberately small archival format and a focused reader for preserving that context. Its filesystem-first model keeps records portable, reviewable, and independent of a database for archive content itself.
- Filesystem-backed email and document chains with validated YAML frontmatter.
- Optional collections and categories without moving chain folders.
- Responsive timelines with GitHub-flavored Markdown and practical previews.
- Safe attachment streaming and a focused cross-chain file browser.
- Request-time filesystem reads so normal content changes appear on refresh.
- Agent-guided import of PDF, text, and Markdown exports with attachment matching, deduplication, and evidence-preserving cleanup.
- Light and dark themes.
- Cloudflare Access verification for private application routes.
- Revocable, scoped share links for one live chain or collection.
Markdown + attachment files
│
▼
Deadletter archive loaders ──► Next.js reader and file routes
│ │
└── archive.yml / chain.yml ├── Cloudflare Access private host
└── scoped public share host
Archive records remain ordinary files. SQLite is used only for revocable share grant state, never as the archive-content store. Read Architecture for the model and boundaries.
- Node.js 24
- pnpm 11.15.1
- A filesystem location the Node process can read for archive content
Authentication uses Cloudflare Access in every environment that serves private
archive data. Local development normally uses the fictional fixtures and the
safe values in .env.example, but Deadletter intentionally provides no
unauthenticated development bypass.
pnpm install
cp .env.example .env.local
pnpm devThe example configuration points ARCHIVE_DATA_DIR at committed fictional
fixtures. pnpm dev starts the development server, but browsing private pages
still requires a configured private hostname and a valid Cloudflare Access
assertion. Direct unauthenticated localhost requests are expected to fail.
Keep real correspondence outside Git and out of public/.
For detailed local setup, development-origin behavior, test commands, and resource-ID tooling, read Development.
Deadletter deliberately has no upload screen or automatic mailbox importer.
Instead, a local coding agent such as Codex or Claude Code uses the bundled
import-email-export skill to
inspect exports, reconstruct individual messages, associate original
attachments, and write validated archive files. The original source material
stays read-only and local unless you explicitly authorize otherwise.
Install the repository-local discovery link for your agent from the repository
root. The canonical skill remains under .github/skills/; these links do not
create a second maintained copy.
Codex
mkdir -p .agents/skills
ln -s ../../.github/skills/import-email-export .agents/skills/import-email-exportClaude Code
mkdir -p .claude/skills
ln -s ../../.github/skills/import-email-export .claude/skills/import-email-exportStart a new agent session if the skill is not discovered immediately. Then ask
the agent to use import-email-export and provide:
- the local path or paths to the email export files;
- the original attachment files, when available;
- the desired chain title and whether it is new or existing;
- the archive root and optional collection/category placement;
- the source timezone, especially when exported timestamps omit offsets; and
- whether existing archive entries may be corrected or replaced.
The agent inventories and deduplicates messages, reports unresolved metadata or attachment ownership instead of guessing, writes or stages the archive tree, and runs the repository's existing validation. You remain responsible for the final content review. See Agent-assisted imports for prompts, preparation, safety rules, and the complete interaction model.
An archive root contains flat chain directories. Each chain has a chain.yml,
an emails/ directory of Markdown entries, and referenced files below the same
chain directory. An optional root archive.yml groups chains into collections
and categories.
archive-root/
├── archive.yml
└── example-chain/
├── chain.yml
├── emails/
│ └── 2026-01-15_0900_example-message.md
└── attachments/
└── 2026-01-15_0900/
└── example-note.txt
The full, executable-format-aligned contract is the
Archive Content Format Specification.
Copyable fictional input lives in examples/archive-template/.
ARCHIVE_DATA_DIR may be an absolute archive path or a path relative to the
repository root. The default is data/chains/, which is intentionally ignored
by Git except for an empty placeholder.
Production runs as a Node server:
pnpm build
pnpm startDeadletter expects a private canonical hostname protected by Cloudflare Access. Its optional public share hostname is deliberately separate and exposes only scoped share routes. Deployment environment, state, Tunnel behavior, and release sequencing are documented in Deployment.
Private archive data requires both Cloudflare Access at the edge and an application-side verification of the Access JWT. Share links are separate, bearer-capability URLs for a specifically granted live chain or collection. They are revocable, credential secrets are stored only as digests, and shared file access is reauthorized on every request.
Read Authentication and sharing before enabling public links.
pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm validatepnpm validate is the complete repository check. See
Development for the canonical command reference.
- Architecture
- Agent-assisted imports
- Archive content format
- Authentication and sharing
- Deployment
- Development
Do not commit real correspondence, attachments, credentials, SQLite state, or
production configuration. Archive and attachment paths are untrusted input;
Deadletter validates containment, rejects traversal and symlink escapes, and
never serves archive files from public/. HTML and SVG attachments download
rather than render on the application origin.
The project assumes a correctly configured private Cloudflare Access application and protected origin. Application code independently enforces its private and share authorization boundaries; deployment must preserve those boundaries.
Deadletter is a self-hosted archive reader with a stable filesystem content contract. It intentionally does not provide automatic mailbox ingestion, upload/edit/delete interfaces, full-text search, an archive database, custom Office/PDF rendering, or a general document-management feature set. Import is an explicit, reviewable local-agent workflow rather than an application feature.
Focused improvements are welcome. Preserve the filesystem archive contract, server-only boundary, and path-safety guarantees. Start with Development and repository-local contributor guidance.
Deadletter is released under the MIT License.