diff --git a/scripts/burner_list_verify.py b/scripts/burner_list_verify.py index dc5714b..110bf53 100644 --- a/scripts/burner_list_verify.py +++ b/scripts/burner_list_verify.py @@ -48,8 +48,11 @@ STATUS_RE = re.compile(r"status=(\w+)") # Results we are allowed to UPGRADE to 'send_confirmed'. We never overwrite an # explicit smtp_valid (already best) or a hard_bounced (worse signal wins). -UPGRADABLE = ("catch_all_domain", "catch_all_detected", "mx_unreachable", - "smtp_temp_error", "smtp_unknown_451", "smtp_unknown_450") +# 'mx_probe_blocked' is the big-ISP pool (Comcast/AT&T/Verizon/etc.) the SMTP +# probe couldn't reach — these are the prime burner-verification targets. +UPGRADABLE = ("catch_all_domain", "catch_all_detected", "mx_probe_blocked", + "mx_unreachable", "smtp_temp_error", "smtp_unknown_451", + "smtp_unknown_450") def scan_log(log_path: str) -> tuple[set[str], set[str]]: diff --git a/scripts/workers/email_verifier.py b/scripts/workers/email_verifier.py index f83933d..ccfa583 100644 --- a/scripts/workers/email_verifier.py +++ b/scripts/workers/email_verifier.py @@ -162,8 +162,15 @@ def verify_email(email: str) -> tuple[bool, str]: LOG.debug("SMTP error for %s via %s: %s", email, mx_host, e) continue - # Couldn't connect to any MX — domain exists but server unreachable - return True, "mx_unreachable" + # Could not complete an SMTP probe to ANY MX, even though valid MX records + # exist. This does NOT mean the address is dead — large providers (Comcast, + # AT&T/Yahoo, Verizon, Cox, Charter, etc.) deliberately tarpit / refuse port-25 + # probes from unknown IPs as an anti-spam measure, so the probe times out on + # millions of perfectly deliverable mailboxes. We therefore return + # email_verified=FALSE with 'mx_probe_blocked': the domain has mail servers but + # deliverability is UNKNOWN and must be confirmed by a real send (burner-domain + # verification → 'send_confirmed'). Campaigns must NOT treat this as sendable. + return False, "mx_probe_blocked" def verify_table(table: str, limit: int | None = None, dry_run: bool = False, where: str | None = None) -> dict: