|
| 1 | +# The Operator Cannot Ask Whether Their Own Domain Is Blocked |
| 2 | + |
| 3 | +> ## 📥 The last open criterion of [`p1-e2-email-bounce-detection-suppression`](p1-e2-email-bounce-detection-suppression.md), split out because the rest of that card closed. |
| 4 | +> |
| 5 | +> That card asked for two answers: *"what is my bounce rate?"* and *"is my domain listed?"*. The first |
| 6 | +> is arithmetic the mail log can already do. The second is not built at all, and this card is it. |
| 7 | +
|
| 8 | +- **Status:** To Do |
| 9 | +- **Priority:** High — the failure it prevents has already happened twice, and the second time was |
| 10 | + caught by hand. |
| 11 | +- **Effort:** Small |
| 12 | +- **Area:** OpenOutSend — a reputation read over the sending identity, and somewhere to show it. |
| 13 | + |
| 14 | +## The incident that produced this card |
| 15 | + |
| 16 | +**2026-08-27.** Preparing to resume sending, the operator's configured mailbox was |
| 17 | +`eracle@indieoutreach.app`. Before enabling it, an assistant ran the query by hand: |
| 18 | + |
| 19 | +``` |
| 20 | +multi.surbl.org indieoutreach.app → 127.0.0.64 listed (abuse) |
| 21 | +dbl.spamhaus.org indieoutreach.app → 127.0.1.2 listed (spam domain) |
| 22 | +zen.spamhaus.org not listed |
| 23 | +``` |
| 24 | + |
| 25 | +**Still listed, three weeks after the 2026-08-06 incident.** Authentication was clean the whole time — |
| 26 | +SPF, DKIM, DMARC, Google MX — so nothing the operator could see said anything was wrong. The plan that |
| 27 | +day was to resume cold outreach from that domain and would have gone ahead unquestioned. |
| 28 | + |
| 29 | +**Nothing in the product would have said a word.** `warmth.py` measures the box's own bounce rate and |
| 30 | +halves capacity above tolerance, which is a *lagging* signal: it reacts once mail is already failing. |
| 31 | +A blocklist entry is the thing that makes mail fail, and it is public, free to query, and invisible to |
| 32 | +this system. |
| 33 | + |
| 34 | +The same session found the other half of why that domain got listed: the old install recorded **zero |
| 35 | +bounces across 729 sends** and misfiled three non-delivery reports as human replies, so its true bounce |
| 36 | +rate was never measurable by anyone. Suppression now fixes the cause. This card fixes the *noticing*. |
| 37 | + |
| 38 | +## User Story |
| 39 | + |
| 40 | +As an operator about to send cold email, I want the tool to tell me whether my sending domain is on a |
| 41 | +public blocklist — before it sends anything — so that I find out from my own software rather than from |
| 42 | +a silent collapse in reply rate, and so that I never resume sending from a domain that is already |
| 43 | +burned. |
| 44 | + |
| 45 | +**And once it is listed, I want to know it is still listed**, so that "we fixed the bounces" and "we |
| 46 | +are delisted" stay separate facts and I can tell when the recovery has actually worked. |
| 47 | + |
| 48 | +## What to build |
| 49 | + |
| 50 | +### 1. The check |
| 51 | + |
| 52 | +A DNS query per list, over the **domain** of the mailbox's `from_address`. Listed means an `A` record |
| 53 | +exists at `<domain>.<zone>`; unlisted means `NXDOMAIN`. |
| 54 | + |
| 55 | +| list | zone | what a hit means | |
| 56 | +|---|---|---| |
| 57 | +| Spamhaus DBL | `dbl.spamhaus.org` | the domain itself is classed as spam-associated | |
| 58 | +| SURBL | `multi.surbl.org` | the domain appears in spam bodies; `127.0.0.64` is the abuse list | |
| 59 | + |
| 60 | +**IP blocklists are deliberately out of scope.** Mail leaves through Gmail's or a relay's addresses, |
| 61 | +so the sending IP's reputation is Google's and not the operator's — checking `zen.spamhaus.org` would |
| 62 | +report on somebody else's asset and teach the operator nothing they can act on. Domain reputation is |
| 63 | +the part they own, and the part they can lose. |
| 64 | + |
| 65 | +### 2. Two traps that will otherwise be read as answers |
| 66 | + |
| 67 | +- **A public resolver poisons the result.** Spamhaus refuses queries arriving from large open |
| 68 | + resolvers (`8.8.8.8`, `1.1.1.1`) and answers `127.0.1.255` — *query blocked*, not *listed*. Read |
| 69 | + naively that is a false positive on every domain. The return code has to be interpreted, not merely |
| 70 | + tested for existence, and a blocked query must report **unknown**, never listed and never clean. |
| 71 | + A VM whose `/etc/resolv.conf` points at a cloud metadata resolver may hit exactly this. |
| 72 | +- **A consumer mailbox has no domain reputation to check.** Querying `gmail.com` will always come back |
| 73 | + clean and means nothing — the operator shares that reputation with everyone. For a consumer address |
| 74 | + the honest answer is *"you have no domain of your own; a suspension here takes the mailbox with it"*, |
| 75 | + not a green tick. |
| 76 | + |
| 77 | +### 3. Where it surfaces |
| 78 | + |
| 79 | +**`outsend status`**, a third verb beside `init` and `send`, mirroring the finder's. It answers without |
| 80 | +sending: which mailbox, its measured capacity and headroom, its bounce rate from the mail log, and the |
| 81 | +reputation of its domain. The bounce-rate half needs no new data — `report.bounce_rate` already exists |
| 82 | +and nothing shows it to anybody. |
| 83 | + |
| 84 | +**And once per send pass**, cheaply, so a listing that appears mid-campaign is noticed within a pass |
| 85 | +rather than at the next time somebody thinks to look. |
| 86 | + |
| 87 | +### 4. What it must not do |
| 88 | + |
| 89 | +**It must not stop sending on its own.** A DNS timeout, a rate-limited resolver, or a mirror having a |
| 90 | +bad day would silently halt a campaign, and the operator would have no idea why. Say it loudly, in the |
| 91 | +pass's narration and in `status`; leave the decision with the person. This is the same rule |
| 92 | +`warmth.py` follows — it *reduces* capacity, it does not refuse to run. |
| 93 | + |
| 94 | +## Done when |
| 95 | + |
| 96 | +- [ ] `outsend status` reports, for each configured mailbox: bounce rate, remaining headroom, and |
| 97 | + whether its domain is listed on DBL and SURBL. |
| 98 | +- [ ] A blocked or failed query reports **unknown** and is visibly different from **clean**. |
| 99 | +- [ ] A consumer mailbox (`gmail.com` and friends) is described as having no domain of its own rather |
| 100 | + than being reported clean. |
| 101 | +- [ ] A send pass surfaces a listing without refusing to send. |
| 102 | +- [ ] No test reaches the network — the resolver is mocked at the boundary, with the real return codes |
| 103 | + (`127.0.1.2`, `127.0.0.64`, `127.0.1.255`, `NXDOMAIN`) as the cases. |
| 104 | + |
| 105 | +## Not this card |
| 106 | + |
| 107 | +- Requesting delisting. That is a form on someone else's website and a human decision. |
| 108 | +- Warming a new domain, or choosing one. Separate, and mostly not software. |
| 109 | +- Postmaster Tools / seed-list placement testing — richer signals, both needing volume this project |
| 110 | + does not have yet. |
0 commit comments