@bobfrankston/rmfmail 1.1.99 → 1.1.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +43 -2
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-list.js +55 -3
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +56 -3
- package/client/styles/components.css +10 -0
- package/npmchanges.md +19 -0
- package/package.json +1 -1
- package/packages/mailx-core/index.d.ts +1 -0
- package/packages/mailx-core/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.d.ts +24 -0
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +96 -4
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +90 -4
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +17 -0
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +15 -0
- package/packages/mailx-service/reconciler.d.ts.map +1 -1
- package/packages/mailx-service/reconciler.js +4 -0
- package/packages/mailx-service/reconciler.js.map +1 -1
- package/packages/mailx-service/reconciler.ts +6 -0
- package/packages/mailx-settings/docs/rmf-tiny.md +31 -2
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/db.d.ts +27 -0
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +99 -5
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +113 -5
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/index.d.ts +1 -0
- package/packages/mailx-types/index.d.ts.map +1 -1
- package/packages/mailx-types/index.js.map +1 -1
- package/packages/mailx-types/index.ts +1 -0
- package/packages/mailx-types/package.json +1 -1
- package/.commitmsg +0 -16
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-75440 → node_modules.npmglobalize-stash-12320}/.package-lock.json +0 -0
|
@@ -2717,6 +2717,36 @@ export class ImapManager extends EventEmitter {
|
|
|
2717
2717
|
clearFolderErrors(accountId, folderPath) {
|
|
2718
2718
|
this.folderErrorCooldown.delete(`${accountId}:${folderPath}`);
|
|
2719
2719
|
}
|
|
2720
|
+
/** UIDs that returned 0 bodies on a successful batch — either stale DB rows
|
|
2721
|
+
* whose UIDVALIDITY has rotated, or a transient iflow parser miss that
|
|
2722
|
+
* dropped the body literal mid-stream. The "NOT pruning" safety guard in
|
|
2723
|
+
* `_prefetchBodies` refuses to delete them so a transient miss can't
|
|
2724
|
+
* trigger data loss; the trade-off is they sit in `getMessagesWithoutBody`
|
|
2725
|
+
* and would otherwise dominate the size-asc query, starving live folders.
|
|
2726
|
+
*
|
|
2727
|
+
* Backoff schedule on repeated failure: 5 min → 30 min → 2 h → 12 h.
|
|
2728
|
+
* Without a TTL the very first transient miss permanently blocklists the
|
|
2729
|
+
* UID for the session and the row never shows the cached-body dot
|
|
2730
|
+
* (the symptom Bob reported 2026-05-19: document/large messages with
|
|
2731
|
+
* no teal mark). With TTL, transient misses self-heal in 5 minutes;
|
|
2732
|
+
* genuine zombies back off to once-per-12-hour retries — bounded load.
|
|
2733
|
+
* Key shape `${accountId}:${folderId}:${uid}`. */
|
|
2734
|
+
prefetchFailures = new Map();
|
|
2735
|
+
markPrefetchEmpty(accountId, folderId, uid) {
|
|
2736
|
+
const key = `${accountId}:${folderId}:${uid}`;
|
|
2737
|
+
const prev = this.prefetchFailures.get(key);
|
|
2738
|
+
this.prefetchFailures.set(key, { count: (prev?.count ?? 0) + 1, lastTried: Date.now() });
|
|
2739
|
+
}
|
|
2740
|
+
isPrefetchEmpty(accountId, folderId, uid) {
|
|
2741
|
+
const f = this.prefetchFailures.get(`${accountId}:${folderId}:${uid}`);
|
|
2742
|
+
if (!f)
|
|
2743
|
+
return false;
|
|
2744
|
+
const backoffMs = f.count <= 1 ? 5 * 60_000
|
|
2745
|
+
: f.count <= 2 ? 30 * 60_000
|
|
2746
|
+
: f.count <= 3 ? 2 * 3600_000
|
|
2747
|
+
: 12 * 3600_000;
|
|
2748
|
+
return Date.now() - f.lastTried < backoffMs;
|
|
2749
|
+
}
|
|
2720
2750
|
/** Background body-cache backfill. Public so the Reconciler can schedule
|
|
2721
2751
|
* the periodic tick under its priority/back-pressure rules; existing
|
|
2722
2752
|
* in-method post-sync nudges (sync, fetchSince, fetchOne) call this
|
|
@@ -2752,8 +2782,21 @@ export class ImapManager extends EventEmitter {
|
|
|
2752
2782
|
// governing unit). IMAP uses the batched `fetchBodiesBatch` path via iflow-direct —
|
|
2753
2783
|
// one SELECT + one UID FETCH per folder per tick instead of N round trips.
|
|
2754
2784
|
let announced = false;
|
|
2755
|
-
|
|
2756
|
-
|
|
2785
|
+
// Cap iterations so a fully-blocked queue can't loop forever. With
|
|
2786
|
+
// BATCH_SIZE=100 and the blocklist filter below, each iteration
|
|
2787
|
+
// either advances or terminates; this bound is a belt-and-braces
|
|
2788
|
+
// safety net for unexpected DB states.
|
|
2789
|
+
let iterationsRemaining = 50;
|
|
2790
|
+
while (iterationsRemaining-- > 0) {
|
|
2791
|
+
// Pull more than BATCH_SIZE from the DB so the empty-uid filter
|
|
2792
|
+
// can drop blocklisted rows and still hand a useful batch to the
|
|
2793
|
+
// fetcher. Without this, a queue dominated by stale UIDs returns
|
|
2794
|
+
// BATCH_SIZE rows that all get filtered to zero, and the loop
|
|
2795
|
+
// terminates without ever trying live messages further down the
|
|
2796
|
+
// size-asc list.
|
|
2797
|
+
const raw = this.db.getMessagesWithoutBody(accountId, BATCH_SIZE * 4);
|
|
2798
|
+
const missing = raw.filter(m => !this.isPrefetchEmpty(accountId, m.folderId, m.uid))
|
|
2799
|
+
.slice(0, BATCH_SIZE);
|
|
2757
2800
|
if (missing.length === 0)
|
|
2758
2801
|
break;
|
|
2759
2802
|
if (!announced) {
|
|
@@ -2794,7 +2837,8 @@ export class ImapManager extends EventEmitter {
|
|
|
2794
2837
|
try {
|
|
2795
2838
|
const raw = Buffer.from(source, "utf-8");
|
|
2796
2839
|
const bodyPath = await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
2797
|
-
|
|
2840
|
+
const parsed = await extractPreview(source);
|
|
2841
|
+
this.db.updateBodyMeta(accountId, uid, bodyPath, parsed.hasAttachments, parsed.preview);
|
|
2798
2842
|
this.emit("bodyCached", accountId, uid);
|
|
2799
2843
|
counters.totalFetched++;
|
|
2800
2844
|
madeProgress = true;
|
|
@@ -2844,6 +2888,8 @@ export class ImapManager extends EventEmitter {
|
|
|
2844
2888
|
}
|
|
2845
2889
|
else if (batchSucceeded && !someReceived) {
|
|
2846
2890
|
console.error(` [prefetch] ${accountId}/${folder.path}: Gmail batch returned 0/${uidsInFolder.length} bodies — NOT pruning (set-diff reconcile owns deletion). UIDs: ${uidsInFolder.slice(0, 5).join(",")}${uidsInFolder.length > 5 ? "..." : ""}`);
|
|
2891
|
+
for (const uid of uidsInFolder)
|
|
2892
|
+
this.markPrefetchEmpty(accountId, folderId, uid);
|
|
2847
2893
|
}
|
|
2848
2894
|
if (counters.errors >= ERROR_BUDGET)
|
|
2849
2895
|
break;
|
|
@@ -2923,7 +2969,8 @@ export class ImapManager extends EventEmitter {
|
|
|
2923
2969
|
try {
|
|
2924
2970
|
const raw = Buffer.from(source, "utf-8");
|
|
2925
2971
|
const bodyPath = await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
2926
|
-
|
|
2972
|
+
const parsed = await extractPreview(source);
|
|
2973
|
+
this.db.updateBodyMeta(accountId, uid, bodyPath, parsed.hasAttachments, parsed.preview);
|
|
2927
2974
|
this.emit("bodyCached", accountId, uid);
|
|
2928
2975
|
counters.totalFetched++;
|
|
2929
2976
|
madeProgress = true;
|
|
@@ -2974,6 +3021,8 @@ export class ImapManager extends EventEmitter {
|
|
|
2974
3021
|
}
|
|
2975
3022
|
else if (batchSucceeded && !someReceived) {
|
|
2976
3023
|
console.error(` [prefetch] ${accountId}/${folder.path}: chunk ${chunkStart}-${chunkStart + chunk.length - 1} returned 0/${chunk.length} bodies — NOT pruning (set-diff reconcile owns deletion). UIDs: ${chunk.slice(0, 5).join(",")}${chunk.length > 5 ? "..." : ""}`);
|
|
3024
|
+
for (const uid of chunk)
|
|
3025
|
+
this.markPrefetchEmpty(accountId, folderId, uid);
|
|
2977
3026
|
}
|
|
2978
3027
|
}
|
|
2979
3028
|
if (counters.errors >= ERROR_BUDGET)
|
|
@@ -3000,6 +3049,49 @@ export class ImapManager extends EventEmitter {
|
|
|
3000
3049
|
getBodyStore() {
|
|
3001
3050
|
return this.bodyStore;
|
|
3002
3051
|
}
|
|
3052
|
+
/** Re-parse existing .eml files from disk to backfill has_attachments
|
|
3053
|
+
* + preview for messages whose body landed before the prefetch-reparse
|
|
3054
|
+
* fix shipped. No IMAP traffic — pure local-disk work. Throttled by the
|
|
3055
|
+
* reconciler the same way as prefetch (back off when interactive lane
|
|
3056
|
+
* has clicks pending) so it doesn't fight with the user. */
|
|
3057
|
+
backfillingAccounts = new Set();
|
|
3058
|
+
async backfillBodyMeta(accountId, limit = 100) {
|
|
3059
|
+
if (this.backfillingAccounts.has(accountId))
|
|
3060
|
+
return;
|
|
3061
|
+
if (!this.configs.has(accountId))
|
|
3062
|
+
return;
|
|
3063
|
+
this.backfillingAccounts.add(accountId);
|
|
3064
|
+
try {
|
|
3065
|
+
const rows = this.db.getMessagesNeedingBodyReparse(accountId, limit);
|
|
3066
|
+
if (rows.length === 0)
|
|
3067
|
+
return;
|
|
3068
|
+
let ok = 0, fail = 0;
|
|
3069
|
+
for (const row of rows) {
|
|
3070
|
+
try {
|
|
3071
|
+
if (!(await this.bodyStore.hasByPath(row.bodyPath))) {
|
|
3072
|
+
// Body file vanished — stamp so we don't keep retrying.
|
|
3073
|
+
this.db.updateBodyMetaById(row.id, false, "");
|
|
3074
|
+
continue;
|
|
3075
|
+
}
|
|
3076
|
+
const buf = await this.bodyStore.readByPath(row.bodyPath);
|
|
3077
|
+
const parsed = await extractPreview(buf.toString("utf-8"));
|
|
3078
|
+
this.db.updateBodyMetaById(row.id, parsed.hasAttachments, parsed.preview);
|
|
3079
|
+
ok++;
|
|
3080
|
+
}
|
|
3081
|
+
catch (e) {
|
|
3082
|
+
fail++;
|
|
3083
|
+
console.error(` [backfill] ${accountId}/${row.uid}: ${e?.message || e}`);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
if (ok > 0 || fail > 0) {
|
|
3087
|
+
console.log(` [backfill] ${accountId}: reparsed ${ok}, failed ${fail}`);
|
|
3088
|
+
this.emit("folderCountsChanged", accountId, {});
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
finally {
|
|
3092
|
+
this.backfillingAccounts.delete(accountId);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3003
3095
|
/** Move message across accounts using iflow's moveMessageToServer */
|
|
3004
3096
|
async moveMessageCrossAccount(fromAccountId, uid, fromFolderId, toAccountId, toFolderId) {
|
|
3005
3097
|
const fromFolders = this.db.getFolders(fromAccountId);
|