Skip to content

Build1 publisher3 min readPublished

Suppressing on the three-digit code converts a policy refusal into a dead address

In four codebases a dev.to author found bounce handlers that read 5.x.x and stopped, so a receiver's verdict on the sending domain got written down as a dead mailbox. The RFC 3463 subject digit separates the two.

The Engineer · Build desk

Illustration accompanying Suppressing on the three-digit code converts a policy refusal into a dead address

What happened

  • A dev.to post describes a bounce handler that parses the status code, sees a 5.x.x, concludes the address is bad, and writes the recipient to the suppression list.
  • 550 5.1.1 and 550 5.7.1 are both permanent and both 550, but the second is a policy rejection in which the receiver evaluated the sender's IP, domain, authentication or reputation and declined.
  • The post singles out 5.2.2, which is formatted as permanent but behaves as temporary, because mailboxes get emptied.
  • Its classifier keys on the RFC 3463 enhanced code: subject 1 is safe to suppress, subject 7 is about the sender and never suppressed, and a response with no enhanced code is recorded as unknown.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint The bug moves the metric an operator would use to catch it in the direction that looks good, because suppressed addresses stop being retried, so a falling bounce rate is not evidence the list is healthy.
  • decision Fixing it changes the schema: refusals have to be keyed by sending identity and receiving provider. That means a second table and a pause switch per pair.
  • cost Mis-suppression puts the cost in the wrong place, converting a DNS-level authentication fault that is checkable in thirty seconds into permanent list loss the sender cannot undo.
  • exposure The addresses at risk are live mailboxes belonging to people who never bounced, so the damage falls on the most engaged part of a list rather than on dead records.

The fix is a dispatch on one digit. A regex, `\b([245])\.(\d{1,3})\.(\d{1,3})\b`, searches the response for an enhanced status code; class 5 with subject 1 returns `bad_recipient` and is safe to suppress, subject 7 returns `policy` and is never suppressed, and a response with no enhanced code at all returns `unknown` instead of a guess [14][20].

Subject 2 falls through to `other`. A full mailbox, 5.2.2, lands there, which keeps it out of the suppression path even though the receiver formatted it as permanent [7][1].

Two parse details decide whether any of that holds. The search runs over the whole response, because multi-line replies routinely carry the enhanced code on a continuation line [15]. And plenty of servers put a code in the human-readable text that differs from the one in the status field [16], so the leftmost match wins, and which of the two codes that is depends on how the server formatted its reply [2].

Then the ledger. Policy refusals go in their own table, keyed by sending domain or IP, receiving provider, code and raw text, and the decisions come from aggregates [9]. Rising 5.7.x from one provider against one sending domain means pausing that pair and fixing the cause [10]. The same code across every provider points at the domain or the IP [11]. The hold pauses sending from that sender to that provider, leaves the recipients untouched, and lifts when the cause is fixed [13].

Subject 7 is coarse, and it costs you at diagnosis time. Google's 5.7.26 means unauthenticated, which the post describes as an authentication failure fixable in DNS this afternoon and not a reputation verdict [12]. Under the classifier it is `policy`, the same bucket as a 5.7.1 unsolicited-mail refusal [3]. SPF, DKIM and DMARC against live DNS is a thirty-second check, while reputation is the expensive case and no DNS change fixes it [19]. A handler that reads only the subject digit suppresses correctly and diagnoses nothing.

The failure is hard to spot because it improves the number you would use to spot it. Suppressing policy refusals lowers the bounce rate, since the sender stops retrying the addresses that were refusing it, and the list gets smaller and cleaner-looking [17].

On the ambiguous case the author wrote that a bare 550 from an unfamiliar server "is not enough to destroy an address over" [18]. In my view that is the right default anywhere suppression writes are permanent.

The evidence here is four codebases, one of them the author's own [1]. The post does not measure how often policy refusals end up on suppression lists. What it does support is checkable in your own source: the line that maps a 5xx to a suppression write either reads the subject digit or it does not.

What to watch

  • Whether any sender publishes a measured rate for the share of suppressions that came from 5.7.x refusals, which would turn a four-codebase anecdote into a figure.
  • Whether major receivers keep the enhanced code in the status field and not only in the reply text, since the first-match parse depends on it.
  • Whether a provider renumbers or retires a code like 5.7.26, which would break handlers that read the detail digits for diagnosis.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories