generated from coulomb/repo-seed
feat: start mailbox evidence scanner
This commit is contained in:
12
tests/fixtures/mailbox/hard_bounce.eml
vendored
Normal file
12
tests/fixtures/mailbox/hard_bounce.eml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
From: Mail Delivery Subsystem <mailer-daemon@example.net>
|
||||
To: sender@example.com
|
||||
Subject: Delivery Status Notification (Failure)
|
||||
Date: Tue, 02 Jun 2026 10:00:00 +0000
|
||||
Message-ID: <hard-bounce@example.net>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Delivery failure.
|
||||
Final-Recipient: rfc822; missing@example.com
|
||||
Action: failed
|
||||
Status: 5.1.1
|
||||
Diagnostic-Code: smtp; 550 5.1.1 User unknown
|
||||
8
tests/fixtures/mailbox/human_reply.eml
vendored
Normal file
8
tests/fixtures/mailbox/human_reply.eml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
From: Recipient <recipient@example.com>
|
||||
To: sender@example.com
|
||||
Subject: Re: Your notification
|
||||
Date: Tue, 02 Jun 2026 10:03:00 +0000
|
||||
Message-ID: <reply@example.com>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Thanks, I received this and will review it today.
|
||||
9
tests/fixtures/mailbox/out_of_office.eml
vendored
Normal file
9
tests/fixtures/mailbox/out_of_office.eml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
From: Recipient <recipient@example.com>
|
||||
To: sender@example.com
|
||||
Subject: Auto-reply: Out of office
|
||||
Date: Tue, 02 Jun 2026 10:02:00 +0000
|
||||
Message-ID: <ooo@example.com>
|
||||
Auto-Submitted: auto-replied
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
I am out of office until next week.
|
||||
12
tests/fixtures/mailbox/soft_bounce.eml
vendored
Normal file
12
tests/fixtures/mailbox/soft_bounce.eml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
From: Mail Delivery Subsystem <mailer-daemon@example.net>
|
||||
To: sender@example.com
|
||||
Subject: Delivery temporarily delayed
|
||||
Date: Tue, 02 Jun 2026 10:01:00 +0000
|
||||
Message-ID: <soft-bounce@example.net>
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Delivery Status Notification.
|
||||
Final-Recipient: rfc822; full@example.com
|
||||
Action: delayed
|
||||
Status: 4.2.2
|
||||
Diagnostic-Code: smtp; 452 4.2.2 Mailbox full
|
||||
43
tests/test_parser.py
Normal file
43
tests/test_parser.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from email_connect.models import MessageClass
|
||||
from email_connect.parser import parse_message_file
|
||||
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures" / "mailbox"
|
||||
|
||||
|
||||
class ParserTests(unittest.TestCase):
|
||||
def test_hard_bounce_maps_to_permanent_rejection(self) -> None:
|
||||
_inbound, parsed, candidate = parse_message_file(FIXTURES / "hard_bounce.eml", mailbox_id="test")
|
||||
self.assertEqual(parsed.message_class, MessageClass.HARD_BOUNCE)
|
||||
self.assertIsNotNone(candidate)
|
||||
self.assertEqual(candidate.event_type, "notification.endpoint.rejected_permanent")
|
||||
self.assertEqual(candidate.assessment_subclass, "fail.hard_bounce")
|
||||
|
||||
def test_soft_bounce_maps_to_temporary_rejection(self) -> None:
|
||||
_inbound, parsed, candidate = parse_message_file(FIXTURES / "soft_bounce.eml", mailbox_id="test")
|
||||
self.assertEqual(parsed.message_class, MessageClass.SOFT_BOUNCE)
|
||||
self.assertIsNotNone(candidate)
|
||||
self.assertEqual(candidate.event_type, "notification.endpoint.rejected_temporary")
|
||||
|
||||
def test_out_of_office_stays_undef(self) -> None:
|
||||
_inbound, parsed, candidate = parse_message_file(FIXTURES / "out_of_office.eml", mailbox_id="test")
|
||||
self.assertEqual(parsed.message_class, MessageClass.OUT_OF_OFFICE)
|
||||
self.assertIsNotNone(candidate)
|
||||
self.assertEqual(candidate.assessment_category.value, "undef")
|
||||
self.assertEqual(candidate.event_type, "interaction.out_of_office_received")
|
||||
|
||||
def test_human_reply_is_email_channel_success_only(self) -> None:
|
||||
_inbound, parsed, candidate = parse_message_file(FIXTURES / "human_reply.eml", mailbox_id="test")
|
||||
self.assertEqual(parsed.message_class, MessageClass.HUMAN_REPLY)
|
||||
self.assertIsNotNone(candidate)
|
||||
self.assertEqual(candidate.event_type, "interaction.reply_received")
|
||||
self.assertEqual(candidate.assessment_subclass, "success.reply_received")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
37
tests/test_scanner.py
Normal file
37
tests/test_scanner.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from email_connect.config import AppConfig, MailboxConfig, ReportsConfig, ScanConfig, SourceConfig, StorageConfig
|
||||
from email_connect.scanner import scan_mailbox
|
||||
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures" / "mailbox"
|
||||
|
||||
|
||||
class ScannerTests(unittest.TestCase):
|
||||
def test_scan_fixture_directory_writes_report_and_deduplicates(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
config = AppConfig(
|
||||
mailbox=MailboxConfig(id="test-mailbox", protocol="fixture"),
|
||||
scan=ScanConfig(),
|
||||
storage=StorageConfig(path=str(root / "state.sqlite")),
|
||||
reports=ReportsConfig(output_dir=str(root / "reports")),
|
||||
source=SourceConfig(fixture_dir=str(FIXTURES)),
|
||||
)
|
||||
first = scan_mailbox(config)
|
||||
second = scan_mailbox(config)
|
||||
|
||||
self.assertEqual(first.scan.messages_seen, 4)
|
||||
self.assertEqual(first.scan.messages_new, 4)
|
||||
self.assertGreaterEqual(first.scan.evidence_events_created, 4)
|
||||
self.assertEqual(second.scan.messages_new, 0)
|
||||
self.assertEqual(second.scan.evidence_events_created, 0)
|
||||
self.assertTrue(first.report_path and first.report_path.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user