perf: use imap_tools uid_list fetch to skip manual UID batching

imap_tools 1.14.0 adds a uid_list fetch arg that fetches by UID
directly without issuing a SEARCH command, and does its own
bulk-fetch batching. Use it in place of the manual per-batch
AND(uid=...) fetch loop.
This commit is contained in:
stumpylog
2026-07-24 12:57:14 -07:00
parent 8391b4c524
commit d7084ee1ab
4 changed files with 559 additions and 554 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ dependencies = [
"gotenberg-client~=0.14.0",
"httpx-oauth~=0.16",
"ijson>=3.2",
"imap-tools~=1.13.0",
"imap-tools~=1.14.0",
"jinja2~=3.1.5",
"langdetect~=1.0.9",
"llama-index-core>=0.14.22",
+10 -19
View File
@@ -712,25 +712,16 @@ class MailAccountHandler(LoggingMixin):
return 0
sorted_new_uids = sorted(new_uids, key=int)
message_batches = []
# ikvk/imap_tools#268 requests a direct UID-list fetch that would let us drop this manual batching loop
for batch_start in range(0, len(sorted_new_uids), MAIL_FETCH_BATCH_SIZE):
batch = sorted_new_uids[batch_start : batch_start + MAIL_FETCH_BATCH_SIZE]
try:
message_batches.append(
M.fetch(
criteria=AND(uid=batch),
mark_seen=False,
charset=rule.account.character_set,
bulk=True,
),
)
except Exception as err:
raise MailError(
f"Rule {rule}: Error while fetching folder {rule.folder}",
) from err
messages = itertools.chain(*message_batches)
try:
messages = M.fetch(
uid_list=sorted_new_uids,
mark_seen=False,
bulk=MAIL_FETCH_BATCH_SIZE,
)
except Exception as err:
raise MailError(
f"Rule {rule}: Error while fetching folder {rule.folder}",
) from err
mails_processed = 0
total_processed_files = 0
+20 -6
View File
@@ -134,7 +134,17 @@ class BogusMailBox(AbstractContextManager):
if username != self.USERNAME or access_token != self.ACCESS_TOKEN:
raise MailboxLoginError("BAD", "OK")
def fetch(self, criteria, mark_seen, charset="", *, bulk=True):
def fetch(
self,
criteria="ALL",
charset="",
*,
mark_seen=True,
bulk=True,
uid_list=None,
):
if uid_list is not None:
return [m for m in self.messages if m.uid in uid_list]
return self._filter_messages(criteria)
def uids(self, criteria, charset="") -> list[str]:
@@ -416,7 +426,7 @@ def assert_eventually_equals(
deadline = time.time() + timeout
while time.time() < deadline:
if getter_fn() == expected_value:
return None
return
time.sleep(interval)
actual = getter_fn()
raise AssertionError(f"Expected {expected_value}, but got {actual}")
@@ -443,7 +453,8 @@ class TestMail(
WHEN:
- The mail account is processed
THEN:
- The body fetch is issued in multiple batches
- The body fetch is issued once, with all UIDs and the configured batch size
handed to imap_tools so it can bulk-fetch in batches server-side
- Every message is still processed (none dropped at a batch boundary)
"""
account = MailAccount.objects.create(
@@ -476,8 +487,11 @@ class TestMail(
) as fetch_spy:
self.mail_account_handler.handle_mail_account(account)
# ceil(12 / 5) == 3 batches
self.assertEqual(fetch_spy.call_count, 3)
# A single fetch() call hands the full UID list and batch size to imap_tools,
# which does its own bulk-fetching in batches of MAIL_FETCH_BATCH_SIZE.
fetch_spy.assert_called_once()
self.assertEqual(fetch_spy.call_args.kwargs["bulk"], 5)
self.assertEqual(len(fetch_spy.call_args.kwargs["uid_list"]), message_count)
self.assertEqual(
ProcessedMail.objects.filter(rule=rule).count(),
message_count,
@@ -1673,7 +1687,7 @@ class TestMail(
if message.from_ == "amazon@amazon.de":
raise ValueError("Does not compute.")
else:
return None
return
m.side_effect = get_correspondent_fake
Generated
+528 -528
View File
File diff suppressed because it is too large Load Diff