Skip to content

fix(page): use cached mailboxes for the initial bootstrap#13207

Draft
ktogias wants to merge 2 commits into
nextcloud:mainfrom
ktogias:fix/bootstrap-cached-mailboxes
Draft

fix(page): use cached mailboxes for the initial bootstrap#13207
ktogias wants to merge 2 commits into
nextcloud:mainfrom
ktogias:fix/bootstrap-cached-mailboxes

Conversation

@ktogias

@ktogias ktogias commented Jul 3, 2026

Copy link
Copy Markdown

Summary

PageController::index() loops over every connected account synchronously in a single request to build the initial page bootstrap state, calling getMailboxes() for each one:

foreach ($mailAccounts as $mailAccount) {
    try {
        $mailboxes = $this->mailManager->getMailboxes($mailAccount); // does a live IMAP sync
        $json['mailboxes'] = $mailboxes;
    } catch (Throwable $ex) {
        $json['mailboxes'] = [];
        $json['error'] = true; // never reached on a hang, only on a thrown exception
    }
}

getMailboxes() calls mailboxSync->sync() first (a live IMAP touch) before reading the cached list. One account with a slow-to-respond or temporarily unreachable IMAP server was therefore enough to delay/block the bootstrap for every other account too, since this loop is synchronous. The existing try/catch only helps if the call throws quickly — it does nothing for a hang, since the loop never reaches the catch until the call actually returns or errors.

Fix

Adds getCachedMailboxes() to IMailManager/MailManager — a DB-only read with no live IMAP touch — and switches both account loops in PageController::index() (regular and delegated accounts) to use it. The bootstrap's speed is now fully decoupled from any account's live IMAP health. Periodic background sync jobs and opening an account's own mailbox view still do the live sync work and keep things up to date; the bootstrap just no longer needs to wait on it.

Test plan

  • Updated the existing PageControllerTest::testIndex() mock expectation from getMailboxes to getCachedMailboxes.
  • Added MailManagerTest::testGetCachedMailboxesDoesNotSync() confirming the new method never calls MailboxSync::sync().
  • php -l clean on all changed files. I don't have a local PHP/Composer/PHPUnit environment to run the full suite in this session — happy to iterate if CI surfaces anything.
  • Ran this exact change against a live production instance for several hours (an account with an intermittently unresponsive IMAP provider) — the bootstrap loads instantly regardless of that account's live IMAP state, matching the intended behavior.

PageController::index() loops over every connected account
synchronously in one request to build the initial page state, calling
getMailboxes() for each -- which does a live IMAP sync before reading
the cached list. One account with a slow or unreachable IMAP server
was therefore enough to delay or block the whole bootstrap for every
other account too, since this loop is synchronous. The existing
try/catch only helps if the call throws quickly; it does nothing for a
hang, since a hang never reaches the catch block.

Adds getCachedMailboxes() to IMailManager/MailManager, a DB-only read
with no live IMAP touch, and switches both account loops in
PageController::index() to use it. The bootstrap's speed is now fully
decoupled from any account's live IMAP health; periodic background
sync jobs and opening an account's own mailbox view still do the live
sync work and keep things up to date.

Updated the existing PageControllerTest expectation to match, and
added a unit test for the new method confirming it doesn't touch
MailboxSync at all.

Signed-off-by: Konstantinos Togias <info@ktogias.gr>

@ChristophWurst ChristophWurst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the patch

This also appears to be the result of agentic AI. You need to disclose that and honor CONTRIBUTING.md and AGENTS.md

Explain in your words what this change fixes. Perhaps you can demo it with before/after data flows.

@ktogias

ktogias commented Jul 3, 2026

Copy link
Copy Markdown
Author

You're right, and I'm sorry. I missed the guideline about the disclosure.

Step by step, what this fixes:

  1. On app load, PageController::index() loops through every account
    serially, calling getMailboxes() -- which does a live IMAP sync before
    returning anything.
  2. So the whole page load is only as fast as the slowest account's live
    IMAP response, one at a time. A slow/large account (mine regularly takes
    20-29s and gets killed by our pool timeout) delays or hangs the entire
    app, no matter how healthy every other account is.
  3. The existing try/catch doesn't help: it only catches quick exceptions,
    not a call that's just slow or hanging.

Fix: add getCachedMailboxes(), a DB-only read with no live IMAP call, and
use it for the bootstrap instead. Message listing within an open mailbox
was already cache-first and unaffected; a specific account's own folder
list still does a live sync today (separate call path, out of scope here).

@ktogias ktogias requested a review from ChristophWurst July 3, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants