@bobfrankston/rmfmail 1.2.272 → 1.2.276

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 (63) hide show
  1. package/.commitmsg +26 -0
  2. package/client/android-bootstrap.bundle.js +184 -0
  3. package/client/android-bootstrap.bundle.js.map +2 -2
  4. package/client/app.bundle.js +235 -31
  5. package/client/app.bundle.js.map +4 -4
  6. package/client/app.js +35 -1
  7. package/client/app.js.map +1 -1
  8. package/client/app.ts +36 -1
  9. package/client/components/invite-card.js +107 -0
  10. package/client/components/invite-card.js.map +1 -0
  11. package/client/components/invite-card.ts +119 -0
  12. package/client/components/message-list.js +9 -1
  13. package/client/components/message-list.js.map +1 -1
  14. package/client/components/message-list.ts +9 -1
  15. package/client/components/message-viewer.js +82 -19
  16. package/client/components/message-viewer.js.map +1 -1
  17. package/client/components/message-viewer.ts +79 -18
  18. package/client/compose/compose.bundle.js +4 -0
  19. package/client/compose/compose.bundle.js.map +2 -2
  20. package/client/lib/api-client.js +6 -0
  21. package/client/lib/api-client.js.map +1 -1
  22. package/client/lib/api-client.ts +6 -0
  23. package/client/lib/approved-senders.js +66 -0
  24. package/client/lib/approved-senders.js.map +1 -0
  25. package/client/lib/approved-senders.ts +63 -0
  26. package/client/lib/mailxapi.js +3 -0
  27. package/client/package.json +1 -1
  28. package/client/styles/components.css +48 -0
  29. package/package.json +10 -10
  30. package/packages/mailx-imap/package-lock.json +2 -2
  31. package/packages/mailx-imap/package.json +2 -2
  32. package/packages/mailx-service/index.d.ts +15 -0
  33. package/packages/mailx-service/index.d.ts.map +1 -1
  34. package/packages/mailx-service/index.js +18 -0
  35. package/packages/mailx-service/index.js.map +1 -1
  36. package/packages/mailx-service/index.ts +19 -0
  37. package/packages/mailx-service/jsonrpc.js +2 -0
  38. package/packages/mailx-service/jsonrpc.js.map +1 -1
  39. package/packages/mailx-service/jsonrpc.ts +2 -0
  40. package/packages/mailx-service/package.json +1 -1
  41. package/packages/mailx-settings/package.json +1 -1
  42. package/packages/mailx-store/package.json +1 -1
  43. package/packages/mailx-store/store.d.ts +5 -0
  44. package/packages/mailx-store/store.d.ts.map +1 -1
  45. package/packages/mailx-store/store.js +21 -0
  46. package/packages/mailx-store/store.js.map +1 -1
  47. package/packages/mailx-store/store.ts +27 -0
  48. package/packages/mailx-store-web/android-bootstrap.js +1 -0
  49. package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
  50. package/packages/mailx-store-web/android-bootstrap.ts +1 -0
  51. package/packages/mailx-store-web/package.json +1 -1
  52. package/packages/mailx-store-web/web-service.d.ts +11 -0
  53. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  54. package/packages/mailx-store-web/web-service.js +14 -0
  55. package/packages/mailx-store-web/web-service.js.map +1 -1
  56. package/packages/mailx-store-web/web-service.ts +15 -0
  57. package/packages/mailx-types/index.d.ts +56 -0
  58. package/packages/mailx-types/index.d.ts.map +1 -1
  59. package/packages/mailx-types/index.js +187 -0
  60. package/packages/mailx-types/index.js.map +1 -1
  61. package/packages/mailx-types/index.ts +192 -0
  62. package/packages/mailx-types/package.json +1 -1
  63. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-72180 → node_modules.npmglobalize-stash-59216}/.package-lock.json +0 -0
@@ -10,12 +10,14 @@
10
10
  */
11
11
 
12
12
  import { getMessage, updateFlags, allowRemoteContent, flagSenderOrDomain, getAttachment, openAttachment, getMessageSource, addContact, listContacts, upsertContact, unsubscribeOneClick, addPreferredContact, onEvent, subscribeStore } from "../lib/api-client.js";
13
+ import { isSenderApproved, noteApprovedLocally, refreshApprovedSenders } from "../lib/approved-senders.js";
14
+ import { buildInviteCard } from "./invite-card.js";
13
15
  import { showContextMenu } from "./context-menu.js";
14
16
  import type { MenuItem } from "./context-menu.js";
15
17
  import type { ListMessage } from "../lib/message-state.js";
16
18
  import { updateMessageFlags as stateUpdateFlags } from "../lib/message-state.js";
17
19
  import { setRowSeen } from "./message-list.js";
