# Deliverability

Polite sending, bounces, complaints and reputation on autopilot.

## Polite sending

Bunzzle keeps a speed per destination (per provider for Gmail, Microsoft, Yahoo and others), starting from conservative limits. When a receiver says "too many connections", the speed is halved and held; when it says your IP is blocked, sending there pauses, then resumes with one probe email and climbs back slowly. Connections are reused. Emails sent with `"lane": "bulk"` never delay transactional ones: a password reset goes ahead of any queued newsletter, while bulk still gets a small guaranteed share. `GET /paths` (or `bunzzle paths`) shows each destination's current speed, its limits and every change with the reason.

## Bounces, complaints and unsubscribes

Receivers report back, and Bunzzle acts on it:

- Signed return path. Once `bounce.your.domain` has the MX and SPF records `domains add` prints, mail is sent with `MAIL FROM:<b-…@bounce.your.domain>`. The token is signed, so every bounce maps to the exact email, and forged bounces are refused. SPF then also aligns with your domain.
- An inbound listener (`BUNZZLE_INBOUND_PORT`, 25 in production) accepts bounce, unsubscribe and feedback addresses, and addresses with a route. It reads standard bounces (DSN), non-standard bounces, complaint reports (ARF, to `fbl@bounce.your.domain`) and ignores auto-replies. With a certificate (`BUNZZLE_INBOUND_TLS_CERT_FILE`, `BUNZZLE_INBOUND_TLS_KEY_FILE`) it offers STARTTLS, so bounces and complaints, which quote your recipients' addresses, arrive encrypted. An address that keeps guessing recipients, sending unknown commands or idling is refused for 15 minutes; the submission ports do the same.
- Inbound routes. `bunzzle routes add support@your.domain https://app.your.domain/inbound` (or `POST /routes`) posts every message for that address, or for `*@your.domain`, to your app as JSON: envelope, headers, text, HTML and attachments, or the raw message with `--raw`. SPF, DKIM, DMARC and blocklist results are in `auth`, and `BUNZZLE_INBOUND_REFUSE` refuses failing mail. `bunzzle hooks add inbound URL --max-score N` sends each message for a route to a spam or virus filter before it is accepted, the same way as a `scan` hook, with the client's IP in `Ip` and its HELO name in `Helo`. The score is in `spam_score`; at `--max-score` or above the message is refused with 550, and a filter that does not answer gets 451 with `--on-error refuse`. Requests are signed in the Standard Webhooks format with the secret shown once. 429 and 5xx answers are retried, with `Retry-After` honored, for about a day; any other 4xx stops at once, and `bunzzle routes retry ID` sends those again. Point the domain's MX at this server.
- A suppression list, checked for every email: hard bounces stop all mail for a year, a domain that doesn't exist for 30 days; complaints and unsubscribes stop bulk mail only, for good. Password resets still reach someone who unsubscribed from a newsletter. `GET/POST/DELETE /suppressions` or `bunzzle suppressions`.
- One-click unsubscribe (required by Gmail and Yahoo for bulk senders) is added to every `"lane": "bulk"` email, signed into DKIM, hosted at `BUNZZLE_PUBLIC_URL/u/…`.
- Webhooks: `email.delivered`, `email.delivery_delayed`, `email.bounced`, `email.complained`, `email.unsubscribed`, `email.suppressed`, `email.failed`, `email.canceled`, `email.opened`, `email.clicked`, `project.limit_approaching`. Signed in the Standard Webhooks format. Retried for about a day. Webhook URLs that point to private networks are refused. `bunzzle webhooks deliveries ID` (or `GET /webhooks/:id/deliveries`) lists each delivery with every attempt, and `bunzzle webhooks resend` sends one delivery again with the same `webhook-id`, or queues a time range (`POST /webhooks/:id/resend`) that goes out in small batches at the pace the endpoint accepts.
- Event log. With `BUNZZLE_EVENT_LOG=true` every event also goes to local files, one JSON object per line, rotated daily and gzipped, for log shippers and `grep` (see `docs/configuration.md`).

