File size: 6,594 Bytes
f5afa05
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0eff3fe
f5afa05
 
0eff3fe
 
f5afa05
 
 
 
 
 
 
 
 
 
0eff3fe
 
 
 
 
 
 
 
 
 
 
 
f5afa05
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9649ce6
 
 
 
 
 
 
 
 
 
0eff3fe
9649ce6
0eff3fe
 
9649ce6
 
 
 
0eff3fe
 
 
 
 
9649ce6
 
 
 
 
 
 
 
f5afa05
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
"""Receiving-provider intelligence — the knowledge layer of the autonomous engine.

A raw SMTP probe returns different *meanings* depending on who is answering:

  - Google Workspace / consumer Gmail reject unknown mailboxes at RCPT with
    550 5.1.1, so a 250 there is real evidence the mailbox exists.
  - Microsoft 365 (and Mimecast/Proofpoint gateways in front of it) accept
    *every* recipient at SMTP time and emit the bounce asynchronously hours
    later. A 250 from those hosts proves nothing, so it must never be read as
    "mailbox verified".
  - Many smaller hosts are catch-all by accident (cPanel defaults).

Recognising the provider from its MX host lets the engine apply the correct
policy instead of treating every 250 as success. This is the standard technique
used by commercial validators, implemented here with no third-party API.
"""
from __future__ import annotations

import re
from dataclasses import dataclass

# MX host suffix -> (provider, accepts_all, trusts_rcpt)
#   accepts_all: accepts every recipient at RCPT (250 is not proof)
#   trusts_rcpt: a 250/550 at RCPT is meaningful evidence
_MX_PATTERNS: tuple[tuple[str, str, bool, bool], ...] = (
    ("google.com", "google", False, True),
    ("googlemail.com", "google", False, True),
    ("outlook.com", "microsoft365", True, False),
    ("protection.outlook.com", "microsoft365", True, False),
    ("office365.com", "microsoft365", True, False),
    ("mimecast.com", "mimecast", True, False),
    ("pphosted.com", "proofpoint", True, False),
    ("ppe-hosted.com", "proofpoint", True, False),
    ("proofpoint.com", "proofpoint", True, False),
    ("barracudanetworks.com", "barracuda", True, False),
    ("mailchannels.net", "mailchannels", True, False),
    ("emailsecurity.app", "emailsecurity", True, False),
    ("yahoodns.net", "yahoo", False, True),
    ("yahoo.com", "yahoo", False, True),
    ("aol.com", "yahoo", False, True),
    ("icloud.com", "apple", False, True),
    ("me.com", "apple", False, True),
    ("zoho.com", "zoho", False, True),
    ("zoho.eu", "zoho", False, True),
    ("protonmail.ch", "proton", False, True),
    ("proton.me", "proton", False, True),
    ("secureserver.net", "godaddy", False, False),
    ("registrar-servers.com", "namecheap", False, False),
    ("ionos.com", "ionos", False, False),
    ("1and1.com", "ionos", False, False),
    ("serverdata.net", "serverdata", False, False),
    ("hostgator.com", "hostgator", False, False),
    ("bluehost.com", "bluehost", False, False),
    ("siteground.com", "siteground", False, False),
    ("dreamhost.com", "dreamhost", False, False),
    ("networksolutions.com", "netsol", False, False),
    ("rackspace.com", "rackspace", False, False),
    ("emailsrvr.com", "rackspace", False, False),
    ("workmail.com", "amazon", False, False),
    ("fastmail.com", "fastmail", False, True),
    ("messagingengine.com", "fastmail", False, True),
    ("yandex.net", "yandex", False, True),
    ("qq.com", "tencent", False, True),
    ("163.com", "netease", False, True),
    ("amazonaws.com", "amazon", False, False),
    ("amazonses.com", "amazon", False, False),
)

_RCPT_CODE_RE = re.compile(r"^\s*(\d{3})")


@dataclass(frozen=True)
class MxPolicy:
    """How to interpret an SMTP response from a given receiving host."""

    provider: str
    trusts_rcpt: bool
    accepts_all: bool


def classify_mx(mx_host: str) -> MxPolicy:
    """Identify the receiving provider from its MX hostname.

    Unknown hosts get a neutral policy: not assumed malicious, but a lone 250
    is not accepted as proof — the catch-all probe must corroborate it.
    """
    host = (mx_host or "").strip().lower().rstrip(".")
    for suffix, provider, accepts_all, trusts_rcpt in _MX_PATTERNS:
        if host == suffix or host.endswith("." + suffix):
            return MxPolicy(
                provider=provider, trusts_rcpt=trusts_rcpt, accepts_all=accepts_all
            )
    return MxPolicy(provider="unknown", trusts_rcpt=False, accepts_all=False)


# Platforms where a CUSTOM domain requires a paid/business account. A
# disposable domain essentially never runs on these — they cost money and
# verify identity — while disposable domains live on cheap self-hosted or
# free-tier MX. When the MX is one of these, the domain-string classifier's
# opinion is unnecessary and, given an imbalanced training set, harmful: it
# flagged a legitimate small business (Google Workspace MX) as disposable.
_TRUSTED_MX_PROVIDERS = frozenset({
    "google",        # Google Workspace (paid for custom domains)
    "microsoft365",  # Microsoft 365 / Exchange Online
    "mimecast",      # enterprise email security gateway
    "proofpoint",    # enterprise + Essentials gateway (pphosted/ppe-hosted)
    "barracuda",     # enterprise email security gateway
    "mailchannels",  # mail infrastructure for hosting providers (paid)
    "emailsecurity", # hosted email security gateway
    "zoho",          # Zoho Mail (paid for custom domains)
    "proton",        # Proton Business
    "fastmail",      # Fastmail (paid custom domain)
    "apple",         # iCloud+ Custom Email Domain
    # Business hosting / mailbox providers: a paid, domain-verified account is
    # required, so their customers are real businesses (observed in the user's
    # 800-row export: ionos, namecheap, serverdata).
    "godaddy", "namecheap", "ionos", "serverdata", "hostgator",
    "bluehost", "siteground", "dreamhost", "netsol", "rackspace", "amazon",
})


def mx_is_trusted(mx_host: str) -> bool:
    """True when the MX runs on a paid enterprise/business mail platform."""
    return classify_mx(mx_host).provider in _TRUSTED_MX_PROVIDERS


def parse_rcpt_code(response: str) -> int | None:
    """Extract the numeric SMTP status from a response line, or None."""
    match = _RCPT_CODE_RE.match(response or "")
    return int(match.group(1)) if match else None


def is_greylist_code(code: int | None) -> bool:
    """4xx — temporary (greylisting / rate limit / mailbox busy)."""
    return code is not None and 400 <= code < 500


def is_permanent_reject(code: int | None) -> bool:
    """5xx — permanent failure (mailbox does not exist, domain refuses)."""
    return code is not None and 500 <= code < 600


def should_retry(code: int | None, *, transport_error: bool = False) -> bool:
    """Whether the same address is worth a second attempt later.

    Only temporary conditions qualify: 4xx greylisting and transport failures.
    A 5xx or a 250 is final.
    """
    if transport_error:
        return True
    return is_greylist_code(code)