@bobfrankston/rmfmail 1.2.285 → 1.2.287

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 (40) hide show
  1. package/.commitmsg +28 -32
  2. package/TODO.md +2 -0
  3. package/client/android-bootstrap.bundle.js +202 -19
  4. package/client/android-bootstrap.bundle.js.map +2 -2
  5. package/client/app.bundle.js +7 -4
  6. package/client/app.bundle.js.map +2 -2
  7. package/client/components/message-viewer.js +32 -9
  8. package/client/components/message-viewer.js.map +1 -1
  9. package/client/components/message-viewer.ts +36 -10
  10. package/client/compose/compose.bundle.js +106 -38
  11. package/client/compose/compose.bundle.js.map +2 -2
  12. package/client/compose/harper-lint.js +125 -42
  13. package/client/compose/harper-lint.js.map +1 -1
  14. package/client/compose/harper-lint.ts +120 -40
  15. package/client/styles/components.css +9 -0
  16. package/docs/spam-false-positives.md +208 -0
  17. package/npmchanges.md +86 -0
  18. package/package.json +6 -6
  19. package/packages/mailx-imap/package-lock.json +2 -2
  20. package/packages/mailx-imap/package.json +1 -1
  21. package/packages/mailx-service/index.d.ts.map +1 -1
  22. package/packages/mailx-service/index.js +20 -3
  23. package/packages/mailx-service/index.js.map +1 -1
  24. package/packages/mailx-service/index.ts +20 -3
  25. package/packages/mailx-service/package.json +1 -1
  26. package/packages/mailx-settings/docs/spam-false-positives.md +208 -0
  27. package/packages/mailx-settings/package.json +1 -1
  28. package/packages/mailx-store/package.json +1 -1
  29. package/packages/mailx-store/store.d.ts.map +1 -1
  30. package/packages/mailx-store/store.js +7 -3
  31. package/packages/mailx-store/store.js.map +1 -1
  32. package/packages/mailx-store/store.ts +7 -3
  33. package/packages/mailx-store-web/package.json +1 -1
  34. package/packages/mailx-types/package.json +1 -1
  35. package/packages/mailx-types/trust.d.ts +16 -3
  36. package/packages/mailx-types/trust.d.ts.map +1 -1
  37. package/packages/mailx-types/trust.js +262 -23
  38. package/packages/mailx-types/trust.js.map +1 -1
  39. package/packages/mailx-types/trust.ts +343 -25
  40. /package/packages/mailx-imap/{node_modules.npmglobalize-stash-85316 → node_modules.npmglobalize-stash-38644}/.package-lock.json +0 -0
