Email a secret address, and the message is posted to /dev/null as a commit from your own account. A small Laravel application: SparkPost's inbound relay receives the mail and webhooks it here, and the gateway posts it through /dev/null's signed-device API.
This is a proof of concept, built against the device authorization flow proposed in dev-null.sh PR #585 — it will not link accounts against an instance that has not merged it.
- Sign in with your email. A six-digit code is mailed to you; there are no passwords. Verifying the code is what proves you own the address, so accounts are born verified.
- Link your /dev/null account. The gateway generates an ed25519 keypair for you alone and asks /dev/null for a device authorization (RFC 8628). You approve the device on /dev/null itself — the gateway never sees your /dev/null credentials — and you can revoke it there, under Settings → Devices, at any time.
- Email your inbound address. Each account gets a secret address
(
<random>@<inbound domain>). Mail sent to it from your account's email address is posted to /dev/null as a commit: the plain-text body, or the subject if the body is empty, cut at /dev/null's 4000-character limit.
The inbound address is the credential, not the sender. A From header is trivially forged, so it can never authorize anything on its own. Posting requires both:
- the secret inbound address — 20 random characters, regenerable from the dashboard the moment it leaks (and it will leak: CCs, forwards, address books);
- a From matching the account's verified email — weak alone, but it means a leaked address by itself is still not enough.
Each user gets their own device key. There is no master credential: the keypair is generated
per account, stored encrypted at rest, and approved by the user on /dev/null as a named device they
can revoke unilaterally. Every API request is SSHSIG-signed (the format of ssh-keygen -Y sign)
over a timestamp, a single-use nonce, the method, the path and the raw body.
The webhook trusts a token, because that is all SparkPost offers. Relay webhooks carry no HMAC
signature — only the configured auth_token, echoed in X-MessageSystems-Webhook-Token and
compared in constant time. No configured token refuses everything.
Nobody is replied to except the account owner. Mail from an unknown or mismatched sender is dropped silently; answering it would bounce to whoever was spoofed. Failure notices go only to the account's own verified email, rate-limited so a runaway sender cannot flood it.
Message content is never stored. The webhook queues only the fields needed to post (recipient, sender, subject, text); the processing job records an outcome and a hashed Message-ID — for deduplicating SparkPost's retries — and nothing else, and those records prune after seven days. What the gateway holds per user is an email address, an inbound token and an encrypted device key.
composer install
cp .env.example .env
php artisan key:generate
php artisan migrateThere is no frontend build: the views are plain Blade with inline styles.
A queue worker must be running (php artisan queue:work) — device-approval polling and inbound
processing are queued — and the scheduler (php artisan schedule:run from cron) prunes outcome
records.
| key | what it is |
|---|---|
DEVNULL_URL |
the /dev/null instance to post to |
INBOUND_MAIL_DOMAIN |
the domain of users' inbound addresses |
SPARKPOST_RELAY_WEBHOOK_TOKEN |
the auth_token on the SparkPost relay webhook |
On the SparkPost side: add INBOUND_MAIL_DOMAIN as an inbound domain (MX records pointing at
SparkPost), then create a relay webhook targeting https://<app>/webhooks/sparkpost with the same
auth_token.
php artisan testThe SSHSIG signer is tested against the real ssh-keygen -Y verify, not a reimplementation, so a
passing suite means /dev/null's verifier will accept our signatures.
MIT. Copyright (c) Simon Hampel.