@bobfrankston/rmfmail 1.2.307 → 1.2.309

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 (50) hide show
  1. package/.commitmsg +10 -19
  2. package/client/android-bootstrap.bundle.js +47 -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 +51 -0
  17. package/package.json +8 -5
  18. package/packages/mailx-imap/package-lock.json +2 -2
  19. package/packages/mailx-imap/package.json +1 -1
  20. package/packages/mailx-service/index.d.ts +2 -1
  21. package/packages/mailx-service/index.d.ts.map +1 -1
  22. package/packages/mailx-service/index.js +4 -1
  23. package/packages/mailx-service/index.js.map +1 -1
  24. package/packages/mailx-service/index.ts +6 -3
  25. package/packages/mailx-service/package.json +1 -1
  26. package/packages/mailx-settings/docs/allowlist.md +19 -1
  27. package/packages/mailx-settings/docs/rules-design.md +43 -0
  28. package/packages/mailx-settings/index.d.ts +1 -0
  29. package/packages/mailx-settings/index.d.ts.map +1 -1
  30. package/packages/mailx-settings/index.js +4 -0
  31. package/packages/mailx-settings/index.js.map +1 -1
  32. package/packages/mailx-settings/index.ts +4 -0
  33. package/packages/mailx-settings/package.json +1 -1
  34. package/packages/mailx-store/package.json +1 -1
  35. package/packages/mailx-store/store.d.ts.map +1 -1
  36. package/packages/mailx-store/store.js +3 -0
  37. package/packages/mailx-store/store.js.map +1 -1
  38. package/packages/mailx-store/store.ts +3 -0
  39. package/packages/mailx-store-web/package.json +1 -1
  40. package/packages/mailx-store-web/web-service.d.ts +2 -1
  41. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  42. package/packages/mailx-store-web/web-service.js +3 -1
  43. package/packages/mailx-store-web/web-service.js.map +1 -1
  44. package/packages/mailx-store-web/web-service.ts +5 -3
  45. package/packages/mailx-types/package.json +1 -1
  46. package/packages/mailx-types/trust.d.ts +17 -0
  47. package/packages/mailx-types/trust.d.ts.map +1 -1
  48. package/packages/mailx-types/trust.js +76 -4
  49. package/packages/mailx-types/trust.js.map +1 -1
  50. package/packages/mailx-types/trust.ts +93 -4
package/.commitmsg CHANGED
@@ -1,20 +1,11 @@
1
- Google Contacts sync: a renamed contact is now renamed in mailx
1
+ Trust banner names the intermediary and the action that trusts it
2
2
 
3
- "When I gave a name 'Tyler Nichols' why didn't it match 'tyler k nichols'?"
4
- (Bob 2026-09-06). The matcher was fine: every typed word must appear in the
5
- name or address. The Gmail row for that contact was stored under its OLD
6
- Google name "T Nichols", and the People sync never wrote `name` on a google
7
- row, so a contact renamed in Google kept its first-seen name here forever.
8
- Worse, the sync's UPDATE ... WHERE email = ? also tried to convert the
9
- discovered duplicate into a second google row with the same (source, email,
10
- name) and hit the UNIQUE constraint, so it failed outright 4,505
11
- "[contacts] field update failed" lines in one day's log, meaning no contact
12
- with any mail traffic ever received its card fields either.
13
-
14
- New MailxDB.upsertGoogleContact(googleId, name, email, card) owns the google
15
- row: name and card follow Google on every sync, discovered rows are left
16
- alone, and stray duplicate google rows for one address are collapsed. The
17
- People sync calls it instead of recordSentAddress + inline SQL (which also
18
- inflated use_count on every full sync). A one-time full resync per account
19
- (kv contacts/resync:<account>) refreshes the names already stored stale.
20
- Packages: mailx-store 0.1.113, mailx-imap 0.1.174.
3
+ "Is there a 'trust this intermediary' operation or do I need to edit it
4
+ manually? If so the banner should tell which and how" (Bob 2026-09-08). The
5
+ button existed since v1.2.308 but the finding text did not mention it. The
6
+ spam-verdict note now ends with: Sent on the author's behalf by google.com,
7
+ which signed it; "Trust google.com as an intermediary" stops this note for
8
+ mail google.com signs. The redirector warning ends the same way, naming the
9
+ signer or, when nobody signed for the redirector host, says plainly that
10
+ there is no intermediary to trust and only the sender could vouch for the
11
+ link. mailx-types 0.1.85.
@@ -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,13 @@ 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 } : {};
2395
+ const relayNote = relay && !a.proved ? `. Sent on the author's behalf by ${relay}, which signed it; "Trust ${relay} as an intermediary" stops this note for mail ${relay} signs.` : "";
2365
2396
  const numbers = Number.isFinite(a.score) && Number.isFinite(a.threshold) ? `SpamAssassin score ${a.score} of ${a.threshold}` : "flagged by SpamAssassin";
