@bobfrankston/rmfmail 1.0.680 → 1.0.686

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 (83) hide show
  1. package/bin/build-quill.js +35 -0
  2. package/bin/lean-accounts.js +0 -1
  3. package/client/app.bundle.js +172 -55
  4. package/client/app.bundle.js.map +2 -2
  5. package/client/app.js +73 -27
  6. package/client/app.js.map +1 -1
  7. package/client/app.ts +73 -29
  8. package/client/components/context-menu.js +2 -0
  9. package/client/components/context-menu.js.map +1 -1
  10. package/client/components/context-menu.ts +6 -0
  11. package/client/components/folder-tree.js +26 -4
  12. package/client/components/folder-tree.js.map +1 -1
  13. package/client/components/folder-tree.ts +21 -4
  14. package/client/components/message-list.js +108 -40
  15. package/client/components/message-list.js.map +1 -1
  16. package/client/components/message-list.ts +103 -38
  17. package/client/compose/compose.bundle.js +189 -17
  18. package/client/compose/compose.bundle.js.map +3 -3
  19. package/client/compose/compose.js +51 -3
  20. package/client/compose/compose.js.map +1 -1
  21. package/client/compose/compose.ts +47 -3
  22. package/client/compose/spellcheck.js +178 -12
  23. package/client/compose/spellcheck.js.map +1 -1
  24. package/client/compose/spellcheck.ts +168 -8
  25. package/client/lib/api-client.js +3 -0
  26. package/client/lib/api-client.js.map +1 -1
  27. package/client/lib/api-client.ts +4 -0
  28. package/client/lib/mailxapi.js +3 -0
  29. package/client/lib/quill/quill.js +3 -0
  30. package/client/lib/quill/quill.snow.css +10 -0
  31. package/client/lib/rmf-tiny.js +25 -6
  32. package/docs/accounts.md +7 -2
  33. package/package.json +8 -8
  34. package/packages/mailx-core/index.d.ts.map +1 -1
  35. package/packages/mailx-core/index.js +2 -12
  36. package/packages/mailx-core/index.js.map +1 -1
  37. package/packages/mailx-core/index.ts +2 -12
  38. package/packages/mailx-imap/index.d.ts.map +1 -1
  39. package/packages/mailx-imap/index.js +31 -6
  40. package/packages/mailx-imap/index.js.map +1 -1
  41. package/packages/mailx-imap/index.ts +32 -6
  42. package/packages/mailx-imap/node_modules.npmglobalize-stash-11884/.package-lock.json +116 -0
  43. package/packages/mailx-imap/package-lock.json +2 -2
  44. package/packages/mailx-imap/package.json +1 -1
  45. package/packages/mailx-service/index.d.ts +22 -0
  46. package/packages/mailx-service/index.d.ts.map +1 -1
  47. package/packages/mailx-service/index.js +134 -6
  48. package/packages/mailx-service/index.js.map +1 -1
  49. package/packages/mailx-service/index.ts +128 -11
  50. package/packages/mailx-service/jsonrpc.js +3 -0
  51. package/packages/mailx-service/jsonrpc.js.map +1 -1
  52. package/packages/mailx-service/jsonrpc.ts +3 -0
  53. package/packages/mailx-service/local-store.d.ts.map +1 -1
  54. package/packages/mailx-service/local-store.js +15 -12
  55. package/packages/mailx-service/local-store.js.map +1 -1
  56. package/packages/mailx-service/local-store.ts +15 -12
  57. package/packages/mailx-settings/docs/accounts.md +14 -1
  58. package/packages/mailx-settings/docs/npmglobalize-disttag.md +90 -0
  59. package/packages/mailx-settings/docs/prod-android.md +88 -0
  60. package/packages/mailx-settings/docs/prod.md +224 -0
  61. package/packages/mailx-settings/docs/push-relay.md +141 -0
  62. package/packages/mailx-settings/docs/rmf-tiny.md +156 -0
  63. package/packages/mailx-settings/index.d.ts +2 -2
  64. package/packages/mailx-settings/index.d.ts.map +1 -1
  65. package/packages/mailx-settings/index.js +13 -10
  66. package/packages/mailx-settings/index.js.map +1 -1
  67. package/packages/mailx-settings/index.ts +13 -9
  68. package/packages/mailx-settings/package.json +1 -1
  69. package/packages/mailx-store/db.d.ts.map +1 -1
  70. package/packages/mailx-store/db.js +44 -6
  71. package/packages/mailx-store/db.js.map +1 -1
  72. package/packages/mailx-store/db.ts +47 -6
  73. package/packages/mailx-store/package.json +1 -1
  74. package/packages/mailx-store-web/package.json +4 -1
  75. package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
  76. package/packages/mailx-store-web/web-settings.js +0 -1
  77. package/packages/mailx-store-web/web-settings.js.map +1 -1
  78. package/packages/mailx-store-web/web-settings.ts +0 -1
  79. package/packages/mailx-types/index.d.ts +1 -2
  80. package/packages/mailx-types/index.d.ts.map +1 -1
  81. package/packages/mailx-types/index.js.map +1 -1
  82. package/packages/mailx-types/index.ts +1 -2
  83. package/packages/mailx-types/package.json +1 -1
