@bobfrankston/rmfmail 1.2.310 → 1.2.313

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 +21 -12
  2. package/.llm/trusted-lists.md +49 -0
  3. package/TODO.md +2 -2
  4. package/client/android-bootstrap.bundle.js +78 -15
  5. package/client/android-bootstrap.bundle.js.map +2 -2
  6. package/client/app.bundle.js +84 -2
  7. package/client/app.bundle.js.map +2 -2
  8. package/client/components/message-viewer.js +128 -1
  9. package/client/components/message-viewer.js.map +1 -1
  10. package/client/components/message-viewer.ts +123 -2
  11. package/client/compose/compose.bundle.js +1 -1
  12. package/client/compose/compose.bundle.js.map +2 -2
  13. package/client/lib/api-client.js +1 -1
  14. package/client/lib/api-client.js.map +1 -1
  15. package/client/lib/api-client.ts +3 -3
  16. package/client/styles/components.css +3 -0
  17. package/docs/allowlist.md +28 -3
  18. package/docs/spam-false-positives.md +23 -0
  19. package/npmchanges.md +58 -0
  20. package/package.json +5 -5
  21. package/packages/mailx-imap/package-lock.json +2 -2
  22. package/packages/mailx-imap/package.json +1 -1
  23. package/packages/mailx-service/index.d.ts +3 -1
  24. package/packages/mailx-service/index.d.ts.map +1 -1
  25. package/packages/mailx-service/index.js +4 -2
  26. package/packages/mailx-service/index.js.map +1 -1
  27. package/packages/mailx-service/index.ts +6 -4
  28. package/packages/mailx-service/package.json +1 -1
  29. package/packages/mailx-settings/docs/allowlist.md +28 -3
  30. package/packages/mailx-settings/docs/spam-false-positives.md +23 -0
  31. package/packages/mailx-settings/index.d.ts +1 -0
  32. package/packages/mailx-settings/index.d.ts.map +1 -1
  33. package/packages/mailx-settings/index.js +7 -0
  34. package/packages/mailx-settings/index.js.map +1 -1
  35. package/packages/mailx-settings/index.ts +7 -0
  36. package/packages/mailx-settings/package.json +1 -1
  37. package/packages/mailx-store/package.json +1 -1
  38. package/packages/mailx-store/store.d.ts.map +1 -1
  39. package/packages/mailx-store/store.js +3 -0
  40. package/packages/mailx-store/store.js.map +1 -1
  41. package/packages/mailx-store/store.ts +3 -0
  42. package/packages/mailx-store-web/package.json +1 -1
  43. package/packages/mailx-store-web/web-service.d.ts +3 -2
  44. package/packages/mailx-store-web/web-service.d.ts.map +1 -1
  45. package/packages/mailx-store-web/web-service.js +4 -2
  46. package/packages/mailx-store-web/web-service.js.map +1 -1
  47. package/packages/mailx-store-web/web-service.ts +6 -4
  48. package/packages/mailx-store-web/web-settings.d.ts +1 -0
  49. package/packages/mailx-store-web/web-settings.d.ts.map +1 -1
  50. package/packages/mailx-store-web/web-settings.js +2 -0
  51. package/packages/mailx-store-web/web-settings.js.map +1 -1
  52. package/packages/mailx-store-web/web-settings.ts +2 -0
  53. package/packages/mailx-types/index.d.ts +2 -2
  54. package/packages/mailx-types/index.d.ts.map +1 -1
  55. package/packages/mailx-types/index.js +1 -1
  56. package/packages/mailx-types/index.js.map +1 -1
  57. package/packages/mailx-types/index.ts +2 -2
  58. package/packages/mailx-types/package.json +1 -1
  59. package/packages/mailx-types/trust.d.ts +35 -0
  60. package/packages/mailx-types/trust.d.ts.map +1 -1
  61. package/packages/mailx-types/trust.js +115 -8
  62. package/packages/mailx-types/trust.js.map +1 -1
  63. package/packages/mailx-types/trust.ts +139 -8
@@ -1371,6 +1371,57 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1371
1371
  if (dom) items.push({ label: `✓ Trust all senders at ${dom}`, action: () => void approve("domain", dom) });
1372
1372
  items.push({ label: "", action: () => {}, separator: true });
1373
1373
  }
