From 4277786459bd8b673a38cc4f778a7a95a6e94d17 Mon Sep 17 00:00:00 2001 From: Flamur Veliqi Date: Mon, 23 Feb 2026 15:06:52 +0100 Subject: [PATCH] nx-webmail: add robust paged inbox hydration after login --- nx-webmail/README.md | 4 +- nx-webmail/docker-compose.yml | 2 +- nx-webmail/src/components/Webmail.jsx | 85 +++++++++++++++++++++++---- nx-webmail/umbrel-app.yml | 4 +- 4 files changed, 79 insertions(+), 16 deletions(-) diff --git a/nx-webmail/README.md b/nx-webmail/README.md index f22bc60..a25daac 100644 --- a/nx-webmail/README.md +++ b/nx-webmail/README.md @@ -30,7 +30,7 @@ Umbrel installation is most reliable when your app uses a prebuilt image from a - `git.weektab.org/nexus/nx-webmail:latest` 3. The workflow then pins `nx-webmail/docker-compose.yml` to `tag@sha256:digest` automatically. 4. Manual fallback: - docker buildx build --platform linux/amd64,linux/arm64 -t git.weektab.org/nexus/nx-webmail:1.0.7 --push . + docker buildx build --platform linux/amd64,linux/arm64 -t git.weektab.org/nexus/nx-webmail:1.0.8 --push . ## Umbrel app packaging @@ -55,7 +55,7 @@ This repository is prepared for Umbrel app-store usage. Notes: - Umbrel uses the `app_proxy` service in `docker-compose.yml`. - Internal app port is `3001`. -- `server` uses a prebuilt registry image (`git.weektab.org/nexus/nx-webmail:1.0.7`). +- `server` uses a prebuilt registry image (`git.weektab.org/nexus/nx-webmail:1.0.8`). - If your store prefix changes, update `id` in `umbrel-app.yml` and `APP_HOST` in `docker-compose.yml`. ## Notes diff --git a/nx-webmail/docker-compose.yml b/nx-webmail/docker-compose.yml index 6195b93..0612490 100644 --- a/nx-webmail/docker-compose.yml +++ b/nx-webmail/docker-compose.yml @@ -7,7 +7,7 @@ services: APP_PORT: 3001 server: - image: git.weektab.org/nexus/nx-webmail:1.0.7@sha256:01dfba4f671f490f3dddf9b5ddc3a98bfff7f10e8a87aa47b4873f8ac1a3d332 + image: git.weektab.org/nexus/nx-webmail:1.0.8@sha256:db773eea0c6d836360ad52a7e55f25d5f1b3684a73e2cbce21d4c1fec55137f5 init: true restart: on-failure stop_grace_period: 1m diff --git a/nx-webmail/src/components/Webmail.jsx b/nx-webmail/src/components/Webmail.jsx index 647489d..203fc13 100644 --- a/nx-webmail/src/components/Webmail.jsx +++ b/nx-webmail/src/components/Webmail.jsx @@ -533,6 +533,67 @@ export default function Webmail() { } }; + const hydrateFolderFully = async (folderName, authOverride = authPayload, credOverride = credentials) => { + const folder = folderName || activeFolder || 'INBOX'; + const accountId = makeAccountId(authOverride); + const direction = String(sortOrder || 'newest'); + const seen = new Set(); + const merged = []; + let offset = 0; + let total = 0; + let hasMore = true; + let guard = 0; + + while (hasMore && guard < 200) { + const data = await postJson('/api/webmail/inbox', { + ...authOverride, + folder, + offset, + limit: INBOX_PAGE_SIZE, + fetchAll: false, + direction + }); + + const batch = Array.isArray(data?.emails) ? data.emails : []; + for (const msg of batch) { + const uid = Number(msg?.id || 0); + if (!seen.has(uid)) { + seen.add(uid); + merged.push(msg); + } + } + + total = Number(data?.total || Math.max(total, merged.length)); + const nextOffset = Number(data?.nextOffset || (offset + batch.length)); + const canAdvance = nextOffset > offset; + hasMore = Boolean(data?.hasMore) && canAdvance && batch.length > 0; + offset = nextOffset; + guard += 1; + + if (activeFolderRef.current !== folder || activeAccountIdRef.current !== accountId) { + return; + } + + const safeTotal = Math.max(total, merged.length); + const paging = { + total: safeTotal, + nextOffset: merged.length, + hasMore: merged.length < safeTotal, + loadedAll: merged.length >= safeTotal, + direction, + updatedAt: Date.now() + }; + setEmails([...merged]); + setFolderPaging(accountId, folder, paging); + setViewCache(accountId, folder, direction, merged, paging); + upsertAccountCache({ + accountId, + folderName: folder, + folderEmails: merged + }); + } + }; + const connectWithCredentials = async (auth, lastFolder, credOverride = credentials, options = {}) => { const { skipInitialFetchIfCached = false } = options; try { @@ -573,12 +634,12 @@ export default function Webmail() { await fetchInbox(defaultFolder, auth, true, credOverride, { keepSelection: true, silent: Boolean(cachedDefaultEmails) || skipInitialFetchIfCached, - fetchAll: true, - resetToFirstPage: false + resetToFirstPage: true }); - saveSession(credOverride, defaultFolder); - upsertAccount(credOverride, defaultFolder); - return true; + await hydrateFolderFully(defaultFolder, auth, credOverride); + saveSession(credOverride, defaultFolder); + upsertAccount(credOverride, defaultFolder); + return true; } catch (error) { setLoginError(error.message || 'Login failed.'); setIsLoggedIn(false); @@ -1312,12 +1373,14 @@ export default function Webmail() { setIsRefreshingInbox(true); try { const shouldFetchAll = !Boolean(activeFolderPaging?.loadedAll); - await fetchInbox(activeFolder, authPayload, true, credentials, { - keepSelection: true, - syncOnly: !shouldFetchAll, - fetchAll: shouldFetchAll, - forceRefresh: shouldFetchAll - }); + if (shouldFetchAll) { + await hydrateFolderFully(activeFolder, authPayload, credentials); + } else { + await fetchInbox(activeFolder, authPayload, true, credentials, { + keepSelection: true, + syncOnly: true + }); + } await fetchFolderSizes(authPayload); await fetchQuota(authPayload); } finally { diff --git a/nx-webmail/umbrel-app.yml b/nx-webmail/umbrel-app.yml index 2a39889..2d60c4c 100644 --- a/nx-webmail/umbrel-app.yml +++ b/nx-webmail/umbrel-app.yml @@ -4,7 +4,7 @@ name: Webmail tagline: Self-hosted IMAP/SMTP webmail client icon: https://git.weektab.org/nexus/umbrel-apps/raw/branch/main/gallery/webmail/icon.png category: utilities -version: "1.0.7" +version: "1.0.8" port: 3001 description: >- Webmail is a lightweight, self-hosted webmail app for connecting to external @@ -17,7 +17,7 @@ repo: https://git.weektab.org/nexus/webmail support: https://git.weektab.org/nexus/webmail/issues gallery: [] releaseNotes: >- - Improved IMAP full-sync reliability for large inboxes in container deployments. + Added robust paged auto-hydration after login to improve sync consistency in Docker. dependencies: [] path: "" defaultUsername: ""