1616from typing import List , Set
1717
1818from sqlalchemy import bindparam , desc
19- from sqlalchemy .orm import Session
19+ from sqlalchemy .orm import Query , Session
2020from sqlalchemy .orm .exc import NoResultFound
2121from sqlalchemy .sql .expression import func
2222
@@ -73,22 +73,33 @@ def lastseenuid(account_id, session, folder_id):
7373 return res or 0
7474
7575
76+ IMAPUID_PER_MESSAGE_SANITY_LIMIT = 100
77+
78+
7679def update_message_metadata (
7780 session : Session , account : Account , message : Message , is_draft : bool
7881) -> None :
7982 """Update the message's metadata"""
80- # Sort imapuids in a way that the ones that were added later come last
81- now = datetime .utcnow ()
82- sorted_imapuids : List [ImapUid ] = sorted (
83- message .imapuids , key = lambda imapuid : imapuid .updated_at or now
83+ # Sort imapuids in a way that the ones that were added later come first.
84+ # There are non-conforming IMAP servers that can list the same message thousands of times
85+ # in the same folder. This is a workaround to limit the memory pressure caused by such
86+ # servers. The metadata is meaningless for such messages anyway.
87+ latest_imapuids = (
88+ imapuids_for_message_query (
89+ account_id = account .id ,
90+ message_id = message .id ,
91+ only_latest = IMAPUID_PER_MESSAGE_SANITY_LIMIT ,
92+ )
93+ .with_session (session )
94+ .all ()
8495 )
8596
86- message .is_read = any (imapuid .is_seen for imapuid in sorted_imapuids )
87- message .is_starred = any (imapuid .is_flagged for imapuid in sorted_imapuids )
97+ message .is_read = any (imapuid .is_seen for imapuid in latest_imapuids )
98+ message .is_starred = any (imapuid .is_flagged for imapuid in latest_imapuids )
8899 message .is_draft = is_draft
89100
90- sorted_categories : List [Category ] = [
91- category for imapuid in sorted_imapuids for category in imapuid .categories
101+ latest_categories : List [Category ] = [
102+ category for imapuid in latest_imapuids for category in imapuid .categories
92103 ]
93104
94105 categories : Set [Category ]
@@ -101,9 +112,9 @@ def update_message_metadata(
101112 # (and in turn one category) depending on the order they were returned
102113 # from the database. This makes it deterministic and more-correct because a message
103114 # is likely in a folder (and category) it was added to last.
104- categories = {sorted_categories [ - 1 ]} if sorted_categories else set ()
115+ categories = {latest_categories [ 0 ]} if latest_categories else set ()
105116 elif account .category_type == "label" :
106- categories = set (sorted_categories )
117+ categories = set (latest_categories )
107118 else :
108119 raise AssertionError ("Unreachable" )
109120
@@ -198,6 +209,18 @@ def update_metadata(account_id, folder_id, folder_role, new_flags, session):
198209 log .info ("Updated UID metadata" , changed = change_count , out_of = len (new_flags ))
199210
200211
212+ def imapuids_for_message_query (
213+ * , account_id : int , message_id : int , only_latest : int | None = None
214+ ) -> Query :
215+ query = Query ([ImapUid ]).filter (
216+ ImapUid .account_id == account_id , ImapUid .message_id == message_id
217+ )
218+ if only_latest is not None :
219+ query = query .order_by (ImapUid .updated_at .desc ()).limit (only_latest )
220+
221+ return query
222+
223+
201224def remove_deleted_uids (account_id , folder_id , uids ):
202225 """
203226 Make sure you're holding a db write lock on the account. (We don't try
@@ -238,7 +261,13 @@ def remove_deleted_uids(account_id, folder_id, uids):
238261 db_session .delete (imapuid )
239262
240263 if message is not None :
241- if not message .imapuids and message .is_draft :
264+ message_imapuids_exist = db_session .query (
265+ imapuids_for_message_query (
266+ account_id = account_id , message_id = message .id
267+ ).exists ()
268+ ).scalar ()
269+
270+ if not message_imapuids_exist and message .is_draft :
242271 # Synchronously delete drafts.
243272 thread = message .thread
244273 if thread is not None :
@@ -257,7 +286,7 @@ def remove_deleted_uids(account_id, folder_id, uids):
257286 update_message_metadata (
258287 db_session , account , message , message .is_draft
259288 )
260- if not message . imapuids :
289+ if not message_imapuids_exist :
261290 # But don't outright delete messages. Just mark them as
262291 # 'deleted' and wait for the asynchronous
263292 # dangling-message-collector to delete them.
0 commit comments