@bobfrankston/rmfmail 1.2.173 → 1.2.177

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 (45) hide show
  1. package/TODO.md +3 -3
  2. package/bin/build-rmfshare-exe.js +79 -0
  3. package/bin/mailx.js +52 -1
  4. package/bin/mailx.js.map +1 -1
  5. package/bin/mailx.ts +51 -1
  6. package/bin/rmfshare-src/Program.cs +225 -0
  7. package/bin/rmfshare-src/rmfshare.csproj +36 -0
  8. package/bin/rmfshare.exe +0 -0
  9. package/bin/share-target.js +314 -0
  10. package/bin/share-target.js.map +1 -0
  11. package/bin/share-target.ts +331 -0
  12. package/client/app.bundle.js +70 -9
  13. package/client/app.bundle.js.map +3 -3
  14. package/client/app.js +69 -0
  15. package/client/app.js.map +1 -1
  16. package/client/app.ts +72 -0
  17. package/client/components/alarms.js +19 -9
  18. package/client/components/alarms.js.map +1 -1
  19. package/client/components/alarms.ts +21 -7
  20. package/client/compose/compose.bundle.js +4 -0
  21. package/client/compose/compose.bundle.js.map +2 -2
  22. package/client/lib/api-client.js +7 -0
  23. package/client/lib/api-client.js.map +1 -1
  24. package/client/lib/api-client.ts +12 -0
  25. package/client/package.json +1 -1
  26. package/package.json +6 -6
  27. package/packages/mailx-imap/package-lock.json +2 -2
  28. package/packages/mailx-imap/package.json +1 -1
  29. package/packages/mailx-service/index.d.ts +22 -0
  30. package/packages/mailx-service/index.d.ts.map +1 -1
  31. package/packages/mailx-service/index.js +82 -0
  32. package/packages/mailx-service/index.js.map +1 -1
  33. package/packages/mailx-service/index.ts +76 -0
  34. package/packages/mailx-service/jsonrpc.js +2 -0
  35. package/packages/mailx-service/jsonrpc.js.map +1 -1
  36. package/packages/mailx-service/jsonrpc.ts +2 -0
  37. package/packages/mailx-service/package.json +1 -1
  38. package/packages/mailx-settings/package.json +1 -1
  39. package/packages/mailx-store/package.json +1 -1
  40. package/packages/mailx-store-web/package.json +1 -1
  41. package/packages/mailx-types/mailx-api.d.ts +3 -0
  42. package/packages/mailx-types/mailx-api.d.ts.map +1 -1
  43. package/packages/mailx-types/mailx-api.ts +3 -0
  44. package/packages/mailx-types/package.json +1 -1
  45. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-7284 → node_modules.npmglobalize-stash-69680}/.package-lock.json +0 -0
package/TODO.md CHANGED
@@ -219,7 +219,7 @@ Items blocked on a design decision. Short-form question here; full context in th
219
219
  | **C41** | PARTIAL | Reply quoting — already wraps in `<blockquote>`; read-only + non-editable-above-the-line still owed (needs Quill module work). |
220
220
  | **C43** | OPEN | Message list column controls + threading flags. ✓ (sort-by-column-click is small) |
221
221
  | **C44** | DONE (v1.2.105) | Real OS popup windows / pop-out — floating-overlay popout landed (Q64 v1.0.344); v1.2.105 unblocked the real OS window: 🗗 asks the daemon, which spawns a native msger window (`showMessageBoxEx({url})`) pointed at the loopback popout server. Toolbar actions POST back → `popoutAction` event → main window opens editable compose / flags / deletes (delete also closes the window). Browser-window option deferred (same URL, just `openExternal` it). |
