@bobfrankston/rmfmail 1.2.50 → 1.2.66

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 (90) hide show
  1. package/client/android-bootstrap.bundle.js +63 -2
  2. package/client/android-bootstrap.bundle.js.map +4 -4
  3. package/client/app.bundle.js +417 -63
  4. package/client/app.bundle.js.map +3 -3
  5. package/client/app.js +182 -24
  6. package/client/app.js.map +1 -1
  7. package/client/app.ts +166 -21
  8. package/client/components/calendar-sidebar.js +5 -5
  9. package/client/components/calendar-sidebar.js.map +1 -1
  10. package/client/components/calendar-sidebar.ts +5 -5
  11. package/client/components/folder-tree.js +81 -0
  12. package/client/components/folder-tree.js.map +1 -1
  13. package/client/components/folder-tree.ts +70 -0
  14. package/client/components/message-list.js +282 -38
  15. package/client/components/message-list.js.map +1 -1
  16. package/client/components/message-list.ts +264 -36
  17. package/client/compose/compose.bundle.js +70 -14
  18. package/client/compose/compose.bundle.js.map +2 -2
  19. package/client/compose/compose.css +28 -7
  20. package/client/compose/compose.js +64 -4
  21. package/client/compose/compose.js.map +1 -1
  22. package/client/compose/compose.ts +66 -4
  23. package/client/compose/editor.js +23 -7
  24. package/client/compose/editor.js.map +1 -1
  25. package/client/compose/editor.ts +23 -7
  26. package/client/help/search-help.js +4 -3
  27. package/client/help/search-help.js.map +1 -1
  28. package/client/help/search-help.ts +4 -3
  29. package/client/index.html +5 -1
  30. package/client/lib/api-client.js +4 -2
  31. package/client/lib/api-client.js.map +1 -1
  32. package/client/lib/api-client.ts +4 -2
  33. package/client/lib/mailxapi.js +2 -2
  34. package/client/lib/rmf-tiny.js +39 -6
  35. package/client/styles/components.css +11 -0
  36. package/docs/azure.md +58 -0
  37. package/docs/config-help.md +131 -0
  38. package/docs/edit-in-word-docx.md +98 -0
  39. package/docs/host-abstraction-plan.md +169 -0
  40. package/docs/local-first-plan.md +303 -0
  41. package/docs/npmglobalize-transitive-workspace-deps.md +107 -0
  42. package/docs/outlook.md +80 -18
  43. package/docs/outlook.txt +35 -0
  44. package/docs/rules-design.md +172 -0
  45. package/package.json +9 -9
  46. package/packages/mailx-imap/index.d.ts +48 -1
  47. package/packages/mailx-imap/index.d.ts.map +1 -1
  48. package/packages/mailx-imap/index.js +320 -68
  49. package/packages/mailx-imap/index.js.map +1 -1
  50. package/packages/mailx-imap/index.ts +311 -65
  51. package/packages/mailx-imap/microsoft-credentials.json +8 -0
  52. package/packages/mailx-imap/package-lock.json +2 -2
  53. package/packages/mailx-imap/package.json +1 -1
  54. package/packages/mailx-imap/providers/outlook-api.d.ts +7 -0
  55. package/packages/mailx-imap/providers/outlook-api.d.ts.map +1 -0
  56. package/packages/mailx-imap/providers/outlook-api.js +7 -0
  57. package/packages/mailx-imap/providers/outlook-api.js.map +1 -0
  58. package/packages/mailx-service/index.d.ts +7 -1
  59. package/packages/mailx-service/index.d.ts.map +1 -1
  60. package/packages/mailx-service/index.js +8 -8
  61. package/packages/mailx-service/index.js.map +1 -1
  62. package/packages/mailx-service/index.ts +8 -7
  63. package/packages/mailx-service/jsonrpc.js +1 -1
  64. package/packages/mailx-service/jsonrpc.js.map +1 -1
  65. package/packages/mailx-service/jsonrpc.ts +1 -1
  66. package/packages/mailx-settings/docs/azure.md +58 -0
  67. package/packages/mailx-settings/docs/config-help.md +131 -0
  68. package/packages/mailx-settings/docs/edit-in-word-docx.md +98 -0
  69. package/packages/mailx-settings/docs/host-abstraction-plan.md +169 -0
  70. package/packages/mailx-settings/docs/local-first-plan.md +303 -0
  71. package/packages/mailx-settings/docs/npmglobalize-transitive-workspace-deps.md +107 -0
  72. package/packages/mailx-settings/docs/outlook.md +80 -18
  73. package/packages/mailx-settings/docs/rules-design.md +172 -0
  74. package/packages/mailx-settings/index.d.ts.map +1 -1
  75. package/packages/mailx-settings/index.js +9 -8
  76. package/packages/mailx-settings/index.js.map +1 -1
  77. package/packages/mailx-settings/index.ts +9 -8
  78. package/packages/mailx-settings/package.json +1 -1
  79. package/packages/mailx-store/db.d.ts +36 -1
  80. package/packages/mailx-store/db.d.ts.map +1 -1
  81. package/packages/mailx-store/db.js +100 -6
  82. package/packages/mailx-store/db.js.map +1 -1
  83. package/packages/mailx-store/db.ts +100 -6
  84. package/packages/mailx-store/package.json +1 -1
  85. package/packages/mailx-types/contact-rules.d.ts +1 -1
  86. package/packages/mailx-types/contact-rules.js +1 -1
  87. package/packages/mailx-types/contact-rules.js.map +1 -1
  88. package/packages/mailx-types/contact-rules.jsonc +1 -1
  89. package/packages/mailx-types/contact-rules.ts +1 -1
  90. package/packages/mailx-types/package.json +1 -1
