spamscanner 7.0.11 → 7.0.12
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/dist/cjs/arf.cjs.map +1 -1
- package/dist/cjs/index.cjs +245 -129
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/esm/arf.js.map +1 -1
- package/dist/esm/cli.js +249 -132
- package/dist/esm/cli.js.map +3 -3
- package/dist/esm/index.js +241 -125
- package/dist/esm/index.js.map +3 -3
- package/dist/types/links.d.ts +5 -3
- package/dist/types/tokenizer.d.ts +9 -0
- package/package.json +1 -1
package/dist/esm/cli.js
CHANGED
|
@@ -549,9 +549,12 @@ function rewriteMessage(source, headers = [], options = {}) {
|
|
|
549
549
|
}
|
|
550
550
|
}
|
|
551
551
|
const added = headers.map(([name, value]) => foldHeader(name, Buffer2.from(String(value), "utf8").toString("latin1"), newline));
|
|
552
|
-
const mbox = kept.length > 0 && kept[0][0].startsWith("From ") ? kept.shift() : [];
|
|
552
|
+
const mbox = kept.length > 0 && kept[0][0].startsWith("From ") ? kept.shift().slice(0, 1) : [];
|
|
553
|
+
if (kept.length > 0 && /^[ \t]/.test(kept[0][0])) {
|
|
554
|
+
kept.shift();
|
|
555
|
+
}
|
|
553
556
|
const head = [...mbox, ...added, ...kept.map((field) => field.join(newline))].join(newline);
|
|
554
|
-
return Buffer2.concat([Buffer2.from(head === ""
|
|
557
|
+
return Buffer2.concat([Buffer2.from(head === "" ? newline : `${head}${newline}${newline}`, "latin1"), body]);
|
|
555
558
|
}
|
|
556
559
|
|
|
557
560
|
// src/llm.js
|
|
@@ -1887,7 +1890,7 @@ var NAMED = [
|
|
|
1887
1890
|
",itilde=129,iukcy=456,iuml=ef,jcirc=135,jcy=439,jfr=1d527,jmath=237,jopf=1d55b,jscr=1d4bf,jsercy=458",
|
|
1888
1891
|
",jukcy=454,kappa=3ba,kappav=3f0,kcedil=137,kcy=43a,kfr=1d528,kgreen=138,khcy=445,kjcy=45c,kopf=1d55c",
|
|
1889
1892
|
",kscr=1d4c0,lAarr=21da,lArr=21d0,lAtail=291b,lBarr=290e,lE=2266,lEg=2a8b,lHar=2962,lacute=13a,laempt",
|
|
1890
|
-
"yv=29b4,lagran=2112,lambda=3bb,lang=
|
|
1893
|
+
"yv=29b4,lagran=2112,lambda=3bb,lang=27e8,langd=2991,langle=27e8,lap=2a85,laquo=ab,larr=2190,larrb=21",
|
|
1891
1894
|
"e4,larrbfs=291f,larrfs=291d,larrhk=21a9,larrlp=21ab,larrpl=2939,larrsim=2973,larrtl=21a2,lat=2aab,la",
|
|
1892
1895
|
"tail=2919,late=2aad,lates=2aad.fe00,lbarr=290c,lbbrk=2772,lbrace=7b,lbrack=5b,lbrke=298b,lbrksld=298",
|
|
1893
1896
|
"f,lbrkslu=298d,lcaron=13e,lcedil=13c,lceil=2308,lcub=7b,lcy=43b,ldca=2936,ldquo=201c,ldquor=201e,ldr",
|
|
@@ -1945,7 +1948,7 @@ var NAMED = [
|
|
|
1945
1948
|
"line=2312,profsurf=2313,prop=221d,propto=221d,prsim=227e,prurel=22b0,pscr=1d4c5,psi=3c8,puncsp=2008,",
|
|
1946
1949
|
"qfr=1d52e,qint=2a0c,qopf=1d562,qprime=2057,qscr=1d4c6,quaternions=210d,quatint=2a16,quest=3f,questeq",
|
|
1947
1950
|
"=225f,quot=22,rAarr=21db,rArr=21d2,rAtail=291c,rBarr=290f,rHar=2964,race=223d.331,racute=155,radic=2",
|
|
1948
|
-
"21a,raemptyv=29b3,rang=
|
|
1951
|
+
"21a,raemptyv=29b3,rang=27e9,rangd=2992,range=29a5,rangle=27e9,raquo=bb,rarr=2192,rarrap=2975,rarrb=2",
|
|
1949
1952
|
"1e5,rarrbfs=2920,rarrc=2933,rarrfs=291e,rarrhk=21aa,rarrlp=21ac,rarrpl=2945,rarrsim=2974,rarrtl=21a3",
|
|
1950
1953
|
",rarrw=219d,ratail=291a,ratio=2236,rationals=211a,rbarr=290d,rbbrk=2773,rbrace=7d,rbrack=5d,rbrke=29",
|
|
1951
1954
|
"8c,rbrksld=298e,rbrkslu=2990,rcaron=159,rcedil=157,rceil=2309,rcub=7d,rcy=440,rdca=2937,rdldhar=2969",
|
|
@@ -2888,6 +2891,34 @@ function continuesName(text, index) {
|
|
|
2888
2891
|
var FILE_TLDS = /* @__PURE__ */ new Set(["ai", "app", "md", "mov", "pm", "ps", "py", "rs", "sh", "so", "zip"]);
|
|
2889
2892
|
var LINK_FILE_TLDS = /* @__PURE__ */ new Set(["md", "mov", "pm", "ps", "py", "rs", "sh", "so", "zip"]);
|
|
2890
2893
|
var CLICK_TRACKERS = ["sendgrid.net", "list-manage.com", "mandrillapp.com", "awstrack.me", "pstmrk.it", "hubspotlinks.com", "rs6.net", "klclick.com", "mjt.lu", "exacttarget.com", "convertkit-mail.com", "convertkit-mail2.com", "kit-mail3.com", "sendibt2.com", "sendibt3.com", "createsend1.com", "cmail20.com", "clicks.mlsend.com", "acemlnb.com", "link.mail.beehiiv.com", "bhclick.com", "safelinks.protection.outlook.com", "urldefense.com", "urldefense.proofpoint.com", "mimecast.com", "mimecastprotect.com", "cudasvc.com", "secure-web.cisco.com"];
|
|
2894
|
+
var MAX_URL_HOST = 4096;
|
|
2895
|
+
var AUTHORITY_END = /[/\\?#]/g;
|
|
2896
|
+
var IGNORED_IN_HOST = /[\u00AD\u034F\u180B-\u180D\u180F\u200B\u2060\u2064\uFE00-\uFE0F\uFEFF]|\uD82F[\uDCA0-\uDCA3]|\uDB40[\uDD00-\uDDEF]/g;
|
|
2897
|
+
function boundedUrl(address2) {
|
|
2898
|
+
const text = String(address2).replaceAll(/[\t\n\r]/g, "");
|
|
2899
|
+
const colon = text.indexOf(":");
|
|
2900
|
+
let start = colon === -1 ? 0 : colon + 1;
|
|
2901
|
+
while (start < text.length && (text[start] === "/" || text[start] === "\\")) {
|
|
2902
|
+
start++;
|
|
2903
|
+
}
|
|
2904
|
+
AUTHORITY_END.lastIndex = start;
|
|
2905
|
+
const end = AUTHORITY_END.exec(text)?.index ?? text.length;
|
|
2906
|
+
const file = /^[\0- ]*file:/i.test(text);
|
|
2907
|
+
const at = file ? start : Math.max(start, text.lastIndexOf("@", end - 1) + 1);
|
|
2908
|
+
const bracket = text.lastIndexOf("]", end - 1);
|
|
2909
|
+
const port = text.lastIndexOf(":", end - 1);
|
|
2910
|
+
let host = text.slice(at, !file && port > Math.max(at - 1, bracket) ? port : end);
|
|
2911
|
+
if (host.length > MAX_URL_HOST && host.includes("%")) {
|
|
2912
|
+
try {
|
|
2913
|
+
host = decodeURIComponent(host);
|
|
2914
|
+
} catch {
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
if (host.length > MAX_URL_HOST && host.replaceAll(IGNORED_IN_HOST, "").length > MAX_URL_HOST) {
|
|
2918
|
+
throw new TypeError("Invalid URL: host too long");
|
|
2919
|
+
}
|
|
2920
|
+
return new URL(text);
|
|
2921
|
+
}
|
|
2891
2922
|
var isUnder = (host, domain) => host === domain || host.endsWith(`.${domain}`);
|
|
2892
2923
|
function isClickTracker(url) {
|
|
2893
2924
|
const host = url.hostname.toLowerCase();
|
|
@@ -2916,7 +2947,7 @@ var MAX_GATEWAYS = 16;
|
|
|
2916
2947
|
function unwrapLink(address2) {
|
|
2917
2948
|
let url;
|
|
2918
2949
|
try {
|
|
2919
|
-
url =
|
|
2950
|
+
url = boundedUrl(address2);
|
|
2920
2951
|
} catch {
|
|
2921
2952
|
return null;
|
|
2922
2953
|
}
|
|
@@ -2927,7 +2958,7 @@ function unwrapLink(address2) {
|
|
|
2927
2958
|
}
|
|
2928
2959
|
let next;
|
|
2929
2960
|
try {
|
|
2930
|
-
next =
|
|
2961
|
+
next = boundedUrl(target);
|
|
2931
2962
|
} catch {
|
|
2932
2963
|
break;
|
|
2933
2964
|
}
|
|
@@ -3205,7 +3236,7 @@ function extractLinks(text, html, { maxLength = 5e5, bareHosts: bareHosts2 = fal
|
|
|
3205
3236
|
}
|
|
3206
3237
|
let url;
|
|
3207
3238
|
try {
|
|
3208
|
-
url =
|
|
3239
|
+
url = boundedUrl(href);
|
|
3209
3240
|
} catch {
|
|
3210
3241
|
return;
|
|
3211
3242
|
}
|
|
@@ -3530,7 +3561,7 @@ function isDeceptiveLink(link, { from } = {}) {
|
|
|
3530
3561
|
}
|
|
3531
3562
|
let shownUrl;
|
|
3532
3563
|
try {
|
|
3533
|
-
shownUrl =
|
|
3564
|
+
shownUrl = boundedUrl(/^https?:\/\//i.test(link.text) ? link.text : `http://${link.text}`);
|
|
3534
3565
|
} catch {
|
|
3535
3566
|
return false;
|
|
3536
3567
|
}
|
|
@@ -5343,7 +5374,7 @@ async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PA
|
|
|
5343
5374
|
}
|
|
5344
5375
|
}
|
|
5345
5376
|
async function expandAttachments(attachments = [], errors = [], parse = parseMessage, gets = {}) {
|
|
5346
|
-
const { unopened = [], mails = [] } = Array.isArray(gets) ? { unopened: gets } : gets;
|
|
5377
|
+
const { unopened = [], mails = [] } = Array.isArray(gets) ? { unopened: gets } : gets ?? {};
|
|
5347
5378
|
const all = [];
|
|
5348
5379
|
const deadline = Date.now() + PARSE_TIMEOUT;
|
|
5349
5380
|
let budget = MAX_NESTED_BYTES;
|
|
@@ -5411,6 +5442,11 @@ import { isIP } from "node:net";
|
|
|
5411
5442
|
import { domainToASCII } from "node:url";
|
|
5412
5443
|
import { getPublicSuffix } from "tldts";
|
|
5413
5444
|
var LOOKUP_LABEL = /^[a-z\d_-]{1,63}$/;
|
|
5445
|
+
var LABEL_SEPARATOR = /[.\u3002\uFF0E\uFF61]/g;
|
|
5446
|
+
function lastLabels(name) {
|
|
5447
|
+
const separators = [...name.matchAll(LABEL_SEPARATOR)];
|
|
5448
|
+
return separators.length < 128 ? 0 : separators.at(-128).index + 1;
|
|
5449
|
+
}
|
|
5414
5450
|
function lookupName(host) {
|
|
5415
5451
|
if (typeof host !== "string") {
|
|
5416
5452
|
return null;
|
|
@@ -5421,6 +5457,7 @@ function lookupName(host) {
|
|
|
5421
5457
|
}
|
|
5422
5458
|
let name = host.slice(0, end).toLowerCase();
|
|
5423
5459
|
if (/[\u0080-\u{10FFFF}]/u.test(name)) {
|
|
5460
|
+
name = name.slice(lastLabels(name));
|
|
5424
5461
|
name = domainToASCII(name) || name;
|
|
5425
5462
|
}
|
|
5426
5463
|
if (isIP(name.replaceAll(/^\[|]$/g, "")) !== 0) {
|
|
@@ -5691,7 +5728,7 @@ function asciiHostname(value) {
|
|
|
5691
5728
|
return null;
|
|
5692
5729
|
}
|
|
5693
5730
|
const lower = value.normalize("NFKC").toLowerCase().replace(/\.$/, "");
|
|
5694
|
-
if (lower === "" || /[\0-,/:-^`{-\u007F]/.test(lower)) {
|
|
5731
|
+
if (lower === "" || lower.length > 1024 || /[\0-,/:-^`{-\u007F]/.test(lower)) {
|
|
5695
5732
|
return null;
|
|
5696
5733
|
}
|
|
5697
5734
|
const ascii = domainToASCII2(lower);
|
|
@@ -5873,8 +5910,8 @@ function trustReputation(result, trusted = []) {
|
|
|
5873
5910
|
for (const value of result.checkedValues.filter((item) => allowed.has(item))) {
|
|
5874
5911
|
const signers = allowed.get(value);
|
|
5875
5912
|
const counts = (entry) => Boolean(entry) && (signers === null || signers.some((signer) => vouches(normalizeEntry(entry), signer)));
|
|
5876
|
-
const detail = result.details[value];
|
|
5877
|
-
const listed = result.allowlisted[value] || (detail?.isAllowlisted ? detail.allowlistValue : null);
|
|
5913
|
+
const detail = Object.hasOwn(result.details, value) ? result.details[value] : void 0;
|
|
5914
|
+
const listed = Object.hasOwn(result.allowlisted, value) && result.allowlisted[value] || (detail?.isAllowlisted ? detail.allowlistValue : null);
|
|
5878
5915
|
if (counts(listed) && !out.isAllowlisted) {
|
|
5879
5916
|
out.isAllowlisted = true;
|
|
5880
5917
|
out.allowlistValue = listed;
|
|
@@ -6273,7 +6310,7 @@ import net from "node:net";
|
|
|
6273
6310
|
var package_default = {
|
|
6274
6311
|
name: "spamscanner",
|
|
6275
6312
|
description: "Spam, phishing and malware filter for email in any language: a trainable classifier, link and attachment checks, SPF/DKIM/DMARC, and an optional AI second opinion from any LLM, including local Ollama models. Works as a library, CLI, Postfix milter or HTTP API.",
|
|
6276
|
-
version: "7.0.
|
|
6313
|
+
version: "7.0.12",
|
|
6277
6314
|
author: "Forward Email (https://forwardemail.net)",
|
|
6278
6315
|
bin: {
|
|
6279
6316
|
spamscanner: "./dist/esm/cli.js"
|
|
@@ -6423,7 +6460,12 @@ var VERSION = package_default.version;
|
|
|
6423
6460
|
var MAX_MESSAGE_SIZE = 25 * 1024 * 1024;
|
|
6424
6461
|
var MAX_CONNECTIONS = 200;
|
|
6425
6462
|
function serializeResult(result, options = {}) {
|
|
6426
|
-
const {
|
|
6463
|
+
const {
|
|
6464
|
+
mail,
|
|
6465
|
+
tokens,
|
|
6466
|
+
attached,
|
|
6467
|
+
...rest
|
|
6468
|
+
} = result;
|
|
6427
6469
|
const output = {
|
|
6428
6470
|
...rest,
|
|
6429
6471
|
results: { ...result.results }
|
|
@@ -7324,6 +7366,7 @@ function reply(code, text, headers = [], body = null) {
|
|
|
7324
7366
|
lines2.push(...headers.map(([name, value]) => `${name}: ${value}`), "", "");
|
|
7325
7367
|
return body === null ? Buffer9.from(lines2.join("\r\n")) : Buffer9.concat([Buffer9.from(lines2.join("\r\n")), body]);
|
|
7326
7368
|
}
|
|
7369
|
+
var oneLine2 = (text) => String(text ?? "").replaceAll(/[\p{Cc}\u061C\u200E\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]+/gu, " ");
|
|
7327
7370
|
function report(result) {
|
|
7328
7371
|
const lines2 = [
|
|
7329
7372
|
`Spam detection software, running on the system "spamscanner", has ${result.isSpam ? "identified this incoming email as possible spam" : "scanned this incoming email"}.`,
|
|
@@ -7332,7 +7375,7 @@ function report(result) {
|
|
|
7332
7375
|
"",
|
|
7333
7376
|
" pts rule name description",
|
|
7334
7377
|
"---- ---------------------- --------------------------------------------------",
|
|
7335
|
-
...result.tests.map((test) => `${test.score.toFixed(1).padStart(4)} ${test.name.padEnd(22)} ${test.description}`)
|
|
7378
|
+
...result.tests.map((test) => `${test.score.toFixed(1).padStart(4)} ${oneLine2(test.name).padEnd(22)} ${oneLine2(test.description)}`)
|
|
7336
7379
|
];
|
|
7337
7380
|
return `${lines2.join("\r\n")}\r
|
|
7338
7381
|
`;
|
|
@@ -8355,7 +8398,8 @@ var MAX_EMBEDDED_LENGTH = 10 * 1024 * 1024;
|
|
|
8355
8398
|
var MAX_ATTACHED = 100;
|
|
8356
8399
|
var MAX_ATTACHED_BYTES = 50 * 1024 * 1024;
|
|
8357
8400
|
var MAX_READ_TIME = 5e3;
|
|
8358
|
-
var
|
|
8401
|
+
var MAX_CARD_BYTES = 4 * 1024 * 1024;
|
|
8402
|
+
var MAX_SHOWN_BYTES = 1024 * 1024;
|
|
8359
8403
|
function isHostCode2(code) {
|
|
8360
8404
|
return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 45 || code === 46;
|
|
8361
8405
|
}
|
|
@@ -8384,7 +8428,7 @@ function urlOf(value) {
|
|
|
8384
8428
|
return null;
|
|
8385
8429
|
}
|
|
8386
8430
|
try {
|
|
8387
|
-
return
|
|
8431
|
+
return boundedUrl(href);
|
|
8388
8432
|
} catch {
|
|
8389
8433
|
return null;
|
|
8390
8434
|
}
|
|
@@ -8474,7 +8518,13 @@ function* attributesOf(html) {
|
|
|
8474
8518
|
TAG_START.lastIndex = Math.max(i, match.index + 1);
|
|
8475
8519
|
}
|
|
8476
8520
|
}
|
|
8477
|
-
|
|
8521
|
+
var MAX_VALUE2 = 1024 * 1024;
|
|
8522
|
+
var LONG_VALUE = 64 * 1024;
|
|
8523
|
+
function linksOf(name, value, { add, embed, cut }) {
|
|
8524
|
+
if (value.length > MAX_VALUE2) {
|
|
8525
|
+
cut();
|
|
8526
|
+
value = value.slice(0, MAX_VALUE2);
|
|
8527
|
+
}
|
|
8478
8528
|
const decoded = decodeEntities(value, { attribute: true });
|
|
8479
8529
|
switch (name) {
|
|
8480
8530
|
case "srcdoc": {
|
|
@@ -8515,7 +8565,7 @@ function linksOf(name, value, add, embed) {
|
|
|
8515
8565
|
}
|
|
8516
8566
|
var MAX_LENIENT_VALUE = 4096;
|
|
8517
8567
|
var UNQUOTED_END = /* @__PURE__ */ new Set(["<", ">", '"', "'", "`", "="]);
|
|
8518
|
-
function lenientLinks(html,
|
|
8568
|
+
function lenientLinks(html, done, reader) {
|
|
8519
8569
|
LENIENT.lastIndex = 0;
|
|
8520
8570
|
let match;
|
|
8521
8571
|
while (!done() && (match = LENIENT.exec(html)) !== null) {
|
|
@@ -8531,8 +8581,8 @@ function lenientLinks(html, add, done, embed) {
|
|
|
8531
8581
|
}
|
|
8532
8582
|
if (end < html.length && html[end] !== quote) {
|
|
8533
8583
|
const close = html.indexOf(quote, end);
|
|
8534
|
-
if (close !== -1 && close - i <= MAX_LENIENT_VALUE) {
|
|
8535
|
-
linksOf(name, html.slice(i, close),
|
|
8584
|
+
if (close !== -1 && close - i <= MAX_LENIENT_VALUE && !reader.spend(close - i)) {
|
|
8585
|
+
linksOf(name, html.slice(i, close), reader);
|
|
8536
8586
|
}
|
|
8537
8587
|
}
|
|
8538
8588
|
} else {
|
|
@@ -8540,7 +8590,10 @@ function lenientLinks(html, add, done, embed) {
|
|
|
8540
8590
|
end++;
|
|
8541
8591
|
}
|
|
8542
8592
|
}
|
|
8543
|
-
|
|
8593
|
+
if (reader.spend(end - i)) {
|
|
8594
|
+
break;
|
|
8595
|
+
}
|
|
8596
|
+
linksOf(name, html.slice(i, end), reader);
|
|
8544
8597
|
}
|
|
8545
8598
|
}
|
|
8546
8599
|
function endsLink2(text, i) {
|
|
@@ -8711,8 +8764,17 @@ function* chunksOf(html) {
|
|
|
8711
8764
|
cutStart = false;
|
|
8712
8765
|
continue;
|
|
8713
8766
|
}
|
|
8714
|
-
|
|
8715
|
-
start
|
|
8767
|
+
let hard = end;
|
|
8768
|
+
while (hard > start + CHUNK_OVERLAP + 1 && /[\dA-Za-z#]/.test(html[hard - 1])) {
|
|
8769
|
+
hard--;
|
|
8770
|
+
}
|
|
8771
|
+
if (hard < end && html[hard - 1] === "&" && hard - 1 > start + CHUNK_OVERLAP) {
|
|
8772
|
+
hard--;
|
|
8773
|
+
} else {
|
|
8774
|
+
hard = end;
|
|
8775
|
+
}
|
|
8776
|
+
yield { text: html.slice(start, hard), cutStart, cutEnd: true };
|
|
8777
|
+
start = hard - CHUNK_OVERLAP;
|
|
8716
8778
|
cutStart = true;
|
|
8717
8779
|
continue;
|
|
8718
8780
|
}
|
|
@@ -8882,27 +8944,14 @@ function quotedPrintableValues(text) {
|
|
|
8882
8944
|
}
|
|
8883
8945
|
return out.join("\n");
|
|
8884
8946
|
}
|
|
8885
|
-
function
|
|
8886
|
-
const unfolded = text.replaceAll(FOLD, "");
|
|
8887
|
-
if (!/quoted-printable/i.test(unfolded)) {
|
|
8888
|
-
return false;
|
|
8889
|
-
}
|
|
8890
|
-
return unfolded.split(LINE_BREAK).some((line) => {
|
|
8891
|
-
const colon = valueColon(line);
|
|
8892
|
-
return colon !== -1 && QUOTED_PRINTABLE.test(line.slice(0, colon));
|
|
8893
|
-
});
|
|
8894
|
-
}
|
|
8895
|
-
function shownText(text, full = true) {
|
|
8947
|
+
function shownText(text) {
|
|
8896
8948
|
const unfolded = text.replaceAll(FOLD, "");
|
|
8897
8949
|
const shown = /* @__PURE__ */ new Set([text, readEscapes(unfolded)]);
|
|
8898
|
-
|
|
8899
|
-
if (
|
|
8900
|
-
decoded
|
|
8901
|
-
if (
|
|
8902
|
-
shown.add(
|
|
8903
|
-
if (unfolded !== text) {
|
|
8904
|
-
shown.add(quotedPrintableValues(text));
|
|
8905
|
-
}
|
|
8950
|
+
const decoded = quotedPrintableValues(unfolded);
|
|
8951
|
+
if (decoded !== unfolded) {
|
|
8952
|
+
shown.add(readEscapes(decoded));
|
|
8953
|
+
if (unfolded !== text) {
|
|
8954
|
+
shown.add(quotedPrintableValues(text));
|
|
8906
8955
|
}
|
|
8907
8956
|
}
|
|
8908
8957
|
if (/\\[^nN,;\\]/.test(decoded)) {
|
|
@@ -9007,59 +9056,73 @@ function charsetOf(attachment) {
|
|
|
9007
9056
|
const charset = headers && typeof headers.get === "function" ? headers.get("content-type")?.params?.charset : null;
|
|
9008
9057
|
return typeof charset === "string" ? charset.toLowerCase() : "";
|
|
9009
9058
|
}
|
|
9010
|
-
function
|
|
9011
|
-
const
|
|
9059
|
+
function kindOf(attachment) {
|
|
9060
|
+
const { content } = attachment ?? {};
|
|
9061
|
+
if (!Buffer13.isBuffer(content) || content.length === 0) {
|
|
9062
|
+
return null;
|
|
9063
|
+
}
|
|
9064
|
+
const type = typeof attachment.contentType === "string" ? attachment.contentType.toLowerCase() : "";
|
|
9065
|
+
const extensions = extensionsOf(attachment);
|
|
9066
|
+
const has = (set) => [...extensions].some((name) => set.has(name));
|
|
9067
|
+
if (MESSAGE_TYPES.has(type) || extensions.has("eml") || extensions.has("u8msg")) {
|
|
9068
|
+
return null;
|
|
9069
|
+
}
|
|
9070
|
+
const isHtml = HTML_TYPES.has(type) || has(HTML_EXTENSIONS);
|
|
9071
|
+
const isCard = !isHtml && (CARD_TYPES.has(type) || has(CARD_EXTENSIONS));
|
|
9072
|
+
const isText = isCard || !isHtml && type === "text/plain";
|
|
9073
|
+
return isHtml || isText ? { isHtml, isText, isCard } : null;
|
|
9074
|
+
}
|
|
9075
|
+
function* documentsOf(attachments, state, late) {
|
|
9076
|
+
const read = [];
|
|
9012
9077
|
let bytes = 0;
|
|
9013
9078
|
let opened = 0;
|
|
9014
9079
|
for (const attachment of attachments) {
|
|
9015
|
-
const
|
|
9016
|
-
if (!
|
|
9080
|
+
const kind = kindOf(attachment);
|
|
9081
|
+
if (!kind) {
|
|
9017
9082
|
continue;
|
|
9018
9083
|
}
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
if (MESSAGE_TYPES.has(type) || extensions.has("eml") || extensions.has("u8msg")) {
|
|
9023
|
-
continue;
|
|
9024
|
-
}
|
|
9025
|
-
const isHtml = HTML_TYPES.has(type) || has(HTML_EXTENSIONS);
|
|
9026
|
-
const isCard = !isHtml && (CARD_TYPES.has(type) || has(CARD_EXTENSIONS));
|
|
9027
|
-
const isText = isCard || !isHtml && type === "text/plain";
|
|
9028
|
-
if (!isHtml && !isText) {
|
|
9029
|
-
continue;
|
|
9030
|
-
}
|
|
9031
|
-
if (!isText && ++opened > MAX_ATTACHED || bytes + content.length > MAX_ATTACHED_BYTES) {
|
|
9032
|
-
documents.tooComplex = true;
|
|
9033
|
-
return documents;
|
|
9084
|
+
if (!kind.isText && ++opened > MAX_ATTACHED || bytes + attachment.content.length > MAX_ATTACHED_BYTES) {
|
|
9085
|
+
state.tooComplex = true;
|
|
9086
|
+
break;
|
|
9034
9087
|
}
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9088
|
+
bytes += attachment.content.length;
|
|
9089
|
+
read.push({ attachment, kind });
|
|
9090
|
+
}
|
|
9091
|
+
for (const { attachment, kind: { isHtml, isCard } } of read) {
|
|
9092
|
+
if (late()) {
|
|
9093
|
+
state.incomplete = true;
|
|
9094
|
+
return;
|
|
9038
9095
|
}
|
|
9039
|
-
|
|
9040
|
-
const text = decodeText(content, charsetOf(attachment));
|
|
9096
|
+
const text = decodeText(attachment.content, charsetOf(attachment));
|
|
9041
9097
|
if (isHtml) {
|
|
9042
|
-
|
|
9098
|
+
yield { html: text };
|
|
9043
9099
|
continue;
|
|
9044
9100
|
}
|
|
9045
9101
|
if (!isCard) {
|
|
9046
|
-
|
|
9102
|
+
yield { text };
|
|
9047
9103
|
continue;
|
|
9048
9104
|
}
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
9105
|
+
let readable = withoutBinaryValues(text.length > MAX_CARD_BYTES ? text.slice(0, MAX_CARD_BYTES) : text);
|
|
9106
|
+
if (text.length > MAX_CARD_BYTES || readable.length > MAX_SHOWN_BYTES) {
|
|
9107
|
+
state.tooComplex = true;
|
|
9108
|
+
readable = readable.slice(0, MAX_SHOWN_BYTES);
|
|
9109
|
+
}
|
|
9110
|
+
if (late()) {
|
|
9111
|
+
state.incomplete = true;
|
|
9112
|
+
return;
|
|
9113
|
+
}
|
|
9114
|
+
yield { text: shownText(readable) };
|
|
9115
|
+
if (late()) {
|
|
9116
|
+
state.incomplete = true;
|
|
9117
|
+
return;
|
|
9054
9118
|
}
|
|
9055
|
-
documents.push({ text: shownText(readable, full) });
|
|
9056
9119
|
const html = htmlDescriptions(readable);
|
|
9057
9120
|
if (html !== "") {
|
|
9058
|
-
|
|
9121
|
+
yield { html };
|
|
9059
9122
|
}
|
|
9060
9123
|
}
|
|
9061
|
-
return documents;
|
|
9062
9124
|
}
|
|
9125
|
+
var LATE_EVERY = 1024;
|
|
9063
9126
|
function carriedLinkHosts(mail, {
|
|
9064
9127
|
mails = [],
|
|
9065
9128
|
attachments = [],
|
|
@@ -9072,40 +9135,45 @@ function carriedLinkHosts(mail, {
|
|
|
9072
9135
|
const seenBare = /* @__PURE__ */ new Set();
|
|
9073
9136
|
const seenLoose = /* @__PURE__ */ new Set();
|
|
9074
9137
|
let more = false;
|
|
9075
|
-
let
|
|
9076
|
-
const
|
|
9077
|
-
const
|
|
9138
|
+
let full = false;
|
|
9139
|
+
const domains = /* @__PURE__ */ new Set();
|
|
9140
|
+
const state = { tooComplex: false, incomplete: false };
|
|
9141
|
+
const until = Date.now() + MAX_READ_TIME;
|
|
9142
|
+
const late = () => {
|
|
9143
|
+
if (!state.incomplete && Date.now() > until) {
|
|
9144
|
+
state.incomplete = true;
|
|
9145
|
+
}
|
|
9146
|
+
return state.incomplete;
|
|
9147
|
+
};
|
|
9148
|
+
let calls = 0;
|
|
9149
|
+
const done = () => full || state.incomplete || ++calls % LATE_EVERY === 0 && late();
|
|
9150
|
+
const looseDone = () => done() || loose.size > maxHosts;
|
|
9078
9151
|
const addLoose = (host) => {
|
|
9079
9152
|
if (!looseDone()) {
|
|
9080
9153
|
loose.add(host);
|
|
9081
9154
|
}
|
|
9082
9155
|
};
|
|
9083
|
-
const
|
|
9084
|
-
|
|
9085
|
-
const late = () => {
|
|
9086
|
-
if (!incomplete && Date.now() > until) {
|
|
9087
|
-
incomplete = true;
|
|
9088
|
-
}
|
|
9089
|
-
return incomplete;
|
|
9090
|
-
};
|
|
9091
|
-
const carried = documentsOf(attachments, until);
|
|
9092
|
-
tooComplex = Boolean(carried.tooComplex);
|
|
9093
|
-
incomplete = Boolean(carried.incomplete);
|
|
9094
|
-
const queue = [mail, ...mails, ...carried].map((document) => ({ document, depth: 0 }));
|
|
9156
|
+
const queue = [mail, ...mails].map((document) => ({ document, depth: 0 }));
|
|
9157
|
+
const carried = documentsOf(attachments, state, late);
|
|
9095
9158
|
let depth = 0;
|
|
9096
9159
|
let embedded = 0;
|
|
9097
9160
|
let embeddedLength = 0;
|
|
9161
|
+
const embeddedSeen = /* @__PURE__ */ new Set();
|
|
9098
9162
|
const embed = (html) => {
|
|
9163
|
+
if (html === "" || embeddedSeen.has(html)) {
|
|
9164
|
+
return;
|
|
9165
|
+
}
|
|
9099
9166
|
if (depth >= MAX_EMBED_DEPTH || embedded >= MAX_EMBEDDED || embeddedLength + html.length > MAX_EMBEDDED_LENGTH) {
|
|
9100
|
-
tooComplex = true;
|
|
9167
|
+
state.tooComplex = true;
|
|
9101
9168
|
return;
|
|
9102
9169
|
}
|
|
9170
|
+
embeddedSeen.add(html);
|
|
9103
9171
|
embedded++;
|
|
9104
9172
|
embeddedLength += html.length;
|
|
9105
9173
|
queue.push({ document: { html }, depth: depth + 1 });
|
|
9106
9174
|
};
|
|
9107
9175
|
const add = (value) => {
|
|
9108
|
-
if (
|
|
9176
|
+
if (full || typeof value !== "string" || value === "" || seen.has(value)) {
|
|
9109
9177
|
return;
|
|
9110
9178
|
}
|
|
9111
9179
|
if (seen.size < MAX_SEEN) {
|
|
@@ -9120,34 +9188,64 @@ function carriedLinkHosts(mail, {
|
|
|
9120
9188
|
}
|
|
9121
9189
|
const found = hostsOf(value);
|
|
9122
9190
|
if (found.unresolved) {
|
|
9123
|
-
tooComplex = true;
|
|
9191
|
+
state.tooComplex = true;
|
|
9124
9192
|
}
|
|
9125
9193
|
for (const host of found) {
|
|
9126
9194
|
if (hosts.has(host)) {
|
|
9127
9195
|
continue;
|
|
9128
9196
|
}
|
|
9129
|
-
|
|
9130
|
-
|
|
9197
|
+
const domain = registrableDomain(host);
|
|
9198
|
+
if (hosts.size < maxHosts || !domains.has(domain) && domains.size < maxHosts) {
|
|
9199
|
+
hosts.add(host);
|
|
9200
|
+
domains.add(domain);
|
|
9201
|
+
continue;
|
|
9202
|
+
}
|
|
9203
|
+
more = true;
|
|
9204
|
+
if (domains.size >= maxHosts) {
|
|
9205
|
+
full = true;
|
|
9131
9206
|
return;
|
|
9132
9207
|
}
|
|
9133
|
-
hosts.add(host);
|
|
9134
9208
|
}
|
|
9135
9209
|
};
|
|
9136
|
-
|
|
9210
|
+
let spent = 0;
|
|
9211
|
+
const spend = (length) => {
|
|
9212
|
+
spent += length;
|
|
9213
|
+
if (spent < LONG_VALUE) {
|
|
9214
|
+
return false;
|
|
9215
|
+
}
|
|
9216
|
+
spent = 0;
|
|
9217
|
+
return late();
|
|
9218
|
+
};
|
|
9219
|
+
const reader = {
|
|
9220
|
+
add,
|
|
9221
|
+
embed,
|
|
9222
|
+
spend,
|
|
9223
|
+
cut() {
|
|
9224
|
+
state.tooComplex = true;
|
|
9225
|
+
}
|
|
9226
|
+
};
|
|
9227
|
+
const readText = (source, { cutStart = false, cutEnd = false, exact = false } = {}) => {
|
|
9137
9228
|
for (const match of source.matchAll(URL_START)) {
|
|
9138
|
-
if (
|
|
9229
|
+
if (done()) {
|
|
9139
9230
|
break;
|
|
9140
9231
|
}
|
|
9141
9232
|
if (cutStart && match.index === 0) {
|
|
9142
9233
|
continue;
|
|
9143
9234
|
}
|
|
9144
9235
|
if (cutEnd && match.index + match[0].length === source.length) {
|
|
9236
|
+
if (exact && source.length - match.index < CHUNK_OVERLAP) {
|
|
9237
|
+
continue;
|
|
9238
|
+
}
|
|
9145
9239
|
const value = match[0];
|
|
9146
9240
|
const at = /^www\./i.test(value) ? 0 : value.indexOf("//") + 2;
|
|
9147
|
-
if (
|
|
9148
|
-
|
|
9149
|
-
|
|
9150
|
-
|
|
9241
|
+
if (!/[/?#\\]/.test(value.slice(at))) {
|
|
9242
|
+
state.tooComplex = true;
|
|
9243
|
+
continue;
|
|
9244
|
+
}
|
|
9245
|
+
add(value);
|
|
9246
|
+
const url = urlOf(value);
|
|
9247
|
+
if (url && isClickTracker(url)) {
|
|
9248
|
+
state.tooComplex = true;
|
|
9151
9249
|
}
|
|
9152
9250
|
continue;
|
|
9153
9251
|
}
|
|
@@ -9169,42 +9267,60 @@ function carriedLinkHosts(mail, {
|
|
|
9169
9267
|
});
|
|
9170
9268
|
}
|
|
9171
9269
|
};
|
|
9172
|
-
for (
|
|
9173
|
-
if (
|
|
9270
|
+
for (let index = 0; ; index++) {
|
|
9271
|
+
if (full || late()) {
|
|
9174
9272
|
break;
|
|
9175
9273
|
}
|
|
9176
|
-
(
|
|
9177
|
-
|
|
9274
|
+
if (index >= queue.length) {
|
|
9275
|
+
const next = carried.next();
|
|
9276
|
+
if (next.done) {
|
|
9277
|
+
break;
|
|
9278
|
+
}
|
|
9279
|
+
queue.push({ document: next.value, depth: 0 });
|
|
9280
|
+
}
|
|
9281
|
+
({ depth } = queue[index]);
|
|
9282
|
+
const { document } = queue[index];
|
|
9178
9283
|
const html = withoutNul(typeof document?.html === "string" ? document.html : "");
|
|
9179
9284
|
const text = withoutNul(typeof document?.text === "string" ? document.text : "");
|
|
9180
9285
|
for (const [name, value] of attributesOf(html)) {
|
|
9181
|
-
if (
|
|
9286
|
+
if (done()) {
|
|
9182
9287
|
break;
|
|
9183
9288
|
}
|
|
9184
|
-
if (LINK_ATTRIBUTES.has(name)) {
|
|
9185
|
-
|
|
9289
|
+
if (!LINK_ATTRIBUTES.has(name)) {
|
|
9290
|
+
continue;
|
|
9186
9291
|
}
|
|
9292
|
+
if (spend(value.length)) {
|
|
9293
|
+
break;
|
|
9294
|
+
}
|
|
9295
|
+
linksOf(name, value, reader);
|
|
9187
9296
|
}
|
|
9188
|
-
lenientLinks(html,
|
|
9297
|
+
lenientLinks(html, done, reader);
|
|
9189
9298
|
for (const pattern of [CSS_URL, CSS_IMPORT]) {
|
|
9190
9299
|
for (const [, link] of html.matchAll(pattern)) {
|
|
9191
|
-
if (
|
|
9300
|
+
if (done() || spend(link.length)) {
|
|
9192
9301
|
break;
|
|
9193
9302
|
}
|
|
9194
|
-
|
|
9303
|
+
if (link.length > MAX_VALUE2) {
|
|
9304
|
+
state.tooComplex = true;
|
|
9305
|
+
}
|
|
9306
|
+
add(decodeEntities(link.slice(0, MAX_VALUE2), { attribute: true }));
|
|
9195
9307
|
}
|
|
9196
9308
|
}
|
|
9197
|
-
|
|
9198
|
-
|
|
9309
|
+
for (const chunk2 of chunksOf(text)) {
|
|
9310
|
+
if (full || late()) {
|
|
9311
|
+
break;
|
|
9312
|
+
}
|
|
9313
|
+
readText(chunk2.text, { ...chunk2, exact: true });
|
|
9199
9314
|
}
|
|
9200
9315
|
for (const chunk2 of chunksOf(html)) {
|
|
9201
|
-
if (
|
|
9316
|
+
if (full || late()) {
|
|
9202
9317
|
break;
|
|
9203
9318
|
}
|
|
9204
|
-
|
|
9319
|
+
const decoded = decodeEntities(chunk2.text);
|
|
9320
|
+
readText(decoded, { ...chunk2, exact: decoded.endsWith(chunk2.text.slice(-CHUNK_OVERLAP)) });
|
|
9205
9321
|
}
|
|
9206
9322
|
}
|
|
9207
|
-
if (!more && !incomplete) {
|
|
9323
|
+
if (!more && !state.incomplete) {
|
|
9208
9324
|
for (const name of loose) {
|
|
9209
9325
|
if (hosts.size >= maxHosts) {
|
|
9210
9326
|
break;
|
|
@@ -9218,8 +9334,8 @@ function carriedLinkHosts(mail, {
|
|
|
9218
9334
|
return {
|
|
9219
9335
|
hosts: [...hosts],
|
|
9220
9336
|
more,
|
|
9221
|
-
tooComplex,
|
|
9222
|
-
incomplete
|
|
9337
|
+
tooComplex: state.tooComplex,
|
|
9338
|
+
incomplete: state.incomplete
|
|
9223
9339
|
};
|
|
9224
9340
|
}
|
|
9225
9341
|
|
|
@@ -10120,7 +10236,7 @@ var SpamScanner = class {
|
|
|
10120
10236
|
// (`from`: the sender's domain; `carried`: the link hosts of the whole
|
|
10121
10237
|
// message and of what it carries, see carriedLinkHosts)
|
|
10122
10238
|
async checkLinks(links, config, hosts = [], options = {}) {
|
|
10123
|
-
const { from = "", carried = null } = typeof options === "string" ? { from: options } : options;
|
|
10239
|
+
const { from = "", carried = null } = typeof options === "string" ? { from: options } : options ?? {};
|
|
10124
10240
|
const phishing = [];
|
|
10125
10241
|
const homographs = [];
|
|
10126
10242
|
const seenHosts = /* @__PURE__ */ new Set();
|
|
@@ -10180,15 +10296,15 @@ var SpamScanner = class {
|
|
|
10180
10296
|
for (const host of carriedHosts) {
|
|
10181
10297
|
seenHosts.add(host);
|
|
10182
10298
|
}
|
|
10183
|
-
const
|
|
10299
|
+
const own = [...found.filter((link) => link.text), ...found.filter((link) => !link.text), ...bare].map((link) => lookupName(destination(link))).filter(Boolean);
|
|
10184
10300
|
const firsts = /* @__PURE__ */ new Map();
|
|
10185
|
-
for (const host of
|
|
10301
|
+
for (const host of [...own, ...carriedHosts]) {
|
|
10186
10302
|
const domain = registrableDomain(host);
|
|
10187
10303
|
if (!firsts.has(domain)) {
|
|
10188
10304
|
firsts.set(domain, host);
|
|
10189
10305
|
}
|
|
10190
10306
|
}
|
|
10191
|
-
const checked = [.../* @__PURE__ */ new Set([...firsts.values(), ...
|
|
10307
|
+
const checked = [.../* @__PURE__ */ new Set([...firsts.values(), ...own, ...carriedHosts])].slice(0, config.phishing.maxHosts);
|
|
10192
10308
|
const domains = [...new Set(checked.map((host) => registrableDomain(host)))];
|
|
10193
10309
|
const blocked = [];
|
|
10194
10310
|
let answered = 0;
|
|
@@ -10925,17 +11041,18 @@ async function readInput(file, stdin, maxSize) {
|
|
|
10925
11041
|
}
|
|
10926
11042
|
return data;
|
|
10927
11043
|
}
|
|
11044
|
+
var printable = (text) => String(text ?? "").replaceAll(/[\p{Cc}\u061C\u200E\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]+/gu, " ");
|
|
10928
11045
|
function formatResult(result, verbose) {
|
|
10929
11046
|
const lines2 = [`${result.isSpam ? "SPAM" : "HAM"} score ${result.score.toFixed(1)} (spam at ${result.threshold.toFixed(1)}, reject at ${result.rejectThreshold.toFixed(1)}) action: ${result.action}${result.language ? ` language: ${result.language}` : ""}`];
|
|
10930
11047
|
const tests = verbose ? result.tests : result.tests.filter((test) => test.score !== 0);
|
|
10931
11048
|
for (const test of [...tests].sort((a, b) => Math.abs(b.score) - Math.abs(a.score))) {
|
|
10932
|
-
lines2.push(` ${`${test.score > 0 ? "+" : ""}${test.score.toFixed(1)}`.padStart(7)} ${test.name.padEnd(28)} ${test.description}`);
|
|
11049
|
+
lines2.push(` ${`${test.score > 0 ? "+" : ""}${test.score.toFixed(1)}`.padStart(7)} ${printable(test.name).padEnd(28)} ${printable(test.description)}`);
|
|
10933
11050
|
}
|
|
10934
11051
|
if (verbose && result.results.classification.clues?.length > 0) {
|
|
10935
|
-
lines2.push(" Strongest clues:", ...result.results.classification.clues.slice(0, 10).map((clue) => ` ${clue.probability.toFixed(3)} ${clue.feature}`));
|
|
11052
|
+
lines2.push(" Strongest clues:", ...result.results.classification.clues.slice(0, 10).map((clue) => ` ${clue.probability.toFixed(3)} ${printable(clue.feature)}`));
|
|
10936
11053
|
}
|
|
10937
11054
|
if (result.results.llm?.error) {
|
|
10938
|
-
lines2.push(` Language model failed: ${result.results.llm.error}`);
|
|
11055
|
+
lines2.push(` Language model failed: ${printable(result.results.llm.error)}`);
|
|
10939
11056
|
}
|
|
10940
11057
|
return lines2.join("\n");
|
|
10941
11058
|
}
|
|
@@ -11073,11 +11190,11 @@ var HANDLERS = {
|
|
|
11073
11190
|
message = rewriteMessage(raw, spamHeaders(result, { version: VERSION }), { subjectTag: result.isSpam ? values["subject-tag"] ?? null : null });
|
|
11074
11191
|
} catch (error) {
|
|
11075
11192
|
if (!accept) {
|
|
11076
|
-
stderr.write(`filter: scan failed, deferring: ${error.message}
|
|
11193
|
+
stderr.write(`filter: scan failed, deferring: ${printable(error.message)}
|
|
11077
11194
|
`);
|
|
11078
11195
|
return 75;
|
|
11079
11196
|
}
|
|
11080
|
-
stderr.write(`filter: scan failed, passing the message on: ${error.message}
|
|
11197
|
+
stderr.write(`filter: scan failed, passing the message on: ${printable(error.message)}
|
|
11081
11198
|
`);
|
|
11082
11199
|
result = null;
|
|
11083
11200
|
message = rewriteMessage(raw, [["X-Spam-Virus-Scan", "failed"]]);
|
|
@@ -11110,10 +11227,10 @@ var HANDLERS = {
|
|
|
11110
11227
|
hostname: values.name || "spamscanner",
|
|
11111
11228
|
onError: onError(values)
|
|
11112
11229
|
});
|
|
11113
|
-
milter.on("error", (error) => stderr.write(`milter: ${error.message}
|
|
11230
|
+
milter.on("error", (error) => stderr.write(`milter: ${printable(error.message)}
|
|
11114
11231
|
`));
|
|
11115
11232
|
if (values.verbose) {
|
|
11116
|
-
milter.on("scan", ({ session, result }) => stderr.write(`${session.remoteAddress || "-"} ${session.envelope?.mailFrom?.address || "<>"} ${result.score.toFixed(1)} ${result.action} ${result.tests.map((test) => test.name).join(",")}
|
|
11233
|
+
milter.on("scan", ({ session, result }) => stderr.write(`${session.remoteAddress || "-"} ${printable(session.envelope?.mailFrom?.address) || "<>"} ${result.score.toFixed(1)} ${result.action} ${result.tests.map((test) => test.name).join(",")}
|
|
11117
11234
|
`));
|
|
11118
11235
|
}
|
|
11119
11236
|
await listen(milter.server, values, 7831, { stderr });
|