@bobfrankston/rmfmail 1.2.306 → 1.2.308

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 (59) hide show
  1. package/.commitmsg +24 -12
  2. package/client/android-bootstrap.bundle.js +45 -7
  3. package/client/android-bootstrap.bundle.js.map +2 -2
  4. package/client/app.bundle.js +26 -1
  5. package/client/app.bundle.js.map +2 -2
  6. package/client/components/message-viewer.js +36 -0
  7. package/client/components/message-viewer.js.map +1 -1
  8. package/client/components/message-viewer.ts +35 -1
  9. package/client/compose/compose.bundle.js +1 -1
  10. package/client/compose/compose.bundle.js.map +2 -2
  11. package/client/lib/api-client.js +1 -1
  12. package/client/lib/api-client.js.map +1 -1
  13. package/client/lib/api-client.ts +3 -3
  14. package/client/package.json +1 -1
  15. package/docs/allowlist.md +19 -1
  16. package/npmchanges.md +39 -0
  17. package/package.json +8 -5
  18. package/packages/mailx-imap/index.d.ts.map +1 -1
  19. package/packages/mailx-imap/index.js +22 -15
  20. package/packages/mailx-imap/index.js.map +1 -1
  21. package/packages/mailx-imap/index.ts +23 -19
  22. package/packages/mailx-imap/package-lock.json +2 -2
  23. package/packages/mailx-imap/package.json +1 -1
  24. package/packages/mailx-service/index.d.ts +2 -1
  25. package/packages/mailx-service/index.d.ts.map +1 -1
  26. package/packages/mailx-service/index.js +4 -1
  27. package/packages/mailx-service/index.js.map +1 -1
  28. package/packages/mailx-service/index.ts +6 -3
  29. package/packages/mailx-service/package.json +1 -1
  30. package/packages/mailx-settings/docs/allowlist.md +19 -1
  31. package/packages/mailx-settings/docs/rules-design.md +43 -0
  32. package/packages/mailx-settings/index.d.ts +1 -0
  33. package/packages/mailx-settings/index.d.ts.map +1 -1
  34. package/packages/mailx-settings/index.js +4 -0
  35. package/packages/mailx-settings/index.js.map +1 -1
  36. package/packages/mailx-settings/index.ts +4 -0
  37. package/packages/mailx-settings/package.json +1 -1
  38. package/packages/mailx-store/db.d.ts +23 -0
  39. package/packages/mailx-store/db.d.ts.map +1 -1
  40. package/packages/mailx-store/db.js +47 -0
  41. package/packages/mailx-store/db.js.map +1 -1
  42. package/packages/mailx-store/db.ts +52 -0
  43. package/packages/mailx-store/package.json +1 -1
  44. package/packages/mailx-store/store.d.ts.map +1 -1
  45. package/packages/mailx-store/store.js +3 -0
  46. package/packages/mailx-store/store.js.map +1 -1
  47. package/packages/mailx-store/store.ts +3 -0
  48. package/packages/mailx-store-web/package.json +1 -1
  49. package/packages/mailx-store-web/web-service.d.ts +2 -1
  50. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  51. package/packages/mailx-store-web/web-service.js +3 -1
  52. package/packages/mailx-store-web/web-service.js.map +1 -1
  53. package/packages/mailx-store-web/web-service.ts +5 -3
  54. package/packages/mailx-types/package.json +1 -1
  55. package/packages/mailx-types/trust.d.ts +17 -0
  56. package/packages/mailx-types/trust.d.ts.map +1 -1
  57. package/packages/mailx-types/trust.js +63 -2
  58. package/packages/mailx-types/trust.js.map +1 -1
  59. package/packages/mailx-types/trust.ts +80 -2
package/.commitmsg CHANGED
@@ -1,13 +1,25 @@
1
- Pop-out window: Ctrl+wheel and Ctrl+= / Ctrl+- / Ctrl+0 now zoom the window
1
+ Trusted intermediaries: services that relay mail on other people's behalf
2
2
 
