nx-webmail: add robust paged inbox hydration after login
Some checks failed
Publish nx-webmail Image (Gitea) / publish (push) Has been cancelled

This commit is contained in:
2026-02-23 15:06:52 +01:00
parent 08a8ae182e
commit 4277786459
4 changed files with 79 additions and 16 deletions

View File

@@ -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` - `git.weektab.org/nexus/nx-webmail:latest`
3. The workflow then pins `nx-webmail/docker-compose.yml` to `tag@sha256:digest` automatically. 3. The workflow then pins `nx-webmail/docker-compose.yml` to `tag@sha256:digest` automatically.
4. Manual fallback: 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 ## Umbrel app packaging
@@ -55,7 +55,7 @@ This repository is prepared for Umbrel app-store usage.
Notes: Notes:
- Umbrel uses the `app_proxy` service in `docker-compose.yml`. - Umbrel uses the `app_proxy` service in `docker-compose.yml`.
- Internal app port is `3001`. - 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`. - If your store prefix changes, update `id` in `umbrel-app.yml` and `APP_HOST` in `docker-compose.yml`.
## Notes ## Notes

View File

@@ -7,7 +7,7 @@ services:
APP_PORT: 3001 APP_PORT: 3001
server: 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 init: true
restart: on-failure restart: on-failure
stop_grace_period: 1m stop_grace_period: 1m

View File

@@ -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 connectWithCredentials = async (auth, lastFolder, credOverride = credentials, options = {}) => {
const { skipInitialFetchIfCached = false } = options; const { skipInitialFetchIfCached = false } = options;
try { try {
@@ -573,12 +634,12 @@ export default function Webmail() {
await fetchInbox(defaultFolder, auth, true, credOverride, { await fetchInbox(defaultFolder, auth, true, credOverride, {
keepSelection: true, keepSelection: true,
silent: Boolean(cachedDefaultEmails) || skipInitialFetchIfCached, silent: Boolean(cachedDefaultEmails) || skipInitialFetchIfCached,
fetchAll: true, resetToFirstPage: true
resetToFirstPage: false
}); });
saveSession(credOverride, defaultFolder); await hydrateFolderFully(defaultFolder, auth, credOverride);
upsertAccount(credOverride, defaultFolder); saveSession(credOverride, defaultFolder);
return true; upsertAccount(credOverride, defaultFolder);
return true;
} catch (error) { } catch (error) {
setLoginError(error.message || 'Login failed.'); setLoginError(error.message || 'Login failed.');
setIsLoggedIn(false); setIsLoggedIn(false);
@@ -1312,12 +1373,14 @@ export default function Webmail() {
setIsRefreshingInbox(true); setIsRefreshingInbox(true);
try { try {
const shouldFetchAll = !Boolean(activeFolderPaging?.loadedAll); const shouldFetchAll = !Boolean(activeFolderPaging?.loadedAll);
await fetchInbox(activeFolder, authPayload, true, credentials, { if (shouldFetchAll) {
keepSelection: true, await hydrateFolderFully(activeFolder, authPayload, credentials);
syncOnly: !shouldFetchAll, } else {
fetchAll: shouldFetchAll, await fetchInbox(activeFolder, authPayload, true, credentials, {
forceRefresh: shouldFetchAll keepSelection: true,
}); syncOnly: true
});
}
await fetchFolderSizes(authPayload); await fetchFolderSizes(authPayload);
await fetchQuota(authPayload); await fetchQuota(authPayload);
} finally { } finally {

View File

@@ -4,7 +4,7 @@ name: Webmail
tagline: Self-hosted IMAP/SMTP webmail client tagline: Self-hosted IMAP/SMTP webmail client
icon: https://git.weektab.org/nexus/umbrel-apps/raw/branch/main/gallery/webmail/icon.png icon: https://git.weektab.org/nexus/umbrel-apps/raw/branch/main/gallery/webmail/icon.png
category: utilities category: utilities
version: "1.0.7" version: "1.0.8"
port: 3001 port: 3001
description: >- description: >-
Webmail is a lightweight, self-hosted webmail app for connecting to external 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 support: https://git.weektab.org/nexus/webmail/issues
gallery: [] gallery: []
releaseNotes: >- 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: [] dependencies: []
path: "" path: ""
defaultUsername: "" defaultUsername: ""