1374
+ // "Trust the ‹list› mailing list" (Bob 2026-09-09: "how do I
1375
+ // say that this is a valid mailing list message"). A list
1376
+ // that keeps the poster's From — MIT's Mailman, for a Gmail
1377
+ // author — leaves nothing per-sender to trust: the author
1378
+ // is unproved on every post, by the list's own doing. The
1379
+ // LIST is proved (its host passed SPF for the bounce
1380
+ // address), so the list is what the reader vouches for.
1381
+ // Offered from the chip's own advice, which is the only
1382
+ // thing an unflagged list post shows; the banner has its
1383
+ // own button.
1384
+ //
1385
+ // Offered on every proved post, trusted or not: the write
1386
+ // is a toggle, and once the list is trusted the chip and
1387
+ // banner are gone, so this menu is the only place an
1388
+ // approval can be undone without editing allowlist.jsonc
1389
+ // by hand (Bob 2026-09-09: "how would I undo an approval").
1390
+ const listSpam = (msg as any).spamScore as { listId?: string; listProved?: boolean; listTrusted?: boolean };
1391
+ if (listSpam?.listProved && listSpam.listId) {
1392
+ const listId = listSpam.listId;
1393
+ const untrust = !!listSpam.listTrusted;
1394
+ items.push({
1395
+ label: untrust ? `✗ Stop trusting the ${listId} mailing list` : `✓ Trust the ${listId} mailing list`,
1396
+ action: async () => {
1397
+ const st = document.getElementById("status-sync");
1398
+ try {
1399
+ // A toggle: the answer says which way it went,
1400
+ // and it must match what was asked for — a
1401
+ // mismatch means the write did not land.
1402
+ const res = await trustSenderOrDomain("list", listId);
1403
+ if (!res || !!res.trusted === untrust) throw new Error("the list did not save");
1404
+ listSpam.listTrusted = res.trusted;
1405
+ if (res.trusted) {
1406
+ document.querySelector(".mv-spamscore")?.remove();
1407
+ document.querySelector(".mv-spamscore-action")?.remove();
1408
+ document.querySelector(".mv-trust")?.remove();
1409
+ }
1410
+ if (st) st.textContent = res.trusted
1411
+ ? `Trusting the ${listId} mailing list`
1412
+ : `No longer trusting the ${listId} mailing list — reopen the message to see its score again`;
1413
+ } catch (e: any) {
1414
+ // The reader chose an action and must know whether it took.
1415
+ if (st) {
1416
+ st.textContent = `Couldn't trust the ${listId} list — ${e?.message || e}`;
1417
+ st.style.color = "oklch(0.65 0.2 25)";
1418
+ }
1419
+ console.error("[allowlist] trust list failed:", e);
1420
+ }
1421
+ },
1422
+ });
1423
+ items.push({ label: "", action: () => {}, separator: true });
1424
+ }
1374
1425
  }
1375
1426
  if (omitted > 0) {
1376
1427
  items.push({
@@ -1678,6 +1729,7 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1678
1729
  score: number; threshold: number; flagged: boolean;
1679
1730
  kind?: string; proved?: boolean; provedBy?: string; bulkOnly?: boolean;
1680
1731
  reasons?: string[]; trusted?: boolean;
1732
+ listId?: string; listProved?: boolean; listTrusted?: boolean;
1681
1733
  };
1682
1734
  // A sender the reader has approved, whose identity is proved: the chip
1683
1735
  // goes with the banner. Repeating the score after the reader has said
@@ -1715,12 +1767,49 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1715
1767
  : "No forgery rule scored. The rules that did are listed below.")
1716
1768
  : level === "high"
1717
1769
  ? "Your mail server scored this at or above its spam threshold and flagged it."
1718
- : "Below your server's spam threshold, but more than halfway to it.") + why;
1770
+ : "Below your server's spam threshold, but more than halfway to it.") + why
1771
+ // The list that provably delivered this, and where the action
1772
+ // lives — the chip is the only thing an unflagged list post
1773
+ // shows, so it has to say how to make it stop (2026-09-09).
1774
+ + (spam.listProved && spam.listId && !spam.listTrusted
1775
+ ? `\n\nDelivered by the ${spam.listId} mailing list, which your server proved sent it. Right-click the From address to trust the list.`
1776
+ : "");
1719
1777
  chip.textContent = `spam score ${spam.score} of ${spam.threshold}`;
1720
1778
  bodyEl.appendChild(chip);
