sitcod3 commited on
Commit
e01d702
·
verified ·
1 Parent(s): c8d0215

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, smtp_code=550, smtp_message=error or "provider: invalid"
 
 
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(