@@ -1665,33 +1665,59 @@ export async function showMessage(accountId: string, uid: number, folderId?: num
1665
1665
  // prompted all of this scored 2.7 against 4.5 and was delivered
1666
1666
  // unflagged, so showing only the flagged ones would miss exactly the
1667
1667
  // case worth showing.
1668
- const spam = (msg as any).spamScore as { score: number; threshold: number; flagged: boolean };
1668
+ //
1669
+ // The COLOUR comes from what the rules that scored are evidence of, not
1670
+ // from the number (Claude Code 2026-08-27): a newsletter DMARC proved,
1671
+ // or one whose whole score is bulk-mail rules, gets the neutral chip
1672
+ // however high it climbed. Blue Orchestra scored 10.9 on a Spamhaus
1673
+ // listing of a linked advocacy site and a bulk-mail hit; painting that
1674
+ // the same red as a forged sender is what teaches a reader to ignore
1675
+ // the chip. The floor stays at half the server's own threshold —
1676
+ // see docs/spam-false-positives.md on why tuning it is not the fix.
1677
+ const spam = (msg as any).spamScore as {
1678
+ score: number; threshold: number; flagged: boolean;
1679
+ kind?: string; proved?: boolean; reasons?: string[];
1680
+ };
1669
1681
  if (spam && Number.isFinite(spam.score) && (spam.flagged || spam.score >= spam.threshold / 2)) {
1670
- const level = spam.flagged ? "high" : "mid";
1682
+ const benign = !!spam.proved || spam.kind === "bulk";
1683
+ const level = benign ? "bulk" : spam.flagged ? "high" : "mid";
1671
1684
  const chip = document.createElement("div");
1672
1685
  chip.className = `mv-spamscore mv-spamscore-${level}`;
1673
- chip.title = level === "high"
1674
- ? "Your mail server scored this at or above its spam threshold and flagged it."
1675
- : "Below your server's spam threshold, but more than halfway to it.";
1686
+ const why = (spam.reasons || []).length ? "\n\n" + spam.reasons.join("\n") : "";
1687
+ chip.title = (level === "bulk"
1688
+ ? (spam.proved
1689
+ ? "Scored by bulk-mail or blocklist rules; the sending domain passed DMARC, so the sender is proved."
1690
+ : "Every rule that scored is a bulk-mail test — none of them test who sent this.")
1691
+ : level === "high"
1692
+ ? "Your mail server scored this at or above its spam threshold and flagged it."
1693
+ : "Below your server's spam threshold, but more than halfway to it.") + why;
1676
1694
  chip.textContent = `spam score ${spam.score} of ${spam.threshold}`;
1677
1695
  bodyEl.appendChild(chip);
1678
1696
  }
1679
1697
 
1680
1698
  const trust = ((msg as any).trust || []) as { id: string; severity: string; text: string; detail: string }[];
1681
1699
  if (trust.length) {
1682
- const worst = trust.some(f => f.severity === "danger") ? "danger" : "caution";
1700
+ // An `info` finding is not an accusation, so it gets neither the
1701
+ // headline nor the "do not open anything" footer — it renders as a
1702
+ // plain note in the register of the remote-content banner. A single
1703
+ // danger or caution finding anywhere in the set still sets the
1704
+ // whole banner, because the worst evidence is the one that matters.
1705
+ // (Claude Code 2026-08-27)
1706
+ const worst = trust.some(f => f.severity === "danger") ? "danger"
1707
+ : trust.some(f => f.severity === "caution") ? "caution" : "info";
1708
+ const head = worst === "danger" ? "This message is not what it says it is"
1709
+ : worst === "caution" ? "Check this message before acting on it" : "";
1683
1710
  const warn = document.createElement("div");
1684
1711
  warn.className = `mv-trust mv-trust-${worst}`;
1685
1712
  warn.innerHTML =
1686
- `<div class="mv-trust-head">${worst === "danger"
1687
- ? "This message is not what it says it is"
1688
- : "Check this message before acting on it"}</div>` +
1713
+ (head ? `<div class="mv-trust-head">${head}</div>` : "") +
1689
1714
  trust.map(f =>
1690
1715
  `<div class="mv-trust-item mv-trust-${escapeText(f.severity)}">` +
1691
1716
  `<div class="mv-trust-text">${escapeText(f.text)}</div>` +
1692
1717
  `<div class="mv-trust-detail">${escapeText(f.detail)}</div>` +
1693
1718
  `</div>`).join("") +
1694
- `<div class="mv-trust-foot">Links and attachments here should not be opened.</div>`;
1719
+ (worst === "info" ? ""
1720
+ : `<div class="mv-trust-foot">Links and attachments here should not be opened.</div>`);
1695
1721
  bodyEl.appendChild(warn);
1696
1722
  }
1697
1723
 
@@ -3710,7 +3710,13 @@ var init_spellcheck = __esm({
3710
3710
  // client/compose/harper-lint.js
3711
3711
  var harper_lint_exports = {};
3712
3712
  __export(harper_lint_exports, {
3713
- initHarperLint: () => initHarperLint
3713
+ blockText: () => blockText,
3714
+ caretOffset: () => caretOffset,
3715
+ collectBlocks: () => collectBlocks,
3716
+ initHarperLint: () => initHarperLint,
3717
+ lineBox: () => lineBox,
3718
+ sentenceStart: () => sentenceStart,
3719
+ spanToRange: () => spanToRange
3714
3720
  });
3715
3721
  function initHarperLint(ed) {
3716
3722
  let disposed = false;
@@ -3735,42 +3741,6 @@ function initHarperLint(ed) {
3735
3741
  doc.getElementById(OVERLAY_ID2)?.remove();
3736
3742
  doc.getElementById(POPUP_ID)?.remove();
3737
3743
  };
3738
- const collectBlocks = (root) => {
3739
- const kids = Array.from(root.children).filter((c) => c instanceof HTMLElement);
3740
- const blocks = (kids.length ? kids : [root]).filter((b) => b.tagName !== "BLOCKQUOTE" && !b.closest("blockquote") && !b.querySelector("blockquote"));
3741
- return blocks;
3742
- };
3743
- const blockText = (block, doc) => {
3744
- const walker = doc.createTreeWalker(block, NodeFilter.SHOW_TEXT);
3745
- const nodes = [];
3746
- let text = "";
3747
- for (let n = walker.nextNode(); n; n = walker.nextNode()) {
3748
- nodes.push({ node: n, from: text.length });
3749
- text += n.data;
3750
- }
3751
- return { text, nodes };
3752
- };
3753
- const spanToRange = (doc, nodes, start, end) => {
3754
- let a = null;
3755
- let b = null;
3756
- for (const e of nodes) {
3757
- const len = e.node.data.length;
3758
- if (!a && start >= e.from && start <= e.from + len)
3759
- a = { node: e.node, off: start - e.from };
3760
- if (end >= e.from && end <= e.from + len)
3761
- b = { node: e.node, off: end - e.from };
3762
- }
3763
- if (!a || !b)
3764
- return null;
3765
- try {
3766
- const r = doc.createRange();
3767
- r.setStart(a.node, a.off);
3768
- r.setEnd(b.node, b.off);
3769
- return r;
3770
- } catch {
3771
- return null;
3772
- }
3773
- };
3774
3744
  const paint = () => {
3775
3745
  if (disposed)
3776
3746
  return;
@@ -3834,9 +3804,13 @@ function initHarperLint(ed) {
3834
3804
  const tx = texts[i];
3835
3805
  if (!tx)
3836
3806
  return;
3807
+ const caret = caretOffset(t.doc, tx.nodes);
3808
+ const writingFrom = caret < 0 ? -1 : sentenceStart(tx.text, caret);
3837
3809
  for (const l of lints || []) {
3838
3810
  if (SKIP_KINDS.has(l.kind))
3839
3811
  continue;
3812
+ if (writingFrom >= 0 && l.end > writingFrom && l.start <= caret)
3813
+ continue;
3840
3814
  const range = spanToRange(t.doc, tx.nodes, l.start, l.end);
3841
3815
  if (range)
3842
3816
  next.push({ lint: l, range });
@@ -3966,7 +3940,7 @@ function initHarperLint(ed) {
3966
3940
  }
3967
3941
  };
3968
3942
  }
3969
- var DEBOUNCE_MS, OVERLAY_ID2, POPUP_ID, SKIP_KINDS;
3943
+ var DEBOUNCE_MS, OVERLAY_ID2, POPUP_ID, SKIP_KINDS, collectBlocks, LINE_BREAKING, blockText, lineBox, caretOffset, sentenceStart, spanToRange;
3970
3944
  var init_harper_lint = __esm({
3971
3945
  "client/compose/harper-lint.js"() {
3972
3946
  "use strict";
@@ -3975,6 +3949,100 @@ var init_harper_lint = __esm({
3975
3949
  OVERLAY_ID2 = "harper-overlay";
3976
3950
  POPUP_ID = "harper-popup";
3977
3951
  SKIP_KINDS = /* @__PURE__ */ new Set(["Spelling"]);
3952
+ collectBlocks = (root) => {
3953
+ const kids = Array.from(root.children).filter((c) => c instanceof HTMLElement);
3954
+ const blocks = (kids.length ? kids : [root]).filter((b) => b.tagName !== "BLOCKQUOTE" && !b.closest("blockquote") && !b.querySelector("blockquote"));
3955
+ return blocks;
3956
+ };
3957
+ LINE_BREAKING = /* @__PURE__ */ new Set([
3958
+ "BR",
3959
+ "P",
3960
+ "DIV",
3961
+ "LI",
3962
+ "UL",
3963
+ "OL",
3964
+ "TR",
3965
+ "TD",
3966
+ "TH",
3967
+ "TABLE",
3968
+ "PRE",
3969
+ "HR",
3970
+ "BLOCKQUOTE",
3971
+ "H1",
3972
+ "H2",
3973
+ "H3",
3974
+ "H4",
3975
+ "H5",
3976
+ "H6",
3977
+ "SECTION",
3978
+ "ARTICLE",
3979
+ "FIGURE"
3980
+ ]);
3981
+ blockText = (block, doc) => {
3982
+ const walker = doc.createTreeWalker(block, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
3983
+ const nodes = [];
3984
+ let text = "";
3985
+ let pendingBreak = false;
3986
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
3987
+ if (n.nodeType === Node.ELEMENT_NODE) {
3988
+ if (LINE_BREAKING.has(n.tagName))
3989
+ pendingBreak = true;
3990
+ continue;
3991
+ }
3992
+ const data = n.data;
3993
+ if (!data)
3994
+ continue;
3995
+ if (pendingBreak && text)
3996
+ text += "\n";
3997
+ pendingBreak = false;
3998
+ nodes.push({ node: n, from: text.length });
3999
+ text += data;
4000
+ }
4001
+ return { text, nodes };
4002
+ };
4003
+ lineBox = (node) => {
4004
+ for (let e = node; e; e = e.parentNode)
4005
+ if (e.nodeType === Node.ELEMENT_NODE && LINE_BREAKING.has(e.tagName))
4006
+ return e;
4007
+ return null;
4008
+ };
4009
+ caretOffset = (doc, nodes) => {
4010
+ const sel = doc.getSelection?.();
4011
+ if (!sel || !sel.focusNode)
4012
+ return -1;
4013
+ for (const e of nodes)
4014
+ if (e.node === sel.focusNode)
4015
+ return e.from + sel.focusOffset;
4016
+ return -1;
4017
+ };
4018
+ sentenceStart = (text, caret) => {
4019
+ const before = text.slice(0, caret);
4020
+ const m = before.match(/[.!?…\n][^.!?…\n]*$/);
4021
+ return m ? caret - (m[0].length - 1) : 0;
4022
+ };
4023
+ spanToRange = (doc, nodes, start, end) => {
4024
+ let a = null;
4025
+ let b = null;
4026
+ for (const e of nodes) {
4027
+ const len = e.node.data.length;
4028
+ if (!a && start >= e.from && start <= e.from + len)
4029
+ a = { node: e.node, off: start - e.from };
4030
+ if (end >= e.from && end <= e.from + len)
4031
+ b = { node: e.node, off: end - e.from };
4032
+ }
4033
+ if (!a || !b)
4034
+ return null;
4035
+ if (lineBox(a.node) !== lineBox(b.node))
4036
+ return null;
4037
+ try {
4038
+ const r = doc.createRange();
4039
+ r.setStart(a.node, a.off);
4040
+ r.setEnd(b.node, b.off);
4041
+ return r;
4042
+ } catch {
4043
+ return null;
4044
+ }
4045
+ };
3978
4046
  }
3979
4047
  });
3980
4048