comail #
A cooperative email reputation layer for atproto. PDS operators pool sending volume and reputation through shared infrastructure, verified by a labeling service that checks DNS configuration and publishes signed attestations to the atproto network.
Starting with transactional email (verification codes, password resets, notifications) to build collective domain reputation, with the long-term goal of making self-hosted personal email viable again.
See comail-vision.md for the full proposal.
Status #
Alpha. The relay, labeler, web frontend, and admin dashboard are live; the cooperative is accepting its first external members.
- Relay runs at
smtp.atmos.emailwith STARTTLS on 587 and inbound FBL/DSN on 25. - Labeler consumes the atproto firehose via Jetstream and issues
verified-mail-operator+relay-memberlabels. - Every outbound message is dual-signed (
d=<member-domain>for DMARC alignment,d=atmos.emailfor pool-level FBL) and carriesFeedback-ID+X-Atmos-Member-Didfor attribution. - Warming tier caps protect the shared IP during the first 14 days of a new member's lifetime (2/hour, 10/day weeks 1; 10/hour, 50/day week 2).
- Per-domain auto-pause on 5% bounce rate or 0.08% spam-complaint rate over a 24h window.
- Pool-level FBL registrations: Gmail Postmaster verified, Microsoft SNDS + JMRP registered, Yahoo CFL verified.
To enroll a new sending domain, visit https://comail.at and sign in with your atproto handle. The flow handles atproto OAuth, DKIM keypair issuance, and the DNS records you need to publish.
How it works #
- A PDS operator publishes an
email.atmos.attestationrecord declaring their mail domain and DKIM selectors - The labeler watches the atproto firehose (via Jetstream) for these records
- For each attestation, it verifies:
- Domain control: the operator's DID handle matches the domain, or a
_atproto.<domain>TXT record points to the DID - MX: at least one MX record exists
- SPF: a
v=spf1record exists without+all - DKIM: every declared selector has a
v=DKIM1record - DMARC: a record exists at
_dmarc.<domain>with policy quarantine or reject
- Domain control: the operator's DID handle matches the domain, or a
- If all checks pass, the labeler signs and publishes
verified-mail-operator(and optionallyrelay-member) labels on the operator's DID - Labels are queryable via standard atproto XRPC endpoints (
com.atproto.label.queryLabels,com.atproto.label.subscribeLabels) - A scheduler re-verifies and negates labels if DNS degrades. Default interval is 24h; production runs at 1h via NixOS config.
Lexicons #
The NSID namespace is email.atmos.*, backed by atmos.email. See lexicons/README.md for schemas and open questions.
Repository layout #
cmd/
relay/ Mail engine: SMTP submission, /v1/send HTTP API, queue, DKIM, inbound DSN/FBL
web/ Public HTTP frontend: marketing, /signup, /account, /docs, OAuth callback
labeler/ atproto attestation verifier + label issuer (CBOR-signed labels)
label-api/ PostgreSQL bridge for Osprey to query labeler state
rotate-dkim/ Helper to rotate per-domain DKIM keys
sendtest/ Manual SMTP test harness
internal/
relay/ Queue, warming, DKIM signer, category policy, suppression, bounces
relaystore/ SQLite schema + migrations for the relay
admin/ Admin HTTP API + member self-service endpoints
atpoauth/ atproto OAuth client (DPoP, PAR, callback handling)
sessions/ JWT issuer/verifier for enroll-auth + recovery flows
jetstream/ Firehose consumer with collection filtering
scheduler/ Re-verification scheduler + PLC tombstone polling
osprey/ Trust-and-safety event emitter (Kafka, on-disk DLQ)
dns/ MX, SPF, DKIM, DMARC verifier (injectable resolver)
domain/ DID-to-domain control verification
...
web-frontend/ Astro SPA (per-domain dashboard, signup, marketing pages)
lexicons/ email.atmos.* lexicon definitions
infra/ Terraform (DNS via Bunny) + NixOS modules for relay + ops hosts
docs/ Operator runbook, deployment specs, audits
Building #
The repo is a monorepo with multiple binaries. Build everything:
go build ./...
Or one at a time:
go build ./cmd/relay # mail engine
go build ./cmd/web # HTTP frontend
go build ./cmd/labeler # attestation verifier + label issuer
go build ./cmd/label-api # PostgreSQL label bridge
Running the labeler #
The labeler is the easiest component to run in isolation (no SMTP, no Postgres dependency). Useful as a starting point.
Initialize state directory and signing key:
./labeler -init
This creates ./state/config.json (if missing) and generates a secp256k1 signing key. The labeler's did:key is printed to stdout.
Start the labeler:
./labeler -config ./state/config.json
Labeler configuration #
Copy config.json.example to ./state/config.json. All fields have defaults:
| Field | Default | Description |
|---|---|---|
listenAddr |
:8081 |
XRPC server bind address |
stateDir |
./state |
SQLite database (labels.sqlite) and key storage |
jetstreamURL |
wss://jetstream1.us-east.bsky.network/subscribe |
Jetstream endpoint |
signingKeyPath |
./state/signing.key |
secp256k1 private key (hex) |
reverifyInterval |
24h |
Re-verification cadence (production deploys override to 1h) |
Config files support comments and trailing commas (hujson).
Running the relay + web #
The relay and web binaries are deployed together on a VPS via NixOS. See infra/nixos/ for the production modules and docs/operator-runbook.md for the operational playbook. A standalone dev setup is non-trivial (requires Postgres for label-api, optional Kafka for Osprey, and DNS for the labeler to verify against); the runbook is the canonical guide.
Testing #
go test ./...
Architecture #
See docs/operator-runbook.md for the live deployment topology and docs/phase-5c-spec.md for the current HTTP-split design (cmd/relay owns the public TLS listener and reverse-proxies HTML routes to cmd/web on loopback).
License #
AGPL-3.0-or-later. See LICENSE.
Community #
- CONTRIBUTING: workflow, branching, commits, DCO sign-off.
- CODE_OF_CONDUCT: Contributor Covenant 2.1.
- SECURITY: vulnerability disclosure policy. Report to
security@comail.at.
Author #
Scott Lanoue (@scottlanoue.com)