Self-hosted shared inbox for all your domains.
DeadDrop lets you collect inbound email and website form submissions into one dashboard you control. It is built for the “many projects, many domains, one mailbox stack” workflow.
- Domain ownership verification via DNS TXT.
- Mailboxes with two stream types:
formstream for website widget submissions.emailstream for inbound SMTP delivery.
- Conversation inbox with open/closed states and in-dashboard replies.
- Embeddable widget (
/static/widget.js) that works on any site. - One-command self-host installer for Linux servers.
- No external SaaS dependency required for core operation.
Domain: a verified domain you control (for exampleopenclaw.london).Mailbox: a team inbox under a domain (for exampleSupport).Stream: a channel connected to a mailbox:formstream has awidget_idfor the JS embed.emailstream has an email address (for examplecontact@openclaw.london).
Conversation: a thread created from a form submission or inbound email.
Run this on your server:
curl -fsSL https://raw.githubusercontent.com/ZNZ-systems/DeadDrop/master/docker/install.sh | INSTALL_DIR=deaddrop DASHBOARD_PORT=8080 bashWhat this does:
- Installs Docker if missing (Linux).
- Downloads production compose + env template.
- Generates DB credentials and
DATABASE_URL. - Starts Postgres, app, and Caddy.
- Validates
/healthbefore finishing.
After install, open the printed dashboard URL (usually http://<server-ip>:8080).
git clone https://github.com/ZNZ-systems/DeadDrop.git
cd DeadDrop/docker
cp .env.example .env
docker compose up -dOpen http://localhost:8080.
- Sign up (there are no default credentials).
- Create a domain in
Domains. - Add the DNS TXT record shown in the domain page.
- Click
Check Verification. - Create a mailbox in
Mailboxes(example from-address:contact@yourdomain.com). - Use the generated streams:
formstream: copy widget snippet to your website.emailstream: route MX to your server and send mail to that address.
Use the form stream widget ID, not the domain ID.
<script
src="https://YOUR-DEADDROP-HOST/static/widget.js"
data-deaddrop-id="FORM_STREAM_WIDGET_ID">
</script>Where to find FORM_STREAM_WIDGET_ID:
- Dashboard →
Mailboxes→ open mailbox →Streams→formstream.
For a domain like openclaw.london:
-
TXT verification record (from dashboard):
- Type:
TXT - Host/Name:
@ - Value:
deaddrop-verify=<token-from-dashboard>
- Type:
-
MX routing for inbound email:
- Create an A record for your mail host (example
mx.openclaw.london -> <server-ip>). - Add MX:
- Host/Name:
@ - Value:
mx.openclaw.london - Priority:
10
- Host/Name:
- Create an A record for your mail host (example
-
Recommended deliverability records:
- SPF:
v=spf1 mx a:mx.openclaw.london ip4:<server-ip> -all - DMARC:
_dmarcTXT likev=DMARC1; p=none; rua=mailto:dmarc@yourdomain
- SPF:
Inbound email is handled by the app’s SMTP listener (INBOUND_SMTP_ADDR).
In development compose, this is already wired (25 -> 2525).
For production-like setups, ensure both are true:
INBOUND_SMTP_ADDRis set (for example:2525).- Host port
25is published to app container port2525.
Example compose override:
services:
app:
ports:
- "25:2525"
environment:
- INBOUND_SMTP_ADDR=:2525
- INBOUND_SMTP_DOMAIN=yourdomain.comDeadDrop can send mailbox replies via SMTP.
Default approach:
- Use bundled
smtp-relaycontainer. - App points at
SMTP_HOST=smtp-relayandSMTP_PORT=25.
If your provider blocks direct port 25 egress, configure smarthost relay envs:
RELAYHOSTRELAYHOST_USERNAMERELAYHOST_PASSWORD
Primary env vars:
PORT(default8080)DATABASE_URLBASE_URLSECURE_COOKIES(falsefor plain HTTP/IP testing)SESSION_MAX_AGE_HOURSSMTP_ENABLEDSMTP_HOST,SMTP_PORT,SMTP_USER,SMTP_PASS,SMTP_FROMINBOUND_SMTP_ADDR,INBOUND_SMTP_DOMAINRATE_LIMIT_RPS,RATE_LIMIT_BURSTDNS_OVERRIDE_FILE(used for deterministic e2e DNS tests)
go test ./...E2E harness:
bash e2e/run-e2e.sh- Go + Chi router
- Server-rendered HTML templates + HTMX
- PostgreSQL (repository pattern)
- Embedded static assets/templates/migrations
- Optional inbound SMTP server in-process
Main entrypoint: /Users/pz/CodeProjects/DeadDrop/cmd/deaddrop/main.go
/Users/pz/CodeProjects/DeadDrop/cmd/deaddrop- app entrypoint/Users/pz/CodeProjects/DeadDrop/internal/web- router, handlers, middleware, renderer/Users/pz/CodeProjects/DeadDrop/internal/domain- domain verification service/Users/pz/CodeProjects/DeadDrop/internal/mailbox- mailbox logic/Users/pz/CodeProjects/DeadDrop/internal/conversation- inbox threads + replies/Users/pz/CodeProjects/DeadDrop/internal/inbound- SMTP inbound server/Users/pz/CodeProjects/DeadDrop/internal/store/postgres- persistence layer/Users/pz/CodeProjects/DeadDrop/static- widget and static files/Users/pz/CodeProjects/DeadDrop/templates- HTML templates/Users/pz/CodeProjects/DeadDrop/docker- docker compose + installer
-
CSRF token mismatch:- Usually
BASE_URL/SECURE_COOKIESmismatch. - For IP + HTTP testing, set
BASE_URL=http://<ip>:8080andSECURE_COOKIES=false.
- Usually
-
Widget script loads but no messages:
- Confirm
data-deaddrop-idis a form stream widget ID. - Confirm browser can reach
https://YOUR-DEADDROP-HOST/static/widget.js.
- Confirm
-
DNS verification not passing:
- Verify TXT record value is exact.
- Wait for propagation.
- Re-run
Check Verificationin domain page.
-
Inbound email not appearing:
- Confirm MX points to host with A record.
- Confirm SMTP port 25 reachable externally.
- Confirm
INBOUND_SMTP_ADDRis enabled.
- Create a branch.
- Make changes.
- Run
go test ./.... - Open a PR with scope and test notes.
TBD