mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
4fa4d0100a
* fix(email): keep FETCH attributes Gmail sends after the header literal imaplib returns a UID FETCH response as an interleaved list of (meta, literal) tuples plus bare bytes elements. Which attributes land where is server-specific: Dovecot sends FLAGS before the RFC822.HEADER literal (inside the tuple meta), Gmail sends them after it, as a bare ` FLAGS (\Seen))` element. The email list grouping loop and the search loop only inspected tuples, so on Gmail every message lost its FLAGS and the whole mailbox rendered as unread/unflagged, with mark-read appearing to have no effect. Extract the grouping into _group_uid_fetch_records(), fold bare bytes parts into the current message meta there, and reuse it in both the batched list fetch and the per-UID search fetch. Covered by unit tests with captured Gmail-shaped and Dovecot-shaped responses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(email): use raw byte literals for IMAP backslash escapes --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
"""Regression tests for _group_uid_fetch_records (Gmail FLAGS placement).
|
|
|
|
imaplib hands back UID FETCH responses as an interleaved list of
|
|
``(meta, literal)`` tuples and bare ``bytes`` elements. Dovecot sends FLAGS
|
|
before the RFC822.HEADER literal, so they sit inside the tuple meta; Gmail
|
|
sends FLAGS *after* the literal, as a bare ``b' FLAGS (\\Seen))'`` element.
|
|
The old grouping loop only looked at tuples, so on Gmail every message lost
|
|
its FLAGS and rendered as unread/unflagged in the email library.
|
|
"""
|
|
|
|
import re
|
|
|
|
from routes.email_routes import _group_uid_fetch_records, _uid_from_fetch_meta
|
|
|
|
|
|
def _flags(meta_b: bytes) -> str:
|
|
m = re.search(rb"FLAGS \(([^)]*)\)", meta_b)
|
|
return m.group(1).decode() if m else ""
|
|
|
|
|
|
# Captured shape of a real Gmail response to
|
|
# UID FETCH a,b (UID FLAGS RFC822.HEADER RFC822.SIZE):
|
|
GMAIL_RESPONSE = [
|
|
(b"10779 (UID 18723 RFC822.SIZE 54308 RFC822.HEADER {24}", b"Subject: read one\r\n\r\n"),
|
|
rb" FLAGS (\Seen))",
|
|
(b"10780 (UID 18724 RFC822.SIZE 124310 RFC822.HEADER {26}", b"Subject: unread one\r\n\r\n"),
|
|
rb" FLAGS ())",
|
|
]
|
|
|
|
# Dovecot puts FLAGS before the literal and terminates with a bare b')'.
|
|
DOVECOT_RESPONSE = [
|
|
(rb"1 (UID 5 FLAGS (\Seen) RFC822.SIZE 100 RFC822.HEADER {18}", b"Subject: hi\r\n\r\n"),
|
|
b")",
|
|
(b"2 (UID 6 FLAGS () RFC822.SIZE 90 RFC822.HEADER {19}", b"Subject: new\r\n\r\n"),
|
|
b")",
|
|
]
|
|
|
|
|
|
def test_gmail_post_literal_flags_attach_to_their_own_message():
|
|
grouped = _group_uid_fetch_records(GMAIL_RESPONSE)
|
|
|
|
assert len(grouped) == 2
|
|
assert _uid_from_fetch_meta(grouped[0][0]) == "18723"
|
|
assert _flags(grouped[0][0]) == r"\Seen"
|
|
assert grouped[0][1] == b"Subject: read one\r\n\r\n"
|
|
|
|
assert _uid_from_fetch_meta(grouped[1][0]) == "18724"
|
|
assert _flags(grouped[1][0]) == ""
|
|
assert grouped[1][1] == b"Subject: unread one\r\n\r\n"
|
|
|
|
|
|
def test_dovecot_pre_literal_flags_unchanged():
|
|
grouped = _group_uid_fetch_records(DOVECOT_RESPONSE)
|
|
|
|
assert len(grouped) == 2
|
|
assert _flags(grouped[0][0]) == r"\Seen"
|
|
assert _flags(grouped[1][0]) == ""
|
|
assert grouped[1][1] == b"Subject: new\r\n\r\n"
|
|
|
|
|
|
def test_size_and_uid_survive_grouping():
|
|
grouped = _group_uid_fetch_records(GMAIL_RESPONSE)
|
|
sizes = [re.search(rb"RFC822\.SIZE (\d+)", m).group(1) for m, _ in grouped]
|
|
assert sizes == [b"54308", b"124310"]
|
|
|
|
|
|
def test_empty_and_none_inputs():
|
|
assert _group_uid_fetch_records(None) == []
|
|
assert _group_uid_fetch_records([]) == []
|
|
# A stray bare element before any tuple opens no record and must not crash.
|
|
assert _group_uid_fetch_records([rb" FLAGS (\Seen))"]) == []
|