fix(email): never fall back to sequence-number IMAP ops for move/flag (#2732)

_store_email_flag and _move_email_message (used by the archive / delete / move /
mark-read endpoints) had an else branch that, when _uid_exists returned False,
ran conn.store(uid, ...) / conn.copy(uid, ...) followed by a folder-wide
conn.expunge(). But imaplib's plain store()/copy() take a message SEQUENCE
NUMBER, not a UID, so the op landed on whichever message occupied sequence
position == the UID value, and the expunge then permanently removed it. A stale
cached UID (or a server whose UID probe misbehaves) therefore deleted an
unrelated email instead of reporting 'not found'.

There is no valid case where treating a UID as a sequence number is correct, so
drop the fallback: when the UID isn't present, return False — callers already
surface 'Email not found'. Only the UID command path remains.

Sibling of #1874 (which fixes the auto-spam poller's _imap_move in
email_helpers.py); this covers the user-facing endpoints in email_routes.py.
Part of #2124.
This commit is contained in:
L1
2026-07-10 23:27:28 -03:00
committed by GitHub
parent def3483032
commit e0fd68160f
2 changed files with 108 additions and 17 deletions
+20 -17
View File
@@ -923,29 +923,32 @@ def _resolve_send_config(account_id: str | None = None, owner: str = "") -> dict
def _store_email_flag(conn, uid: str, flag: str, add: bool = True) -> bool:
# imaplib's plain store() takes a message SEQUENCE NUMBER, not a UID, so the
# old `else` fallback flagged whichever message happened to occupy sequence
# position == the UID value. When the UID isn't present, fail safe (callers
# surface "Email not found") rather than touch an unrelated message.
if not _uid_exists(conn, uid):
return False
op = "+FLAGS" if add else "-FLAGS"
if _uid_exists(conn, uid):
status, _ = conn.uid("STORE", _uid_bytes(uid), op, flag)
else:
status, _ = conn.store(_uid_bytes(uid), op, flag)
status, _ = conn.uid("STORE", _uid_bytes(uid), op, flag)
return status == "OK"
def _move_email_message(conn, uid: str, dest: str, role: str = "") -> bool:
dest = _resolve_mail_folder(conn, dest, role or _folder_role_from_name(dest))
if _uid_exists(conn, uid):
status, _ = conn.uid("MOVE", _uid_bytes(uid), _q(dest))
if status == "OK":
return True
status, _ = conn.uid("COPY", _uid_bytes(uid), _q(dest))
if status != "OK":
return False
status, _ = conn.uid("STORE", _uid_bytes(uid), "+FLAGS", "\\Deleted")
else:
status, _ = conn.copy(_uid_bytes(uid), _q(dest))
if status != "OK":
return False
status, _ = conn.store(_uid_bytes(uid), "+FLAGS", "\\Deleted")
# copy()/store() are SEQUENCE-NUMBER commands; using them with a UID (the old
# `else` branch) copied + \Deleted-flagged the wrong message and then
# expunge() permanently removed it. There is no valid case where treating a
# UID as a sequence number is correct, so fail safe when the UID is absent.
if not _uid_exists(conn, uid):
return False
status, _ = conn.uid("MOVE", _uid_bytes(uid), _q(dest))
if status == "OK":
return True
status, _ = conn.uid("COPY", _uid_bytes(uid), _q(dest))
if status != "OK":
return False
status, _ = conn.uid("STORE", _uid_bytes(uid), "+FLAGS", "\\Deleted")
if status == "OK":
conn.expunge()
return True