bounce-sync: stop blocklisting good carriers on first auth/policy bounce
This script ran every 5 min and blocklisted on the FIRST hard bounce of ANY 5xx DSN via direct SQL, bypassing Listmonk's count-based bounce.actions rule. That is the actual mechanism that wrongly killed ~17,000 good carriers during the broken-DKIM window (their mail got 5.7.1 DMARC-reject, not bad-mailbox). Fix: only genuine bad-mailbox DSNs (5.1.1/5.1.0/5.0.0/5.4.1/5.5.0) count toward a blocklist, and a subscriber must accumulate >=3 such hard bounces (matching Listmonk's threshold) before being blocklisted. Reputation/policy 5.7.x and quota/greylist 5.2.x never trigger a blocklist.
This commit is contained in:
parent
f3442872f2
commit
bfdbf8f031
1 changed files with 34 additions and 5 deletions
|
|
@ -157,21 +157,50 @@ def main():
|
|||
|
||||
print(f" Inserted: {inserted}, Skipped (existing): {skipped}, No subscriber: {no_subscriber}")
|
||||
|
||||
# Blocklist subscribers with hard bounces (Listmonk's own behavior)
|
||||
# Blocklist subscribers ONLY for genuine bad-mailbox bounces, and only
|
||||
# once they cross the same 3-strike threshold Listmonk's bounce.actions
|
||||
# uses. This script previously blocklisted on the FIRST hard bounce of
|
||||
# ANY 5xx DSN -- including 5.7.1 (DMARC/auth/"low reputation" policy
|
||||
# rejections). During the Jun broken-DKIM window that wrongly killed
|
||||
# ~17,000 good carriers in one pass (a deliverability bug, not bad
|
||||
# addresses). Now: policy/reputation DSNs (5.7.x) and greylist/quota
|
||||
# (5.2.x) never trigger a blocklist, and a real bad-mailbox address must
|
||||
# accumulate >= HARD_BOUNCE_BLOCKLIST_THRESHOLD distinct hard bounces.
|
||||
if not dry_run and inserted > 0:
|
||||
# DSN prefixes that indicate a genuinely undeliverable mailbox
|
||||
# (vs. sender-reputation/policy or transient mailbox-full issues).
|
||||
BAD_MAILBOX_DSNS = ("5.1.1", "5.1.10", "5.1.0", "5.0.0", "5.4.1", "5.5.0")
|
||||
HARD_BOUNCE_BLOCKLIST_THRESHOLD = 3
|
||||
|
||||
new_hard = [
|
||||
b for b in bounces
|
||||
if b["type"] == "hard"
|
||||
and b["email"] in sub_map
|
||||
and b["email"] not in already_bounced
|
||||
and b["dsn"].startswith(BAD_MAILBOX_DSNS)
|
||||
]
|
||||
if new_hard:
|
||||
# Count TOTAL distinct hard bounces per subscriber (existing in
|
||||
# the DB + the new one) and blocklist only those at/over the
|
||||
# threshold, exactly as Listmonk's own count-based rule would.
|
||||
sids = ",".join(str(sub_map[b["email"]]) for b in new_hard)
|
||||
run_sql(
|
||||
f"UPDATE subscribers SET status = 'blocklisted' "
|
||||
f"WHERE id IN ({sids}) AND status != 'blocklisted';"
|
||||
over_threshold = run_sql(
|
||||
f"SELECT subscriber_id FROM bounces "
|
||||
f"WHERE subscriber_id IN ({sids}) AND type = 'hard' "
|
||||
f"GROUP BY subscriber_id "
|
||||
f"HAVING count(*) >= {HARD_BOUNCE_BLOCKLIST_THRESHOLD};"
|
||||
)
|
||||
print(f" Blocklisted {len(new_hard)} hard-bounce subscribers")
|
||||
bl_sids = [s.strip() for s in over_threshold.split("\n") if s.strip()]
|
||||
if bl_sids:
|
||||
run_sql(
|
||||
f"UPDATE subscribers SET status = 'blocklisted' "
|
||||
f"WHERE id IN ({','.join(bl_sids)}) AND status != 'blocklisted';"
|
||||
)
|
||||
print(f" Blocklisted {len(bl_sids)} subscribers "
|
||||
f"(>= {HARD_BOUNCE_BLOCKLIST_THRESHOLD} bad-mailbox hard bounces)")
|
||||
else:
|
||||
print(f" Recorded {len(new_hard)} bad-mailbox hard bounces; "
|
||||
f"none yet at {HARD_BOUNCE_BLOCKLIST_THRESHOLD}-strike blocklist threshold")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue