@bobfrankston/mailx 1.0.338 → 1.0.339
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/package.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Stub for `marked` — pulled in transitively via msger → msgcommon → markdown.ts.
|
|
2
|
+
// mailx-host doesn't use marked at runtime; the type chain only references it
|
|
3
|
+
// because msgcommon ships markdown.ts alongside its other modules. Without
|
|
4
|
+
// this shim, tsc errors with "Cannot find module 'marked'" while building
|
|
5
|
+
// mailx-host. The proper fix is `npm install` inside msgcommon (which has
|
|
6
|
+
// marked declared as a dep) or installing marked at the mailx root; this
|
|
7
|
+
// shim is a fallback so the build doesn't block on either.
|
|
8
|
+
declare module "marked" {
|
|
9
|
+
export const marked: {
|
|
10
|
+
parse(input: string, opts?: any): string | Promise<string>;
|
|
11
|
+
Renderer: any;
|
|
12
|
+
[k: string]: any;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -1640,10 +1640,42 @@ export class ImapManager extends EventEmitter {
|
|
|
1640
1640
|
// "gmail: 17266796 bodies cached" in the logs, which is the counter
|
|
1641
1641
|
// spinning on the same 100 rows.
|
|
1642
1642
|
if (await this.bodyStore.hasMessage(accountId, folderId, uid)) {
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1643
|
+
// COMINGLING GUARD: verify the cached body's Message-ID matches the
|
|
1644
|
+
// DB row's messageId. If UIDVALIDITY changed server-side (mailbox
|
|
1645
|
+
// recreated, server quirk) the same integer UID can point at a
|
|
1646
|
+
// different message — the on-disk .eml becomes stale but hasMessage()
|
|
1647
|
+
// still returns true. User-reported: "Peter Hoddie letter comingled
|
|
1648
|
+
// with a much older letter." Check fixes it regardless of root cause.
|
|
1649
|
+
const cached = await this.bodyStore.getMessage(accountId, folderId, uid);
|
|
1650
|
+
const envelope = this.db.getMessageByUid(accountId, uid, folderId);
|
|
1651
|
+
const expectedId = envelope?.messageId || "";
|
|
1652
|
+
if (expectedId) {
|
|
1653
|
+
// Scan headers only — Message-ID should land in the first few KB.
|
|
1654
|
+
const head = cached.subarray(0, Math.min(cached.length, 16 * 1024)).toString("utf-8");
|
|
1655
|
+
const m = head.match(/^Message-ID:\s*<([^>\r\n]+)>/im);
|
|
1656
|
+
const cachedId = m ? `<${m[1]}>` : "";
|
|
1657
|
+
if (cachedId && expectedId && cachedId !== expectedId) {
|
|
1658
|
+
console.error(` [body] COMINGLING DETECTED ${accountId}/${folderId}/${uid}: expected ${expectedId}, cached ${cachedId} — dropping cache, re-fetching`);
|
|
1659
|
+
try {
|
|
1660
|
+
await this.bodyStore.deleteMessage(accountId, folderId, uid);
|
|
1661
|
+
}
|
|
1662
|
+
catch { /* */ }
|
|
1663
|
+
// fall through to re-fetch path
|
|
1664
|
+
}
|
|
1665
|
+
else {
|
|
1666
|
+
const existingPath = this.bodyStore.getMessagePath?.(accountId, folderId, uid);
|
|
1667
|
+
if (existingPath)
|
|
1668
|
+
this.db.updateBodyPath(accountId, uid, existingPath);
|
|
1669
|
+
return cached;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
else {
|
|
1673
|
+
// No messageId on the DB row (shouldn't happen but be permissive).
|
|
1674
|
+
const existingPath = this.bodyStore.getMessagePath?.(accountId, folderId, uid);
|
|
1675
|
+
if (existingPath)
|
|
1676
|
+
this.db.updateBodyPath(accountId, uid, existingPath);
|
|
1677
|
+
return cached;
|
|
1678
|
+
}
|
|
1647
1679
|
}
|
|
1648
1680
|
if (!this.configs.has(accountId))
|
|
1649
1681
|
return null;
|
|
@@ -2969,10 +3001,20 @@ export class ImapManager extends EventEmitter {
|
|
|
2969
3001
|
const files = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc", "config.jsonc"];
|
|
2970
3002
|
const configDir = getConfigDir();
|
|
2971
3003
|
const debounce = new Map();
|
|
3004
|
+
// Cache the last-seen normalized content per file. fs.watch fires on
|
|
3005
|
+
// metadata-only events (atime, attrib change) AND on no-op rewrites
|
|
3006
|
+
// that land identical bytes — both would fire a spurious banner.
|
|
3007
|
+
// Compare after the debounce window and only emit on a real change.
|
|
3008
|
+
const normalize = (s) => s.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/[ \t\r\n]+$/, "");
|
|
3009
|
+
const lastContent = new Map();
|
|
2972
3010
|
for (const filename of files) {
|
|
2973
3011
|
const full = path.join(configDir, filename);
|
|
2974
3012
|
if (!fs.existsSync(full))
|
|
2975
3013
|
continue;
|
|
3014
|
+
try {
|
|
3015
|
+
lastContent.set(filename, normalize(fs.readFileSync(full, "utf-8")));
|
|
3016
|
+
}
|
|
3017
|
+
catch { /* */ }
|
|
2976
3018
|
try {
|
|
2977
3019
|
const watcher = fs.watch(full, () => {
|
|
2978
3020
|
const prev = debounce.get(filename);
|
|
@@ -2980,7 +3022,32 @@ export class ImapManager extends EventEmitter {
|
|
|
2980
3022
|
clearTimeout(prev);
|
|
2981
3023
|
debounce.set(filename, setTimeout(() => {
|
|
2982
3024
|
debounce.delete(filename);
|
|
2983
|
-
|
|
3025
|
+
let current = "";
|
|
3026
|
+
try {
|
|
3027
|
+
current = normalize(fs.readFileSync(full, "utf-8"));
|
|
3028
|
+
}
|
|
3029
|
+
catch { /* missing */ }
|
|
3030
|
+
const previous = lastContent.get(filename) || "";
|
|
3031
|
+
if (current === previous) {
|
|
3032
|
+
console.log(` [watch] ${filename} fs.watch fired but content unchanged — no banner`);
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
// Log a short diff hint so repeat-firings are diagnosable.
|
|
3036
|
+
const prevSize = previous.length;
|
|
3037
|
+
const curSize = current.length;
|
|
3038
|
+
const firstDiff = (() => {
|
|
3039
|
+
const n = Math.min(prevSize, curSize);
|
|
3040
|
+
for (let i = 0; i < n; i++)
|
|
3041
|
+
if (previous[i] !== current[i])
|
|
3042
|
+
return i;
|
|
3043
|
+
return n;
|
|
3044
|
+
})();
|
|
3045
|
+
const prevSnip = previous.slice(Math.max(0, firstDiff - 20), firstDiff + 40).replace(/\n/g, "\\n");
|
|
3046
|
+
const curSnip = current.slice(Math.max(0, firstDiff - 20), firstDiff + 40).replace(/\n/g, "\\n");
|
|
3047
|
+
console.log(` [watch] ${filename} changed: size ${prevSize}→${curSize}, first diff at byte ${firstDiff}`);
|
|
3048
|
+
console.log(` [watch] was: ${JSON.stringify(prevSnip)}`);
|
|
3049
|
+
console.log(` [watch] now: ${JSON.stringify(curSnip)}`);
|
|
3050
|
+
lastContent.set(filename, current);
|
|
2984
3051
|
this.emit("configChanged", filename);
|
|
2985
3052
|
}, 500));
|
|
2986
3053
|
});
|
|
@@ -2999,11 +3066,9 @@ export class ImapManager extends EventEmitter {
|
|
|
2999
3066
|
// config.jsonc is per-machine / local-only — never polled.
|
|
3000
3067
|
const cloudFiles = ["accounts.jsonc", "allowlist.jsonc", "clients.jsonc"];
|
|
3001
3068
|
const CLOUD_POLL_MS = 3 * 60 * 1000;
|
|
3002
|
-
//
|
|
3003
|
-
//
|
|
3004
|
-
//
|
|
3005
|
-
// fs.watch, which shows the spurious "accounts.jsonc changed" banner.
|
|
3006
|
-
const normalize = (s) => s.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/[ \t\r\n]+$/, "");
|
|
3069
|
+
// normalize() reused from the fs.watch block above — same intent:
|
|
3070
|
+
// cloud round-trips that re-wrap newlines / add a trailing newline are
|
|
3071
|
+
// semantically identical; don't overwrite local on those.
|
|
3007
3072
|
const pollCloud = async () => {
|
|
3008
3073
|
let cloudRead;
|
|
3009
3074
|
let parseJsonc;
|