Sync: mail with no Message-ID is keyed on its UID, which repeats across folders, mailboxes and servers #11
Labels
No labels
bug
documentation
enhancement
performance
security
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo-imap#11
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo-imap at
cc09c6381, following the identifier a suggestion is keyed on and running two passes over two folders with the plugin's fake mailbox.A suggestion is filed once per
(owner, source, external_id). For a message without aMessage-IDheader the plugin usesuid-<number>, the message's IMAP UID. A UID is unique only inside one folder under one UIDVALIDITY value, and the key carries neither the connection, the folder nor that value. A later message that happens to have the same number is taken for the earlier one: no suggestion is filed, it is counted as skipped, and it is flagged as read.src/postulo_imap/mailbox.py:150,parse:message_id=_decoded(message.get("Message-ID")) or f"uid-{uid}".src/postulo_imap/sync.py:188-191:suggest(connection.owner, source=self.name, external_id=mail.message_id, …), withsourcealways"imap".applications/suggestions.py:56-61:Suggestion.objects.filter(owner=owner, source=source, external_id=external_id[:250]).first()returns the existing row andFalse, whether that suggestion is waiting, accepted or declined.src/postulo_imap/sync.py:211-215:createdfalse counts as skipped, and the uid is flagged with the rest.Message-IDis uid 1 in Jobs; the pass files a suggestion withexternal_iduid-1. The connection is then pointed at another folder, where a rejection with noMessage-IDis also uid 1. The second pass reportspulled == 0, skipped == 1, the only suggestion is still the acknowledgement, and the rejection carries$Postulo, so it is never looked at again. The same happens:imap;"sha256-" + hashlib.sha256(raw).hexdigest()of the raw bytesparsealready has, which is stable across passes, folders and servers and cannot be mistaken for a real identifier or for a threading reference. If only part of the message is fetched in future, hash the header block. Suggestions already filed underuid-Nkeep their key; that only means one such message could be suggested a second time after the change.Done when
tests/test_imap.pyhas a test in which two different messages without aMessage-IDhave the same uid in two folders, and a pass over each files two suggestions; today the second pass files none.parse(1, raw).message_id == parse(7, raw).message_idfor a message without the header, and that it differs for a different message.Fixed by pull request #25.