Where messages
go to rest.
A local-first cockpit for SMTP, Kafka, Redis, and webhooks — so your next test email lands in a UI, not a customer inbox. One binary. One port. Zero accidental deliveries.
A cockpit you'll actually want to open.
No terminal required. deadLetter ships a static Next.js UI that
lives at localhost:8025 —
inbox, Kafka workspace, Redis tools, webhooks, settings. Live updates
over SSE. Dark mode by default.
where messages go to rest
Capture, inspect, and debug SMTP · Kafka · Redis · Webhooks — one place, one shell, zero noise.
- AN Welcome to Acme
- MY Build report
- PX Build report
- YN Automated email
- KD Random notification
- HS Hello there 👋
- IA Automated email
- YA Deadletter ping
Welcome to Acme
Welcome aboard
Hey there — your account is ready. Click the button below to get started.
If you didn't sign up, ignore this message.
- notif:user:19profile.updated
- notif:order:A7F31shipped
- notif:user:42login
- notif:cart:8abandoned
- 200dev-bot https://hooks.local/dev
- 202slack https://hooks.slack.com/…
- 429analytics https://analytics.local/in
- 200qa-inbox https://qa.local/capture
- 500legacy-bus https://old.local/recv
Everything your messages touch,
in one cockpit.
Watch SMTP, tail Kafka, poke at Redis, fire webhooks — from one UI, one API, one binary. Built for the two places production doesn't want you: your laptop and CI.
SMTP capture
:1025 · raw .eml + JSON
Point your app at localhost:1025. deadLetter accepts the
message, persists the raw envelope, extracts headers and MIME
parts, and rebuilds its index on boot so captures survive restarts.
- Welcome to deadLetter app@local 19:23
- [CI] build #1247 passed ci@runner 19:22
- Possible duplicate · password reset auth@svc 19:21
HTML sandbox
Render sanitized HTML previews safely while keeping the original raw source untouched on disk.
Webhooks
Fan out captured events to HTTP endpoints with retries, HMAC signatures, and delivery logs.
/api/webhooks Kafka observability
Brokers, topics, partitions, consumer groups, offsets. Tail a partition, reset a group, manage topics, and deserialize Confluent wire-format with Schema Registry integration.
Redis tools
Pub/Sub monitoring, live channel subscriptions over SSE, server info, key discovery, and DB usage — all from the same UI.
Replay, opt-in
Re-send a captured message to a real SMTP target only when you explicitly enable it. Zero surprises.
Retention
Keep messages for a configurable window. Scheduled or manual cleanup. Disk never surprises you.
Live SSE
The UI streams backend events — email, replay, retention, Kafka, webhooks — so what you see is what just happened.
deadletterctl
List, show, replay, and export captured emails from a bundled CLI. Friendly to automation, CI, and muscle memory.
$ deadletterctl replay 01HZX… --to qa@internal Duplicate detection
deadLetter flags likely repeats based on sender + subject within a short window, so misbehaving workers stop hiding.
The only tool that covers
all four surfaces.
Everyone else nails one protocol. deadletter collapses four into a single local binary — SMTP, Kafka, Redis, webhooks — with no SaaS, no signup, no telemetry.
| Tool | Kafka | Redis | Webhooks | Price | |
|---|---|---|---|---|---|
| deadletter | Free · local | ||||
| Mailpit | — | — | — | Free | |
| MailHog | — | — | — | Free · unmaintained | |
| MailCatcher | — | — | — | Free | |
| Mailtrap | — | — | — | Free tier · paid | |
| webhook.site | — | — | — | Free · paid | |
| Smee.io | — | — | — | Free | |
| Hookdeck | — | — | — | Free tier · paid | |
| ngrok inspect | — | — | — | Free · paid | |
| Kafka UI | — | — | — | Free | |
| Redpanda Console | — | — | — | Free · Enterprise | |
| AKHQ | — | — | — | Free | |
| Conduktor | — | — | — | Free tier · paid | |
| RedisInsight | — | — | — | Free | |
| Redis Commander | — | — | — | Free |
Scope: tools that replace at least one of deadletter's four surfaces. General-purpose tools (Postman, Wireshark, log aggregators) excluded.
Honest JSON. Honest streams.
Every view in the UI is backed by a documented HTTP endpoint and a live SSE topic. Nothing is hidden, nothing is proprietary.
/api/emails/:id 200 · application/json // Full metadata + parsed MIME for one capture { "id": "01HZX5A6QH7B0Q2F3TJ6PE8M7N", "from": "app@local", "to": ["alice@example.com"], "subject": "Welcome to deadLetter", "receivedAt": "2026-04-17T19:23:11Z", "size": 4123, "parts": ["text/plain", "text/html"], "duplicate": false, "replayed": false, "links": { "raw": "/api/emails/01HZX…/raw", "html": "/api/emails/01HZX…/html", "text": "/api/emails/01HZX…/text" } }
/events/stream text/event-stream
// Live backend events event: email.captured data: {"id":"01HZX…","from":"app@local","size":4123} event: email.duplicate data: {"id":"01HZX…","within":"30s"} event: webhook.delivered data: {"endpoint":"dev-bot","status":200,"ms":47} event: kafka.message data: {"topic":"orders","partition":2,"offset":1407} event: retention.swept data: {"removed":14,"keep":"24h"}
One binary.
One UI. Many sources.
A Go backend with a static Next.js UI baked in. Ingest SMTP, tail Kafka, watch Redis — everything funnels through the same live SSE bus, so the UI never lies about what the backend just saw.
- SMTP sink + .eml store
- MIME parser + sanitizer
- in-memory index (rebuild on boot)
- kafka + redis clients
- retention scheduler
- webhook dispatcher
- SSE event bus
Pick a path.
Ship in a minute.
Two commands on macOS, one-liner on Docker, full Kafka + Redis stack when you need it. Copy, paste, done.
1025 free port 8025 free Kafka / Redis (optional) $ docker pull peixotomdb/deadletter:latest $ docker run -d \ --name deadletter \ -p 1025:1025 -p 8025:8025 \ -v "$PWD/data:/data" \ peixotomdb/deadletter:latest # UI & API → http://localhost:8025 # SMTP sink → localhost:1025
$ curl -s localhost:8025/health {"status":"ok","version":"3.6.0","smtp":"1025","uptime":"3s"}
$ docker run -d \ --name deadletter \ -p 1025:1025 -p 8025:8025 \ -v "$PWD/data:/data" \ -e KAFKA_BROKERS=host.docker.internal:9092 \ -e SCHEMA_REGISTRY_URL=http://host.docker.internal:8081 \ -e REDIS_ADDR=host.docker.internal:6379 \ peixotomdb/deadletter:latest # Kafka topics, consumers & schemas show up in /kafka # Redis keys & pub/sub channels show up in /redis
KAFKA_BROKERS—comma-sep list · SASL supportedSCHEMA_REGISTRY_URL—optional · enables Avro decodingREDIS_ADDR—single node · user:pass@host okREPLAY_ENABLEDfalseopt-in SMTP re-sendRETENTION24hGo duration string$ brew install deadletter-engine/deadletter/deadletter $ brew services start deadletter # done — UI at http://localhost:8025 · SMTP on :1025 # relaunches on login · upgrade: deadletter update
$ git clone https://github.com/deadletter-engine/deadletter.git $ cd deadletter-ui && pnpm install && pnpm build $ cd .. && go build -o deadletter ./cmd/deadletter $ ./deadletter # or live UI + backend dev $ make dev
Something broke?
Error: bind: address already in use Another tool (MailHog? Mailpit?) holds 1025/8025. Map different host ports: -p 2525:1025 -p 18025:8025. connection refused · host.docker.internal Linux hosts need --add-host=host.docker.internal:host-gateway. Docker Desktop resolves it automatically. No events in UI Check curl localhost:8025/events/stream returns. If yes, your app is silent — verify SMTP host, not firewall. Default ports
- :1025 SMTP sinkpoint your app here
- :8025 HTTP API + UIJSON endpoints, SSE, static UI
- :9092 Kafka (compose)optional — only in full stack
- :8081 Schema Registryoptional — only in full stack
- :6379 Redis (compose)optional — only in full stack
UI surfaces
-
/Dashboardcounts, live feed, recent captures -
/inboxInboxsearchable list · raw · rendered · MIME -
/kafkaKafka workspacebrokers · topics · tail · consumer groups -
/redisRedis workspacepub/sub · channels · keys · info -
/webhooksWebhooksendpoints · signatures · delivery logs -
/settingsSettingsretention · replay · theme · sound
Safe by default.
Loud when it isn't.
deadLetter is built for local and CI environments where an accidental delivery can mean a real user gets a test email. Every destructive action is opt-in, logged, and visible in the UI.
Replay is opt-in
Until you flip REPLAY_ENABLED=true and point
it at a target, replay returns an error.
Raw is untouched
The .eml on disk is byte-exact. Previews
sanitize separately.
Sandboxed HTML
Rendered HTML is served in a sandboxed frame with no network egress by default.
Signed webhooks
Outbound webhooks carry an HMAC-SHA256 signature and retry with exponential backoff. Logs are keepable.
What deadLetter is not
Not a production SMTP relay. Not a deliverability service. Not a hardened multi-tenant mailbox. It is a local-first observability cockpit — deliberately small, deliberately loud about what it sees.
Stop sending test email into the void.
One docker run — SMTP, Kafka, Redis, and webhooks
all land in a UI you can actually reason about.