package/client/app.js CHANGED
@@ -174,9 +174,13 @@ function updateBadge(count) {
174
174
  async function updateNewMessageCount() {
175
175
  try {
176
176
  const accounts = await getAccounts();
177
+ // Fan out folder queries in parallel — earlier code awaited each
178
+ // account's `getFolders` in series, so an N-account setup paid N
179
+ // back-to-back IPC round-trips on every count refresh (folderCountsChanged,
180
+ // sync events, IDLE updates).
181
+ const folderLists = await Promise.all(accounts.map((acct) => getFolders(acct.id).catch(() => [])));
177
182
  let totalUnread = 0;
178
- for (const acct of accounts) {
179
- const folders = await getFolders(acct.id);
183
+ for (const folders of folderLists) {
180
184
  const inbox = folders.find((f) => f.specialUse === "inbox");
181
185
  if (inbox)
182
186
  totalUnread += inbox.unreadCount || 0;
@@ -252,9 +256,10 @@ window.addEventListener("focus", stopTitleFlash);
252
256
  /** Call when user actively views messages — resets the badge */
253
257
  function markAsSeen() {
254
258
  getAccounts().then(async (accounts) => {
259
+ // Parallel folder fetch — see updateNewMessageCount for rationale.
260
+ const folderLists = await Promise.all(accounts.map((acct) => getFolders(acct.id).catch(() => [])));
255
261
  let total = 0;
256
- for (const acct of accounts) {
257
- const folders = await getFolders(acct.id);
262
+ for (const folders of folderLists) {
258
263
  const inbox = folders.find((f) => f.specialUse === "inbox");
259
264
  if (inbox)
260
265
  total += inbox.unreadCount || 0;
@@ -750,9 +755,29 @@ async function openCompose(mode) {
750
755
  console.warn(`[compose] ${mode} — no message selected`);
751
756
  return;
752
757
  }
753
- const accounts = await getAccounts();
754
- const accountId = current?.accountId || accounts[0]?.id || "";
758
+ // Parallel-load: kick off getAccounts AND open the iframe in the same
759
+ // tick. The iframe doesn't need the account list until after its editor
760
+ // bootstraps (200-500 ms for TinyMCE, less for Quill); by then the IPC
761
+ // round-trip has resolved. Earlier code awaited getAccounts FIRST,
762
+ // adding the IPC latency to the perceived Ctrl+N → editor-visible time.
763
+ // We post `compose-init-ready` to the iframe once init is in
764
+ // sessionStorage so compose.ts's IIFE can read synchronously without
765
+ // polling.
766
+ const accountsP = getAccounts();
755
767
  const msg = current?.message;
768
+ // Title bar text needs the subject from msg (no IPC dependency) — build
769
+ // it now so the iframe can be opened with the final title and avoid a
770
+ // flash of placeholder text.
771
+ const titlePrefix = mode === "reply" ? "Reply" :
772
+ mode === "replyAll" ? "Reply All" :
773
+ mode === "forward" ? "Forward" :
774
+ "Compose";
775
+ const titleSubject = mode === "new" ? "" : (msg?.subject || "");
776
+ const frame = showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
777
+ // Now finish initialisation off the critical path — editor bootstrap
778
+ // inside the iframe runs concurrently with this await.
779
+ const accounts = await accountsP;
780
+ const accountId = current?.accountId || accounts[0]?.id || "";
756
781
  const rePrefix = /^(re|fwd?):\s*/i;
757
782
  const cleanSubject = msg ? msg.subject.replace(rePrefix, "") : "";
758
783
  const init = {
@@ -764,7 +789,7 @@ async function openCompose(mode) {
764
789
  bodyHtml: "",
765
790
  inReplyTo: "",
766
791
  references: [],
767
- accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature })),
792
+ accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
768
793
  };
769
794
  // Auto-detect reply From: if the message was delivered to an identity address
770
795
  // (an alias on the account's domain, or the explicit `identityDomains` list
@@ -838,19 +863,16 @@ async function openCompose(mode) {
838
863
  init.bodyHtml = forwardBody(msg);
839
864
  init.fromAddress = detectReplyFrom();
840
865
  }
841
- // Store init data for compose window to pick up
866
+ // Store init data for compose window to pick up. sessionStorage is the
867
+ // canonical handoff path — same origin between parent and iframe; the
868
+ // compose IIFE reads it after the editor finishes booting. We also
869
+ // postMessage the iframe so it can short-circuit the listen-for-message
870
+ // wait if it's already past editor init.
842
871
  sessionStorage.setItem("composeInit", JSON.stringify(init));
843
- // Inline compose: load compose.html in an overlay iframe (same origin, same IPC)
844
- // Popup windows don't work in IPC mode (custom protocol doesn't propagate to child windows)
845
- // Title reflects mode + subject so the user can see what they're replying to
846
- // ("Re: Stars of STEM 2026" instead of just "Compose"). Forward shows the
847
- // forward target subject; new compose stays generic.
848
- const titlePrefix = mode === "reply" ? "Reply" :
849
- mode === "replyAll" ? "Reply All" :
850
- mode === "forward" ? "Forward" :
851
- "Compose";
852
- const titleSubject = mode === "new" ? "" : (msg?.subject || init.subject || "");
853
- showComposeOverlay(titleSubject ? `${titlePrefix}: ${titleSubject}` : titlePrefix);
872
+ try {
873
+ frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*");
874
+ }
875
+ catch { /* */ }
854
876
  }
855
877
  function showComposeOverlay(title = "Compose") {
856
878
  const wrapper = document.createElement("div");
@@ -1000,6 +1022,7 @@ function showComposeOverlay(title = "Compose") {
1000
1022
  if (!isSmall)
1001
1023
  addComposeResizeHandles(wrapper, frame);
1002
1024
  document.body.appendChild(wrapper);
1025
+ return frame;
1003
1026
  }
1004
1027
  /** Drop a transparent full-viewport shield in front of every other element
1005
1028
  * so mousemove events stay in the document during a drag. Setting
@@ -1110,8 +1133,19 @@ function sanitizeQuotedBody(msg) {
1110
1133
  // like `<!--` / `-->` / `<!` in plain-text bodies can be misread
1111
1134
  // by the parser. Per Bob 2026-05-12: "not just ugly, it breaks
1112
1135
  // the HTML." Trivial source-clutter is the lesser evil.
1136
+ //
1137
+ // CRLF → <br>. `white-space:pre-wrap` alone is not enough: TinyMCE
1138
+ // (and other HTML editors) normalize text-node whitespace when
1139
+ // setContent ingests the HTML, collapsing `\n` to spaces BEFORE
1140
+ // CSS runs. The literal `<br>` survives the normalization, so we
1141
+ // get one line break per source line whether the editor preserves
1142
+ // raw whitespace or not. pre-wrap stays as belt-and-braces and to
1143
+ // keep multi-space runs (alignment, indented quote-markers like
1144
+ // `> > >`) visible.
1113
1145
  const escaped = String(msg.bodyText || "")
1114
- .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1146
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
1147
+ .replace(/\r\n?/g, "\n")
1148
+ .replace(/\n/g, "<br>");
1115
1149
  return `<div style="white-space:pre-wrap;font-family:inherit;margin:0">${escaped}</div>`;
1116
1150
  }
1117
1151
  let body = msg.bodyHtml;
@@ -2646,7 +2680,12 @@ onWsEvent((event) => {
2646
2680
  * rather than a single line, escaping `<` so embedded angle brackets in a
2647
2681
  * signature/template don't get interpreted as tags. */
2648
2682
  async function openComposeFromMailto(m) {
2649
- const accounts = await getAccounts();
2683
+ // Open the iframe immediately and load accounts in parallel — same
2684
+ // pattern as openCompose. The mailto handler should never feel like
2685
+ // "waiting for the system" to a user who clicked a link.
2686
+ const accountsP = getAccounts();
2687
+ const frame = showComposeOverlay(m.subject ? `Compose: ${m.subject}` : "Compose");
2688
+ const accounts = await accountsP;
2650
2689
  const accountId = accounts[0]?.id || "";
2651
2690
  const escape = (s) => s.replace(/[&<>]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" }[c]));
2652
2691
  const bodyHtml = m.body
@@ -2662,10 +2701,13 @@ async function openComposeFromMailto(m) {
2662
2701
  bodyHtml,
2663
2702
  inReplyTo: m.inReplyTo,
2664
2703
  references: m.inReplyTo ? [m.inReplyTo] : [],
2665
- accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature })),
2704
+ accounts: accounts.map((a) => ({ id: a.id, name: a.name, email: a.email, signature: a.signature, sig: a.sig })),
2666
2705
  };
2667
2706
  sessionStorage.setItem("composeInit", JSON.stringify(init));
2668
- showComposeOverlay(m.subject ? `Compose: ${m.subject}` : "Compose");
2707
+ try {
2708
+ frame?.contentWindow?.postMessage({ type: "compose-init-ready" }, "*");
2709
+ }
2710
+ catch { /* */ }
2669
2711
  }
2670
2712
  // ── Keyboard shortcuts ──
2671
2713
  // Capture-phase pre-handler: intercept WebView accelerator keys that would
@@ -4395,8 +4437,11 @@ function renderOutboxStatus(s) {
4395
4437
  setInterval(async () => {
4396
4438
  try {
4397
4439
  const { getOutboxStatus, getDiagnostics } = await import("./lib/api-client.js");
4398
- renderOutboxStatus(await getOutboxStatus());
4399
- renderDiagnosticsBadge(await getDiagnostics());
4440
+ // Run in parallel — neither call depends on the other and each is a
4441
+ // separate IPC round-trip. Earlier code awaited them serially.
4442
+ const [outbox, diag] = await Promise.all([getOutboxStatus(), getDiagnostics()]);
4443
+ renderOutboxStatus(outbox);
4444
+ renderDiagnosticsBadge(diag);
4400
4445
  }
4401
4446
  catch { /* service unreachable */ }
4402
4447
  }, 15000);
@@ -4404,8 +4449,9 @@ setInterval(async () => {
4404
4449
  (async () => {
4405
4450
  try {
4406
4451
  const { getOutboxStatus, getDiagnostics } = await import("./lib/api-client.js");
4407
- renderOutboxStatus(await getOutboxStatus());
4408
- renderDiagnosticsBadge(await getDiagnostics());
4452
+ const [outbox, diag] = await Promise.all([getOutboxStatus(), getDiagnostics()]);
4453
+ renderOutboxStatus(outbox);
4454
+ renderDiagnosticsBadge(diag);
4409
4455
  }
4410
4456
  catch { /* */ }
4411
4457
  })();