On this page

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.
esc