18
- import { setSeen, seenOf, formatSender } from "@bobfrankston/mailx-types";
20
+ import { setSeen, seenOf, formatSender, displayNameClaimsOtherAddress } from "@bobfrankston/mailx-types";
19
21
 
20
22
  // ── "Copy image" bridge ──
21
23
  // The preview iframe encodes the image and posts the bytes back; these track
@@ -715,22 +717,27 @@ function installPreviewControls(iframe: HTMLIFrameElement): void {
715
717
  // don't preventDefault — the browser's native select-all inside
716
718
  // the iframe selects exactly this document.
717
719
  if (e.ctrlKey && !e.altKey && !e.metaKey && (e.key === "a" || e.key === "A")) return;
718
- // Forward EVERY keydown to the parent no duplicated hotkey list.
719
- // If the parent's handler calls preventDefault (because it owns the
720
- // shortcut), dispatchEvent returns false, and we preventDefault on
721
- // the iframe side too so the browser doesn't ALSO act on it
722
- // (Ctrl+N otherwise pops a new browser window in some hosts).
723
- // Single source of truth = app.ts hotkey handlers. Plain typing in
724
- // the email body — letters, etc. propagates with no parent
725
- // handler matching, so dispatchEvent returns true and the iframe
726
- // event is left alone.
727
- const synth = new KeyboardEvent("keydown", {
728
- key: e.key, code: e.code,
729
- ctrlKey: e.ctrlKey, shiftKey: e.shiftKey, altKey: e.altKey, metaKey: e.metaKey,
730
- bubbles: true, cancelable: true,
731
- });
732
- const allowDefault = document.dispatchEvent(synth);
733
- if (!allowDefault) e.preventDefault();
720
+ // Everything else is FORWARDED BY THE IFRAME ITSELF the inline
721
+ // script wrapHtmlBody installs posts every keydown to the parent
722
+ // as `previewKey`, and app.ts re-dispatches it on document. This
723
+ // listener must NOT forward as well.
724
+ //
725
+ // It used to, and both routes are live on desktop WebView2 (the
726
+ // srcdoc document is same-origin, so this contentDocument listener
727
+ // attaches, and the inline script runs too). One Ctrl+F in the
728
+ // preview therefore reached app.ts TWICE and opened TWO compose
729
+ // windows one of which never finished loading and sat blank and
730
+ // "(Not Responding)" until it was killed (Bob 2026-08-23,
731
+ // "Compose: Fwd: FirstCare Slide Deck (Not Responding)"; daemon
732
+ // log shows openCompose-entry mode=forward twice in the same
733
+ // millisecond and two popoutCompose windows). Ctrl+D / Ctrl+P
734
+ // doubled the same way.
735
+ //
736
+ // The inline forwarder is the one that survives, because it is the
737
+ // only one that works on Android WebView, where parent-side
738
+ // contentDocument listeners never fire. Zoom and Ctrl+A stay here:
739
+ // they act on THIS document and the inline script deliberately
740
+ // skips them. (Claude Code 2026-08-23)
734
741
  });
735
742
 
736
743
  doc.addEventListener("wheel", (e) => {
@@ -788,6 +795,10 @@ export function clearViewer(): void {
788
795
  if (bodyEl) bodyEl.innerHTML = `<div class="mv-empty">Select a message to read</div>`;
789
796
  if (headerEl) headerEl.hidden = true;
790
797
  if (attEl) attEl.hidden = true;
798
+ // The invite card is a SIBLING of the body (it sits after the header), so
799
+ // clearing bodyEl leaves it behind — a stale meeting hovering over "Select
800
+ // a message to read" is exactly the viewer-must-clear failure.
801
+ document.getElementById("mv-invite")?.remove();
791
802
  // Divergence watchdog: highlight and viewer are supposed to be one
792
803
  // thing. If a second after a clear some row still shows .selected while
793
804
  // the pane shows the placeholder, that's the "message selected but it's
@@ -1215,6 +1226,22 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1215
1226
  fromEl.textContent = formatAddr(msg.from);
1216
1227
  setRecipientLine(toEl, msg.to, msg.cc, msg.deliveredTo);
1217
1228
  headerEl.querySelector(".mv-subject")!.textContent = msg.subject;
1229
+
1230
+ // Calendar invite card. Lives BETWEEN the header and the body as its
1231
+ // own element, not inside .mv-body — the body's innerHTML is replaced
1232
+ // wholesale on every render, which would take the card with it.
1233
+ // Removed and rebuilt per message so a non-invite never inherits the
1234
+ // previous one (the "viewer must clear" rule).
1235
+ document.getElementById("mv-invite")?.remove();
1236
+ try {
1237
+ const card = buildInviteCard((msg as any).invite, String(msg.bodyHtml || ""));
1238
+ if (card) {
1239
+ card.id = "mv-invite";
1240
+ headerEl.insertAdjacentElement("afterend", card);
1241
+ }
1242
+ } catch (e: any) {
1243
+ console.warn("[viewer] invite card failed:", e?.message || e);
1244
+ }
1218
1245
  document.dispatchEvent(new CustomEvent("mailx-message-shown", { detail: { accountId } }));
1219
1246
 
1220
1247
  // Right-click on email addresses in header: copy name, copy address,
@@ -1312,6 +1339,39 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1312
1339
  });
1313
1340
  items.push({ label: "", action: () => {}, separator: true });
1314
1341
  }
