@bobfrankston/rmfmail 1.1.169 → 1.1.174

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 (43) hide show
  1. package/bin/mailx.js +110 -2
  2. package/bin/mailx.js.map +1 -1
  3. package/bin/mailx.ts +95 -2
  4. package/client/android-bootstrap.bundle.js +4 -3
  5. package/client/android-bootstrap.bundle.js.map +2 -2
  6. package/client/app.bundle.js +55 -6
  7. package/client/app.bundle.js.map +2 -2
  8. package/client/components/message-list.js +96 -7
  9. package/client/components/message-list.js.map +1 -1
  10. package/client/components/message-list.ts +94 -6
  11. package/package.json +5 -5
  12. package/packages/mailx-core/index.js +1 -1
  13. package/packages/mailx-core/index.js.map +1 -1
  14. package/packages/mailx-core/index.ts +1 -1
  15. package/packages/mailx-imap/index.d.ts +11 -0
  16. package/packages/mailx-imap/index.d.ts.map +1 -1
  17. package/packages/mailx-imap/index.js +111 -5
  18. package/packages/mailx-imap/index.js.map +1 -1
  19. package/packages/mailx-imap/index.ts +102 -5
  20. package/packages/mailx-imap/package-lock.json +2 -2
  21. package/packages/mailx-imap/package.json +1 -1
  22. package/packages/mailx-store/db.d.ts +9 -1
  23. package/packages/mailx-store/db.d.ts.map +1 -1
  24. package/packages/mailx-store/db.js +15 -2
  25. package/packages/mailx-store/db.js.map +1 -1
  26. package/packages/mailx-store/db.ts +18 -4
  27. package/packages/mailx-store/package.json +1 -1
  28. package/packages/mailx-store/store.js +1 -1
  29. package/packages/mailx-store/store.js.map +1 -1
  30. package/packages/mailx-store/store.ts +1 -1
  31. package/packages/mailx-store-web/android-bootstrap.js +1 -1
  32. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  33. package/packages/mailx-store-web/android-bootstrap.ts +1 -1
  34. package/packages/mailx-store-web/db.d.ts +2 -1
  35. package/packages/mailx-store-web/db.d.ts.map +1 -1
  36. package/packages/mailx-store-web/db.js +3 -2
  37. package/packages/mailx-store-web/db.js.map +1 -1
  38. package/packages/mailx-store-web/db.ts +4 -3
  39. package/packages/mailx-store-web/package.json +1 -1
  40. package/packages/mailx-store-web/sync-manager.js +1 -1
  41. package/packages/mailx-store-web/sync-manager.js.map +1 -1
  42. package/packages/mailx-store-web/sync-manager.ts +1 -1
  43. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-61952 → node_modules.npmglobalize-stash-50120}/.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", "import", "log", "reauth"];
