nextcloud / nextcloud/mail

Search in a large mailbox freezes the browser: sync ignores the filter when the query has a subject term

Open
#13,673 2 comments 2 reactions 1 assignee View on GitHub

@kesselb is already working on this.

Since Sep 15, 2026.

0. to triage bug
Dominant language
JavaScript
Stars
1k
Forks
348
Avg merge
12h 28m
Merged PRs (30d)
91

Description

Steps to reproduce
  1. Have a mailbox with many messages spread over several years (mine: 14,911 messages, ~10,500 threads, 2015 to today).
  2. 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).
  3. 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}/sync with the matching ids as ids and the filter as query.
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():

https://github.com/nextcloud/mail/blob/f0460858291c984aa6defa9c7532ef3f1ce900db/lib/Service/Sync/SyncService.php#L141-L145

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:

https://github.com/nextcloud/mail/blob/f0460858291c984aa6defa9c7532ef3f1ce900db/lib/Db/MessageMapper.php#L924-L937

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.