b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
110 lines
3.7 KiB
Python
110 lines
3.7 KiB
Python
"""Email adapter robustness against malformed IMAP responses (salvage of #2794).
|
|
|
|
Validates that:
|
|
- Malformed IMAP fetch responses are skipped instead of aborting the batch
|
|
(UIDs are marked seen before fetch, so an abort permanently loses messages)
|
|
- Message-ID generation handles a missing '@' in EMAIL_ADDRESS
|
|
"""
|
|
|
|
import os
|
|
import unittest
|
|
import uuid
|
|
from email.mime.text import MIMEText
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
def _make_adapter(address="hermes@test.com"):
|
|
from gateway.config import PlatformConfig
|
|
|
|
with patch.dict(os.environ, {
|
|
"EMAIL_ADDRESS": address,
|
|
"EMAIL_PASSWORD": "secret",
|
|
"EMAIL_IMAP_HOST": "imap.test.com",
|
|
"EMAIL_SMTP_HOST": "smtp.test.com",
|
|
}):
|
|
from plugins.platforms.email.adapter import EmailAdapter
|
|
|
|
adapter = EmailAdapter(PlatformConfig(enabled=True))
|
|
return adapter
|
|
|
|
|
|
def _raw_email(sender="user@test.com", subject="Hello"):
|
|
msg = MIMEText("Test body", "plain", "utf-8")
|
|
msg["From"] = sender
|
|
msg["Subject"] = subject
|
|
msg["Message-ID"] = f"<{uuid.uuid4().hex[:8]}@test.com>"
|
|
return msg.as_bytes()
|
|
|
|
|
|
class TestImapResponseGuard(unittest.TestCase):
|
|
"""_fetch_new_messages skips messages with unexpected IMAP structure."""
|
|
|
|
def _fetch_with(self, fetch_responses):
|
|
adapter = _make_adapter()
|
|
uids = b" ".join(
|
|
str(i + 1).encode() for i in range(len(fetch_responses))
|
|
)
|
|
fetch_iter = iter(fetch_responses)
|
|
|
|
def uid_handler(command, *args):
|
|
if command == "search":
|
|
return ("OK", [uids])
|
|
if command == "fetch":
|
|
return next(fetch_iter)
|
|
return ("NO", [])
|
|
|
|
mock_imap = MagicMock()
|
|
mock_imap.uid.side_effect = uid_handler
|
|
with patch("imaplib.IMAP4_SSL", return_value=mock_imap):
|
|
return adapter._fetch_new_messages()
|
|
|
|
def test_normal_response_parses(self):
|
|
results = self._fetch_with([("OK", [(b"1 (RFC822 {123}", _raw_email())])])
|
|
self.assertEqual(len(results), 1)
|
|
self.assertEqual(results[0]["sender_addr"], "user@test.com")
|
|
|
|
def test_none_element_skipped(self):
|
|
results = self._fetch_with([("OK", [None])])
|
|
self.assertEqual(results, [])
|
|
|
|
def test_empty_list_skipped(self):
|
|
results = self._fetch_with([("OK", [])])
|
|
self.assertEqual(results, [])
|
|
|
|
def test_bare_bytes_element_skipped(self):
|
|
# Single bytes item instead of a (header, payload) tuple
|
|
results = self._fetch_with([("OK", [b"not-a-tuple"])])
|
|
self.assertEqual(results, [])
|
|
|
|
def test_non_bytes_payload_skipped(self):
|
|
results = self._fetch_with([("OK", [(b"1", None)])])
|
|
self.assertEqual(results, [])
|
|
|
|
def test_malformed_does_not_abort_batch(self):
|
|
"""A malformed response mid-batch must not lose the messages after it."""
|
|
results = self._fetch_with([
|
|
("OK", [None]), # UID 1 malformed
|
|
("OK", [(b"2 (RFC822 {123}", _raw_email())]), # UID 2 fine
|
|
])
|
|
self.assertEqual(len(results), 1)
|
|
|
|
|
|
class TestMessageIdDomain(unittest.TestCase):
|
|
"""Message-ID generation tolerates EMAIL_ADDRESS without '@'."""
|
|
|
|
def test_normal_address(self):
|
|
adapter = _make_adapter("hermes@example.org")
|
|
self.assertEqual(adapter._message_id_domain(), "example.org")
|
|
|
|
def test_address_without_at(self):
|
|
adapter = _make_adapter("not-an-email")
|
|
self.assertEqual(adapter._message_id_domain(), "localhost")
|
|
|
|
def test_address_trailing_at(self):
|
|
adapter = _make_adapter("weird@")
|
|
self.assertEqual(adapter._message_id_domain(), "localhost")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|