1342
+ // "Mark as valid" — the display-name warning is only useful if
1343
+ // it can be silenced per sender. Reuses the shared allowlist
1344
+ // (same senders/domains that gate remote images), so one act of
1345
+ // trust covers both (Bob 2026-08-20: "another variation on
1346
+ // approved senders. If a message is flagged I should be able to
1347
+ // mark it as valid").
1348
+ if (el === fromEl && msg.from?.address) {
1349
+ const addr = String(msg.from.address);
1350
+ const dom = addr.split("@").pop() || "";
1351
+ const claimed = displayNameClaimsOtherAddress(String(msg.from.name || ""), addr);
1352
+ if (claimed && !isSenderApproved(addr)) {
1353
+ const approve = async (type: "sender" | "domain", value: string) => {
1354
+ noteApprovedLocally(type, value); // optimistic — row updates now
1355
+ try {
1356
+ await allowRemoteContent(type, value);
1357
+ await refreshApprovedSenders();
1358
+ document.dispatchEvent(new CustomEvent("mailx-allowlist-changed"));
1359
+ const st = document.getElementById("status-sync");
1360
+ if (st) st.textContent = `Marked valid: ${value}`;
1361
+ } catch (e: any) {
1362
+ const st = document.getElementById("status-sync");
1363
+ if (st) {
1364
+ st.textContent = `Couldn't save "${value}" to the allowlist — ${e?.message || e}`;
1365
+ st.style.color = "oklch(0.65 0.2 25)";
1366
+ }
1367
+ console.error("[allowlist] approve failed:", e);
1368
+ }
1369
+ };
1370
+ items.push({ label: `✓ Mark ${addr} as a valid sender`, action: () => void approve("sender", addr) });
1371
+ if (dom) items.push({ label: `✓ Trust all senders at ${dom}`, action: () => void approve("domain", dom) });
1372
+ items.push({ label: "", action: () => {}, separator: true });
1373
+ }
1374
+ }
1315
1375
  if (omitted > 0) {
1316
1376
  items.push({
1317
1377
  label: `… ${omitted} more recipients — copy the whole list`,
@@ -2146,7 +2206,8 @@ function formatAddr(addr: { name: string; address: string }): string {
2146
2206
  // 2026-08-19) and reports when a display name asserts a DIFFERENT address
2147
2207
  // than the envelope's, which is the display-name spoof.
2148
2208
  const { text, claimed } = formatSender(addr);
2149
- if (!claimed) return text;
2209
+ // Approved senders are never called out — see approved-senders.ts.
2210
+ if (!claimed || isSenderApproved(addr.address)) return text;
2150
2211
  // Never let the claim stand unqualified: name the real address explicitly.
2151
2212
  return `${text} — display name claims ${claimed}`;
2152
2213
  }
@@ -1053,6 +1053,7 @@ __export(api_client_exports, {
1053
1053
  flagSenderOrDomain: () => flagSenderOrDomain,
1054
1054
  formatJsonc: () => formatJsonc,
1055
1055
  getAccounts: () => getAccounts,
1056
+ getAllowlist: () => getAllowlist,
1056
1057
  getAttachment: () => getAttachment,
1057
1058
  getAutocompleteSettings: () => getAutocompleteSettings,
1058
1059
  getCalendarEvents: () => getCalendarEvents,
@@ -1347,6 +1348,9 @@ function addUserDictWords(words) {
1347
1348
  function removeUserDictWord(word) {
1348
1349
  return ipc().removeUserDictWord?.(word) ?? Promise.resolve([]);
1349
1350
  }
1351
+ function getAllowlist() {
1352
+ return ipc().getAllowlist?.() ?? Promise.resolve({ senders: [], domains: [], recipients: [], flaggedSenders: [], flaggedDomains: [] });
1353
+ }
1350
1354
  function flagSenderOrDomain(type, value) {
1351
1355
  return ipc().flagSenderOrDomain?.(type, value) ?? Promise.resolve({ flagged: false });
1352
1356
  }