3
- "The wheel didn't work in a popped out window. It did work in the main preview
4
- window" (Bob 2026-09-06). The separate-OS-window pop-out (bin/popout-server.ts)
5
- renders the letter in a sandboxed iframe. msger's own zoom handler is injected
6
- into every frame and posts its request over window.ipc, but wry only listens
7
- to the top-level document's WebMessageReceived, so a request posted from the
8
- body frame was silently dropped after the handler had already preventDefault'd
9
- the gesture: no zoom, no error. The body page now forwards Ctrl+wheel and the
10
- zoom keys to the parent page (postMessage `popout-zoom`), which re-dispatches
11
- them as real events on the top-level document so the host applies its zoom.
12
- The page never learns msger's IPC protocol. The in-window overlay pop-out was
13
- already fine (it installs the viewer's own zoom controls).
3
+ "How do I mark zoom and sites like that as trusted intermediaries?" (Bob
4
+ 2026-09-08, on a Google Calendar invitation From the organizer, signed
5
+ d=google.com, every link wrapped as google.com/url?q=us02web.zoom.us). Two
6
+ cautions fired the server's spam verdict (GOOG_REDIR/KAM_GOOGLE_REDIR) and
7
+ "a link hides where it goes" and nothing in the app could answer either:
8
+ the sender is not DMARC-proved, so "stop warning about this sender" was not
9
+ offered, and the redirector rule's escape covers only the proved From.
10
+
11
+ New allowlist.jsonc field `trustedIntermediaries` (e.g. google.com, zoom.us).
12
+ An entry counts only on a message the service PROVABLY handled — a verified
13
+ DKIM signature for its domain in the topmost Authentication-Results, or a
14
+ DMARC pass when it is the From. Then its own redirector is routing, not
15
+ laundering, and the bulk/association spam note is quieted exactly as a
16
+ proved trusted sender quiets it. A message that merely links through
17
+ google.com/url proves nothing and stays flagged; forgery evidence is never
18
+ silenced. Findings now carry `via` (the signer behind the redirector, or the
19
+ signed Sender: domain) and the viewer offers "Trust ‹domain› as an
20
+ intermediary" under the banner; trustSenderOrDomain takes type
21
+ "intermediary" (desktop + Android mirror), getAllowlist returns the list.
22
+ docs/allowlist.md documents trustedSenders/trustedDomains (were missing) and
23
+ the new list. Packages: mailx-types 0.1.83, mailx-store 0.1.115,
24
+ mailx-settings 0.1.77, mailx-service 0.1.34, mailx-store-web 0.1.107,
25
+ client 1.0.46.
@@ -2250,6 +2250,31 @@ function classifyRule(name, dmarcFailed) {
2250
2250
  return "bulk";
2251
2251
  return "other";
2252
2252
  }
2253
+ function dkimSigners(input) {
2254
+ const auth = headerAll(input.headerLines, "authentication-results")[0] || "";
2255
+ const out = [];
2256
+ for (const part of auth.split(";")) {
2257
+ if (!/\bdkim=pass\b/i.test(part))
2258
+ continue;
2259
+ const d = (part.match(/header\.d=([^\s;,]+)/i)?.[1] || "").toLowerCase();
2260
+ if (d && !out.includes(d))
2261
+ out.push(d);
2262
+ }
2263
+ return out;
2264
+ }
2265
+ function provedIntermediary(input) {
2266
+ const list = (input.trustedIntermediaries || []).map((d) => (d || "").trim().toLowerCase()).filter(Boolean);
2267
+ if (!list.length)
2268
+ return "";
2269
+ const proved = dkimSigners(input);
2270
+ const dmarc = dmarcProof(input);
2271
+ if (dmarc.pass && dmarc.domain)
2272
+ proved.push(dmarc.domain);
2273
+ for (const entry of list)
2274
+ if (proved.some((p) => sameOrg(p, entry)))
2275
+ return entry;
2276
+ return "";
2277
+ }
2253
2278
  function dmarcProof(input) {
2254
2279
  const auth = headerAll(input.headerLines, "authentication-results")[0] || "";
2255
2280
  if (!auth)
@@ -2340,7 +2365,9 @@ function analyzeServerSpam(input) {
2340
2365
  provedPolicy: dmarc.policy,
2341
2366
  bayesHam: rules.some((r) => r.name === "BAYES_00" || r.name === "BAYES_01"),
2342
2367
  authFailures: dmarc.authFailures,
2343
- reasons
2368
+ reasons,
2369
+ intermediary: provedIntermediary(input),
2370
+ signers: dkimSigners(input)
2344
2371
  };
2345
2372
  }
2346
2373
  function provedNote(a) {
@@ -2359,9 +2386,12 @@ function serverSpamVerdict(input) {
2359
2386
  const a = analyzeServerSpam(input);
2360
2387
  if (!a || !a.flagged)
2361
2388
  return null;
2362
- const trustedAndProved = input.senderTrusted && a.proved;
2389
+ const trustedAndProved = input.senderTrusted && a.proved || !!a.intermediary;
2363
2390
  if (trustedAndProved && a.kind !== "forgery" && !a.authFailures.length)
2364
2391
  return null;
2392
+ const senderDomain = (bare(header(input.headerLines, "sender")).split("@")[1] || "").toLowerCase();
2393
+ const relay = senderDomain ? a.signers.find((s) => sameOrg(s, senderDomain)) : "";
2394
+ const via = relay && !a.proved ? { via: relay } : {};
2365
2395
  const numbers = Number.isFinite(a.score) && Number.isFinite(a.threshold) ? `SpamAssassin score ${a.score} of ${a.threshold}` : "flagged by SpamAssassin";
2366
2396
  const why = a.reasons.length ? ` \u2014 ${a.reasons.join("; ")}` : "";
2367
2397
  const bayes = a.bayesHam ? "; your own trained filter puts it at 0-1% spam (BAYES_00)" : "";
@@ -2398,7 +2428,8 @@ function serverSpamVerdict(input) {
2398
2428
  id: "server-spam-verdict",
2399
2429
  severity: "caution",
2400
2430
  text: "Your mail server classified this as spam before delivering it.",
2401
- detail: `${proof}${numbers}${why}${bayes}`
2431
+ detail: `${proof}${numbers}${why}${bayes}`,
2432
+ ...via
2402
2433
  };
2403
2434
  }
2404
2435
  function selfSpoof(input) {
@@ -2475,6 +2506,8 @@ function provedSenderDomain(input) {
2475
2506
  function redirectorLink(input) {
2476
2507
  const bodyHtml = input.bodyHtml || "";
2477
2508
  const senderDomain = provedSenderDomain(input);
2509
+ const intermediary = provedIntermediary(input);
2510
+ const signers = dkimSigners(input);
2478
2511
  for (const m of bodyHtml.matchAll(/href\s*=\s*["']([^"']+)["']/gi)) {
2479
2512
  let url;
2480
2513
  try {
@@ -2486,6 +2519,8 @@ function redirectorLink(input) {
2486
2519
  continue;
2487
2520
  if (senderDomain && sameOrg(url.hostname, senderDomain))
2488
2521
  continue;
2522
+ if (intermediary && sameOrg(url.hostname, intermediary))
2523
+ continue;
2489
2524
  for (const [, value] of url.searchParams) {
2490
2525
  let target;
2491
2526
  try {
@@ -2497,11 +2532,13 @@ function redirectorLink(input) {
2497
2532
  continue;
2498
2533
  if (sameOrg(target.hostname, url.hostname))
2499
2534
  continue;
2535
+ const signer = signers.find((s) => sameOrg(s, url.hostname));
2500
2536
  return {
2501
2537
  id: "redirector-link",
2502
2538
  severity: "caution",
2503
2539
  text: `A link hides where it goes: it passes through ${url.hostname} and ends at ${target.hostname}.`,
2504
- detail: `${url.hostname} -> ${target.hostname}`
2540
+ detail: `${url.hostname} -> ${target.hostname}`,
2541
+ ...signer ? { via: signer } : {}
2505
2542
  };
2506
2543
  }
2507
2544
  }
@@ -2564,7 +2601,7 @@ function spamScoreOf(input) {
2564
2601
  proved: a.proved,
2565
2602
  provedBy: a.provedBy,
2566
2603
  bulkOnly: a.bulkOnly,
2567
- trusted: !!input.senderTrusted && a.proved && a.kind !== "forgery" && !a.authFailures.length,
2604
+ trusted: (!!input.senderTrusted && a.proved || !!a.intermediary) && a.kind !== "forgery" && !a.authFailures.length,
2568
2605
  reasons: a.reasons
2569
2606
  };
2570
2607
  }
@@ -7099,7 +7136,7 @@ var WebMailxService = class _WebMailxService {
7099
7136
  return { trusted: false };
7100
7137
  let trusted = false;
7101
7138
  await updateAllowlist((list) => {
7102
- const key = type === "sender" ? "trustedSenders" : "trustedDomains";
7139
+ const key = type === "sender" ? "trustedSenders" : type === "intermediary" ? "trustedIntermediaries" : "trustedDomains";
7103
7140
  const arr = Array.isArray(list[key]) ? list[key] : [];
7104
7141
  const idx = arr.findIndex((x) => (x || "").toLowerCase() === v);
7105
7142
  if (idx >= 0) {
@@ -7132,7 +7169,8 @@ var WebMailxService = class _WebMailxService {
7132
7169
  flaggedSenders: list.flaggedSenders || [],
7133
7170
  flaggedDomains: list.flaggedDomains || [],
7134
7171
  trustedSenders: list.trustedSenders || [],
7135
- trustedDomains: list.trustedDomains || []
7172
+ trustedDomains: list.trustedDomains || [],
7173
+ trustedIntermediaries: list.trustedIntermediaries || []
7136
7174
  };
7137
7175
  }
7138
7176
  async allowRemoteContent(type, value) {