@bobfrankston/rmfmail 1.2.112 → 1.2.114

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 (41) hide show
  1. package/TODO.md +5 -5
  2. package/bin/mailx.js +72 -3
  3. package/bin/mailx.js.map +1 -1
  4. package/bin/mailx.ts +62 -3
  5. package/client/android-bootstrap.bundle.js +60 -3
  6. package/client/android-bootstrap.bundle.js.map +2 -2
  7. package/client/app.bundle.js +42 -0
  8. package/client/app.bundle.js.map +2 -2
  9. package/client/app.js +58 -0
  10. package/client/app.js.map +1 -1
  11. package/client/app.ts +60 -0
  12. package/client/compose/compose.bundle.js +50 -5
  13. package/client/compose/compose.bundle.js.map +2 -2
  14. package/client/compose/compose.ts +13 -5
  15. package/client/compose/spellcheck-core.js +59 -3
  16. package/client/compose/spellcheck-core.js.map +1 -1
  17. package/client/compose/spellcheck-core.ts +58 -3
  18. package/client/compose/spellcheck.js +15 -6
  19. package/client/compose/spellcheck.js.map +1 -1
  20. package/client/compose/spellcheck.ts +15 -6
  21. package/package.json +1 -1
  22. package/packages/mailx-imap/index.d.ts +4 -1
  23. package/packages/mailx-imap/index.d.ts.map +1 -1
  24. package/packages/mailx-imap/index.js +59 -5
  25. package/packages/mailx-imap/index.js.map +1 -1
  26. package/packages/mailx-imap/index.ts +55 -5
  27. package/packages/mailx-imap/package-lock.json +2 -2
  28. package/packages/mailx-imap/package.json +1 -1
  29. package/packages/mailx-store-web/android-bootstrap.js +48 -3
  30. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  31. package/packages/mailx-store-web/android-bootstrap.ts +47 -4
  32. package/packages/mailx-store-web/package.json +1 -1
  33. package/packages/mailx-store-web/web-service.d.ts +21 -0
  34. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  35. package/packages/mailx-store-web/web-service.js +11 -0
  36. package/packages/mailx-store-web/web-service.js.map +1 -1
  37. package/packages/mailx-store-web/web-service.ts +11 -0
  38. package/packages/mailx-types/mailx-api.d.ts +22 -0
  39. package/packages/mailx-types/mailx-api.d.ts.map +1 -1
  40. package/packages/mailx-types/mailx-api.ts +7 -0
  41. package/packages/mailx-types/package.json +1 -1
package/TODO.md CHANGED
@@ -13,13 +13,13 @@ These items don't need user input — source-only changes, compile-verified befo
13
13
  | Order | Item | Scope | Status |
14
14
  |---|---|---|---|
15
15
  | ~~1~~ | ~~**C118 — STATUS-before-SELECT in syncFolder**~~ — **DONE** (already implemented at `mailx-imap/index.ts:1079`; TODO entry was stale). | S | |
