@bobfrankston/rmfmail 1.1.195 → 1.1.197
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/bin/mailx.js +94 -2
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +75 -2
- package/client/android-bootstrap.bundle.js +6 -4
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/package.json +5 -5
- package/packages/mailx-imap/index.js +4 -4
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +4 -4
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-store/db.d.ts +11 -3
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +48 -6
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +51 -9
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store-web/android-bootstrap.js +2 -2
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +2 -2
- package/packages/mailx-store-web/db.d.ts +3 -1
- package/packages/mailx-store-web/db.d.ts.map +1 -1
- package/packages/mailx-store-web/db.js +4 -2
- package/packages/mailx-store-web/db.js.map +1 -1
- package/packages/mailx-store-web/db.ts +5 -3
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-store-web/sync-manager.js +1 -1
- package/packages/mailx-store-web/sync-manager.js.map +1 -1
- package/packages/mailx-store-web/sync-manager.ts +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-43840 → node_modules.npmglobalize-stash-76388}/.package-lock.json +0 -0
package/bin/mailx.ts
CHANGED
|
@@ -399,7 +399,7 @@ function pidIsMailx(pid: number): boolean {
|
|
|
399
399
|
// on an old UI with no indication that the install has been upgraded.
|
|
400
400
|
// Skip this logic for command-only flags (kill, rebuild, setup, ...) and for
|
|
401
401
|
// the internal --daemon respawn.
|
|
402
|
-
const __commandFlags = ["kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "recover", "fix-flags", "import", "log", "reauth"];
|
|
402
|
+
const __commandFlags = ["kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "recover", "fix-flags", "relink-bodies", "import", "log", "reauth"];
|
|
403
403
|
const __isCommandInvocation = process.argv.slice(2).some(a => __commandFlags.includes(a.replace(/^--?/, "")));
|
|
404
404
|
// `--another` opts out of the replace-on-launch sweep so the user can run
|
|
405
405
|
// multiple rmfmail instances side by side (testing, parallel sessions, etc.).
|
|
@@ -473,7 +473,7 @@ const recoverMode = hasFlag("recover");
|
|
|
473
473
|
const importMode = hasFlag("import");
|
|
474
474
|
|
|
475
475
|
// Validate arguments
|
|
476
|
-
const knownFlags = ["verbose", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "recover", "fix-flags", "log", "import", "email", "mail", "daemon", "reauth", "mailto", "register-mailto", "unregister-mailto", "allow-elevated", "another", "server", "no-browser", "debug-server", "send", "account"];
|
|
476
|
+
const knownFlags = ["verbose", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "recover", "fix-flags", "relink-bodies", "log", "import", "email", "mail", "daemon", "reauth", "mailto", "register-mailto", "unregister-mailto", "allow-elevated", "another", "server", "no-browser", "debug-server", "send", "account"];
|
|
477
477
|
for (const arg of args) {
|
|
478
478
|
// Strip a leading -/-- and any `=value` suffix before checking.
|
|
479
479
|
const flag = arg.replace(/^--?/, "").split("=")[0];
|
|
@@ -1027,6 +1027,79 @@ if (hasFlag("fix-flags")) {
|
|
|
1027
1027
|
process.exit(0);
|
|
1028
1028
|
}
|
|
1029
1029
|
|
|
1030
|
+
// Relink message bodies from on-disk .eml files by Message-ID (no network).
|
|
1031
|
+
// Heals the body_path cross-wiring from the pre-1.1.196 updateBodyMeta bug:
|
|
1032
|
+
// the .eml files on disk are correct; only the DB pointers were wrong. Match
|
|
1033
|
+
// each file to its row by Message-ID and repair body_path — far cheaper than
|
|
1034
|
+
// clearing 55k pointers and re-downloading from the server (Bob 2026-05-29).
|
|
1035
|
+
if (hasFlag("relink-bodies")) {
|
|
1036
|
+
const { getConfigDir, getStorePath } = await import("@bobfrankston/mailx-settings");
|
|
1037
|
+
const dbDir = getConfigDir();
|
|
1038
|
+
const storePath = getStorePath();
|
|
1039
|
+
const dbPath = path.join(dbDir, "mailx.db");
|
|
1040
|
+
if (!fs.existsSync(dbPath)) { console.error("No database found."); process.exit(1); }
|
|
1041
|
+
if (!fs.existsSync(storePath)) { console.error(`No mailxstore at ${storePath}.`); process.exit(1); }
|
|
1042
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
1043
|
+
const db = new DatabaseSync(dbPath);
|
|
1044
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
1045
|
+
console.log("Relinking bodies from on-disk .eml by Message-ID (local-only)...");
|
|
1046
|
+
|
|
1047
|
+
const readMsgId = (p: string): string => {
|
|
1048
|
+
try {
|
|
1049
|
+
const fd = fs.openSync(p, "r");
|
|
1050
|
+
const buf = Buffer.alloc(8192);
|
|
1051
|
+
const n = fs.readSync(fd, buf, 0, 8192, 0);
|
|
1052
|
+
fs.closeSync(fd);
|
|
1053
|
+
const head = buf.slice(0, n).toString("utf-8");
|
|
1054
|
+
const end = head.search(/\r?\n\r?\n/);
|
|
1055
|
+
const hdr = end >= 0 ? head.slice(0, end) : head;
|
|
1056
|
+
const m = hdr.match(/^Message-I[dD]:[ \t]*(.*(?:\r?\n[ \t]+.*)*)/im);
|
|
1057
|
+
return m ? m[1].replace(/\r?\n[ \t]+/g, " ").trim().replace(/^<|>$/g, "") : "";
|
|
1058
|
+
} catch { return ""; }
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
// 1. Index every .eml by account + Message-ID → relative path.
|
|
1062
|
+
const midToPath = new Map<string, string>(); // key: `${acct}\x00${mid}`
|
|
1063
|
+
let scanned = 0;
|
|
1064
|
+
const accountDirs = fs.readdirSync(storePath, { withFileTypes: true })
|
|
1065
|
+
.filter(d => d.isDirectory()).map(d => d.name);
|
|
1066
|
+
const walk = (acct: string, dir: string): void => {
|
|
1067
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1068
|
+
const p = path.join(dir, e.name);
|
|
1069
|
+
if (e.isDirectory()) { walk(acct, p); continue; }
|
|
1070
|
+
if (!e.isFile() || !e.name.endsWith(".eml")) continue;
|
|
1071
|
+
scanned++;
|
|
1072
|
+
const mid = readMsgId(p);
|
|
1073
|
+
if (!mid) continue;
|
|
1074
|
+
const key = `${acct}\x00${mid}`;
|
|
1075
|
+
if (!midToPath.has(key)) midToPath.set(key, path.relative(storePath, p).replace(/\\/g, "/"));
|
|
1076
|
+
if (scanned % 5000 === 0) console.log(` scanned ${scanned} .eml...`);
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
for (const acct of accountDirs) walk(acct, path.join(storePath, acct));
|
|
1080
|
+
console.log(` indexed ${midToPath.size} unique Message-IDs from ${scanned} .eml files`);
|
|
1081
|
+
|
|
1082
|
+
// 2. For each row with a Message-ID, point body_path at the matching .eml
|
|
1083
|
+
// (or clear it when no file on disk matches → re-fetch on demand).
|
|
1084
|
+
const rows = db.prepare("SELECT id, account_id, message_id, body_path FROM messages WHERE message_id IS NOT NULL AND message_id != ''").all() as any[];
|
|
1085
|
+
const upd = db.prepare("UPDATE messages SET body_path = ?, body_parsed_at = NULL WHERE id = ?");
|
|
1086
|
+
let relinked = 0, already = 0, cleared = 0;
|
|
1087
|
+
for (const r of rows) {
|
|
1088
|
+
const mid = String(r.message_id).replace(/^<|>$/g, "");
|
|
1089
|
+
const correct = midToPath.get(`${r.account_id}\x00${mid}`);
|
|
1090
|
+
if (correct) {
|
|
1091
|
+
if (r.body_path !== correct) { upd.run(correct, r.id); relinked++; }
|
|
1092
|
+
else already++;
|
|
1093
|
+
} else if (r.body_path) {
|
|
1094
|
+
upd.run("", r.id); cleared++;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
db.close();
|
|
1098
|
+
console.log(`Relink done: ${relinked} repaired, ${already} already correct, ${cleared} cleared (no matching .eml → will re-fetch).`);
|
|
1099
|
+
console.log(" Run 'rmfmail' to start the daemon.");
|
|
1100
|
+
process.exit(0);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1030
1103
|
// Import accounts from a local file into GDrive
|
|
1031
1104
|
if (importMode) {
|
|
1032
1105
|
const importPath = args.find(a => !a.startsWith("-"));
|
|
@@ -5213,8 +5213,10 @@ var WebMailxDB = class {
|
|
|
5213
5213
|
updateMessageFlags(accountId, folderId, uid, flags) {
|
|
5214
5214
|
this.run("UPDATE messages SET flags_json = ? WHERE account_id = ? AND folder_id = ? AND uid = ?", [JSON.stringify(flags), accountId, folderId, uid]);
|
|
5215
5215
|
}
|
|
5216
|
-
|
|
5217
|
-
|
|
5216
|
+
/** folderId REQUIRED — see main mailx-store/db.ts: a (account, uid) write
|
|
5217
|
+
* cross-wires body_path across every folder sharing the numeric UID. */
|
|
5218
|
+
updateBodyPath(accountId, folderId, uid, bodyPath) {
|
|
5219
|
+
this.run("UPDATE messages SET body_path = ? WHERE account_id = ? AND folder_id = ? AND uid = ?", [bodyPath, accountId, folderId, uid]);
|
|
5218
5220
|
}
|
|
5219
5221
|
getMessagesWithoutBody(accountId, limit = 50) {
|
|
5220
5222
|
return this.all("SELECT uid, folder_id as folderId FROM messages WHERE account_id = ? AND (body_path IS NULL OR body_path NOT LIKE 'idb:%') ORDER BY (size IS NULL OR size = 0), size ASC, date DESC LIMIT ?", [accountId, limit]);
|
|
@@ -9779,7 +9781,7 @@ var AndroidSyncManager = class {
|
|
|
9779
9781
|
await new Promise((r) => setTimeout(r, rateLimitCooldownUntil - now));
|
|
9780
9782
|
}
|
|
9781
9783
|
if (await this.bodyStore.hasMessage(accountId, m.folderId, m.uid)) {
|
|
9782
|
-
this.db.updateBodyPath(accountId, m.uid, `idb:${accountId}/${m.folderId}/${m.uid}`);
|
|
9784
|
+
this.db.updateBodyPath(accountId, m.folderId, m.uid, `idb:${accountId}/${m.folderId}/${m.uid}`);
|
|
9783
9785
|
progressedThisBatch = true;
|
|
9784
9786
|
continue;
|
|
9785
9787
|
}
|
|
@@ -9991,7 +9993,7 @@ var AndroidSyncManager = class {
|
|
|
9991
9993
|
}
|
|
9992
9994
|
const raw = new TextEncoder().encode(msg.source);
|
|
9993
9995
|
await this.bodyStore.putMessage(accountId, folderId, uid, raw);
|
|
9994
|
-
this.db.updateBodyPath(accountId, uid, `idb:${accountId}/${folderId}/${uid}`);
|
|
9996
|
+
this.db.updateBodyPath(accountId, folderId, uid, `idb:${accountId}/${folderId}/${uid}`);
|
|
9995
9997
|
console.log(`[fetchBody] fetched + cached ${accountId}/${folderId}/${uid} (${raw.byteLength} bytes, ${Date.now() - t0}ms)`);
|
|
9996
9998
|
return raw;
|
|
9997
9999
|
}
|