From 79f47121a41129e4f6c5d76181db0ced6833eae2 Mon Sep 17 00:00:00 2001 From: Kili Date: Mon, 9 Mar 2026 22:33:42 +0100 Subject: [PATCH] Pass mailbox since filter through watch_inbox callback (#670) * Pass mailbox since through watch loop and add regression test * Add CLI regression test for mailbox since in watch mode --- parsedmarc/__init__.py | 3 ++ parsedmarc/cli.py | 1 + tests.py | 64 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/parsedmarc/__init__.py b/parsedmarc/__init__.py index 4f96dc1..280c7b0 100644 --- a/parsedmarc/__init__.py +++ b/parsedmarc/__init__.py @@ -2186,6 +2186,7 @@ def watch_inbox( dns_timeout: float = 6.0, strip_attachment_payloads: bool = False, batch_size: int = 10, + since: Optional[Union[datetime, date, str]] = None, normalize_timespan_threshold_hours: float = 24, ): """ @@ -2212,6 +2213,7 @@ def watch_inbox( strip_attachment_payloads (bool): Replace attachment payloads in forensic report samples with None batch_size (int): Number of messages to read and process before saving + since: Search for messages since certain time normalize_timespan_threshold_hours (float): Normalize timespans beyond this """ @@ -2231,6 +2233,7 @@ def watch_inbox( dns_timeout=dns_timeout, strip_attachment_payloads=strip_attachment_payloads, batch_size=batch_size, + since=since, create_folders=False, normalize_timespan_threshold_hours=normalize_timespan_threshold_hours, ) diff --git a/parsedmarc/cli.py b/parsedmarc/cli.py index b9b9b9f..599e507 100644 --- a/parsedmarc/cli.py +++ b/parsedmarc/cli.py @@ -1879,6 +1879,7 @@ def _main(): dns_timeout=opts.dns_timeout, strip_attachment_payloads=opts.strip_attachment_payloads, batch_size=mailbox_batch_size_value, + since=opts.mailbox_since, ip_db_path=opts.ip_db_path, always_use_local_files=opts.always_use_local_files, reverse_dns_map_path=opts.reverse_dns_map_path, diff --git a/tests.py b/tests.py index f93bb76..a0b5e5c 100755 --- a/tests.py +++ b/tests.py @@ -11,6 +11,7 @@ from base64 import urlsafe_b64encode from glob import glob from pathlib import Path from tempfile import NamedTemporaryFile, TemporaryDirectory +from types import SimpleNamespace from unittest.mock import MagicMock, patch from lxml import etree @@ -287,7 +288,6 @@ class _FakeGraphResponse: def json(self): return self._payload - class _BreakLoop(BaseException): pass @@ -934,5 +934,67 @@ class TestImapFallbacks(unittest.TestCase): with self.assertRaises(IMAPClientError): connection.move_message(99, "Archive") delete_mock.assert_not_called() + +class TestMailboxWatchSince(unittest.TestCase): + def testWatchInboxPassesSinceToMailboxFetch(self): + mailbox_connection = SimpleNamespace() + + def fake_watch(check_callback, check_timeout): + check_callback(mailbox_connection) + raise _BreakLoop() + + mailbox_connection.watch = fake_watch + callback = MagicMock() + with patch.object( + parsedmarc, "get_dmarc_reports_from_mailbox", return_value={} + ) as mocked: + with self.assertRaises(_BreakLoop): + parsedmarc.watch_inbox( + mailbox_connection=mailbox_connection, + callback=callback, + check_timeout=1, + batch_size=10, + since="1d", + ) + self.assertEqual(mocked.call_args.kwargs.get("since"), "1d") + + @patch("parsedmarc.cli.get_dmarc_reports_from_mailbox") + @patch("parsedmarc.cli.watch_inbox") + @patch("parsedmarc.cli.IMAPConnection") + def testCliPassesSinceToWatchInbox( + self, mock_imap_connection, mock_watch_inbox, mock_get_mailbox_reports + ): + mock_imap_connection.return_value = object() + mock_get_mailbox_reports.return_value = { + "aggregate_reports": [], + "forensic_reports": [], + "smtp_tls_reports": [], + } + mock_watch_inbox.side_effect = FileExistsError("stop-watch-loop") + + config_text = """[general] +silent = true + +[imap] +host = imap.example.com +user = user +password = pass + +[mailbox] +watch = true +since = 2d +""" + + with tempfile.NamedTemporaryFile("w", suffix=".ini", delete=False) as cfg: + cfg.write(config_text) + cfg_path = cfg.name + self.addCleanup(lambda: os.path.exists(cfg_path) and os.remove(cfg_path)) + + with patch.object(sys, "argv", ["parsedmarc", "-c", cfg_path]): + with self.assertRaises(SystemExit) as system_exit: + parsedmarc.cli._main() + + self.assertEqual(system_exit.exception.code, 1) + self.assertEqual(mock_watch_inbox.call_args.kwargs.get("since"), "2d") if __name__ == "__main__": unittest.main(verbosity=2)