222
- | [**C46**](#ext46) | PARTIAL | Mailto handler split out as [P115](#ext115) (registry-only, not blocked). Remaining (Share target, tray, notifications, MSIX) still blocked on MSIX packaging. |
222
+ | [**C46**](#ext46) | PARTIAL | Mailto handler split out as [P115](#ext115) (registry-only, not blocked). Share target SHIPPED v1.2.175 (`-register-share`, loose AppX — no MSIX needed). Remaining (tray, notifications) still blocked on MSIX packaging. |
223
223
  | **C127** | OPTIONAL | **Push relay for calendar / mail / tasks** — design at [`docs/push-relay.md`](push-relay.md). New `MailApps/relayer/` package, alerter-shape (single Node Express+ws process, deploy alongside other small services). Generic API: client POSTs `/subscribe` with upstream + params, relay registers the upstream `events.watch` and returns an SSE/WS endpoint the client holds open. Per-upstream adapter files (`google-calendar.ts`, `google-gmail.ts`, `ms-graph-events.ts`); fan-out + replay buffer + subscription store generic. Client side: `pushRelayUrl` + credentials in settings, falls back to syncToken polling when relay unreachable. **Build only when polling latency demonstrably matters** — for typical use, 5–10 s syncToken cadence is functionally instant. Push is a freshness boost, not a correctness fix. |
224
224
  | **C128** | OPEN | **Body pre-parse on prefetch** — first click on any unviewed message pays `simpleParser` + `sanitizeHtml` cost (~100–500 ms on rich HTML). The parsedCache (added 2026-05-09) eliminates 2nd-and-later view costs but not the first. Fix: extend prefetch to ALSO parse + sanitize the body during background fetch and store the result alongside the .eml (e.g., `<uuid>.parsed.json` next to `<uuid>.eml`, or a `body_html_cached` column). Click-time path reads pre-parsed JSON, no parser invocation. Touches `mailx-imap/index.ts` prefetch path + `mailx-store/file-store.ts` for the parsed-blob storage. |
225
225
  | **C129** | OPEN | **Android prod/dev split via filename/URL channel** — User 2026-05-09 (revised): no separate MAUI project. Same source tree builds; output filenames distinguish channels: Phase 1 (start now) writes `rmfmaildev.apk` alongside today's `mailx-maui.apk`. Phase 2 (when a release is declared) switches to `rmfmail-<version>.apk` versioned + `rmfmaildev.apk` for latest, with promotion = re-pointing `rmfmail.apk` at a chosen version. Single APK contents per build; channel is runtime setting in AppUpdater (Settings toggle for which URL to poll). Side-by-side install on one device NOT supported by this design (single Android package id) — that's a separate need. See `docs/prod-android.md` for full plan. Phase 1 ~30 min; full flow ~2.5 hours when activated. |
@@ -488,8 +488,8 @@ Most likely cause based on the bobma folder layout: Dovecot designates "Deleted
488
488
  ### C46 — Windows OS integration (PARTIAL) [↑ summary](#sum)
489
489
 
490
490
  - [ ] **mailto: handler** → see [P115](#ext115) — split out as standalone priority item, NOT blocked on MSIX (registry-only).
491
- - [ ] **Windows Share target** — register as Share target (requires MSIX packaging)
492
- - [ ] Native right-click context menus, system tray, file associations, notifications
491
+ - [x] **Windows Share target** (v1.2.175) no MSIX signing needed after all: loose AppX deployment (`Add-AppxPackage -Register`, unsigned, needs Developer Mode). `rmfmail -register-share` stages `%LOCALAPPDATA%\rmfmail\share-target\` (rmfshare.exe + AppxManifest with `windows.shareTarget` + logos scaled from icon.png). `bin/rmfshare-src/` is a C# NativeAOT helper (3.8 MB, checked-in like rmfmailto.exe): ShareTarget activation → WinRT DataPackageView → files copied to `~/.rmfmail/share-staging/` → `pending-share.json` → daemon fs.watch base64s files and pushes `openShare` → compose opens with attachments/link/text. Startup-race twin `consumePendingShare` IPC, same shape as P115 mailto. Machines without Developer Mode get a clear message; Settings-UI toggle still owed (lands with P116 Settings rework).
492
+ - [ ] Native right-click context menus, system tray, file associations, notifications (tray + notifications still MSIX-blocked)
493
493
 
494
494
  <a id="ext115"></a>
495
495
 
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build the rmfshare.exe Windows Share-target activation helper (C46).
4
+ *
5
+ * Mirrors build-rmfmailto-exe.js: hooked into `npm run build`, skips when
6
+ * - not on Windows,
7
+ * - the .NET SDK isn't installed (uses the checked-in bin/rmfshare.exe),
8
+ * - the shipped exe is newer than every file in bin/rmfshare-src/.
9
+ *
10
+ * On a real rebuild runs `dotnet publish -c Release -r win-x64` (NativeAOT —
11
+ * needs the VS C++ toolchain; vswhere's dir is appended to PATH because the
12
+ * ILCompiler targets shell out to it) and copies the published exe to
13
+ * bin/rmfshare.exe. The exe is checked into git so `npm install -g` ships
14
+ * it without requiring .NET on install machines.
15
+ */
16
+
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { execSync, spawnSync } from "node:child_process";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const SRC_DIR = path.join(__dirname, "rmfshare-src");
24
+ const BUILT_EXE = path.join(SRC_DIR, "bin", "Release", "net10.0-windows10.0.19041.0", "win-x64", "publish", "rmfshare.exe");
25
+ const SHIPPED_EXE = path.join(__dirname, "rmfshare.exe");
26
+
27
+ if (process.platform !== "win32") {
28
+ console.log("[rmfshare] non-Windows host — skipping (binary is Win-only)");
29
+ process.exit(0);
30
+ }
31
+
32
+ try {
33
+ execSync("dotnet --version", { stdio: "pipe" });
34
+ } catch {
35
+ console.log("[rmfshare] dotnet not found — skipping build (using checked-in bin/rmfshare.exe).");
36
+ process.exit(0);
37
+ }
38
+
39
+ // mtime gate — same shape as build-rmfmailto-exe.js.
40
+ function newestMtime(dir) {
41
+ let newest = 0;
42
+ const stack = [dir];
43
+ while (stack.length) {
44
+ const d = stack.pop();
45
+ for (const name of fs.readdirSync(d)) {
46
+ if (name === "bin" || name === "obj") continue; // dotnet output dirs
47
+ const full = path.join(d, name);
48
+ const st = fs.statSync(full);
49
+ if (st.isDirectory()) stack.push(full);
50
+ else if (st.mtimeMs > newest) newest = st.mtimeMs;
51
+ }
52
+ }
53
+ return newest;
54
+ }
55
+
56
+ const srcMtime = newestMtime(SRC_DIR);
57
+ let shippedMtime = 0;
58
+ try { shippedMtime = fs.statSync(SHIPPED_EXE).mtimeMs; } catch { /* missing */ }
59
+ if (shippedMtime > srcMtime) {
60
+ console.log("[rmfshare] up-to-date — skipping dotnet publish");
61
+ process.exit(0);
62
+ }
63
+
64
+ console.log("[rmfshare] building (dotnet publish NativeAOT)...");
65
+ const t0 = Date.now();
66
+ const vswhereDir = "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer";
67
+ const r = spawnSync("dotnet", ["publish", "-c", "Release", "-r", "win-x64"], {
68
+ cwd: SRC_DIR,
69
+ stdio: "inherit",
70
+ shell: true,
71
+ env: { ...process.env, PATH: `${process.env.PATH};${vswhereDir}` },
72
+ });
73
+ if (r.status !== 0) {
74
+ console.error(`[rmfshare] dotnet publish failed (exit ${r.status})`);
75
+ process.exit(r.status || 1);
76
+ }
77
+ fs.copyFileSync(BUILT_EXE, SHIPPED_EXE);
78
+ const sec = ((Date.now() - t0) / 1000).toFixed(1);
79
+ console.log(`[rmfshare] built and copied to bin/rmfshare.exe (${sec}s)`);
package/bin/mailx.js CHANGED
@@ -389,6 +389,20 @@ if (hasFlag("register-mailto") || hasFlag("unregister-mailto")) {
389
389
  console.log(r.message);
390
390
  process.exit(r.ok ? 0 : 1);
391
391
  }
392
+ // C46: Windows Share-target registration. Loose AppX deployment of
393
+ // bin/rmfshare.exe (see bin/share-target.ts for the full flow). Android
394
+ // gets the same feature from the MAUI manifest; macOS/Linux have no
395
+ // equivalent OS share sheet to register into.
396
+ if (hasFlag("register-share") || hasFlag("unregister-share")) {
397
+ if (process.platform !== "win32") {
398
+ console.error("-register-share / -unregister-share: Windows only (the Share sheet is a Windows shell feature).");
399
+ process.exit(1);
400
+ }
401
+ const { registerShareWindows } = await import("./share-target.js");
402
+ const r = registerShareWindows(hasFlag("unregister-share"));
403
+ console.log(r.message);
404
+ process.exit(r.ok ? 0 : 1);
405
+ }
392
406
  // Read our own version once — used for the instance file + upgrade check below.
393
407
  const __selfRoot = path.join(import.meta.dirname, "..");
394
408
  const __selfVersion = (() => {
@@ -558,7 +572,7 @@ const repairMode = hasFlag("repair");
558
572
  const recoverMode = hasFlag("recover");
559
573
  const importMode = hasFlag("import");
560
574
  // Validate arguments
561
- const knownFlags = ["verbose", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "recover", "fix-flags", "relink-bodies", "log", "import", "email", "mail", "daemon", "reauth", "mailto", "register-mailto", "unregister-mailto", "allow-elevated", "another", "no-browser", "debug-server", "send", "account"];
575
+ const knownFlags = ["verbose", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "recover", "fix-flags", "relink-bodies", "log", "import", "email", "mail", "daemon", "reauth", "mailto", "register-mailto", "unregister-mailto", "register-share", "unregister-share", "allow-elevated", "another", "no-browser", "debug-server", "send", "account"];
562
576
  for (const arg of args) {
563
577
  // Strip a leading -/-- and any `=value` suffix before checking.
564
578
  const flag = arg.replace(/^--?/, "").split("=")[0];
@@ -597,6 +611,10 @@ for (const arg of args) {
597
611
  " -register-mailto Register as the OS-level mailto: handler\n" +
598
612
  " -unregister-mailto Remove the mailto: handler registration\n" +
599
613
  " -mailto <url> (internal) Decode a mailto URL and open compose\n" +
614
+ " Share target (Windows):\n" +
615
+ " -register-share Add rmfmail to the Windows Share menu\n" +
616
+ " (needs Developer Mode for the unsigned manifest)\n" +
617
+ " -unregister-share Remove rmfmail from the Windows Share menu\n" +
600
618
  " Internal:\n" +
601
619
  " -daemon (internal) marker for the detached daemon respawn\n" +
602
620
  " -allow-elevated Bypass the admin/root refusal (debug only)");
@@ -2764,6 +2782,39 @@ RFC 5322 with CRLF line endings. Bodies are quoted-printable encoded (readable i
2764
2782
  console.error(` [mailto] watch setup failed: ${e.message}`);
2765
2783
  }
2766
2784
  }
2785
+ // Pending Windows-share handler (C46). Same two-path pickup as mailto
2786
+ // above: the client's startup consumePendingShare poll covers the
2787
+ // "rmfshare.exe spawned us" race; this fs.watch covers shares that land
2788
+ // while we're already running. The daemon (not the client) base64s the
2789
+ // staged files — the WebView can't read disk.
2790
+ {
2791
+ const { consumePendingShare, loadShareAttachments, cleanupShareStaging, getPendingShareFile } = await import("./share-target.js");
2792
+ try {
2793
+ cleanupShareStaging();
2794
+ const pendingPath = getPendingShareFile();
2795
+ const dir = path.dirname(pendingPath);
2796
+ const baseName = path.basename(pendingPath);
2797
+ fs.mkdirSync(dir, { recursive: true });
2798
+ fs.watch(dir, (_event, filename) => {
2799
+ if (filename !== baseName)
2800
+ return;
2801
+ if (!fs.existsSync(pendingPath))
2802
+ return;
2803
+ setTimeout(() => {
2804
+ const data = consumePendingShare();
2805
+ if (!data)
2806
+ return;
2807
+ const { attachments, skipped } = loadShareAttachments(data.files);
2808
+ handle.send({ _event: "openShare", type: "openShare",
2809
+ title: data.title, text: data.text, url: data.url,
2810
+ attachments, skipped });
2811
+ }, 50);
2812
+ });
2813
+ }
2814
+ catch (e) {
2815
+ console.error(` [share] watch setup failed: ${e.message}`);
2816
+ }
2817
+ }
2767
2818
  // No artificial pause here. msger's stdin is queued — writes from
2768
2819
  // imapManager events arriving before the WebView is ready get buffered
2769
2820
  // and delivered when the WebView attaches. The earlier 500 ms wait