Search in a large mailbox freezes the browser: sync ignores the filter when the query has a subject term
@kesselb is already working on this.
Since Sep 15, 2026.
- Dominant language
- JavaScript
- Stars
- 1k
- Forks
- 348
- Avg merge
- 12h 28m
- Merged PRs (30d)
- 91
Description
Steps to reproduce
- Have a mailbox with many messages spread over several years (mine: 14,911 messages, ~10,500 threads, 2015 to today).
- In that mailbox, type a term in the search box above the envelope list. The frontend sends
to:X from:X subject:X mentions:false match:anyof. Pick a term where at least one match is old (for example a message from 2016). - The search returns the few matching envelopes. Right after that (and again on every 30 s background sync while the search is active) the frontend calls
POST /apps/mail/api/mailboxes/{id}/syncwith the matching ids asidsand the filter asquery.
Expected behavior
The sync response only contains messages that match the search filter, so it stays small (a few envelopes at most).
Actual behavior
The sync response contains every thread in the mailbox that is newer than the oldest known match, as newMessages. In my case one sync response was ~997 KB gzipped (~7-9 MB JSON, ~8,400 envelopes). Firefox shows "This page is slowing down Firefox" and the Mail UI freezes while it adds all those envelopes to the store. Because the sync repeats every 30 s while the search is active, it keeps happening.
I reproduced it server-side by calling SyncService::syncMailbox() directly with the same known ids, once with and once without the subject term:
| query | newMessages | JSON size |
|---|---|---|
to:X from:X subject:X mentions:false match:anyof (what the search box sends) |
8,411 | 7.1 MB |
to:X from:X mentions:false match:anyof (no subject term) |
0 | 0 |
Cause
SyncService::getDatabaseSyncChanges() narrows the candidate "new" messages to the ones that match the filter by passing their UIDs to findIdsByQuery():
findIdsByQuery() treats $uids as the result of an IMAP body search. When the query has a subject term, it ORs the UIDs into the text conditions instead of ANDing them:
So the WHERE clause becomes (from matches OR to matches OR subject matches OR uid IN candidates), and every candidate passes. The OR makes sense for body+subject search in MailSearch::getIdsLocally(), but in the sync path the UIDs are meant as a restriction. Since the search box always adds a subject: term, the sync filter is effectively disabled for every search.
The same applies to the changedIds call a few lines below. There the result also picks up text matches from the whole mailbox that the client never had.
findNewIds() returns every thread newer than the oldest known id (by design), so the size of the leak grows with the age of the oldest match.
Possible fix
Keep the OR semantics for the body-search case, but apply the sync's candidate set as a hard restriction. For example, a separate parameter in findIdsByQuery() that is always ANDed, or in SyncService intersect the filter result with the candidates:
$matching = $this->messageMapper->findIdsByQuery($mailbox, $query, $order, null);
$newIds = array_values(array_intersect($newIds, $matching));
Mail app version
5.10.10 (code on main at f046085 is the same)
Nextcloud version
34.0.1
Mailserver or service
Hosted IMAP (Soverin)
Operating system
Debian 13 (official nextcloud:apache Docker image)
PHP engine version
Other (8.5.8)
Nextcloud memory caching
APCu (local), Redis (locking)
Web server
Apache (supported)
Database
PostgreSQL
Additional info
Found while tracing an intermittent UI freeze, with help from Claude Code. The access log showed one 997 KB mailboxes/{id}/sync response right after a folder search, where normal syncs are ~4 KB.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.