From 342b467590969a3ed0e3a012f2d8849b8d3c1638 Mon Sep 17 00:00:00 2001 From: Sean Whalen <44679+seanthegeek@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:16:42 -0400 Subject: [PATCH] Mark maildir messages as read after they are read (#726) MaildirConnection.fetch_message() previously returned the message body without touching the on-disk file, so messages stayed in new/ with no "S" (Seen) flag and any MUA scanning the same maildir kept showing them as unread. The call site now passes mark_read=not test (mirroring the existing MSGraphConnection plumbing); on True, the message is moved to cur/ and gains the S flag. Test mode leaves the maildir unmodified. Co-authored-by: Sean Whalen --- CHANGELOG.md | 1 + parsedmarc/__init__.py | 4 ++++ parsedmarc/mail/maildir.py | 17 +++++++++++------ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d03717..6889fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- `MaildirConnection.fetch_message()` now marks messages as read after reading them (sets the `S` flag and moves the file from `new/` to `cur/`), unless `--test` is in effect. Previously, a message was processed but its on-disk maildir state was unchanged, so an MUA scanning the same maildir kept showing it as unread. Mirrors the existing `mark_read=not test` pattern used for `MSGraphConnection`. - `get_ip_address_info()` no longer caches weak-fallback attributions (no PTR + no ASN-domain map match → raw `as_name` used as `source_name`, `source_type` left null). `get_reverse_dns()` swallows every `DNSException` as `None`, so a transient PTR lookup failure (timeout, SERVFAIL, socket error) is indistinguishable from a genuine no-PTR case at that layer — caching the weak result would poison the 4-hour cache with a misattribution that persisted even after the PTR became resolvable again. PTR-backed matches and ASN-domain matches (both stable attributions) are still cached as before; only the specific `reverse_dns=None AND type=None AND name=as_name` state skips the cache write so the next lookup retries. ## 9.10.1 diff --git a/parsedmarc/__init__.py b/parsedmarc/__init__.py index ce48c19..8b87b44 100644 --- a/parsedmarc/__init__.py +++ b/parsedmarc/__init__.py @@ -48,6 +48,7 @@ from parsedmarc.mail import ( GmailConnection, IMAPConnection, MailboxConnection, + MaildirConnection, MSGraphConnection, ) from parsedmarc.types import ( @@ -2042,6 +2043,9 @@ def get_dmarc_reports_from_mailbox( elif isinstance(connection, MSGraphConnection): message_id = str(msg_uid) msg_content = connection.fetch_message(message_id, mark_read=not test) + elif isinstance(connection, MaildirConnection): + message_id = str(msg_uid) if not isinstance(msg_uid, str) else msg_uid + msg_content = connection.fetch_message(message_id, mark_read=not test) else: message_id = str(msg_uid) if not isinstance(msg_uid, str) else msg_uid msg_content = connection.fetch_message(message_id) diff --git a/parsedmarc/mail/maildir.py b/parsedmarc/mail/maildir.py index 9f075e0..3e7a4f0 100644 --- a/parsedmarc/mail/maildir.py +++ b/parsedmarc/mail/maildir.py @@ -65,13 +65,18 @@ class MaildirConnection(MailboxConnection): self._active_folder = self._client return self._active_folder.keys() - def fetch_message(self, message_id: str) -> str: + def fetch_message(self, message_id: str, **kwargs) -> str: msg = self._active_folder.get(message_id) - if msg is not None: - msg = msg.as_string() - if msg is not None: - return msg - return "" + if msg is None: + return "" + msg_str = msg.as_string() + if kwargs.get("mark_read"): + # Maildir spec: a message is "read" once it has been moved out of + # new/ into cur/ with the "S" (Seen) flag set in its info field. + msg.set_subdir("cur") + msg.add_flag("S") + self._active_folder[message_id] = msg + return msg_str or "" def delete_message(self, message_id: str): self._active_folder.remove(message_id)