@bobfrankston/rmfmail 1.2.307 → 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 (50) hide show
  1. package/.commitmsg +23 -18
  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 +23 -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 +63 -2
  49. package/packages/mailx-types/trust.js.map +1 -1
  50. package/packages/mailx-types/trust.ts +80 -2
package/.commitmsg CHANGED
@@ -1,20 +1,25 @@
1
- Google Contacts sync: a renamed contact is now renamed in mailx
1
+ Trusted intermediaries: services that relay mail on other people's behalf
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.
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.
13
10
 
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.
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) {