@@ -67,6 +67,24 @@ function isJunkContact(email: string, name: string): boolean {
67
67
  for (const re of _denyPatterns) { if (re.test(lower)) return true; }
68
68
  return false;
69
69
  }
70
+ /** Sanitize a harvested display name. A real display name carries none of
71
+ * `@ < > |` — their presence means an address parser mis-split a header
72
+ * (e.g. a List-Id / "domain | Name@addr" fragment) and the "name" now
73
+ * embeds structural mail punctuation or a whole email address. The worst
74
+ * case is an embedded address that DIFFERS from this contact's own email:
75
+ * autocomplete then shows a TRUSTED name while sending ELSEWHERE. Live
76
+ * example (Bob 2026-06-22): a `discovered` row name `bob.ma | Bob@bob.ma`
77
+ * was wired to `miyakot@kt.rim.or.jp`, so test mail Bob addressed to
78
+ * himself silently went to a stranger in Japan ("MIA"). Drop the name;
79
+ * keep the contact (the email is still useful) — autocomplete falls back
80
+ * to showing the bare address, which the user won't mistake for someone. */
81
+ export function cleanContactName(name: string): string {
82
+ const n = (name || "").trim().replace(/^"(.*)"$/, "$1").trim();
83
+ if (!n) return "";
84
+ if (/[@<>|]/.test(n)) return "";
85
+ return n;
86
+ }
87
+
70
88
  /** User-configured patterns from contacts.jsonc `denylistPatterns`. Compiled
71
89
  * once on settings load; invalid regexes are skipped with a warning. */
