@bobfrankston/rmfmail 1.0.661 → 1.0.663

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.
@@ -0,0 +1,4 @@
1
+ Microsoft Windows [Version 10.0.26200.8328]
2
+ (c) Microsoft Corporation. All rights reserved.
3
+
4
+ 23:07:36.71 Y:\dev\email\mailx\app>
package/TODO.md CHANGED
@@ -236,6 +236,25 @@ Previously shown as showstoppers; moved here because they haven't recurred on re
236
236
 
237
237
  Small, self-contained items. Pick them up between higher-priority blocks without asking. Bump version per fix.
238
238
 
239
+ - **Q138 — Quoted-reply image sizes lost.** Bob 2026-05-10: in the reply quote of a marketing email (Qatar Airways), the App Store / Google Play / AppGallery button images render larger than in the original. Cause: `sanitizeQuotedBody` in `client/app.ts` strips `width` / `height` attributes (along with `align`, `valign`, `bgcolor`, `cellpadding`, `cellspacing`, `border`) to flatten marketing-email layout tables. Stripping those attrs from `<img>` is collateral damage — table-only attributes there, but they also size images. Fix: in the regex, scope the attr strip to non-img tags, OR leave width/height alone everywhere (the layout flattening doesn't actually need them on tables either — `<table>→<div>` rewrites are doing the heavy lifting). ~10 line change. Low priority — visual nit, not data loss.
240
+ - **Q136 — Server / provider capability matrix (forward backlog).** Catalog of server-specific features mailx could exploit. Detection is dynamic everywhere — IMAP via `CAPABILITY` + RFC 2971 `ID`, non-IMAP via provider-type at account setup. Each row below is its own future work item; this entry is the index.
241
+ - **Dovecot** (IMAP — bobma's primary server): `NOTIFY` (✅ shipped 2026-05-10, see startWatching), `IDLE` (✅), `MOVE` (✅), `QRESYNC` / `CONDSTORE` (📋 Q135 — fast incremental sync with VANISHED + flag-since-modseq), `LIST-EXTENDED` (📋 single-roundtrip folder list with status counts), `METADATA` (📋 per-mailbox annotations — could hold per-folder mailx state on the server), `UNSELECT` (📋 fast folder switch without EXPUNGE), Push Framework / RFC 5423 (📋 server-side HTTP push — requires bobma config change, would let us retire IDLE).
242
+ - **Cyrus** (IMAP — common in academia / legacy ISPs): `CONDSTORE` / `QRESYNC` (📋), `LIST-STATUS` (📋 — counts piggybacked on LIST), `MULTIAPPEND` (📋 — batch draft saves), `ANNOTATE-EXPERIMENT-1` (📋 — Cyrus's pre-METADATA cousin, syntax-incompatible). Server fingerprint via `ID` to distinguish from Dovecot when capability sets overlap.
243
+ - **Courier** (IMAP — older shared-hosting): mostly bare RFC 3501. `THREAD=REFERENCES` available but server-side threading is fragile; client-side threading (already in mailx) is the safer path. No fast-sync extensions — skip.
244
+ - **uw-imap** (IMAP — historical): treat as RFC 3501-only. No NOTIFY, no CONDSTORE. Detect via `ID` and disable optional code paths.
245
+ - **Microsoft Exchange (IMAP front-end)**: capability set is weak — no NOTIFY, no QRESYNC, no SPECIAL-USE. *Don't* invest here; the Outlook Graph API path (below) is the right backend. Detect via `ID` returning `name = "Microsoft.Exchange.Imap4"` and surface a setup-time nudge to switch to OAuth + Graph.
246
+ - **Gmail IMAP** (X-GM-EXT-1): label-aware extensions exist but mailx already uses the Gmail REST API path for Gmail accounts (faster, no per-account connection limit). Skip Gmail-IMAP-specific tuning; if anyone falls through to IMAP for a Gmail account, capability detection handles it as plain RFC 3501 IMAP.
247
+ - **Yahoo / AOL IMAP**: standard IMAP, no NOTIFY, limited IDLE reliability. Treat as RFC 3501 + IDLE only. Document the known 25-msgs-per-fetch chunk requirement.
248
+ - **iCloud IMAP**: standard IMAP + IDLE. App-specific password required (already documented in setup). No special tuning.
249
+ - **Fastmail / Topicbox** (Cyrus-based + JMAP): if/when we add JMAP, Fastmail accounts should auto-prefer JMAP over IMAP. Until then, treat as Cyrus IMAP per the row above.
250
+ - **Gmail API** (current default for Gmail accounts): already supports `historyId`-based incremental sync. **Quick wins not yet exploited**: (📋 Q137) Cloud Pub/Sub `users.watch` for real-time push — replaces 15-minute polling; (📋) `batchModify` for bulk label changes when the user multi-selects + flags/moves; (📋) `users.getProfile` with `messagesTotal` for cheap "did anything change at all" checks before issuing `history.list`.
251
+ - **Outlook Graph API** (skeleton exists at `providers/outlook-api.ts`): Azure app registration needed (📋 C23 — blocked on Bob registering the app). Once unblocked: delta queries (`/messages/delta`) for sync; subscriptions (webhook-based push) for real-time; native conversation/thread IDs.
252
+ - **JMAP** (Fastmail leads, IETF standard track): future provider — single HTTP API, native push channel, native threading, server-side search. Adding a `JmapProvider` would give Fastmail users a clean fast path and validate the abstraction for any other JMAP server (Topicbox, Stalwart). Detect via well-known URI `.well-known/jmap` at account setup.
253
+ - **EWS** (legacy Exchange Web Services): pre-Graph Exchange. Avoid — Microsoft has deprecated it. If a corporate user lands here, fall back to IMAP if available, otherwise document the gap.
254
+
255
+ **Implementation pattern reminder**: branches go on capability flags, not server names. `ID`-extension server names are only for *non-capability* quirks (annotate syntax differences, known bugs in specific versions). The Gmail-vs-IMAP-vs-Outlook split lives at the provider level; CAPABILITY-set splits live inside the IMAP provider.
256
+
257
+ - **Q135 — Server-capability gating + Dovecot QRESYNC/CONDSTORE fast path.** Bob 2026-05-10: "with IMAP you need to take advantage of features for each IMAP server. Start with Dovecot, support others as we identify them." Architectural shape: `iflow-direct` exposes a `Capabilities` set from the server's CAPABILITY response (`QRESYNC`, `CONDSTORE`, `MOVE`, `LIST-EXTENDED`, `SPECIAL-USE`, `XLIST`, `ENABLE`, `UIDPLUS`, `LITERAL+`, `NOTIFY`); `mailx-imap` branches per-account. Concrete first win — **QRESYNC/CONDSTORE on bobma**: replace the full UID-set re-fetch in `syncFolder` (`mailx-imap/index.ts`) with `SELECT ... QRESYNC (uidvalidity highestmodseq)` which returns `VANISHED` for deletions + `FETCH FLAGS` only for changed flags. Zero baseline re-fetch on idle folders, no false-positive deletes from truncated UID lists (the 50% safety guards become belt-and-braces rather than load-bearing). Second win — **MOVE (RFC 6851)** atomic move instead of COPY+EXPUNGE pair (already enabled on Gmail; needs detection on Dovecot/others). Third — **NOTIFY (RFC 5465)** per-mailbox interest declaration so IDLE on INBOX also receives FETCH/EXISTS events for OTHER selected mailboxes without re-IDLEing each. Per-server identification via `*ID` extension + capability fingerprint (Dovecot vs Cyrus vs Courier vs uw-imap have distinguishable CAPABILITY sets). Estimate: QRESYNC slice is M (iflow-direct exposes `select(folder, { qresync: { uidvalidity, modseq } })` + parse `VANISHED` + parse `OK [HIGHESTMODSEQ ...]`; mailx-imap persists modseq per folder in DB; reconcile loop uses VANISHED instead of set-diff). Pre-req: iflow-direct doesn't currently expose modseq or QRESYNC params — extension needed there first.
239
258
  - **Q134 — Real popout: compose + preview into independent msger windows.** Current popout button (title-bar icon) toggles the floating compose overlay to fill the host window — fast win but not a true OS window. Bob 2026-05-10: "popout should pop out into an independent window. This should also be true for the preview." Requires daemon refactor: single `handle = showService(...)` in `bin/mailx.ts` (~40 `handle.send` call sites) becomes a `windows` registry — `broadcast(msg)` fans events to all open windows; per-window onRequest closures route request acks back to the originator. New jsonrpc actions `openPopout({ mode, init })` (spawns a fresh `showService` with `contentDir: <client>` pointed at `compose/compose.html` or a `preview/preview.html` for viewer popout) and `getPopoutInit({ token })` (init data delivery — see option below). Client wires the popout button to call `api.openPopout(...)` then closes the floating overlay. Preview gets a matching button in `message-viewer.ts`. Init data delivery: URL-hash token + first-load fetch from daemon cache; popout has no state until it asks. Estimate: half-day. Pre-req for any window-pair workflow (browse main + read popped-out message, write compose while triaging inbox).
240
259
  - **Q133 — TinyMCE: pre-warm CDN fetch / progressive editor swap.** Bob 2026-05-10: "if fetching tinymce takes time it should be done in background and only activate when it is available." Today, picking TinyMCE in Settings means the FIRST compose-open after that setting blocks for ~1s while the CDN bundle downloads. Better: when user toggles to TinyMCE in Settings, fire a fetch (or a `<link rel="modulepreload">`) so the bundle is in browser cache by the time they hit Compose. Even better: open compose with Quill instantly, swap the editor to TinyMCE in place once the bundle resolves. Touches `compose.ts` editor init + `app.ts` Settings toggle handler.
241
260
  - **Q132 — importgen bugs blocking "always use importgen" rule for rmfmail.** Three issues, fix in `y:\dev\utils\importgen`: (1) reads CWD's `package.json`, so running from `client/` reads `client/package.json` (declares only quill); needs a `-p` flag or to walk up. (2) `resolveEntryPoint` blows up on packages with conditional `exports` (e.g. `".": { "import": { "node": "...", "browser": "..." } }`) — `dotExport.import` becomes an object, then `entryPoint.startsWith` throws. (3) Doesn't follow dynamic `await import()` calls if it scans imports rather than package.json deps (still need to verify which path it actually takes). Until fixed, rmfmail's import map is hand-maintained.
package/client/app.js CHANGED
@@ -890,8 +890,7 @@ function showComposeOverlay(title = "Compose") {
890
890
  dragY = e.clientY - rect.top;
891
891
  // Clamp movement to the viewport so the title bar stays grabbable.
892
892
  const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
893
- frame.style.pointerEvents = "none";
894
- document.body.style.userSelect = "none";
893
+ const shield = installDragShield("move");
895
894
  const onMove = (ev) => {
896
895
  ev.preventDefault();
897
896
  const w = wrapper.offsetWidth;
@@ -904,8 +903,7 @@ function showComposeOverlay(title = "Compose") {
904
903
  wrapper.style.right = "auto";
905
904
  };
906
905
  const onUp = () => {
907
- frame.style.pointerEvents = "";
908
- document.body.style.userSelect = "";
906
+ shield.remove();
909
907
  document.removeEventListener("mousemove", onMove);
910
908
  document.removeEventListener("mouseup", onUp);
911
909
  };
@@ -936,6 +934,19 @@ function showComposeOverlay(title = "Compose") {
936
934
  addComposeResizeHandles(wrapper, frame);
937
935
  document.body.appendChild(wrapper);
938
936
  }
937
+ /** Drop a transparent full-viewport shield in front of every other element
938
+ * so mousemove events stay in the document during a drag. Setting
939
+ * pointer-events:none on the compose iframe alone wasn't enough — the
940
+ * message-list / preview iframes underneath still captured the cursor
941
+ * when it crossed their boundaries, freezing the drag at random
942
+ * midpoints (most visibly at the list↔preview seam). One shield blocks
943
+ * every iframe at once. Caller removes it on mouseup. */
944
+ function installDragShield(cursor) {
945
+ const shield = document.createElement("div");
946
+ shield.style.cssText = `position:fixed;inset:0;z-index:9999;background:transparent;cursor:${cursor};user-select:none;`;
947
+ document.body.appendChild(shield);
948
+ return shield;
949
+ }
939
950
  /** Attach eight resize grippers (4 edges + 4 corners) to a positioned wrapper.
940
951
  * CSS `resize:both` only supports a single lower-right corner; replacing it
941
952
  * with manual handles is the only way to make any side / corner draggable.
@@ -969,8 +980,7 @@ function addComposeResizeHandles(wrapper, frame) {
969
980
  wrapper.style.top = `${startT}px`;
970
981
  wrapper.style.right = "auto";
971
982
  wrapper.style.bottom = "auto";
972
- frame.style.pointerEvents = "none";
973
- document.body.style.userSelect = "none";
983
+ const shield = installDragShield(conf.cursor);
974
984
  const onMove = (ev) => {
975
985
  const dx = ev.clientX - startX;
976
986
  const dy = ev.clientY - startY;
@@ -993,8 +1003,7 @@ function addComposeResizeHandles(wrapper, frame) {
993
1003
  wrapper.style.top = `${newT}px`;
994
1004
  };
995
1005
  const onUp = () => {
996
- frame.style.pointerEvents = "";
997
- document.body.style.userSelect = "";
1006
+ shield.remove();
998
1007
  document.removeEventListener("mousemove", onMove);
999
1008
  document.removeEventListener("mouseup", onUp);
1000
1009
  };