@bobfrankston/rmfmail 1.2.240 → 1.2.242
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/bin/build-bundles.mjs +14 -0
- package/bin/check-bridge-contract.mjs +59 -0
- package/client/app.bundle.js +34 -4
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +8 -2
- package/client/app.js.map +1 -1
- package/client/app.ts +8 -2
- package/client/components/message-list.ts +13 -2
- package/client/components/message-viewer.js +62 -1
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +73 -1
- package/client/compose/compose.bundle.js +14 -0
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/compose.css +11 -0
- package/client/lib/mailxapi.js +11 -0
- package/client/lib/rmf-tiny.js +14 -0
- package/package.json +5 -5
- package/packages/mailx-service/index.ts +34 -4
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-54868 → node_modules.npmglobalize-stash-33484}/.package-lock.json +0 -0
package/bin/build-bundles.mjs
CHANGED
|
@@ -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`);
|
package/client/app.bundle.js
CHANGED
|
@@ -1848,6 +1848,23 @@ function showPreviewBodyMenu(absX, absY, selectedText, sourceWindow, linkUrl, li
|
|
|
1848
1848
|
);
|
|
1849
1849
|
showContextMenu(absX, absY, items);
|
|
1850
1850
|
}
|
|
1851
|
+
function renderBodyFetchGaveUp(bodyEl, accountId, uid, folderId, secs, previewText, specialUse, envelope) {
|
|
1852
|
+
const looksImapUidOnApiAccount = uid > 0 && uid < 1e9;
|
|
1853
|
+
const why = looksImapUidOnApiAccount ? `This message was indexed over IMAP, but its account fetches bodies through the Gmail API, which addresses messages by a different id. The body can't be requested for this row.` : `The server accepted the request but never returned the body.`;
|
|
1854
|
+
const snippet = previewText ? `<div class="mv-preview-placeholder">${escapeHtml3(previewText)}<div class="mv-tear-line"><span>\u2702 snippet \u2014 this is all that arrived \u2702</span></div></div>` : "";
|
|
1855
|
+
bodyEl.innerHTML = `${snippet}<div class="mv-system-message mv-system-error">
|
|
1856
|
+
<div class="mv-system-tag">rmfmail</div>
|
|
1857
|
+
<div class="mv-system-title">Body didn't arrive after ${secs}s \u2014 not still loading</div>
|
|
1858
|
+
<div class="mv-system-body">${escapeHtml3(why)}
|
|
1859
|
+
<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">
|
|
1860
|
+
<button id="mv-body-retry" class="tb-btn">Try again</button>
|
|
1861
|
+
</div></div></div>`;
|
|
1862
|
+
bodyEl.querySelector("#mv-body-retry")?.addEventListener("click", () => {
|
|
1863
|
+
invalidateParsedCache(accountId, folderId, uid);
|
|
1864
|
+
showMessage(accountId, uid, folderId, specialUse, true, envelope).catch(() => {
|
|
1865
|
+
});
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1851
1868
|
function toggleFullscreenPreview() {
|
|
1852
1869
|
const body = document.body;
|
|
1853
1870
|
const on = !body.classList.contains("fullscreen-preview");
|
|
@@ -2205,8 +2222,14 @@ async function showMessage(accountId, uid, folderId, specialUse, isRetry = false
|
|
|
2205
2222
|
span.textContent = secs >= 60 ? `(${Math.floor(secs / 60)}m ${secs % 60}s)` : `(${secs}s)`;
|
|
2206
2223
|
if (secs === 30)
|
|
2207
2224
|
span.classList.add("mv-wait-slow");
|
|
2225
|
+
if (secs >= WAIT_GIVE_UP_SECS) {
|
|
2226
|
+
clearInterval(tick);
|
|
2227
|
+
off?.();
|
|
2228
|
+
renderBodyFetchGaveUp(bodyEl, accountId, uid, folderId, secs, previewText, specialUse, envelope || cached || void 0);
|
|
2229
|
+
}
|
|
2208
2230
|
}, 1e3);
|
|
2209
|
-
|
|
2231
|
+
let off;
|
|
2232
|
+
off = subscribeStore("*", (ev) => {
|
|
2210
2233
|
if (ev.kind !== "bodyAvailable")
|
|
2211
2234
|
return;
|
|
2212
2235
|
if (ev.accountId !== accountId || ev.uid !== uid)
|
|
@@ -3679,7 +3702,7 @@ function spawnDesktopPopout(msg, accountId) {
|
|
|
3679
3702
|
function escapeHtmlLocal(s) {
|
|
3680
3703
|
return (s || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3681
3704
|
}
|
|
3682
|
-
var currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom, RENDER_MARK_KEY, RENDER_MARK_MAX_AGE_MS, poisonedMessages, renderKey, PROGRESSIVE_TEXT_THRESHOLD, TEXT_CHUNK_BYTES;
|
|
3705
|
+
var currentMessage, currentAccountId, dragOutUrls, showMessageGeneration, retryCount, lastEnvelope, PARSED_CACHE_LIMIT, parsedCache, sessionAllowedRemote, recentFetchErrors, WAIT_GIVE_UP_SECS, ZOOM_KEY, ZOOM_MIN, ZOOM_MAX, ZOOM_STEP, previewZoom, RENDER_MARK_KEY, RENDER_MARK_MAX_AGE_MS, poisonedMessages, renderKey, PROGRESSIVE_TEXT_THRESHOLD, TEXT_CHUNK_BYTES;
|
|
3683
3706
|
var init_message_viewer = __esm({
|
|
3684
3707
|
"client/components/message-viewer.js"() {
|
|
3685
3708
|
"use strict";
|
|
@@ -3698,6 +3721,7 @@ var init_message_viewer = __esm({
|
|
|
3698
3721
|
parsedCache = /* @__PURE__ */ new Map();
|
|
3699
3722
|
sessionAllowedRemote = /* @__PURE__ */ new Set();
|
|
3700
3723
|
recentFetchErrors = /* @__PURE__ */ new Map();
|
|
3724
|
+
WAIT_GIVE_UP_SECS = 45;
|
|
3701
3725
|
ZOOM_KEY = "mailx-preview-zoom";
|
|
3702
3726
|
ZOOM_MIN = 0.5;
|
|
3703
3727
|
ZOOM_MAX = 3;
|
|
@@ -9910,8 +9934,14 @@ document.addEventListener("mailx-flagged-batch", (e) => {
|
|
|
9910
9934
|
const n = msgs.length;
|
|
9911
9935
|
const label = n === 1 ? "message" : `${n} messages`;
|
|
9912
9936
|
showContextMenu2(x, y, [
|
|
9913
|
-
|
|
9914
|
-
|
|
9937
|
+
// Name the destination, never say "here". A menu that appears
|
|
9938
|
+
// under the pointer still has to read correctly on its own: by the
|
|
9939
|
+
// time the user is reading the menu, the folder they were dragging
|
|
9940
|
+
// over may not be what they think it is, and "here" gives them
|
|
9941
|
+
// nothing to check against (Bob 2026-08-09: "using HERE in a
|
|
9942
|
+
// message is very very bad — give the folder name").
|
|
9943
|
+
{ label: `Move ${label} to ${targetName}`, emphasized: true, action: () => void runRightDragOp(msgs, targetAccount, targetFolderId, targetName, "move") },
|
|
9944
|
+
{ label: `Copy ${label} to ${targetName}`, action: () => void runRightDragOp(msgs, targetAccount, targetFolderId, targetName, "copy") }
|
|
9915
9945
|
]);
|
|
9916
9946
|
}
|
|
9917
9947
|
async function runRightDragOp(msgs, targetAccount, targetFolderId, targetName, op) {
|