2366
2397
  const why = a.reasons.length ? ` \u2014 ${a.reasons.join("; ")}` : "";
2367
2398
  const bayes = a.bayesHam ? "; your own trained filter puts it at 0-1% spam (BAYES_00)" : "";
@@ -2398,7 +2429,8 @@ function serverSpamVerdict(input) {
2398
2429
  id: "server-spam-verdict",
2399
2430
  severity: "caution",
2400
2431
  text: "Your mail server classified this as spam before delivering it.",
2401
- detail: `${proof}${numbers}${why}${bayes}`
2432
+ detail: `${proof}${numbers}${why}${bayes}${relayNote}`,
2433
+ ...via
2402
2434
  };
2403
2435
  }
2404
2436
  function selfSpoof(input) {
@@ -2475,6 +2507,8 @@ function provedSenderDomain(input) {
2475
2507
  function redirectorLink(input) {
2476
2508
  const bodyHtml = input.bodyHtml || "";
2477
2509
  const senderDomain = provedSenderDomain(input);
2510
+ const intermediary = provedIntermediary(input);
2511
+ const signers = dkimSigners(input);
2478
2512
  for (const m of bodyHtml.matchAll(/href\s*=\s*["']([^"']+)["']/gi)) {
2479
2513
  let url;
2480
2514
  try {
@@ -2486,6 +2520,8 @@ function redirectorLink(input) {
2486
2520
  continue;
2487
2521
  if (senderDomain && sameOrg(url.hostname, senderDomain))
2488
2522
  continue;
2523
+ if (intermediary && sameOrg(url.hostname, intermediary))
2524
+ continue;
2489
2525
  for (const [, value] of url.searchParams) {
2490
2526
  let target;
2491
2527
  try {
@@ -2497,11 +2533,14 @@ function redirectorLink(input) {
2497
2533
  continue;
2498
2534
  if (sameOrg(target.hostname, url.hostname))
2499
2535
  continue;
2536
+ const signer = signers.find((s) => sameOrg(s, url.hostname));
2537
+ const how = signer ? ` ${signer} signed this message; "Trust ${signer} as an intermediary" stops this warning for links through ${signer} in mail it signs.` : ` Nobody signed this message for ${url.hostname}, so there is no intermediary to trust; only the sender could vouch for the link.`;
2500
2538
  return {
2501
2539
  id: "redirector-link",
2502
2540
  severity: "caution",
2503
2541
  text: `A link hides where it goes: it passes through ${url.hostname} and ends at ${target.hostname}.`,
2504
- detail: `${url.hostname} -> ${target.hostname}`
2542
+ detail: `${url.hostname} -> ${target.hostname}.${how}`,
2543
+ ...signer ? { via: signer } : {}
2505
2544
  };
2506
2545
  }
2507
2546
  }
@@ -2564,7 +2603,7 @@ function spamScoreOf(input) {
2564
2603
  proved: a.proved,
2565
2604
  provedBy: a.provedBy,
2566
2605
  bulkOnly: a.bulkOnly,
2567
- trusted: !!input.senderTrusted && a.proved && a.kind !== "forgery" && !a.authFailures.length,
2606
+ trusted: (!!input.senderTrusted && a.proved || !!a.intermediary) && a.kind !== "forgery" && !a.authFailures.length,
2568
2607
  reasons: a.reasons
2569
2608
  };
2570
2609
  }
@@ -7099,7 +7138,7 @@ var WebMailxService = class _WebMailxService {
7099
7138
  return { trusted: false };
7100
7139
  let trusted = false;
7101
7140
  await updateAllowlist((list) => {
7102
- const key = type === "sender" ? "trustedSenders" : "trustedDomains";
7141
+ const key = type === "sender" ? "trustedSenders" : type === "intermediary" ? "trustedIntermediaries" : "trustedDomains";
7103
7142
  const arr = Array.isArray(list[key]) ? list[key] : [];
7104
7143
  const idx = arr.findIndex((x) => (x || "").toLowerCase() === v);
7105
7144
  if (idx >= 0) {
@@ -7132,7 +7171,8 @@ var WebMailxService = class _WebMailxService {
7132
7171
  flaggedSenders: list.flaggedSenders || [],
7133
7172
  flaggedDomains: list.flaggedDomains || [],
7134
7173
  trustedSenders: list.trustedSenders || [],
7135
- trustedDomains: list.trustedDomains || []
7174
+ trustedDomains: list.trustedDomains || [],
7175
+ trustedIntermediaries: list.trustedIntermediaries || []
7136
7176
  };
7137
7177
  }
7138
7178
  async allowRemoteContent(type, value) {