Upload folder using huggingface_hub
Browse files
app/validator/mailbox_provider.py
CHANGED
|
@@ -44,8 +44,14 @@ def map_mv_result(payload: dict) -> SMTPResult:
|
|
| 44 |
return SMTPResult(verified=True, smtp_code=250, smtp_message="provider: ok")
|
| 45 |
if result == "invalid":
|
| 46 |
# A provider-confirmed dead mailbox is a permanent (5xx-class) reject.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
return SMTPResult(
|
| 48 |
-
verified=False,
|
|
|
|
|
|
|
| 49 |
)
|
| 50 |
if result in {"catch_all", "unknown", "disposable"}:
|
| 51 |
# None of these proves the specific mailbox exists. catch_all accepts
|
|
|
|
| 44 |
return SMTPResult(verified=True, smtp_code=250, smtp_message="provider: ok")
|
| 45 |
if result == "invalid":
|
| 46 |
# A provider-confirmed dead mailbox is a permanent (5xx-class) reject.
|
| 47 |
+
# The message MUST carry an explicit mailbox-rejection phrase so
|
| 48 |
+
# scoring._is_explicit_mailbox_rejection() hard-fails it to score 0
|
| 49 |
+
# (Invalid) — a bare "invalid" would be softened to -30 "inconclusive"
|
| 50 |
+
# and mis-score a definitive answer as risky.
|
| 51 |
return SMTPResult(
|
| 52 |
+
verified=False,
|
| 53 |
+
smtp_code=550,
|
| 54 |
+
smtp_message=error or "550 mailbox does not exist (provider confirmed)",
|
| 55 |
)
|
| 56 |
if result in {"catch_all", "unknown", "disposable"}:
|
| 57 |
# None of these proves the specific mailbox exists. catch_all accepts
|
tests/test_mailbox_provider.py
CHANGED
|
@@ -27,6 +27,15 @@ class MapMVResultTests(unittest.TestCase):
|
|
| 27 |
self.assertFalse(r.verified)
|
| 28 |
self.assertEqual(r.smtp_code, 550)
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
def test_catch_all_maps_to_inconclusive(self):
|
| 31 |
# A catch-all domain accepts the probe but proves nothing about the
|
| 32 |
# specific mailbox — it must NOT read as verified.
|
|
@@ -78,6 +87,45 @@ class MillionVerifierProviderTests(unittest.IsolatedAsyncioTestCase):
|
|
| 78 |
self.assertGreater(confidence, 0.5)
|
| 79 |
|
| 80 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
class ResolveProviderTests(unittest.TestCase):
|
| 82 |
def test_api_forced(self):
|
| 83 |
with patch.object(settings, "mailbox_provider", "api"), patch.object(
|
|
|
|
| 27 |
self.assertFalse(r.verified)
|
| 28 |
self.assertEqual(r.smtp_code, 550)
|
| 29 |
|
| 30 |
+
def test_invalid_carries_explicit_rejection_phrase(self):
|
| 31 |
+
# A provider-confirmed dead mailbox must score 0 (Invalid), not be
|
| 32 |
+
# softened to -30 "inconclusive". calculate_score() only hard-fails a
|
| 33 |
+
# 5xx when the message carries an explicit mailbox-rejection phrase, so
|
| 34 |
+
# the adapter must supply one — otherwise a definitive "invalid" is
|
| 35 |
+
# mis-scored as risky/valid.
|
| 36 |
+
r = map_mv_result({"result": "invalid", "quality": "bad", "resultcode": 4})
|
| 37 |
+
self.assertIn("does not exist", (r.smtp_message or "").lower())
|
| 38 |
+
|
| 39 |
def test_catch_all_maps_to_inconclusive(self):
|
| 40 |
# A catch-all domain accepts the probe but proves nothing about the
|
| 41 |
# specific mailbox — it must NOT read as verified.
|
|
|
|
| 87 |
self.assertGreater(confidence, 0.5)
|
| 88 |
|
| 89 |
|
| 90 |
+
class EngineProviderVerdictTests(unittest.IsolatedAsyncioTestCase):
|
| 91 |
+
"""End-to-end: a provider-confirmed dead mailbox must hard-fail to score 0
|
| 92 |
+
(Invalid) through the whole engine, not be softened to 'risky' (the live
|
| 93 |
+
mis-score observed before the adapter supplied an explicit rejection phrase)."""
|
| 94 |
+
|
| 95 |
+
async def test_provider_invalid_scores_zero(self):
|
| 96 |
+
from unittest.mock import AsyncMock, patch as _patch
|
| 97 |
+
from app.models import DNSResult, DisposableResult, SyntaxResult
|
| 98 |
+
from app.validator.engine import EmailValidationEngine
|
| 99 |
+
|
| 100 |
+
engine = EmailValidationEngine(mailbox=MillionVerifierProvider(api_key="k"))
|
| 101 |
+
|
| 102 |
+
class DeadMailboxProvider:
|
| 103 |
+
async def verify(self, email, mx_host):
|
| 104 |
+
return map_mv_result({"result": "invalid", "resultcode": 4})
|
| 105 |
+
async def catch_all(self, domain, mx_host):
|
| 106 |
+
return (False, 0.9)
|
| 107 |
+
|
| 108 |
+
engine.mailbox = DeadMailboxProvider()
|
| 109 |
+
with (
|
| 110 |
+
_patch("app.validator.engine.validate_syntax", return_value=SyntaxResult(
|
| 111 |
+
passed=True, normalized_email="a@example.test",
|
| 112 |
+
domain="example.test", local_part="a",
|
| 113 |
+
)),
|
| 114 |
+
_patch("app.validator.engine.check_dns", new=AsyncMock(return_value=DNSResult(
|
| 115 |
+
domain_exists=True, has_mx_records=True,
|
| 116 |
+
mx_records=[{"host": "mx.example.test", "priority": 10}],
|
| 117 |
+
))),
|
| 118 |
+
_patch("app.validator.engine.check_disposable", return_value=DisposableResult(
|
| 119 |
+
is_disposable=False, is_free_provider=False, is_role_based=False,
|
| 120 |
+
)),
|
| 121 |
+
):
|
| 122 |
+
result = await engine.validate("a@example.test", deep=True)
|
| 123 |
+
|
| 124 |
+
self.assertEqual(result.scoring.score, 0)
|
| 125 |
+
self.assertEqual(result.status.value, "invalid")
|
| 126 |
+
self.assertEqual(result.reason_code, "rcpt_5xx")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
class ResolveProviderTests(unittest.TestCase):
|
| 130 |
def test_api_forced(self):
|
| 131 |
with patch.object(settings, "mailbox_provider", "api"), patch.object(
|