72
90
  export function setContactsDenyPatterns(patterns: string[]): void {
@@ -792,14 +810,21 @@ export class MailxDB {
792
810
  const last = this.getKv("contacts", "purge_v");
793
811
  if (last === TARGET) return;
794
812
  let dropped = 0;
813
+ let renamed = 0;
795
814
  try {
796
815
  const rows = this.db.prepare("SELECT id, email, name FROM contacts WHERE source IN ('discovered','sent','received')").all() as { id: number; email: string; name: string }[];
797
816
  const del = this.db.prepare("DELETE FROM contacts WHERE id = ?");
817
+ const blank = this.db.prepare("UPDATE contacts SET name = '' WHERE id = ?");
798
818
  for (const r of rows) {
799
- if (isJunkContact(r.email || "", r.name || "")) { del.run(r.id); dropped++; }
819
+ if (isJunkContact(r.email || "", r.name || "")) { del.run(r.id); dropped++; continue; }
820
+ // Blank mis-parsed display names that embed `@ < > |` — these
821
+ // can impersonate one identity while sending to another (the
822
+ // "bob.ma | Bob@bob.ma" → miyakot@kt.rim.or.jp cross-wire,
823
+ // Bob 2026-06-22). Keep the contact; drop only the unsafe name.
824
+ if (r.name && cleanContactName(r.name) !== r.name) { blank.run(r.id); renamed++; }
800
825
  }
801
826
  this.setKv("contacts", "purge_v", TARGET);
802
- if (dropped > 0) console.log(` [contacts] one-shot purge: dropped ${dropped} junk rows (${TARGET})`);
827
+ if (dropped > 0 || renamed > 0) console.log(` [contacts] one-shot purge: dropped ${dropped} junk rows, blanked ${renamed} unsafe names (${TARGET})`);
803
828
  } catch (e: any) {
804
829
  console.warn(`[contacts] one-shot purge failed: ${e.message}`);
805
830
  }
@@ -1379,6 +1404,55 @@ export class MailxDB {
1379
1404
  return Number(result.lastInsertRowid);
1380
1405
  }
1381
1406
 
1407
+ /** Rename a folder row's path (+leaf name) and rewrite every descendant
1408
+ * folder whose path lives under the old prefix. A single IMAP RENAME on the
1409
+ * server renames child mailboxes too (RFC 3501), and Gmail/Outlook rename
1410
+ * cascades the same way, so the local mirror must move the whole subtree in
1411
+ * lockstep. All in one transaction so a crash can't leave half the subtree
1412
+ * pointing at the old prefix.
1413
+ *
1414
+ * `newName` is the new LEAF name written to the renamed row's `name`
1415
+ * column; descendant rows keep their own leaf names (only the prefix
1416
+ * changes). The (account_id, path) UNIQUE constraint means the caller must
1417
+ * have already verified newPath is free. */
1418
+ renameFolderPath(accountId: string, oldPath: string, newPath: string, newName: string, delimiter: string): void {
1419
+ const childPrefix = oldPath + delimiter;
1420
+ this.db.exec("BEGIN");
1421
+ try {
1422
+ // The folder itself — path + leaf name both change.
1423
+ this.db.prepare(
1424
+ "UPDATE folders SET path = ?, name = ? WHERE account_id = ? AND path = ?"
1425
+ ).run(newPath, newName, accountId, oldPath);
1426
+ // Descendants — rewrite only the path prefix, keep their leaf names.
1427
+ // SUBSTR(path, len(childPrefix)+1) is the part after the old prefix.
1428
+ const descendants = this.db.prepare(
1429
+ "SELECT id, path FROM folders WHERE account_id = ? AND path LIKE ? ESCAPE '\\'"
1430
+ ).all(accountId, childPrefix.replace(/[\\%_]/g, "\\$&") + "%") as { id: number; path: string }[];
1431
+ const upd = this.db.prepare("UPDATE folders SET path = ? WHERE id = ?");
1432
+ for (const d of descendants) {
1433
+ const rest = d.path.slice(oldPath.length); // includes the leading delimiter
1434
+ upd.run(newPath + rest, d.id);
1435
+ }
1436
+ this.db.exec("COMMIT");
1437
+ } catch (e) {
1438
+ this.db.exec("ROLLBACK");
1439
+ throw e;
1440
+ }
1441
+ }
1442
+
1443
+ /** Update only a folder's display `name`, leaving `path` untouched. For
1444
+ * providers whose `path` is an opaque, rename-stable id (Outlook/Graph:
1445
+ * path = folder id), a rename/reparent changes the display name but NOT
1446
+ * the id — so synthesizing a new hierarchical path (as renameFolderPath
1447
+ * does) would corrupt the id-as-path invariant every later Graph op relies
1448
+ * on. This updates the name in place. No descendant rewrite: opaque-id
1449
+ * children don't carry the parent in their path. */
1450
+ renameFolderName(accountId: string, folderId: number, newName: string): void {
1451
+ this.db.prepare(
1452
+ "UPDATE folders SET name = ? WHERE account_id = ? AND id = ?"
1453
+ ).run(newName, accountId, folderId);
1454
+ }
1455
+
1382
1456
  getFolders(accountId: string): Folder[] {
1383
1457
  const rows = this.db.prepare(
1384
1458
  "SELECT * FROM folders WHERE account_id = ? ORDER BY path"
@@ -2823,6 +2897,7 @@ export class MailxDB {
2823
2897
  const lower = email.toLowerCase();
2824
2898
  if (this.isAddressDenylisted(lower)) return;
2825
2899
  if (isJunkContact(lower, name)) return;
2900
+ name = cleanContactName(name);
2826
2901
  const now = Date.now();
2827
2902
  // discovered tier holds one row per email — bump if present, else
2828
2903
  // insert. Doesn't touch preferred or google rows for the same email;
@@ -2937,6 +3012,7 @@ export class MailxDB {
2937
3012
  if (!email || !VALID.test(email)) return;
2938
3013
  if (isJunkContact(email, name)) return;
2939
3014
  if (this.isAddressDenylisted(email)) return;
3015
+ name = cleanContactName(name);
2940
3016
  const e = agg.get(email);
2941
3017
  if (e) {
2942
3018
  e.cnt++;
@@ -3477,6 +3553,7 @@ export class MailxDB {
3477
3553
  for (const part of parts) {
3478
3554
  const fromMatch = part.match(/^from:(.+)$/i);
3479
3555
  const toMatch = part.match(/^to:(.+)$/i);
3556
+ const ccMatch = part.match(/^cc:(.+)$/i);
3480
3557
  const subjectMatch = part.match(/^subject:(.+)$/i);
3481
3558
  const dateMatch = part.match(/^date:([><]?=?)(.+)$/i);
3482
3559
  const afterMatch = part.match(/^after:(.+)$/i);
@@ -3491,8 +3568,15 @@ export class MailxDB {
3491
3568
  const term = ftsClean(fromMatch[1]);
3492
3569
  if (term) frags.push({ s: `{from_name from_address}:${term}*`, op: false });
3493
3570
  } else if (toMatch) {
3571
+ // `to:` deliberately spans To AND Cc — when someone searches
3572
+ // "to:bob" they mean "addressed to bob", and being Cc'd counts.
3573
+ // Use `cc:` for a Cc-only match. (Bcc isn't indexed: it's absent
3574
+ // on received mail by design, and only meaningful in Sent.)
3494
3575
  const term = ftsClean(toMatch[1]);
3495
3576
  if (term) frags.push({ s: `{to_text cc_text}:${term}*`, op: false });
3577
+ } else if (ccMatch) {
3578
+ const term = ftsClean(ccMatch[1]);
3579
+ if (term) frags.push({ s: `cc_text:${term}*`, op: false });
3496
3580
  } else if (subjectMatch) {
3497
3581
  const term = ftsClean(subjectMatch[1]);
3498
3582
  if (term) frags.push({ s: `subject:${term}*`, op: false });
@@ -3564,6 +3648,9 @@ export class MailxDB {
3564
3648
  ftsQuery += sep + frags[i].s;
3565
3649
  }
3566
3650
  ftsQuery = ftsQuery.trim();
3651
+ // (Header-scoped search is reachable via the `subject:`/`from:`/`to:`
3652
+ // qualifiers — no separate "headers only" mode needed, and body search
3653
+ // isn't slow enough to warrant one.)
3567
3654
  // No real FTS term — either qualifier-only ("is:flagged after:1w") or
3568
3655
  // every term sanitized away (user typed only punctuation). There is NO
3569
3656
  // valid FTS5 "match everything" string — `MATCH '*'` is itself an FTS5
@@ -3659,13 +3746,20 @@ export class MailxDB {
3659
3746
  }
3660
3747
  }
3661
3748
 
3662
- /** Rebuild FTS index from existing messages */
3749
+ /** Rebuild FTS index from existing messages.
3750
+ * body_text is seeded from `preview` here (all db.ts can reach); the full
3751
+ * message body is backfilled separately by mailx-imap's backfillFtsBodies()
3752
+ * pass, which can read the cached .eml. */
3663
3753
  rebuildSearchIndex(): number {
3664
- // Drop and recreate in case schema changed
3754
+ // Drop and recreate. MUST be the STANDALONE schema (no content=messages):
3755
+ // the external-content form indexes to_text/cc_text/body_text columns
3756
+ // that don't exist on `messages`, so content-dereferencing silently
3757
+ // drops those terms — the exact breakage migrateFtsSchema() repairs.
3758
+ // Recreating it here used to quietly reintroduce that bug on every
3759
+ // -reindex (Bob 2026-06-05 "search missed to/cc/body").
3665
3760
  try { this.db.exec("DROP TABLE IF EXISTS messages_fts"); } catch { /* ignore */ }
3666
3761
  this.db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
3667
- subject, from_name, from_address, to_text, cc_text, body_text,
3668
- content=messages, content_rowid=id
3762
+ subject, from_name, from_address, to_text, cc_text, body_text
3669
3763
  )`);
3670
3764
 
3671
3765
  // Use a single transaction + prepared statement for speed (~50x faster than individual inserts)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-store",
3
- "version": "0.1.55",
3
+ "version": "0.1.56",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -1,5 +1,5 @@
1
1
  export declare const CONTACT_RULES: {
2
- readonly rulesVersion: "v3-domain-oneoff";
2
+ readonly rulesVersion: "v4-name-hygiene";
3
3
  readonly junk: {
4
4
  readonly localExact: "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$";
5
5
  readonly localSuffix: "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$";
@@ -1,7 +1,7 @@
1
1
  // AUTO-GENERATED from contact-rules.jsonc — do not edit.
2
2
  // Edit the .jsonc and run `node build-rules.js` (or `npm run build`).
3
3
  export const CONTACT_RULES = {
4
- "rulesVersion": "v3-domain-oneoff",
4
+ "rulesVersion": "v4-name-hygiene",
5
5
  "junk": {
6
6
  "localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$",
7
7
  "localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$",
@@ -1 +1 @@
1
- {"version":3,"file":"contact-rules.js","sourceRoot":"","sources":["contact-rules.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,sEAAsE;AAEtE,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,cAAc,EAAE,kBAAkB;IAClC,MAAM,EAAE;QACJ,YAAY,EAAE,6MAA6M;QAC3N,aAAa,EAAE,mEAAmE;QAClF,aAAa,EAAE,4FAA4F;QAC3G,aAAa,EAAE,8CAA8C;QAC7D,QAAQ,EAAE,8EAA8E;KAC3F;CACK,CAAC"}
1
+ {"version":3,"file":"contact-rules.js","sourceRoot":"","sources":["contact-rules.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,sEAAsE;AAEtE,MAAM,CAAC,MAAM,aAAa,GAAG;IACzB,cAAc,EAAE,iBAAiB;IACjC,MAAM,EAAE;QACJ,YAAY,EAAE,6MAA6M;QAC3N,aAAa,EAAE,mEAAmE;QAClF,aAAa,EAAE,4FAA4F;QAC3G,aAAa,EAAE,8CAA8C;QAC7D,QAAQ,EAAE,8EAA8E;KAC3F;CACK,CAAC"}
@@ -17,7 +17,7 @@
17
17
  // (e.g. "16179691997.15082868100.nfblcbll1x")
18
18
  // domain — entire domain match (Google Voice SMS, reply.fb.com)
19
19
  {
20
- "rulesVersion": "v3-domain-oneoff",
20
+ "rulesVersion": "v4-name-hygiene",
21
21
  "junk": {
22
22
  "localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$",
23
23
  "localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$",
@@ -2,7 +2,7 @@
2
2
  // Edit the .jsonc and run `node build-rules.js` (or `npm run build`).
3
3
 
4
4
  export const CONTACT_RULES = {
5
- "rulesVersion": "v3-domain-oneoff",
5
+ "rulesVersion": "v4-name-hygiene",
6
6
  "junk": {
7
7
  "localExact": "^(no-?reply|do-?not-?reply|noreply|mailer-daemon|postmaster|abuse|automated|bounce(s|d)?|list-?(server|admin|owner|manager)?|notification|notifications?|admin@.*automated|root|daemon|nobody|undisclosed)$",
8
8
  "localSuffix": "(-bounces|\\+bounces|-noreply|-no-reply|-notifications?|-mailer)$",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/mailx-types",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",