@bobfrankston/rmfmail 1.2.240 → 1.2.241

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.
@@ -23,6 +23,20 @@ import crypto from "node:crypto";
23
23
  import { fileURLToPath } from "node:url";
24
24
 
25
25
  const root = path.resolve(fileURLToPath(import.meta.url), "..", "..");
26
+
27
+ // A client call the bridge does not carry is a silent no-op, not an error —
28
+ // so it ships. Check before every bundle (this script runs on the way to
29
+ // every publish). See bin/check-bridge-contract.mjs for the case that
30
+ // motivated it.
31
+ {
32
+ const { execFileSync } = await import("node:child_process");
33
+ try {
34
+ execFileSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), "check-bridge-contract.mjs")], { stdio: "inherit" });
35
+ } catch {
36
+ console.error(" [build-bundles] aborting: bridge contract check failed");
37
+ process.exit(1);
38
+ }
39
+ }
26
40
  const watch = process.argv.includes("--watch");
27
41
 
28
42
  /** Cache-bust the bundle references in the HTML entry points with a content
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fail the build when a client API call can never reach the daemon.
4
+ *
5
+ * `api-client.ts` invokes the host bridge as `ipc().foo?.(…)`. If the bridge
6
+ * has no `foo`, optional chaining makes that expression evaluate to
7
+ * `undefined` — no request, no error, no log line. The UI carries on and
8
+ * reports success. That is how right-drag **Copy** was a silent no-op for its
9
+ * entire life: `copyMessages` existed in api-client, in jsonrpc, and in the
10
+ * service — but never in `mailxapi.js`, so every copy quietly did nothing
11
+ * while the status bar said "Copying 3 to Drafts…" (Bob 2026-08-09: "when I
12
+ * used right mouse copy it didn't copy to drafts"). The same audit found
13
+ * `autocomplete` and `cancelServerSearch` dead the same way.
14
+ *
15
+ * The rule this enforces: if the DAEMON can handle it and the CLIENT calls
16
+ * it, the BRIDGE must carry it. A method missing from all three is fine
17
+ * (Android-only surface); a method the client calls with no handler anywhere
18
+ * is a warning, not an error, since some are deliberately optional.
19
+ */
20
+ import fs from "node:fs";
21
+ import path from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+
24
+ const root = path.resolve(fileURLToPath(import.meta.url), "..", "..");
25
+ const read = (p) => fs.readFileSync(path.join(root, p), "utf8");
26
+
27
+ const api = read("client/lib/api-client.ts");
28
+ const bridge = read("client/lib/mailxapi.js");
29
+ const rpc = read("packages/mailx-service/jsonrpc.ts");
30
+
31
+ const called = new Set();
32
+ for (const m of api.matchAll(/ipc\(\)\.(\w+)\s*\??\.?\s*\(/g)) called.add(m[1]);
33
+
34
+ const inBridge = new Set();
35
+ for (const m of bridge.matchAll(/^\s+(\w+)\s*:\s*(?:async\s+)?function/gm)) inBridge.add(m[1]);
36
+
37
+ const handled = new Set();
38
+ for (const m of rpc.matchAll(/case\s+"(\w+)"\s*:/g)) handled.add(m[1]);
39
+
40
+ const broken = []; // client calls it, daemon handles it, bridge doesn't carry it
41
+ const orphan = []; // client calls it, nothing handles it anywhere
42
+ for (const name of [...called].sort()) {
43
+ if (inBridge.has(name)) continue;
44
+ (handled.has(name) ? broken : orphan).push(name);
45
+ }
46
+
47
+ if (orphan.length) {
48
+ console.log(` [bridge-contract] ${orphan.length} client call(s) with no bridge and no daemon handler (optional surface): ${orphan.join(", ")}`);
49
+ }
50
+
51
+ if (broken.length) {
52
+ console.error(`\n [bridge-contract] FAIL — these have a daemon handler but no entry in client/lib/mailxapi.js,`);
53
+ console.error(` so every call silently evaluates to undefined and the feature does nothing:\n`);
54
+ for (const n of broken) console.error(` ${n}`);
55
+ console.error(`\n Add each to mailxapi.js as: ${broken[0]}: function(...) { return callNode("${broken[0]}", { ... }); },\n`);
56
+ process.exit(1);
57
+ }
58
+
59
+ console.log(` [bridge-contract] ok — ${called.size} client calls, all daemon-handled ones present in the bridge`);
@@ -9910,8 +9910,14 @@ document.addEventListener("mailx-flagged-batch", (e) => {
9910
9910
  const n = msgs.length;
9911
9911
  const label = n === 1 ? "message" : `${n} messages`;
9912
9912
  showContextMenu2(x, y, [
9913
- { label: `Move ${label} here`, emphasized: true, action: () => void runRightDragOp(msgs, targetAccount, targetFolderId, targetName, "move") },
9914
- { label: `Copy ${label} here`, action: () => void runRightDragOp(msgs, targetAccount, targetFolderId, targetName, "copy") }
9913
+ // Name the destination, never say "here". A menu that appears
9914
+ // under the pointer still has to read correctly on its own: by the
9915
+ // time the user is reading the menu, the folder they were dragging
9916
+ // over may not be what they think it is, and "here" gives them
9917
+ // nothing to check against (Bob 2026-08-09: "using HERE in a
9918
+ // message is very very bad — give the folder name").
9919
+ { label: `Move ${label} to ${targetName}`, emphasized: true, action: () => void runRightDragOp(msgs, targetAccount, targetFolderId, targetName, "move") },
9920
+ { label: `Copy ${label} to ${targetName}`, action: () => void runRightDragOp(msgs, targetAccount, targetFolderId, targetName, "copy") }
9915
9921
  ]);
9916
9922
  }
9917
9923
  async function runRightDragOp(msgs, targetAccount, targetFolderId, targetName, op) {