@bobfrankston/rmfmail 1.0.665 → 1.0.667
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/TODO.md +8 -5
- package/bin/build-bundles.mjs +96 -0
- package/client/app.bundle.js +8998 -0
- package/client/app.bundle.js.map +7 -0
- package/client/app.js +69 -2
- package/client/app.js.map +1 -1
- package/client/app.ts +69 -2
- package/client/components/alarms.js +59 -14
- package/client/components/alarms.js.map +1 -1
- package/client/components/alarms.ts +58 -15
- package/client/components/calendar-sidebar.js +32 -3
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +30 -2
- package/client/compose/compose.bundle.js +2872 -0
- package/client/compose/compose.bundle.js.map +7 -0
- package/client/compose/compose.html +5 -1
- package/client/index.html +56 -3
- package/client/styles/components.css +4 -0
- package/package.json +3 -2
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +59 -5
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +58 -5
- package/packages/mailx-service/local-store.d.ts +4 -0
- package/packages/mailx-service/local-store.d.ts.map +1 -1
- package/packages/mailx-service/local-store.js +55 -1
- package/packages/mailx-service/local-store.js.map +1 -1
- package/packages/mailx-service/local-store.ts +51 -1
- package/packages/mailx-store/db.d.ts +1 -0
- package/packages/mailx-store/db.d.ts.map +1 -1
- package/packages/mailx-store/db.js +11 -4
- package/packages/mailx-store/db.js.map +1 -1
- package/packages/mailx-store/db.ts +13 -5
package/TODO.md
CHANGED
|
@@ -12,11 +12,11 @@ These items don't need user input — source-only changes, compile-verified befo
|
|
|
12
12
|
|
|
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
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
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: `dns → tcp 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
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. |
|
|
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
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
22
|
~~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).
|
|
@@ -236,7 +236,8 @@ 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
|
-
-
|
|
239
|
+
- ~~**Q138 — Quoted-reply image sizes lost.**~~ DONE 2026-05-11 in `client/app.ts:1000` — strip skips `<img>`. Two-pass loop handles multiple stripped attrs per tag.
|
|
240
|
+
- **Q138 (original entry) — 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
241
|
- **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
242
|
- **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
243
|
- **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.
|
|
@@ -256,10 +257,12 @@ Small, self-contained items. Pick them up between higher-priority blocks without
|
|
|
256
257
|
|
|
257
258
|
- **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.
|
|
258
259
|
- **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).
|
|
259
|
-
-
|
|
260
|
+
- ~~**Q133 — TinyMCE: pre-warm CDN fetch (settings-toggle prefetch).**~~ DONE 2026-05-11. `optEditorTinymce` change handler now appends `<link rel="prefetch">` for the configured TinyMCE URL so the next compose-open finds it cached. Progressive in-place editor swap (Quill → TinyMCE without reopen) is deferred — bigger change, smaller win once the prefetch is in place.
|
|
261
|
+
- **Q133 (original entry) — 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.
|
|
260
262
|
- **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.
|
|
261
263
|
- **Q131 — Show holidays in calendar sidebar.** Thunderbird-style "Show holidays" checkbox. When checked, daemon's `getCalendarEvents` ALSO fetches `en.usa#holiday@group.v.calendar.google.com` (Google's standard US holidays) and merges into results, marking each with `isHoliday: true`. UI renders holiday rows with a distinct style (muted color, no edit/delete actions). Concrete edits: (a) `mailx-service/index.ts` line 1075ish `getCalendarEvents` — also call `gsync.listCalendarEvents(tp, fromMs, toMs, "en.usa#holiday@group.v.calendar.google.com")` when settings.calendar.showHolidays is true, store with `is_holiday=1` flag; (b) `mailx-store/db.ts` `calendar_events` schema — add `is_holiday` column; (c) `client/components/calendar-sidebar.ts` — render rows with `data-holiday="1"` styled distinctly; (d) settings UI radio/checkbox in the calendar sidebar's options popover. ~2 hours.
|
|
262
|
-
-
|
|
264
|
+
- ~~**Q117 — Spring-loaded folders (drag-hover-expand).**~~ DONE — already implemented at `folder-tree.ts:355+` with `DRAG_HOVER_EXPAND_MS = 600` constant, `dragAutoExpanded` set, and `dragend` collapse-back path. TODO entry was stale.
|
|
265
|
+
- **Q117 (original entry) — Spring-loaded folders.** When the user is dragging messages and hovers over a collapsed folder in the folder tree, expand it after a short hover delay so the user can see / target child folders. Auto-collapse back to its prior state when the drag leaves the folder OR the drop completes. Standard pattern in Outlook / Thunderbird / Finder. Implementation: `dragenter` on a folder row starts a timer (named constant `DRAG_HOVER_EXPAND_MS`, ~600ms target); `dragleave` cancels; on timer fire, set an `expanded-during-drag` flag and toggle the disclosure triangle. `dragend` / `drop` walks back up clearing flags and restoring prior state. No persistence — purely transient. (Per `feedback_no_magic_numbers.md`: timing values go in named constants, not inline.)
|
|
263
266
|
- **Verify rail-view / rail-settings dropdowns in narrow mode (paused 2026-04-30).** Added `rail-view` icon (👁) and `openMenuFromRail()` in `client/app.ts` that reparents the toolbar dropdown to `<body>` and switches to `position:fixed` anchored to the rail icon — so the toolbar's `display:none` in narrow mode no longer hides the menu. Compile not yet re-verified after the edits because Bash kept wedging; user paused to start the monorepo reorg (mailx → mailxapp). When resuming: run `tsc` in `client/`, sanity-check that opening View / Settings from the rail in narrow mode shows the dropdown anchored next to the icon, and that toolbar buttons in wide mode still work (they just toggle `hidden` on the same dropdown DOM, which is now a body child but still works).
|
|
264
267
|
|
|
265
268
|
---
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bundle client entry points so cold-start drops from ~43 s to ~2 s.
|
|
4
|
+
*
|
|
5
|
+
* mailx's msger custom protocol routes every fetch through one IPC channel
|
|
6
|
+
* (Rust → daemon stdin → response → stdout), so an ES module import cascade
|
|
7
|
+
* pays a multi-second roundtrip per file. With ~17 client-side modules each
|
|
8
|
+
* pulling in transitive deps, the WebView is paying that cost N times in
|
|
9
|
+
* series.
|
|
10
|
+
*
|
|
11
|
+
* esbuild collapses the import graph into one file per entry point. The
|
|
12
|
+
* boot router in index.html stops cascading after one fetch.
|
|
13
|
+
*
|
|
14
|
+
* Dynamic `await import(...)` calls stay as runtime imports (rmf-tiny
|
|
15
|
+
* adapter for the TinyMCE editor, android-bootstrap fallback). Those are
|
|
16
|
+
* either tiny themselves, or live in a code path the desktop main flow
|
|
17
|
+
* doesn't take.
|
|
18
|
+
*/
|
|
19
|
+
import * as esbuild from "esbuild";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
const root = path.resolve(fileURLToPath(import.meta.url), "..", "..");
|
|
24
|
+
const watch = process.argv.includes("--watch");
|
|
25
|
+
|
|
26
|
+
const entries = [
|
|
27
|
+
{
|
|
28
|
+
in: path.join(root, "client", "app.ts"),
|
|
29
|
+
out: path.join(root, "client", "app.bundle.js"),
|
|
30
|
+
label: "app",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
in: path.join(root, "client", "compose", "compose.ts"),
|
|
34
|
+
out: path.join(root, "client", "compose", "compose.bundle.js"),
|
|
35
|
+
label: "compose",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const shared = {
|
|
40
|
+
bundle: true,
|
|
41
|
+
format: "esm",
|
|
42
|
+
target: "es2022",
|
|
43
|
+
platform: "browser",
|
|
44
|
+
sourcemap: true,
|
|
45
|
+
// Keep dynamic imports for code paths that should still load on demand:
|
|
46
|
+
// - @bobfrankston/rmf-tiny is loaded only when the user picks TinyMCE.
|
|
47
|
+
// - @bobfrankston/mailx-store-web/android-bootstrap is only on Android.
|
|
48
|
+
// - Quill / TinyMCE themselves are loaded by their adapters at runtime.
|
|
49
|
+
// - jsonc-parser is large and only used by the JSONC config editor.
|
|
50
|
+
external: [
|
|
51
|
+
"@bobfrankston/rmf-tiny",
|
|
52
|
+
"@bobfrankston/mailx-store-web/*",
|
|
53
|
+
"tinymce",
|
|
54
|
+
"tinymce/*",
|
|
55
|
+
"quill",
|
|
56
|
+
"quill/*",
|
|
57
|
+
"jsonc-parser",
|
|
58
|
+
"mammoth",
|
|
59
|
+
"html-to-docx",
|
|
60
|
+
],
|
|
61
|
+
// The output has to be ESM-compatible — index.html loads it via
|
|
62
|
+
// <script type="module"> through the boot router's `await import()`.
|
|
63
|
+
// Splitting would create extra chunks served as separate fetches,
|
|
64
|
+
// defeating the purpose; the whole point is one fetch per entry.
|
|
65
|
+
splitting: false,
|
|
66
|
+
// Suppress the "minify saved N bytes" chatter; we want the source
|
|
67
|
+
// readable in DevTools (sourcemap doubles up).
|
|
68
|
+
minify: false,
|
|
69
|
+
logLevel: "info",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async function buildAll() {
|
|
73
|
+
const t0 = Date.now();
|
|
74
|
+
const results = await Promise.all(entries.map(e =>
|
|
75
|
+
esbuild.build({ ...shared, entryPoints: [e.in], outfile: e.out })
|
|
76
|
+
.then(r => ({ ok: true, label: e.label, out: e.out }))
|
|
77
|
+
.catch(err => ({ ok: false, label: e.label, err }))
|
|
78
|
+
));
|
|
79
|
+
for (const r of results) {
|
|
80
|
+
if (r.ok) console.log(` bundle:${r.label} → ${path.relative(root, r.out)}`);
|
|
81
|
+
else console.error(` bundle:${r.label} FAILED: ${r.err?.message || r.err}`);
|
|
82
|
+
}
|
|
83
|
+
const failed = results.filter(r => !r.ok).length;
|
|
84
|
+
if (failed) process.exit(1);
|
|
85
|
+
console.log(`build-bundles: done in ${Date.now() - t0} ms`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (watch) {
|
|
89
|
+
const ctxs = await Promise.all(entries.map(e =>
|
|
90
|
+
esbuild.context({ ...shared, entryPoints: [e.in], outfile: e.out })
|
|
91
|
+
));
|
|
92
|
+
for (const c of ctxs) await c.watch();
|
|
93
|
+
console.log("build-bundles: watch mode");
|
|
94
|
+
} else {
|
|
95
|
+
await buildAll();
|
|
96
|
+
}
|