@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.
Files changed (30) hide show
  1. package/bin/mailx.js +94 -2
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +75 -2
  4. package/client/android-bootstrap.bundle.js +6 -4
  5. package/client/android-bootstrap.bundle.js.map +2 -2
  6. package/package.json +5 -5
  7. package/packages/mailx-imap/index.js +4 -4
  8. package/packages/mailx-imap/index.js.map +1 -1
  9. package/packages/mailx-imap/index.ts +4 -4
  10. package/packages/mailx-imap/package-lock.json +2 -2
  11. package/packages/mailx-imap/package.json +1 -1
  12. package/packages/mailx-store/db.d.ts +11 -3
  13. package/packages/mailx-store/db.d.ts.map +1 -1
  14. package/packages/mailx-store/db.js +48 -6
  15. package/packages/mailx-store/db.js.map +1 -1
  16. package/packages/mailx-store/db.ts +51 -9
  17. package/packages/mailx-store/package.json +1 -1
  18. package/packages/mailx-store-web/android-bootstrap.js +2 -2
  19. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  20. package/packages/mailx-store-web/android-bootstrap.ts +2 -2
  21. package/packages/mailx-store-web/db.d.ts +3 -1
  22. package/packages/mailx-store-web/db.d.ts.map +1 -1
  23. package/packages/mailx-store-web/db.js +4 -2
  24. package/packages/mailx-store-web/db.js.map +1 -1
  25. package/packages/mailx-store-web/db.ts +5 -3
  26. package/packages/mailx-store-web/package.json +1 -1
  27. package/packages/mailx-store-web/sync-manager.js +1 -1
  28. package/packages/mailx-store-web/sync-manager.js.map +1 -1
  29. package/packages/mailx-store-web/sync-manager.ts +1 -1
  30. /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
- updateBodyPath(accountId, uid, bodyPath) {
5217
- this.run("UPDATE messages SET body_path = ? WHERE account_id = ? AND uid = ?", [bodyPath, accountId, uid]);
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
  }