16
- | **2** | **C119 — Lazy folder sync** | M | Don't sync all 96 bobma folders on every periodic pass. Only sync (a) folders the user has expanded in the tree, (b) special-use folders (INBOX/Sent/Drafts/Trash), (c) folders touched by recent local actions. Other folders sync on-demand when user clicks them. Closes most of the Thunderbird-vs-mailx speed gap. Touches `mailx-imap/index.ts` syncAll loop. |
17
- | **3** | **C121 — Counting timer for server connect** | S | Visible elapsed-time counter while the connection is being established, so Bob can tell "DNS slow", "TCP handshake stuck", "TLS hung", "IMAP greeting missing", or "LOGIN slow" apart at a glance. Phases: `dnstcp connect → tls handshake → imap greeting LOGIN ack`. Each phase emits a start event with `phase` + `accountId`; client renders `Connecting to <host> (<phase>, 3.2s)` in the status bar with a 100 ms ticking elapsed counter. On success or failure, drop the indicator and log per-phase durations. Touches `node-tcp-transport.connect()` + `iflow-direct.imap-native.ts:connect`/`login` + a new `imapManager.emit("connectProgress", ...)` chain. Pairs naturally with C120 — same `connId` ID space so the timer indicator can name the connection that later times out. |
18
- | **4** | **C122 — Recent-unread count overlay on taskbar icon** | S | Show recent-unread count as a numeric overlay on the running mailx taskbar icon (Win11 style: small badge in the lower-right corner of the icon). `setOverlayIcon` is already wired (used as a placeholder); needs (a) a number-rendered overlay bitmap (1 / 2 / / 9+ glyphs at 16×16 or via `DrawText` to a memory DC) and (b) hook into the unread-counter event chain so the badge updates when `folderCountsChanged` or `idle` events arrive. Distinct from S65 — that one is "badge persists when mailx isn't running" and is blocked on tray-process / Win11 Notification Center integration; this one is the *while-running* number. |
16
+ | ~~2~~ | ~~**C119 — Lazy folder sync**~~ **DONE** (shipped 2026-06-01; TODO entry was stale). `isLazyEligible` in `mailx-imap/index.ts` syncAccount: never-touched folders (highestUid=0, non-special) are not auto-swept at all they sync on-demand via `syncFolderNow` when opened, then join the periodic set. Previously-seen folders stay in the sweep but C118's STATUS-before-SELECT makes an unchanged folder cost one round-trip. | M | |
17
+ | ~~3~~ | ~~**C121 — Counting timer for server connect**~~ **DONE v1** (2026-07-08, v1.2.111). `newClient` in `mailx-imap/index.ts` wraps the injected transport + `client.connect` and emits `connectProgress` (phases `socket``handshake``done`/`failed`); bin forwards as an event; `client/app.ts handleConnectProgress` renders `Connecting to <host> (<phase> <purpose>, 3.2s)` with a 100 ms ticker, gated to attempts >1.5 s so healthy connects never flicker. Connects >3 s log per-phase timings (`[conn-slow]`). *Residual*: splitting `socket` into dns/tcp/tls needs an `onConnectPhase` hook inside iflow-node's NodeTransport do when next touching iflow. | S | |
18
+ | ~~4~~ | ~~**C122 — Recent-unread count overlay on taskbar icon**~~ **DONE** (shipped ~v1.1.x 2026-05-29; TODO entry was stale). `updateBadge` in `client/app.ts` renders the red count pill on a 32×32 canvas (badge-only, transparent field) and pushes it via `mailxapi.setTaskbarOverlay`; hooked to `folderCountsChanged` + startup via `updateNewMessageCount`. Badge shows NEW-since-last-seen, matching the "recent-unread" spec. S65 (badge while not running) remains open/blocked. | S | |
19
19
  | ~~5~~ | ~~**C126 — Cold-start latency (~28s observed)**~~ — **MOSTLY DONE** (2026-05-11). Root cause IDed from boot log: 43 s of cascading ES module imports through msger's custom protocol IPC (one roundtrip per file). Fixes shipped: (a) bundling via esbuild (`bin/build-bundles.mjs`) collapses the cascade to one fetch per entry point — `client/app.bundle.js` 345 kb, `client/compose/compose.bundle.js` 99 kb; (b) boot-snapshot hydration — bundled app saves `folder-tree` + `ml-body` innerHTML to localStorage every 30 s; inline script in `index.html` restores at cold start before the bundle has fetched. Sub-bullet (c) [500 ms placeholder pause] not in current code at cited line — skipped as stale. Expected cold start now 3-5 s (full WebView2 + bundle parse), with near-instant *feel* via hydrated snapshot after the first run. | M | |