## Reputation

- IPs and warm-up. List your sending IPs in `BUNZZLE_SOURCE_IPS` (`ip` or `ip=bulk` for a bulk pool). Each IP's reverse DNS is checked and its PTR name used as EHLO. A new IP warms up by itself: a daily budget per provider that doubles in week one and grows by half after, but only when yesterday was healthy (few deferrals, bounces, complaints, no blocks); trouble steps it back and alerts you. `bunzzle ips` shows "warming, day 6 of ~14, today's budget 1600". IPs in `BUNZZLE_ESTABLISHED_IPS` skip warm-up.
- Guards. Bulk mail for a domain is held before Gmail's 0.3% complaint line, counted from complaint reports and, once Postmaster Tools is connected, from Gmail's own numbers (or at 5% hard bounces), and bulk mail from an API key is held when it suddenly sends ten times its normal volume, as a leaked key would. Transactional mail keeps flowing. `bunzzle holds` shows why; `bunzzle holds release ID` reopens.
- TLS reports. Domains that ask for them (an `_smtp._tls` record) get a daily report (RFC 8460) on how TLS went with their mail servers: sessions, certificate problems, missing STARTTLS, per MTA-STS policy. HTTPS reports go out by themselves; emailed reports need `BUNZZLE_TLSRPT_FROM`.
- DMARC reports. With the inbound listener set up, the DMARC record `domains add` prints asks mailbox providers to send their daily aggregate reports to `dmarc@bounce.your.domain`. Bunzzle reads them (zip, gzip or plain XML) and `GET /domains/:name/dmarc` or `bunzzle domains reports NAME` shows, per day, which IPs sent mail as your domain and whether it passed DKIM and SPF. A server you don't know in that list is someone sending as you.
- DANE. Recipient domains that sign their DNS and publish TLSA records for their MX hosts get TLS checked against those records, even without a public certificate. Bunzzle checks the DNSSEC signatures itself, from the root down, so no extra resolver is needed; a DNSSEC-validating resolver on the same machine (for example on `127.0.0.1`) is used instead when there is one. Answers whose signatures do not check out are not used: their email waits and is retried. A server whose certificate matches none of them is never used. `GET /emails/:id` shows `dane` as the TLS status when it applied.
- MTA-STS. Recipient domains that publish an MTA-STS policy get exactly what they ask for: only their listed MX hosts, only over TLS with a valid certificate.
- REQUIRETLS. An email sent with `require_tls: true` (`--require-tls`, or `REQUIRETLS` on `MAIL FROM` over SMTP submission) goes only to a mail server found through DNSSEC or an enforced MTA-STS policy, over TLS with a verified certificate, and only if that server supports REQUIRETLS too, so the requirement travels with the email (RFC 8689). Otherwise it bounces right away (class `requiretls-unsupported`, the address is not suppressed); few mailbox providers support REQUIRETLS yet. `BUNZZLE_REQUIRETLS=relaxed` still requires TLS with a verified certificate but drops the other two checks. A `TLS-Required: No` header does the opposite: the email ignores the recipient's MTA-STS and DANE policies, for reaching a server with a broken certificate.
- DKIM rotation. Keys rotate every 90 days (`BUNZZLE_DKIM_ROTATE_DAYS`): new keys are created and published, Bunzzle switches only after their DNS records verify and a real signature checks out against live DNS, and the old keys stay published for 7 days. With `BUNZZLE_CLOUDFLARE_API_TOKEN` (DNS edit for your zone), every record Bunzzle owns is published for you.
- `bunzzle doctor` checks the configuration, every IP's reverse DNS and blocklist listings, every DNS record of every domain (and what to change), signs a test message and verifies it against live DNS, and lists holds, paused destinations and failing webhooks.
- `bunzzle dns-test DOMAIN` publishes a test record through the configured DNS provider, checks that every name server of the zone serves it, and removes it.