1779
+ // The action beside the warning it answers (Bob 2026-09-09: "it
1780
+ // would be nice to have an [approve this list] on the red warning
1781
+ // banner"). An unflagged list post has no banner — the chip IS
1782
+ // the warning — so the button sits next to it. Same write as the
1783
+ // From-menu item and the banner button: trustedLists by List-Id.
1784
+ if (spam.listProved && spam.listId && !spam.listTrusted) {
1785
+ const listId = spam.listId;
1786
+ const b = document.createElement("button");
1787
+ b.className = "mv-trust-action mv-spamscore-action";
1788
+ b.textContent = `Trust the ${listId} list`;
1789
+ b.title = `Delivered by the ${listId} mailing list, which your server proved sent it. Trusting the list hides this score on every post the list delivers.`;
1790
+ b.addEventListener("click", async () => {
1791
+ b.disabled = true;
1792
+ try {
1793
+ // Only ever ADDS, so anything but `trusted: true` means
1794
+ // the write did not land — never hide the chip on a
1795
+ // failed save.
1796
+ const res = await trustSenderOrDomain("list", listId);
1797
+ if (!res?.trusted) throw new Error("the list did not save");
1798
+ spam.listTrusted = true;
1799
+ chip.remove();
1800
+ b.remove();
1801
+ document.querySelector(".mv-trust")?.remove();
1802
+ } catch (e: any) {
1803
+ // The reader pressed a button and must know whether it took.
1804
+ b.disabled = false;
1805
+ b.textContent = `Could not save: ${e?.message || e}`;
1806
+ }
1807
+ });
1808
+ bodyEl.appendChild(b);
1809
+ }
1721
1810
  }
1722
1811
 
1723
- const trust = ((msg as any).trust || []) as { id: string; severity: string; text: string; detail: string; via?: string }[];
1812
+ const trust = ((msg as any).trust || []) as { id: string; severity: string; text: string; detail: string; via?: string; list?: string }[];
1724
1813
  if (trust.length) {
1725
1814
  // An `info` finding is not an accusation, so it gets neither the
1726
1815
  // headline nor the "do not open anything" footer — it renders as a
@@ -1796,6 +1885,38 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1796
1885
  }
1797
1886
  warn.appendChild(actions);
1798
1887
  }
1888
+ // "Trust the ‹list› list" (Bob 2026-09-09: "how do I say that this
1889
+ // is a valid mailing list message"). A finding carries `list` only
1890
+ // when the receiving server proved the list's host sent the
1891
+ // message and the list is not yet trusted; the entry it writes
1892
+ // (`trustedLists`, keyed on List-Id) counts only on such mail.
1893
+ // Not on a danger banner, for the same reason as every other
1894
+ // button here.
1895
+ const lists = worst === "danger" ? [] : Array.from(new Set(trust.map(f => f.list).filter(Boolean)));
1896
+ if (lists.length) {
1897
+ const actions = document.createElement("div");
1898
+ actions.className = "mv-trust-actions";
1899
+ for (const listId of lists) {
1900
+ const b = document.createElement("button");
1901
+ b.className = "mv-trust-action";
1902
+ b.textContent = `Trust the ${listId} list`;
1903
+ b.addEventListener("click", async () => {
1904
+ b.disabled = true;
1905
+ try {
1906
+ const res = await trustSenderOrDomain("list", listId);
1907
+ if (!res?.trusted) throw new Error("the list did not save");
1908
+ warn.remove();
1909
+ document.querySelector(".mv-spamscore")?.remove();
1910
+ } catch (e: any) {
1911
+ // The reader pressed a button and must know whether it took.
1912
+ b.disabled = false;
1913
+ b.textContent = `Could not save: ${e?.message || e}`;
1914
+ }
1915
+ });
1916
+ actions.appendChild(b);
1917
+ }
1918
+ warn.appendChild(actions);
1919
+ }
1799
1920
  // "Trust ‹service› as an intermediary" (Bob 2026-09-08: "how do I
1800
1921
  // mark zoom and sites like that as trusted intermediaries?"). A
1801
1922
  // finding carries `via` only when a party PROVABLY handled the
@@ -1353,7 +1353,7 @@ function trustSenderOrDomain(type, value) {
1353
1353
  return ipc().trustSenderOrDomain?.(type, value) ?? Promise.resolve({ trusted: false });
1354
1354
  }
1355
1355
  function getAllowlist() {
1356
- return ipc().getAllowlist?.() ?? Promise.resolve({ senders: [], domains: [], recipients: [], flaggedSenders: [], flaggedDomains: [], trustedSenders: [], trustedDomains: [], trustedIntermediaries: [] });
1356
+ return ipc().getAllowlist?.() ?? Promise.resolve({ senders: [], domains: [], recipients: [], flaggedSenders: [], flaggedDomains: [], trustedSenders: [], trustedDomains: [], trustedIntermediaries: [], trustedLists: [] });
1357
1357
  }
1358
1358
  function flagSenderOrDomain(type, value) {
1359
1359
  return ipc().flagSenderOrDomain?.(type, value) ?? Promise.resolve({ flagged: false });