20
20
  | **6** | **C125 — Unify desktop + Android IMAP code paths** | M | Today's split (`mailx-imap` Node-only + `mailx-store-web` browser-only) duplicates orchestration that's protocol-identical. Differences boil down to (a) transport factory choice (b) whether clients are persistent — both parameters, not architectural divides. Plan: extract `mailx-imap-core` with ImapManager + ops queue + fast-lane + sync orchestration over a `Transport` and a `Storage` interface, no Node-specific imports. `mailx-imap` and `mailx-store-web` become thin shims that wire engine-specific Storage + Transport. `{persistent: true\|false}` flag on ImapManager covers the lifetime split (no fast-lane queue when ephemeral). Pre-req for Android non-Gmail IMAP without forking sync code. |
21
- | **5** | **C124 — mailto handler on Linux + macOS** | S | Counterpart to P115 (Win). Linux: write a `~/.local/share/applications/rmfmail.desktop` with `MimeType=x-scheme-handler/mailto;` + `Exec=node /path/to/mailx.js --mailto %u`, `xdg-mime default rmfmail.desktop x-scheme-handler/mailto`, `update-desktop-database`. No exe-launcher needed — Linux pickers read `Name=` from the `.desktop` directly. macOS: real `.app` bundle (LaunchServices won't register a bare script) with `Info.plist` `CFBundleURLTypes` declaring `mailto` + a tiny launcher binary; `LSSetDefaultHandlerForURLScheme` programmatically sets the default. Rust `rmfmailto-src/` crate already portable — drop the Win32 imports and `cargo build --target x86_64-apple-darwin` produces the macOS launcher. |
22
- | **7** | **C154 — Audit Android bridge vs MailxApi contract** | S | The hand-written `installBridge()` api object in `mailx-store-web/android-bootstrap.ts` exposes ~45 fewer methods than `client/lib/api-client.ts` can call (`getThreadMessages`, `getOutboxStatus`, `unsubscribeOneClick`, calendar/tasks, user-dict, …). Any missing one fails as `parent bridge has no method "X"` from the compose/app IPC relay that's how the denylist button broke on the phone (fixed v1.2.109: added `addToDenylist`/`addPreferredContact`/`hasBccHistoryTo`/`loadContactsConfig`). Web-service already `implements MailxApi` with explicit stubs, so the gap is purely the bridge layer. Audit each remaining name: expose real impls + notImpl stubs (arg shapes per `client/lib/mailxapi.js`), consciously comment-skip desktop-only ones, and add a typed check or generic stub pass-through so the class of bug can't recur. |
21
+ | **5** | **C124 — mailto handler on macOS** (Linux 2026-07-08 v1.2.111) | S | Linux half shipped: `--register-mailto` on linux writes `~/.local/share/applications/rmfmail.desktop` (`MimeType=x-scheme-handler/mailto;`, quoted node+mailx.js Exec, bare `%u`), runs `xdg-mime default` + `update-desktop-database`; `--unregister-mailto` removes it. **Remaining: macOS** — real `.app` bundle (LaunchServices won't register a bare script) with `Info.plist` `CFBundleURLTypes` declaring `mailto` + a tiny launcher binary; `LSSetDefaultHandlerForURLScheme` programmatically sets the default. Rust `rmfmailto-src/` crate already portable — drop the Win32 imports and `cargo build --target x86_64-apple-darwin` produces the macOS launcher. Needs a Mac to build/verify. |
22
+ | ~~7~~ | ~~**C154 — Audit Android bridge vs MailxApi contract**~~ **DONE** (2026-07-08, v1.2.111). Bridge now has a generic contract-backstop: every service method not explicitly adapted gets a positional pass-through, so a missing method can never again fail as `parent bridge has no method "X"`. Wire-shape mismatches adapted explicitly (`readJsoncFile`/`writeJsoncFile`/`formatJsonc`/`readConfigHelp` `{content}`/`{ok}`, `drainStoreSync`/`syncFolderNow` `{ok}`). Contract gained the missing entries (`getMessageSource`, `copyMessages`, `syncFolderNow`, `cancelServerSearch`, `getReminderSound`, `getCalendars`, optional `popoutWindow`) with web-service stubs per the policy (reads graceful default, writes/OS notImpl). Bonus fixes: bridge `getMessages` was dropping sort/sortDir/search and `searchMessages` was dropping scope/accountId/folderId Android sort toggles and scoped search silently didn't work. | S | |
23
23
  ~~done — C120 read transport diagnostics~~ shipped v1.0.583 (per-folder timeout error now includes `[conn#X r=YB w=ZB writes=N sinceLastRead=Tms]` snapshot for the doomed socket).
24
24
  | 11 | **[P116](#ext116) — Signature edit in Settings menu** | M | Deferred v1.0.401 — turns out to be M (multi-account modal + save path + JSONC round-trip), not S. Real work when Settings UI gets its proper pass. |
25
25
  | 13 | **S56 full AbortController plumbing** | L | Thread `AbortSignal` through `getMessage → fetchMessageBody → provider.fetchOne`. Touches IMAP + Gmail + Outlook providers. |
package/bin/mailx.js CHANGED
@@ -180,11 +180,61 @@ const isDaemon = hasFlag("daemon"); // internal: re-spawned detached process
180
180
  }
181
181
  if (hasFlag("register-mailto") || hasFlag("unregister-mailto")) {
182
182
  const unregister = hasFlag("unregister-mailto");
183
+ // C124: Linux registration — a .desktop entry claiming the mailto:
184
+ // scheme + xdg-mime default. No exe-launcher needed (unlike Windows):
185
+ // Linux pickers read Name= from the .desktop file directly.
186
+ if (process.platform === "linux") {
187
+ const appsDir = path.join(os.homedir(), ".local", "share", "applications");
188
+ const desktopPath = path.join(appsDir, "rmfmail.desktop");
189
+ if (unregister) {
190
+ try {
191
+ fs.unlinkSync(desktopPath);
192
+ }
193
+ catch { /* not registered */ }
194
+ try {
195
+ execSync(`update-desktop-database "${appsDir}"`);
196
+ }
197
+ catch { /* optional tool */ }
198
+ console.log("Unregistered rmfmail mailto: handler.");
199
+ process.exit(0);
200
+ }
201
+ const __mailtoJs = path.join(import.meta.dirname, "mailx.js");
202
+ const __iconPng = path.join(import.meta.dirname, "..", "client", "icon.png");
203
+ // Exec quoting follows the Desktop Entry spec: quoted absolute paths,
204
+ // bare %u field code (quoting %u breaks argument expansion).
205
+ const desktopEntry = [
206
+ "[Desktop Entry]",
207
+ "Type=Application",
208
+ "Name=rmfmail",
209
+ "Comment=rmfmail email client",
210
+ `Exec="${process.execPath}" "${__mailtoJs}" --mailto %u`,
211
+ `Icon=${__iconPng}`,
212
+ "Terminal=false",
213
+ "MimeType=x-scheme-handler/mailto;",
214
+ "Categories=Network;Email;",
215
+ "",
216
+ ].join("\n");
217
+ fs.mkdirSync(appsDir, { recursive: true });
218
+ fs.writeFileSync(desktopPath, desktopEntry);
219
+ try {
220
+ execSync("xdg-mime default rmfmail.desktop x-scheme-handler/mailto");
221
+ }
222
+ catch (e) {
223
+ console.error(`xdg-mime failed (${e.message}) — handler installed but not set as default. Set it in your DE's default-apps settings.`);
224
+ }
225
+ try {
226
+ execSync(`update-desktop-database "${appsDir}"`);
227
+ }
228
+ catch { /* optional tool */ }
229
+ console.log(`Registered rmfmail as mailto: handler (${desktopPath}).`);
230
+ process.exit(0);
231
+ }
183
232
  if (process.platform !== "win32") {
184
- console.error("--register-mailto / --unregister-mailto are Windows-only.");
233
+ // macOS needs a real .app bundle with CFBundleURLTypes — LaunchServices
234
+ // won't register a bare script. Tracked as the macOS half of C124.
235
+ console.error("--register-mailto / --unregister-mailto: macOS registration not implemented yet (needs an .app bundle — TODO C124).");
185
236
  process.exit(1);
186
237
  }
187
- const { execSync } = await import("node:child_process");
188
238
  // Registered command points at the rmfmailto.exe launcher (per-app
189
239
  // binary at %LOCALAPPDATA%\rmfmail\bin\rmfmailto.exe). The exe has its
190
240
  // own VERSIONINFO with FileDescription="rmfmail" so the Win11 "Select
@@ -1973,6 +2023,10 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
1973
2023
  let popoutInfo = null;
1974
2024
  let sendToClient = null;
1975
2025
  const popoutWindows = new Map();
2026
+ // Timestamp of the most recent popout close — reported as a delta in the
2027
+ // main-window shutdown log line to prove/refute the "closing a popout
2028
+ // closes the main window" coupling (Bob 2026-07-06, unreproduced).
2029
+ let lastPopoutClose = null;
1976
2030
  const popoutKey = (a, f, u) => `${a}|${f ?? ""}|${u}`;
1977
2031
  try {
1978
2032
  const { startPopoutServer } = await import("./popout-server.js");
@@ -2133,6 +2187,7 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2133
2187
  popoutWindows.delete(key);
2134
2188
  if (typeof h.pid === "number")
2135
2189
  removeChildPid(h.pid);
2190
+ lastPopoutClose = { at: Date.now(), key };
2136
2191
  console.log(` [popout] window CLOSED pid=${h.pid ?? "?"} key=${key} — main daemon still pid=${process.pid}`);
2137
2192
  });
2138
2193
  return { ok: true };
@@ -2339,6 +2394,13 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2339
2394
  }, 1000);
