@bobfrankston/rmfmail 1.1.170 → 1.1.177
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 +140 -4
- package/bin/mailx.js.map +1 -1
- package/bin/mailx.ts +127 -5
- package/client/android-bootstrap.bundle.js +4 -3
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +91 -6
- package/client/app.bundle.js.map +2 -2
- package/client/app.js +39 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +33 -0
- package/client/components/message-list.js +114 -7
- package/client/components/message-list.js.map +1 -1
- package/client/components/message-list.ts +113 -6
- package/client/compose/compose.bundle.js +2068 -1775
- package/client/compose/compose.bundle.js.map +4 -4
- package/client/compose/editor.js +85 -0
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +79 -0
- package/client/compose/spellcheck-core.js +253 -0
- package/client/compose/spellcheck-core.js.map +1 -0
- package/client/compose/spellcheck-core.ts +242 -0
- package/package.json +5 -5
- package/packages/mailx-core/index.js +1 -1
- package/packages/mailx-core/index.js.map +1 -1
- package/packages/mailx-core/index.ts +1 -1
- package/packages/mailx-imap/index.d.ts.map +1 -1
- package/packages/mailx-imap/index.js +15 -1
- package/packages/mailx-imap/index.js.map +1 -1
- package/packages/mailx-imap/index.ts +9 -1
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-store/db.d.ts +9 -1
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +15 -2
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +18 -4
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store/store.js +1 -1
- package/packages/mailx-store/store.js.map +1 -1
- package/packages/mailx-store/store.ts +1 -1
- package/packages/mailx-store-web/android-bootstrap.js +1 -1
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +1 -1
- package/packages/mailx-store-web/db.d.ts +2 -1
- package/packages/mailx-store-web/db.d.ts.map +1 -1
- package/packages/mailx-store-web/db.js +3 -2
- package/packages/mailx-store-web/db.js.map +1 -1
- package/packages/mailx-store-web/db.ts +4 -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-46236 → node_modules.npmglobalize-stash-50992}/.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" +
|
|
@@ -761,12 +765,28 @@ if (recoverMode) {
|
|
|
761
765
|
|
|
762
766
|
console.log("Recovering rmfmail index from .eml files on disk...");
|
|
763
767
|
console.log(` Store: ${storePath}`);
|
|
764
|
-
console.log("
|
|
768
|
+
console.log(" Local-only — no IMAP/network calls. Existing DB rows preserved.");
|
|
765
769
|
|
|
766
770
|
const { DatabaseSync } = await import("node:sqlite");
|
|
767
771
|
const db = new DatabaseSync(dbPath);
|
|
768
772
|
db.exec("PRAGMA journal_mode = WAL");
|
|
769
773
|
|
|
774
|
+
// BACKFILL existing rows that are missing a message_folders membership.
|
|
775
|
+
// The unified inbox view + per-folder list view both JOIN through
|
|
776
|
+
// message_folders, so a messages row with no membership is INVISIBLE
|
|
777
|
+
// in the UI even though it's in the DB. This catches the pre-1.1.174
|
|
778
|
+
// -recover runs that only populated messages — Bob's symptom was 8304
|
|
779
|
+
// INBOX rows in the folder badge but an empty list view (2026-05-27).
|
|
780
|
+
const backfilled = db.prepare(`
|
|
781
|
+
INSERT OR IGNORE INTO message_folders (message_row_id, folder_id, uid, last_seen_at)
|
|
782
|
+
SELECT m.id, m.folder_id, m.uid, m.cached_at
|
|
783
|
+
FROM messages m
|
|
784
|
+
WHERE NOT EXISTS (SELECT 1 FROM message_folders mf WHERE mf.message_row_id = m.id)
|
|
785
|
+
`).run();
|
|
786
|
+
if ((backfilled as any).changes > 0) {
|
|
787
|
+
console.log(` [backfill] inserted ${(backfilled as any).changes} missing message_folders rows`);
|
|
788
|
+
}
|
|
789
|
+
|
|
770
790
|
// Build the Message-ID set already in the DB so we can skip duplicates.
|
|
771
791
|
const seenStmt = db.prepare("SELECT message_id FROM messages WHERE account_id = ? AND message_id IS NOT NULL AND message_id != ''");
|
|
772
792
|
// INBOX folder per account (target for recovered rows). If no INBOX row
|
|
@@ -784,6 +804,12 @@ if (recoverMode) {
|
|
|
784
804
|
has_attachments, preview, body_path, cached_at)
|
|
785
805
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
786
806
|
`);
|
|
807
|
+
// Insert a membership row for every messages row we create so the
|
|
808
|
+
// list views (which JOIN through message_folders) can see them.
|
|
809
|
+
const insertMembership = db.prepare(`
|
|
810
|
+
INSERT OR IGNORE INTO message_folders (message_row_id, folder_id, uid, last_seen_at)
|
|
811
|
+
VALUES (?, ?, ?, ?)
|
|
812
|
+
`);
|
|
787
813
|
|
|
788
814
|
// Walk every account directory under storePath.
|
|
789
815
|
const accountDirs = fs.readdirSync(storePath, { withFileTypes: true })
|
|
@@ -861,10 +887,11 @@ if (recoverMode) {
|
|
|
861
887
|
const ccList = parseAddrs(ccRaw);
|
|
862
888
|
const stat = fs.statSync(p);
|
|
863
889
|
const rel = path.relative(storePath, p).replace(/\\/g, "/");
|
|
864
|
-
|
|
890
|
+
const thisSynthUid = synthUid--;
|
|
891
|
+
const result = insertMsg.run(
|
|
865
892
|
acct,
|
|
866
893
|
inboxFolderId,
|
|
867
|
-
|
|
894
|
+
thisSynthUid,
|
|
868
895
|
messageId,
|
|
869
896
|
date,
|
|
870
897
|
subject,
|
|
@@ -879,6 +906,12 @@ if (recoverMode) {
|
|
|
879
906
|
rel, // body_path (relative to storePath)
|
|
880
907
|
Date.now(), // cached_at
|
|
881
908
|
);
|
|
909
|
+
// Membership row — list views JOIN through this, without
|
|
910
|
+
// it the recovered row is invisible in the UI.
|
|
911
|
+
const newRowId = Number((result as any).lastInsertRowid);
|
|
912
|
+
if (newRowId > 0) {
|
|
913
|
+
insertMembership.run(newRowId, inboxFolderId, thisSynthUid, Date.now());
|
|
914
|
+
}
|
|
882
915
|
seen.add(messageId);
|
|
883
916
|
indexed++;
|
|
884
917
|
if (indexed % 500 === 0) console.log(` [${acct}] ${indexed} indexed (${scanned} scanned)`);
|
|
@@ -905,6 +938,95 @@ if (recoverMode) {
|
|
|
905
938
|
process.exit(0);
|
|
906
939
|
}
|
|
907
940
|
|
|
941
|
+
// Fix flags: strip every \Flagged that exists ONLY in the local DB but NOT
|
|
942
|
+
// on the IMAP server, per folder. The pre-1.1.172 updateMessageFlags had a
|
|
943
|
+
// UID-without-folder bug that propagated a flag set on one folder's UID to
|
|
944
|
+
// every same-numeric-UID row across the account — leaving stars on hundreds
|
|
945
|
+
// of letters the user never starred. This is the one-shot cleanup that
|
|
946
|
+
// reconciles every local flag against the server's truth.
|
|
947
|
+
//
|
|
948
|
+
// What it does: per IMAP-account folder, runs `UID SEARCH FLAGGED` on the
|
|
949
|
+
// server, takes that as authoritative, and clears \Flagged from every local
|
|
950
|
+
// row whose UID isn't in the server's set.
|
|
951
|
+
//
|
|
952
|
+
// What it does NOT touch: \Seen, \Draft, or any custom flags — only the
|
|
953
|
+
// star (\Flagged). That keeps the cleanup scope tight to the symptom the
|
|
954
|
+
// user reported.
|
|
955
|
+
if (hasFlag("fix-flags")) {
|
|
956
|
+
const { getConfigDir } = await import("@bobfrankston/mailx-settings");
|
|
957
|
+
const { loadSettings } = await import("@bobfrankston/mailx-settings");
|
|
958
|
+
const { NativeImapClient } = await import("@bobfrankston/iflow-direct");
|
|
959
|
+
const { NodeTcpTransport } = await import("@bobfrankston/node-tcp-transport");
|
|
960
|
+
const dbDir = getConfigDir();
|
|
961
|
+
const dbPath = path.join(dbDir, "mailx.db");
|
|
962
|
+
if (!fs.existsSync(dbPath)) { console.error("No database found."); process.exit(1); }
|
|
963
|
+
const settings = loadSettings();
|
|
964
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
965
|
+
const db = new DatabaseSync(dbPath);
|
|
966
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
967
|
+
console.log("Fixing spurious \\Flagged stars...");
|
|
968
|
+
console.log(" Authoritative source: each folder's server-side UID SEARCH FLAGGED.");
|
|
969
|
+
console.log(" Only \\Flagged is touched; \\Seen and others are left alone.");
|
|
970
|
+
let totalCleared = 0;
|
|
971
|
+
let totalFolders = 0;
|
|
972
|
+
for (const acct of settings.accounts) {
|
|
973
|
+
const imap: any = (acct as any).imap;
|
|
974
|
+
if (!imap) { console.log(` [skip] ${acct.id}: no IMAP config (likely Gmail-API)`); continue; }
|
|
975
|
+
if (!imap.password) { console.log(` [skip] ${acct.id}: OAuth account — fix-flags only supports password-auth IMAP for now`); continue; }
|
|
976
|
+
let client: any;
|
|
977
|
+
try {
|
|
978
|
+
client = new (NativeImapClient as any)({
|
|
979
|
+
server: imap.host, port: imap.port || 993,
|
|
980
|
+
user: imap.user || acct.email, password: imap.password,
|
|
981
|
+
useTls: imap.tls !== false,
|
|
982
|
+
}, () => new (NodeTcpTransport as any)());
|
|
983
|
+
await client.connect();
|
|
984
|
+
const folderRows = db.prepare("SELECT id, path FROM folders WHERE account_id = ?").all(acct.id) as { id: number; path: string }[];
|
|
985
|
+
for (const folder of folderRows) {
|
|
986
|
+
try {
|
|
987
|
+
await client.select(folder.path);
|
|
988
|
+
const serverFlaggedUids: number[] = await client.search("FLAGGED");
|
|
989
|
+
const serverSet = new Set<number>(serverFlaggedUids);
|
|
990
|
+
// Pull every local row in this folder that has \Flagged
|
|
991
|
+
const stmt = db.prepare(
|
|
992
|
+
"SELECT uid, flags_json FROM messages WHERE account_id = ? AND folder_id = ? AND flags_json LIKE ?"
|
|
993
|
+
);
|
|
994
|
+
const upd = db.prepare(
|
|
995
|
+
"UPDATE messages SET flags_json = ? WHERE account_id = ? AND folder_id = ? AND uid = ?"
|
|
996
|
+
);
|
|
997
|
+
let cleared = 0;
|
|
998
|
+
for (const r of stmt.iterate(acct.id, folder.id, "%\\\\Flagged%") as any) {
|
|
999
|
+
const uid = r.uid;
|
|
1000
|
+
if (serverSet.has(uid)) continue;
|
|
1001
|
+
let flags: string[] = [];
|
|
1002
|
+
try { flags = JSON.parse(r.flags_json); } catch { /* */ }
|
|
1003
|
+
const without = flags.filter(f => f !== "\\Flagged");
|
|
1004
|
+
if (without.length !== flags.length) {
|
|
1005
|
+
upd.run(JSON.stringify(without), acct.id, folder.id, uid);
|
|
1006
|
+
cleared++;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (cleared > 0) {
|
|
1010
|
+
console.log(` [${acct.id}] ${folder.path}: cleared ${cleared} spurious \\Flagged (server has ${serverFlaggedUids.length} actual)`);
|
|
1011
|
+
totalCleared += cleared;
|
|
1012
|
+
}
|
|
1013
|
+
totalFolders++;
|
|
1014
|
+
} catch (e: any) {
|
|
1015
|
+
console.error(` [${acct.id}] ${folder.path}: ${e?.message || e}`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
} catch (e: any) {
|
|
1019
|
+
console.error(` [${acct.id}] connect failed: ${e?.message || e}`);
|
|
1020
|
+
} finally {
|
|
1021
|
+
try { if (client) await client.logout(); } catch { /* */ }
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
db.close();
|
|
1025
|
+
console.log(`Done. Cleared ${totalCleared} spurious \\Flagged rows across ${totalFolders} folders.`);
|
|
1026
|
+
console.log(" Run 'rmfmail' to start the daemon.");
|
|
1027
|
+
process.exit(0);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
908
1030
|
// Import accounts from a local file into GDrive
|
|
909
1031
|
if (importMode) {
|
|
910
1032
|
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
|
-
|
|
5213
|
-
|
|
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: {} });
|