@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.
- package/.commitmsg +28 -32
- package/TODO.md +2 -0
- package/client/android-bootstrap.bundle.js +202 -19
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +7 -4
- package/client/app.bundle.js.map +2 -2
- package/client/components/message-viewer.js +32 -9
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +36 -10
- package/client/compose/compose.bundle.js +106 -38
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/harper-lint.js +125 -42
- package/client/compose/harper-lint.js.map +1 -1
- package/client/compose/harper-lint.ts +120 -40
- package/client/styles/components.css +9 -0
- package/docs/spam-false-positives.md +208 -0
- package/npmchanges.md +86 -0
- package/package.json +6 -6
- package/packages/mailx-imap/package-lock.json +2 -2
- package/packages/mailx-imap/package.json +1 -1
- package/packages/mailx-service/index.d.ts.map +1 -1
- package/packages/mailx-service/index.js +20 -3
- package/packages/mailx-service/index.js.map +1 -1
- package/packages/mailx-service/index.ts +20 -3
- package/packages/mailx-service/package.json +1 -1
- package/packages/mailx-settings/docs/spam-false-positives.md +208 -0
- package/packages/mailx-settings/package.json +1 -1
- package/packages/mailx-store/package.json +1 -1
- package/packages/mailx-store/store.d.ts.map +1 -1
- package/packages/mailx-store/store.js +7 -3
- package/packages/mailx-store/store.js.map +1 -1
- package/packages/mailx-store/store.ts +7 -3
- package/packages/mailx-store-web/package.json +1 -1
- package/packages/mailx-types/package.json +1 -1
- package/packages/mailx-types/trust.d.ts +16 -3
- package/packages/mailx-types/trust.d.ts.map +1 -1
- package/packages/mailx-types/trust.js +262 -23
- package/packages/mailx-types/trust.js.map +1 -1
- package/packages/mailx-types/trust.ts +343 -25
- /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
|
-
|
|
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
|
|
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
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
-
|
|
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">${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|