2340
2395
  }
2341
2396
  });
2397
+ // C121: connect-phase progress → status-bar "Connecting to <host>
2398
+ // (<phase>, N.Ns)" ticker. Low-rate (a handful of events per connection
2399
+ // attempt), so no batching — the phase TRANSITIONS are the signal and
2400
+ // coalescing them away would defeat the point.
2401
+ imapManager.on("connectProgress", (accountId, info) => {
2402
+ handle.send({ _event: "connectProgress", type: "connectProgress", accountId, ...info });
2403
+ });
2342
2404
  imapManager.on("syncError", (accountId, error) => {
2343
2405
  handle.send({ _event: "error", type: "error", message: `${accountId}: ${error}` });
2344
2406
  });
@@ -2807,8 +2869,15 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2807
2869
  // window ONLY — popout windows are separate showMessageBoxEx children and
2808
2870
  // must never resolve this. Log open popouts at shutdown so if a popout
2809
2871
  // close ever coincides with a main-window close we can see the timing.
2872
+ // The 2026-07-06 incident (log 13:59:56) shows the main child's own Rust
2873
+ // process emitting the close result 123ms after a popout interaction —
2874
+ // the correlation delta below is what will prove/refute the coupling
2875
+ // next time it fires.
2810
2876
  await handle.closed;
2811
- await gracefulShutdown(`Window closed (main service child; ${popoutWindows.size} popout window(s) still tracked)`);
2877
+ const popoutDelta = lastPopoutClose
2878
+ ? `; last popout closed ${Date.now() - lastPopoutClose.at}ms ago (${lastPopoutClose.key})`
2879
+ : "; no popout closed this session";
2880
+ await gracefulShutdown(`Window closed (main service child; ${popoutWindows.size} popout window(s) still tracked${popoutDelta})`);
2812
2881
  }
2813
2882
  main().catch(console.error);
2814
2883
  //# sourceMappingURL=mailx.js.map