402
+ const __commandFlags = ["kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "recover", "fix-flags", "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", "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", "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];
@@ -500,6 +500,10 @@ for (const arg of args) {
500
500
  " (same 200-newest cap; fixes corrupt fields)\n" +
501
501
  " -recover REBUILD DB index from .eml files on disk\n" +
502
502
  " (no network; restores everything you had)\n" +
503
+ " -fix-flags Reconcile local \\Flagged stars against the\n" +
504
+ " IMAP server's actual SEARCH FLAGGED truth\n" +
505
+ " (one-shot cleanup for the pre-1.1.172 UID-\n" +
506
+ " collision bug)\n" +
503
507
  " -reauth Clear cached OAuth tokens; re-consent on next start\n" +
504
508
  " -log Print log file path and exit\n" +
505
509
  " -email <addr> First-time setup with this email (skips prompt)\n" +
@@ -905,6 +909,95 @@ if (recoverMode) {
905
909
  process.exit(0);
906
910
  }
907
911
 
912
+ // Fix flags: strip every \Flagged that exists ONLY in the local DB but NOT
913
+ // on the IMAP server, per folder. The pre-1.1.172 updateMessageFlags had a
914
+ // UID-without-folder bug that propagated a flag set on one folder's UID to
915
+ // every same-numeric-UID row across the account — leaving stars on hundreds
916
+ // of letters the user never starred. This is the one-shot cleanup that
917
+ // reconciles every local flag against the server's truth.
918
+ //
919
+ // What it does: per IMAP-account folder, runs `UID SEARCH FLAGGED` on the
920
+ // server, takes that as authoritative, and clears \Flagged from every local
921
+ // row whose UID isn't in the server's set.
922
+ //
923
+ // What it does NOT touch: \Seen, \Draft, or any custom flags — only the
924
+ // star (\Flagged). That keeps the cleanup scope tight to the symptom the
925
+ // user reported.
926
+ if (hasFlag("fix-flags")) {
927
+ const { getConfigDir } = await import("@bobfrankston/mailx-settings");
928
+ const { loadSettings } = await import("@bobfrankston/mailx-settings");
929
+ const { NativeImapClient } = await import("@bobfrankston/iflow-direct");
930
+ const { NodeTcpTransport } = await import("@bobfrankston/node-tcp-transport");
931
+ const dbDir = getConfigDir();
932
+ const dbPath = path.join(dbDir, "mailx.db");
933
+ if (!fs.existsSync(dbPath)) { console.error("No database found."); process.exit(1); }
934
+ const settings = loadSettings();
935
+ const { DatabaseSync } = await import("node:sqlite");
936
+ const db = new DatabaseSync(dbPath);
937
+ db.exec("PRAGMA journal_mode = WAL");
938
+ console.log("Fixing spurious \\Flagged stars...");
939
+ console.log(" Authoritative source: each folder's server-side UID SEARCH FLAGGED.");
940
+ console.log(" Only \\Flagged is touched; \\Seen and others are left alone.");
941
+ let totalCleared = 0;
942
+ let totalFolders = 0;
943
+ for (const acct of settings.accounts) {
944
+ const imap: any = (acct as any).imap;
945
+ if (!imap) { console.log(` [skip] ${acct.id}: no IMAP config (likely Gmail-API)`); continue; }
946
+ if (!imap.password) { console.log(` [skip] ${acct.id}: OAuth account — fix-flags only supports password-auth IMAP for now`); continue; }
947
+ let client: any;
948
+ try {
949
+ client = new (NativeImapClient as any)({
950
+ server: imap.host, port: imap.port || 993,
951
+ user: imap.user || acct.email, password: imap.password,
952
+ useTls: imap.tls !== false,
953
+ }, () => new (NodeTcpTransport as any)());
954
+ await client.connect();
955
+ const folderRows = db.prepare("SELECT id, path FROM folders WHERE account_id = ?").all(acct.id) as { id: number; path: string }[];
956
+ for (const folder of folderRows) {
957
+ try {
958
+ await client.select(folder.path);
959
+ const serverFlaggedUids: number[] = await client.search("FLAGGED");
960
+ const serverSet = new Set<number>(serverFlaggedUids);
961
+ // Pull every local row in this folder that has \Flagged
962
+ const stmt = db.prepare(
963
+ "SELECT uid, flags_json FROM messages WHERE account_id = ? AND folder_id = ? AND flags_json LIKE ?"
964
+ );
965
+ const upd = db.prepare(
966
+ "UPDATE messages SET flags_json = ? WHERE account_id = ? AND folder_id = ? AND uid = ?"
967
+ );
968
+ let cleared = 0;
969
+ for (const r of stmt.iterate(acct.id, folder.id, "%\\\\Flagged%") as any) {
970
+ const uid = r.uid;
971
+ if (serverSet.has(uid)) continue;
972
+ let flags: string[] = [];
973
+ try { flags = JSON.parse(r.flags_json); } catch { /* */ }
974
+ const without = flags.filter(f => f !== "\\Flagged");
975
+ if (without.length !== flags.length) {
976
+ upd.run(JSON.stringify(without), acct.id, folder.id, uid);
977
+ cleared++;
978
+ }
979
+ }
980
+ if (cleared > 0) {
981
+ console.log(` [${acct.id}] ${folder.path}: cleared ${cleared} spurious \\Flagged (server has ${serverFlaggedUids.length} actual)`);
982
+ totalCleared += cleared;
983
+ }
984
+ totalFolders++;
985
+ } catch (e: any) {
986
+ console.error(` [${acct.id}] ${folder.path}: ${e?.message || e}`);
987
+ }
988
+ }
989
+ } catch (e: any) {
990
+ console.error(` [${acct.id}] connect failed: ${e?.message || e}`);
991
+ } finally {
992
+ try { if (client) await client.logout(); } catch { /* */ }
993
+ }
994
+ }
995
+ db.close();
996
+ console.log(`Done. Cleared ${totalCleared} spurious \\Flagged rows across ${totalFolders} folders.`);
997
+ console.log(" Run 'rmfmail' to start the daemon.");
998
+ process.exit(0);
999
+ }
1000
+
908
1001
  // Import accounts from a local file into GDrive
909
1002
  if (importMode) {
910
1003
  const importPath = args.find(a => !a.startsWith("-"));
@@ -5209,8 +5209,9 @@ var WebMailxDB = class {
5209
5209
  const r = this.get("SELECT provider_id FROM messages WHERE account_id = ? AND uid = ?", [accountId, uid]);
5210
5210
  return r?.provider_id || "";
5211
5211
  }
5212
- updateMessageFlags(accountId, uid, flags) {
5213
- this.run("UPDATE messages SET flags_json = ? WHERE account_id = ? AND uid = ?", [JSON.stringify(flags), accountId, uid]);
5212
+ /** folderId required — see main mailx-store/db.ts for why. */
5213
+ updateMessageFlags(accountId, folderId, uid, flags) {
5214
+ this.run("UPDATE messages SET flags_json = ? WHERE account_id = ? AND folder_id = ? AND uid = ?", [JSON.stringify(flags), accountId, folderId, uid]);
5214
5215
  }
5215
5216
  updateBodyPath(accountId, uid, bodyPath) {
5216
5217
  this.run("UPDATE messages SET body_path = ? WHERE account_id = ? AND uid = ?", [bodyPath, accountId, uid]);
@@ -9995,7 +9996,7 @@ var AndroidSyncManager = class {
9995
9996
  return raw;
9996
9997
  }
9997
9998
  async updateFlagsLocal(accountId, uid, folderId, flags) {
9998
- this.db.updateMessageFlags(accountId, uid, flags);
9999
+ this.db.updateMessageFlags(accountId, folderId, uid, flags);
9999
10000
  this.db.recalcFolderCounts(folderId);
10000
10001
  this.db.queueSyncAction(accountId, "flags", uid, folderId, { flags });
10001
10002
  emitEvent({ type: "folderCountsChanged", accountId, counts: {} });