spamscanner 7.0.11 → 7.0.13
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 +274 -117
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/esm/arf.js.map +1 -1
- package/dist/esm/cli.js +278 -120
- package/dist/esm/cli.js.map +3 -3
- package/dist/esm/index.js +270 -113
- 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/index.js
CHANGED
|
@@ -1264,7 +1264,7 @@ var NAMED = [
|
|
|
1264
1264
|
",itilde=129,iukcy=456,iuml=ef,jcirc=135,jcy=439,jfr=1d527,jmath=237,jopf=1d55b,jscr=1d4bf,jsercy=458",
|
|
1265
1265
|
",jukcy=454,kappa=3ba,kappav=3f0,kcedil=137,kcy=43a,kfr=1d528,kgreen=138,khcy=445,kjcy=45c,kopf=1d55c",
|
|
1266
1266
|
",kscr=1d4c0,lAarr=21da,lArr=21d0,lAtail=291b,lBarr=290e,lE=2266,lEg=2a8b,lHar=2962,lacute=13a,laempt",
|
|
1267
|
-
"yv=29b4,lagran=2112,lambda=3bb,lang=
|
|
1267
|
+
"yv=29b4,lagran=2112,lambda=3bb,lang=27e8,langd=2991,langle=27e8,lap=2a85,laquo=ab,larr=2190,larrb=21",
|
|
1268
1268
|
"e4,larrbfs=291f,larrfs=291d,larrhk=21a9,larrlp=21ab,larrpl=2939,larrsim=2973,larrtl=21a2,lat=2aab,la",
|
|
1269
1269
|
"tail=2919,late=2aad,lates=2aad.fe00,lbarr=290c,lbbrk=2772,lbrace=7b,lbrack=5b,lbrke=298b,lbrksld=298",
|
|
1270
1270
|
"f,lbrkslu=298d,lcaron=13e,lcedil=13c,lceil=2308,lcub=7b,lcy=43b,ldca=2936,ldquo=201c,ldquor=201e,ldr",
|
|
@@ -1322,7 +1322,7 @@ var NAMED = [
|
|
|
1322
1322
|
"line=2312,profsurf=2313,prop=221d,propto=221d,prsim=227e,prurel=22b0,pscr=1d4c5,psi=3c8,puncsp=2008,",
|
|
1323
1323
|
"qfr=1d52e,qint=2a0c,qopf=1d562,qprime=2057,qscr=1d4c6,quaternions=210d,quatint=2a16,quest=3f,questeq",
|
|
1324
1324
|
"=225f,quot=22,rAarr=21db,rArr=21d2,rAtail=291c,rBarr=290f,rHar=2964,race=223d.331,racute=155,radic=2",
|
|
1325
|
-
"21a,raemptyv=29b3,rang=
|
|
1325
|
+
"21a,raemptyv=29b3,rang=27e9,rangd=2992,range=29a5,rangle=27e9,raquo=bb,rarr=2192,rarrap=2975,rarrb=2",
|
|
1326
1326
|
"1e5,rarrbfs=2920,rarrc=2933,rarrfs=291e,rarrhk=21aa,rarrlp=21ac,rarrpl=2945,rarrsim=2974,rarrtl=21a3",
|
|
1327
1327
|
",rarrw=219d,ratail=291a,ratio=2236,rationals=211a,rbarr=290d,rbbrk=2773,rbrace=7d,rbrack=5d,rbrke=29",
|
|
1328
1328
|
"8c,rbrksld=298e,rbrkslu=2990,rcaron=159,rcedil=157,rceil=2309,rcub=7d,rcy=440,rdca=2937,rdldhar=2969",
|
|
@@ -2265,6 +2265,34 @@ function continuesName(text, index) {
|
|
|
2265
2265
|
var FILE_TLDS = /* @__PURE__ */ new Set(["ai", "app", "md", "mov", "pm", "ps", "py", "rs", "sh", "so", "zip"]);
|
|
2266
2266
|
var LINK_FILE_TLDS = /* @__PURE__ */ new Set(["md", "mov", "pm", "ps", "py", "rs", "sh", "so", "zip"]);
|
|
2267
2267
|
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"];
|
|
2268
|
+
var MAX_URL_HOST = 4096;
|
|
2269
|
+
var AUTHORITY_END = /[/\\?#]/g;
|
|
2270
|
+
var IGNORED_IN_HOST = /[\u00AD\u034F\u180B-\u180D\u180F\u200B\u2060\u2064\uFE00-\uFE0F\uFEFF]|\uD82F[\uDCA0-\uDCA3]|\uDB40[\uDD00-\uDDEF]/g;
|
|
2271
|
+
function boundedUrl(address2) {
|
|
2272
|
+
const text = String(address2).replaceAll(/[\t\n\r]/g, "");
|
|
2273
|
+
const colon = text.indexOf(":");
|
|
2274
|
+
let start = colon === -1 ? 0 : colon + 1;
|
|
2275
|
+
while (start < text.length && (text[start] === "/" || text[start] === "\\")) {
|
|
2276
|
+
start++;
|
|
2277
|
+
}
|
|
2278
|
+
AUTHORITY_END.lastIndex = start;
|
|
2279
|
+
const end = AUTHORITY_END.exec(text)?.index ?? text.length;
|
|
2280
|
+
const file = /^[\0- ]*file:/i.test(text);
|
|
2281
|
+
const at = file ? start : Math.max(start, text.lastIndexOf("@", end - 1) + 1);
|
|
2282
|
+
const bracket = text.lastIndexOf("]", end - 1);
|
|
2283
|
+
const port = text.lastIndexOf(":", end - 1);
|
|
2284
|
+
let host = text.slice(at, !file && port > Math.max(at - 1, bracket) ? port : end);
|
|
2285
|
+
if (host.length > MAX_URL_HOST && host.includes("%")) {
|
|
2286
|
+
try {
|
|
2287
|
+
host = decodeURIComponent(host);
|
|
2288
|
+
} catch {
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (host.length > MAX_URL_HOST && host.replaceAll(IGNORED_IN_HOST, "").length > MAX_URL_HOST) {
|
|
2292
|
+
throw new TypeError("Invalid URL: host too long");
|
|
2293
|
+
}
|
|
2294
|
+
return new URL(text);
|
|
2295
|
+
}
|
|
2268
2296
|
var isUnder = (host, domain) => host === domain || host.endsWith(`.${domain}`);
|
|
2269
2297
|
function isClickTracker(url) {
|
|
2270
2298
|
const host = url.hostname.toLowerCase();
|
|
@@ -2293,7 +2321,7 @@ var MAX_GATEWAYS = 16;
|
|
|
2293
2321
|
function unwrapLink(address2) {
|
|
2294
2322
|
let url;
|
|
2295
2323
|
try {
|
|
2296
|
-
url =
|
|
2324
|
+
url = boundedUrl(address2);
|
|
2297
2325
|
} catch {
|
|
2298
2326
|
return null;
|
|
2299
2327
|
}
|
|
@@ -2304,7 +2332,7 @@ function unwrapLink(address2) {
|
|
|
2304
2332
|
}
|
|
2305
2333
|
let next;
|
|
2306
2334
|
try {
|
|
2307
|
-
next =
|
|
2335
|
+
next = boundedUrl(target);
|
|
2308
2336
|
} catch {
|
|
2309
2337
|
break;
|
|
2310
2338
|
}
|
|
@@ -2582,7 +2610,7 @@ function extractLinks(text, html, { maxLength = 5e5, bareHosts: bareHosts2 = fal
|
|
|
2582
2610
|
}
|
|
2583
2611
|
let url;
|
|
2584
2612
|
try {
|
|
2585
|
-
url =
|
|
2613
|
+
url = boundedUrl(href);
|
|
2586
2614
|
} catch {
|
|
2587
2615
|
return;
|
|
2588
2616
|
}
|
|
@@ -2907,7 +2935,7 @@ function isDeceptiveLink(link, { from } = {}) {
|
|
|
2907
2935
|
}
|
|
2908
2936
|
let shownUrl;
|
|
2909
2937
|
try {
|
|
2910
|
-
shownUrl =
|
|
2938
|
+
shownUrl = boundedUrl(/^https?:\/\//i.test(link.text) ? link.text : `http://${link.text}`);
|
|
2911
2939
|
} catch {
|
|
2912
2940
|
return false;
|
|
2913
2941
|
}
|
|
@@ -4154,7 +4182,7 @@ async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PA
|
|
|
4154
4182
|
}
|
|
4155
4183
|
}
|
|
4156
4184
|
async function expandAttachments(attachments = [], errors = [], parse = parseMessage, gets = {}) {
|
|
4157
|
-
const { unopened = [], mails = [] } = Array.isArray(gets) ? { unopened: gets } : gets;
|
|
4185
|
+
const { unopened = [], mails = [] } = Array.isArray(gets) ? { unopened: gets } : gets ?? {};
|
|
4158
4186
|
const all = [];
|
|
4159
4187
|
const deadline = Date.now() + PARSE_TIMEOUT;
|
|
4160
4188
|
let budget = MAX_NESTED_BYTES;
|
|
@@ -4222,6 +4250,11 @@ import { isIP } from "node:net";
|
|
|
4222
4250
|
import { domainToASCII } from "node:url";
|
|
4223
4251
|
import { getPublicSuffix } from "tldts";
|
|
4224
4252
|
var LOOKUP_LABEL = /^[a-z\d_-]{1,63}$/;
|
|
4253
|
+
var LABEL_SEPARATOR = /[.\u3002\uFF0E\uFF61]/g;
|
|
4254
|
+
function lastLabels(name) {
|
|
4255
|
+
const separators = [...name.matchAll(LABEL_SEPARATOR)];
|
|
4256
|
+
return separators.length < 128 ? 0 : separators.at(-128).index + 1;
|
|
4257
|
+
}
|
|
4225
4258
|
function lookupName(host) {
|
|
4226
4259
|
if (typeof host !== "string") {
|
|
4227
4260
|
return null;
|
|
@@ -4232,6 +4265,7 @@ function lookupName(host) {
|
|
|
4232
4265
|
}
|
|
4233
4266
|
let name = host.slice(0, end).toLowerCase();
|
|
4234
4267
|
if (/[\u0080-\u{10FFFF}]/u.test(name)) {
|
|
4268
|
+
name = name.slice(lastLabels(name));
|
|
4235
4269
|
name = domainToASCII(name) || name;
|
|
4236
4270
|
}
|
|
4237
4271
|
if (isIP(name.replaceAll(/^\[|]$/g, "")) !== 0) {
|
|
@@ -4571,7 +4605,7 @@ function asciiHostname(value) {
|
|
|
4571
4605
|
return null;
|
|
4572
4606
|
}
|
|
4573
4607
|
const lower = value.normalize("NFKC").toLowerCase().replace(/\.$/, "");
|
|
4574
|
-
if (lower === "" || /[\0-,/:-^`{-\u007F]/.test(lower)) {
|
|
4608
|
+
if (lower === "" || lower.length > 1024 || /[\0-,/:-^`{-\u007F]/.test(lower)) {
|
|
4575
4609
|
return null;
|
|
4576
4610
|
}
|
|
4577
4611
|
const ascii = domainToASCII2(lower);
|
|
@@ -4753,8 +4787,8 @@ function trustReputation(result, trusted = []) {
|
|
|
4753
4787
|
for (const value of result.checkedValues.filter((item) => allowed.has(item))) {
|
|
4754
4788
|
const signers = allowed.get(value);
|
|
4755
4789
|
const counts = (entry) => Boolean(entry) && (signers === null || signers.some((signer) => vouches(normalizeEntry(entry), signer)));
|
|
4756
|
-
const detail = result.details[value];
|
|
4757
|
-
const listed = result.allowlisted[value] || (detail?.isAllowlisted ? detail.allowlistValue : null);
|
|
4790
|
+
const detail = Object.hasOwn(result.details, value) ? result.details[value] : void 0;
|
|
4791
|
+
const listed = Object.hasOwn(result.allowlisted, value) && result.allowlisted[value] || (detail?.isAllowlisted ? detail.allowlistValue : null);
|
|
4758
4792
|
if (counts(listed) && !out.isAllowlisted) {
|
|
4759
4793
|
out.isAllowlisted = true;
|
|
4760
4794
|
out.allowlistValue = listed;
|
|
@@ -5661,7 +5695,8 @@ var MAX_EMBEDDED_LENGTH = 10 * 1024 * 1024;
|
|
|
5661
5695
|
var MAX_ATTACHED = 100;
|
|
5662
5696
|
var MAX_ATTACHED_BYTES = 50 * 1024 * 1024;
|
|
5663
5697
|
var MAX_READ_TIME = 5e3;
|
|
5664
|
-
var MAX_SHOWN_BYTES =
|
|
5698
|
+
var MAX_SHOWN_BYTES = 1024 * 1024;
|
|
5699
|
+
var MAX_CARD_TEXT = 8 * 1024 * 1024;
|
|
5665
5700
|
function isHostCode2(code) {
|
|
5666
5701
|
return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 45 || code === 46;
|
|
5667
5702
|
}
|
|
@@ -5690,7 +5725,7 @@ function urlOf(value) {
|
|
|
5690
5725
|
return null;
|
|
5691
5726
|
}
|
|
5692
5727
|
try {
|
|
5693
|
-
return
|
|
5728
|
+
return boundedUrl(href);
|
|
5694
5729
|
} catch {
|
|
5695
5730
|
return null;
|
|
5696
5731
|
}
|
|
@@ -5780,7 +5815,26 @@ function* attributesOf(html) {
|
|
|
5780
5815
|
TAG_START.lastIndex = Math.max(i, match.index + 1);
|
|
5781
5816
|
}
|
|
5782
5817
|
}
|
|
5783
|
-
|
|
5818
|
+
var MAX_VALUE2 = 1024 * 1024;
|
|
5819
|
+
var MAX_REFERENCES = 64 * 1024;
|
|
5820
|
+
function isCut(value) {
|
|
5821
|
+
if (value.length <= MAX_VALUE2) {
|
|
5822
|
+
return false;
|
|
5823
|
+
}
|
|
5824
|
+
let references = 0;
|
|
5825
|
+
for (let index = value.indexOf("&"); index !== -1; index = value.indexOf("&", index + 1)) {
|
|
5826
|
+
if (++references > MAX_REFERENCES) {
|
|
5827
|
+
return true;
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5830
|
+
return false;
|
|
5831
|
+
}
|
|
5832
|
+
var LONG_VALUE = 64 * 1024;
|
|
5833
|
+
function linksOf(name, value, { add, embed, cut }) {
|
|
5834
|
+
if (isCut(value)) {
|
|
5835
|
+
cut();
|
|
5836
|
+
value = value.slice(0, MAX_VALUE2);
|
|
5837
|
+
}
|
|
5784
5838
|
const decoded = decodeEntities(value, { attribute: true });
|
|
5785
5839
|
switch (name) {
|
|
5786
5840
|
case "srcdoc": {
|
|
@@ -5821,7 +5875,7 @@ function linksOf(name, value, add, embed) {
|
|
|
5821
5875
|
}
|
|
5822
5876
|
var MAX_LENIENT_VALUE = 4096;
|
|
5823
5877
|
var UNQUOTED_END = /* @__PURE__ */ new Set(["<", ">", '"', "'", "`", "="]);
|
|
5824
|
-
function lenientLinks(html,
|
|
5878
|
+
function lenientLinks(html, done, reader) {
|
|
5825
5879
|
LENIENT.lastIndex = 0;
|
|
5826
5880
|
let match;
|
|
5827
5881
|
while (!done() && (match = LENIENT.exec(html)) !== null) {
|
|
@@ -5837,8 +5891,8 @@ function lenientLinks(html, add, done, embed) {
|
|
|
5837
5891
|
}
|
|
5838
5892
|
if (end < html.length && html[end] !== quote) {
|
|
5839
5893
|
const close = html.indexOf(quote, end);
|
|
5840
|
-
if (close !== -1 && close - i <= MAX_LENIENT_VALUE) {
|
|
5841
|
-
linksOf(name, html.slice(i, close),
|
|
5894
|
+
if (close !== -1 && close - i <= MAX_LENIENT_VALUE && !reader.spend(close - i)) {
|
|
5895
|
+
linksOf(name, html.slice(i, close), reader);
|
|
5842
5896
|
}
|
|
5843
5897
|
}
|
|
5844
5898
|
} else {
|
|
@@ -5846,7 +5900,10 @@ function lenientLinks(html, add, done, embed) {
|
|
|
5846
5900
|
end++;
|
|
5847
5901
|
}
|
|
5848
5902
|
}
|
|
5849
|
-
|
|
5903
|
+
if (reader.spend(end - i)) {
|
|
5904
|
+
break;
|
|
5905
|
+
}
|
|
5906
|
+
linksOf(name, html.slice(i, end), reader);
|
|
5850
5907
|
}
|
|
5851
5908
|
}
|
|
5852
5909
|
function endsLink2(text, i) {
|
|
@@ -6017,8 +6074,17 @@ function* chunksOf(html) {
|
|
|
6017
6074
|
cutStart = false;
|
|
6018
6075
|
continue;
|
|
6019
6076
|
}
|
|
6020
|
-
|
|
6021
|
-
start
|
|
6077
|
+
let hard = end;
|
|
6078
|
+
while (hard > start + CHUNK_OVERLAP + 1 && /[\dA-Za-z#]/.test(html[hard - 1])) {
|
|
6079
|
+
hard--;
|
|
6080
|
+
}
|
|
6081
|
+
if (hard < end && html[hard - 1] === "&" && hard - 1 > start + CHUNK_OVERLAP) {
|
|
6082
|
+
hard--;
|
|
6083
|
+
} else {
|
|
6084
|
+
hard = end;
|
|
6085
|
+
}
|
|
6086
|
+
yield { text: html.slice(start, hard), cutStart, cutEnd: true };
|
|
6087
|
+
start = hard - CHUNK_OVERLAP;
|
|
6022
6088
|
cutStart = true;
|
|
6023
6089
|
continue;
|
|
6024
6090
|
}
|
|
@@ -6241,40 +6307,50 @@ function htmlDescriptions(text) {
|
|
|
6241
6307
|
return html.join("\n");
|
|
6242
6308
|
}
|
|
6243
6309
|
var BINARY = /;\s*(?:(?:encoding\s*=\s*)?"?(?:b|base64)"?|value\s*=\s*"?binary"?)\s*(?:;|$)/i;
|
|
6244
|
-
function withoutBinaryValues(text) {
|
|
6310
|
+
function withoutBinaryValues(text, limit = Number.POSITIVE_INFINITY) {
|
|
6245
6311
|
if (!/base64|binary|encoding\s*=\s*"?b|;\s*b\s*[;:]/i.test(text)) {
|
|
6246
6312
|
return text;
|
|
6247
6313
|
}
|
|
6248
6314
|
const lines2 = text.split(LINE_BREAK);
|
|
6249
6315
|
const out = [];
|
|
6316
|
+
let length = 0;
|
|
6317
|
+
const push = (line) => {
|
|
6318
|
+
out.push(line);
|
|
6319
|
+
length += line.length + 1;
|
|
6320
|
+
};
|
|
6250
6321
|
for (let i = 0; i < lines2.length; i++) {
|
|
6322
|
+
if (length > limit) {
|
|
6323
|
+
break;
|
|
6324
|
+
}
|
|
6251
6325
|
const line = lines2[i];
|
|
6252
6326
|
const colon = valueColon(line);
|
|
6253
6327
|
if (colon === -1 || !BINARY.test(line.slice(0, colon))) {
|
|
6254
|
-
|
|
6328
|
+
push(line);
|
|
6255
6329
|
continue;
|
|
6256
6330
|
}
|
|
6257
|
-
|
|
6331
|
+
let base64 = /^[\w+/=\s]*$/.test(line.slice(colon + 1));
|
|
6258
6332
|
let next = i + 1;
|
|
6259
6333
|
while (next < lines2.length && (lines2[next].startsWith(" ") || lines2[next].startsWith(" "))) {
|
|
6260
|
-
|
|
6334
|
+
base64 &&= /^[\w+/=\s]*$/.test(lines2[next]);
|
|
6335
|
+
next++;
|
|
6261
6336
|
}
|
|
6262
|
-
if (!
|
|
6337
|
+
if (!base64) {
|
|
6263
6338
|
for (let index = i; index < next; index++) {
|
|
6264
|
-
|
|
6339
|
+
push(lines2[index]);
|
|
6265
6340
|
}
|
|
6266
6341
|
i = next - 1;
|
|
6267
6342
|
continue;
|
|
6268
6343
|
}
|
|
6269
6344
|
while (next < lines2.length && lines2[next] !== "" && /^[\w+/=]+$/.test(lines2[next])) {
|
|
6270
|
-
|
|
6345
|
+
next++;
|
|
6271
6346
|
}
|
|
6347
|
+
const end = next;
|
|
6272
6348
|
if (next < lines2.length && lines2[next] === "") {
|
|
6273
6349
|
next++;
|
|
6274
6350
|
}
|
|
6275
|
-
i = next - 1;
|
|
6276
6351
|
const name = line.slice(0, colon).split(";")[0].split(".").pop().trim().toLowerCase();
|
|
6277
|
-
|
|
6352
|
+
push(MEDIA_PROPERTIES.has(name) ? line.slice(0, colon + 1) : line.slice(0, colon + 1) + Buffer7.from([line.slice(colon + 1), ...lines2.slice(i + 1, end)].join("").replaceAll(/\s/g, ""), "base64").toString("utf8"));
|
|
6353
|
+
i = next - 1;
|
|
6278
6354
|
}
|
|
6279
6355
|
return out.join("\n");
|
|
6280
6356
|
}
|
|
@@ -6313,59 +6389,77 @@ function charsetOf(attachment) {
|
|
|
6313
6389
|
const charset = headers && typeof headers.get === "function" ? headers.get("content-type")?.params?.charset : null;
|
|
6314
6390
|
return typeof charset === "string" ? charset.toLowerCase() : "";
|
|
6315
6391
|
}
|
|
6316
|
-
function
|
|
6317
|
-
const
|
|
6392
|
+
function kindOf(attachment) {
|
|
6393
|
+
const { content } = attachment ?? {};
|
|
6394
|
+
if (!Buffer7.isBuffer(content) || content.length === 0) {
|
|
6395
|
+
return null;
|
|
6396
|
+
}
|
|
6397
|
+
const type = typeof attachment.contentType === "string" ? attachment.contentType.toLowerCase() : "";
|
|
6398
|
+
const extensions = extensionsOf(attachment);
|
|
6399
|
+
const has = (set) => [...extensions].some((name) => set.has(name));
|
|
6400
|
+
if (MESSAGE_TYPES.has(type) || extensions.has("eml") || extensions.has("u8msg")) {
|
|
6401
|
+
return null;
|
|
6402
|
+
}
|
|
6403
|
+
const isHtml = HTML_TYPES.has(type) || has(HTML_EXTENSIONS);
|
|
6404
|
+
const isCard = !isHtml && (CARD_TYPES.has(type) || has(CARD_EXTENSIONS));
|
|
6405
|
+
const isText = isCard || !isHtml && type === "text/plain";
|
|
6406
|
+
return isHtml || isText ? { isHtml, isText, isCard } : null;
|
|
6407
|
+
}
|
|
6408
|
+
function* documentsOf(attachments, state, late) {
|
|
6409
|
+
const read = [];
|
|
6318
6410
|
let bytes = 0;
|
|
6319
6411
|
let opened = 0;
|
|
6320
6412
|
for (const attachment of attachments) {
|
|
6321
|
-
const
|
|
6322
|
-
if (!
|
|
6323
|
-
continue;
|
|
6324
|
-
}
|
|
6325
|
-
const type = typeof attachment.contentType === "string" ? attachment.contentType.toLowerCase() : "";
|
|
6326
|
-
const extensions = extensionsOf(attachment);
|
|
6327
|
-
const has = (set) => [...extensions].some((name) => set.has(name));
|
|
6328
|
-
if (MESSAGE_TYPES.has(type) || extensions.has("eml") || extensions.has("u8msg")) {
|
|
6413
|
+
const kind = kindOf(attachment);
|
|
6414
|
+
if (!kind) {
|
|
6329
6415
|
continue;
|
|
6330
6416
|
}
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
if (!isHtml && !isText) {
|
|
6335
|
-
continue;
|
|
6336
|
-
}
|
|
6337
|
-
if (!isText && ++opened > MAX_ATTACHED || bytes + content.length > MAX_ATTACHED_BYTES) {
|
|
6338
|
-
documents.tooComplex = true;
|
|
6339
|
-
return documents;
|
|
6417
|
+
if (!kind.isText && ++opened > MAX_ATTACHED || bytes + attachment.content.length > MAX_ATTACHED_BYTES) {
|
|
6418
|
+
state.tooComplex = true;
|
|
6419
|
+
break;
|
|
6340
6420
|
}
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6421
|
+
bytes += attachment.content.length;
|
|
6422
|
+
read.push({ attachment, kind });
|
|
6423
|
+
}
|
|
6424
|
+
for (const { attachment, kind: { isHtml, isCard } } of read) {
|
|
6425
|
+
if (late()) {
|
|
6426
|
+
state.incomplete = true;
|
|
6427
|
+
return;
|
|
6344
6428
|
}
|
|
6345
|
-
|
|
6346
|
-
const text = decodeText(content, charsetOf(attachment));
|
|
6429
|
+
const text = decodeText(attachment.content, charsetOf(attachment));
|
|
6347
6430
|
if (isHtml) {
|
|
6348
|
-
|
|
6431
|
+
yield { html: text };
|
|
6349
6432
|
continue;
|
|
6350
6433
|
}
|
|
6351
6434
|
if (!isCard) {
|
|
6352
|
-
|
|
6435
|
+
yield { text };
|
|
6353
6436
|
continue;
|
|
6354
6437
|
}
|
|
6355
|
-
|
|
6438
|
+
let readable = withoutBinaryValues(text, MAX_CARD_TEXT);
|
|
6439
|
+
if (readable.length > MAX_CARD_TEXT) {
|
|
6440
|
+
state.tooComplex = true;
|
|
6441
|
+
readable = readable.slice(0, MAX_CARD_TEXT);
|
|
6442
|
+
}
|
|
6356
6443
|
const full = readable.length <= MAX_SHOWN_BYTES;
|
|
6357
6444
|
if (!full && hasQuotedPrintable(readable)) {
|
|
6358
|
-
|
|
6359
|
-
|
|
6445
|
+
state.tooComplex = true;
|
|
6446
|
+
}
|
|
6447
|
+
if (late()) {
|
|
6448
|
+
state.incomplete = true;
|
|
6449
|
+
return;
|
|
6450
|
+
}
|
|
6451
|
+
yield { text: shownText(readable, full) };
|
|
6452
|
+
if (late()) {
|
|
6453
|
+
state.incomplete = true;
|
|
6454
|
+
return;
|
|
6360
6455
|
}
|
|
6361
|
-
documents.push({ text: shownText(readable, full) });
|
|
6362
6456
|
const html = htmlDescriptions(readable);
|
|
6363
6457
|
if (html !== "") {
|
|
6364
|
-
|
|
6458
|
+
yield { html };
|
|
6365
6459
|
}
|
|
6366
6460
|
}
|
|
6367
|
-
return documents;
|
|
6368
6461
|
}
|
|
6462
|
+
var LATE_EVERY = 1024;
|
|
6369
6463
|
function carriedLinkHosts(mail, {
|
|
6370
6464
|
mails = [],
|
|
6371
6465
|
attachments = [],
|
|
@@ -6378,40 +6472,45 @@ function carriedLinkHosts(mail, {
|
|
|
6378
6472
|
const seenBare = /* @__PURE__ */ new Set();
|
|
6379
6473
|
const seenLoose = /* @__PURE__ */ new Set();
|
|
6380
6474
|
let more = false;
|
|
6381
|
-
let
|
|
6382
|
-
const
|
|
6383
|
-
const
|
|
6475
|
+
let full = false;
|
|
6476
|
+
const domains = /* @__PURE__ */ new Set();
|
|
6477
|
+
const state = { tooComplex: false, incomplete: false };
|
|
6478
|
+
const until = Date.now() + MAX_READ_TIME;
|
|
6479
|
+
const late = () => {
|
|
6480
|
+
if (!state.incomplete && Date.now() > until) {
|
|
6481
|
+
state.incomplete = true;
|
|
6482
|
+
}
|
|
6483
|
+
return state.incomplete;
|
|
6484
|
+
};
|
|
6485
|
+
let calls = 0;
|
|
6486
|
+
const done = () => full || state.incomplete || ++calls % LATE_EVERY === 0 && late();
|
|
6487
|
+
const looseDone = () => done() || loose.size > maxHosts;
|
|
6384
6488
|
const addLoose = (host) => {
|
|
6385
6489
|
if (!looseDone()) {
|
|
6386
6490
|
loose.add(host);
|
|
6387
6491
|
}
|
|
6388
6492
|
};
|
|
6389
|
-
const
|
|
6390
|
-
|
|
6391
|
-
const late = () => {
|
|
6392
|
-
if (!incomplete && Date.now() > until) {
|
|
6393
|
-
incomplete = true;
|
|
6394
|
-
}
|
|
6395
|
-
return incomplete;
|
|
6396
|
-
};
|
|
6397
|
-
const carried = documentsOf(attachments, until);
|
|
6398
|
-
tooComplex = Boolean(carried.tooComplex);
|
|
6399
|
-
incomplete = Boolean(carried.incomplete);
|
|
6400
|
-
const queue = [mail, ...mails, ...carried].map((document) => ({ document, depth: 0 }));
|
|
6493
|
+
const queue = [mail, ...mails].map((document) => ({ document, depth: 0 }));
|
|
6494
|
+
const carried = documentsOf(attachments, state, late);
|
|
6401
6495
|
let depth = 0;
|
|
6402
6496
|
let embedded = 0;
|
|
6403
6497
|
let embeddedLength = 0;
|
|
6498
|
+
const embeddedSeen = /* @__PURE__ */ new Set();
|
|
6404
6499
|
const embed = (html) => {
|
|
6500
|
+
if (html === "" || embeddedSeen.has(html)) {
|
|
6501
|
+
return;
|
|
6502
|
+
}
|
|
6405
6503
|
if (depth >= MAX_EMBED_DEPTH || embedded >= MAX_EMBEDDED || embeddedLength + html.length > MAX_EMBEDDED_LENGTH) {
|
|
6406
|
-
tooComplex = true;
|
|
6504
|
+
state.tooComplex = true;
|
|
6407
6505
|
return;
|
|
6408
6506
|
}
|
|
6507
|
+
embeddedSeen.add(html);
|
|
6409
6508
|
embedded++;
|
|
6410
6509
|
embeddedLength += html.length;
|
|
6411
6510
|
queue.push({ document: { html }, depth: depth + 1 });
|
|
6412
6511
|
};
|
|
6413
6512
|
const add = (value) => {
|
|
6414
|
-
if (
|
|
6513
|
+
if (full || typeof value !== "string" || value === "" || seen.has(value)) {
|
|
6415
6514
|
return;
|
|
6416
6515
|
}
|
|
6417
6516
|
if (seen.size < MAX_SEEN) {
|
|
@@ -6426,34 +6525,64 @@ function carriedLinkHosts(mail, {
|
|
|
6426
6525
|
}
|
|
6427
6526
|
const found = hostsOf(value);
|
|
6428
6527
|
if (found.unresolved) {
|
|
6429
|
-
tooComplex = true;
|
|
6528
|
+
state.tooComplex = true;
|
|
6430
6529
|
}
|
|
6431
6530
|
for (const host of found) {
|
|
6432
6531
|
if (hosts.has(host)) {
|
|
6433
6532
|
continue;
|
|
6434
6533
|
}
|
|
6435
|
-
|
|
6436
|
-
|
|
6534
|
+
const domain = registrableDomain(host);
|
|
6535
|
+
if (hosts.size < maxHosts || !domains.has(domain) && domains.size < maxHosts) {
|
|
6536
|
+
hosts.add(host);
|
|
6537
|
+
domains.add(domain);
|
|
6538
|
+
continue;
|
|
6539
|
+
}
|
|
6540
|
+
more = true;
|
|
6541
|
+
if (domains.size >= maxHosts) {
|
|
6542
|
+
full = true;
|
|
6437
6543
|
return;
|
|
6438
6544
|
}
|
|
6439
|
-
hosts.add(host);
|
|
6440
6545
|
}
|
|
6441
6546
|
};
|
|
6442
|
-
|
|
6547
|
+
let spent = 0;
|
|
6548
|
+
const spend = (length) => {
|
|
6549
|
+
spent += length;
|
|
6550
|
+
if (spent < LONG_VALUE) {
|
|
6551
|
+
return false;
|
|
6552
|
+
}
|
|
6553
|
+
spent = 0;
|
|
6554
|
+
return late();
|
|
6555
|
+
};
|
|
6556
|
+
const reader = {
|
|
6557
|
+
add,
|
|
6558
|
+
embed,
|
|
6559
|
+
spend,
|
|
6560
|
+
cut() {
|
|
6561
|
+
state.tooComplex = true;
|
|
6562
|
+
}
|
|
6563
|
+
};
|
|
6564
|
+
const readText = (source, { cutStart = false, cutEnd = false, exact = false } = {}) => {
|
|
6443
6565
|
for (const match of source.matchAll(URL_START)) {
|
|
6444
|
-
if (
|
|
6566
|
+
if (done()) {
|
|
6445
6567
|
break;
|
|
6446
6568
|
}
|
|
6447
6569
|
if (cutStart && match.index === 0) {
|
|
6448
6570
|
continue;
|
|
6449
6571
|
}
|
|
6450
6572
|
if (cutEnd && match.index + match[0].length === source.length) {
|
|
6573
|
+
if (exact && source.length - match.index < CHUNK_OVERLAP) {
|
|
6574
|
+
continue;
|
|
6575
|
+
}
|
|
6451
6576
|
const value = match[0];
|
|
6452
6577
|
const at = /^www\./i.test(value) ? 0 : value.indexOf("//") + 2;
|
|
6453
|
-
if (
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6578
|
+
if (!/[/?#\\]/.test(value.slice(at))) {
|
|
6579
|
+
state.tooComplex = true;
|
|
6580
|
+
continue;
|
|
6581
|
+
}
|
|
6582
|
+
add(value);
|
|
6583
|
+
const url = urlOf(value);
|
|
6584
|
+
if (url && isClickTracker(url)) {
|
|
6585
|
+
state.tooComplex = true;
|
|
6457
6586
|
}
|
|
6458
6587
|
continue;
|
|
6459
6588
|
}
|
|
@@ -6475,42 +6604,61 @@ function carriedLinkHosts(mail, {
|
|
|
6475
6604
|
});
|
|
6476
6605
|
}
|
|
6477
6606
|
};
|
|
6478
|
-
for (
|
|
6479
|
-
if (
|
|
6607
|
+
for (let index = 0; ; index++) {
|
|
6608
|
+
if (full || late()) {
|
|
6480
6609
|
break;
|
|
6481
6610
|
}
|
|
6482
|
-
(
|
|
6483
|
-
|
|
6611
|
+
if (index >= queue.length) {
|
|
6612
|
+
const next = carried.next();
|
|
6613
|
+
if (next.done) {
|
|
6614
|
+
break;
|
|
6615
|
+
}
|
|
6616
|
+
queue.push({ document: next.value, depth: 0 });
|
|
6617
|
+
}
|
|
6618
|
+
({ depth } = queue[index]);
|
|
6619
|
+
const { document } = queue[index];
|
|
6484
6620
|
const html = withoutNul(typeof document?.html === "string" ? document.html : "");
|
|
6485
6621
|
const text = withoutNul(typeof document?.text === "string" ? document.text : "");
|
|
6486
6622
|
for (const [name, value] of attributesOf(html)) {
|
|
6487
|
-
if (
|
|
6623
|
+
if (done()) {
|
|
6488
6624
|
break;
|
|
6489
6625
|
}
|
|
6490
|
-
if (LINK_ATTRIBUTES.has(name)) {
|
|
6491
|
-
|
|
6626
|
+
if (!LINK_ATTRIBUTES.has(name)) {
|
|
6627
|
+
continue;
|
|
6628
|
+
}
|
|
6629
|
+
if (spend(value.length)) {
|
|
6630
|
+
break;
|
|
6492
6631
|
}
|
|
6632
|
+
linksOf(name, value, reader);
|
|
6493
6633
|
}
|
|
6494
|
-
lenientLinks(html,
|
|
6634
|
+
lenientLinks(html, done, reader);
|
|
6495
6635
|
for (const pattern of [CSS_URL, CSS_IMPORT]) {
|
|
6496
6636
|
for (const [, link] of html.matchAll(pattern)) {
|
|
6497
|
-
if (
|
|
6637
|
+
if (done() || spend(link.length)) {
|
|
6498
6638
|
break;
|
|
6499
6639
|
}
|
|
6500
|
-
|
|
6640
|
+
const cut = isCut(link);
|
|
6641
|
+
if (cut) {
|
|
6642
|
+
state.tooComplex = true;
|
|
6643
|
+
}
|
|
6644
|
+
add(decodeEntities(cut ? link.slice(0, MAX_VALUE2) : link, { attribute: true }));
|
|
6501
6645
|
}
|
|
6502
6646
|
}
|
|
6503
|
-
|
|
6504
|
-
|
|
6647
|
+
for (const chunk2 of chunksOf(text)) {
|
|
6648
|
+
if (full || late()) {
|
|
6649
|
+
break;
|
|
6650
|
+
}
|
|
6651
|
+
readText(chunk2.text, { ...chunk2, exact: true });
|
|
6505
6652
|
}
|
|
6506
6653
|
for (const chunk2 of chunksOf(html)) {
|
|
6507
|
-
if (
|
|
6654
|
+
if (full || late()) {
|
|
6508
6655
|
break;
|
|
6509
6656
|
}
|
|
6510
|
-
|
|
6657
|
+
const decoded = decodeEntities(chunk2.text);
|
|
6658
|
+
readText(decoded, { ...chunk2, exact: decoded.endsWith(chunk2.text.slice(-CHUNK_OVERLAP)) });
|
|
6511
6659
|
}
|
|
6512
6660
|
}
|
|
6513
|
-
if (!more && !incomplete) {
|
|
6661
|
+
if (!more && !state.incomplete) {
|
|
6514
6662
|
for (const name of loose) {
|
|
6515
6663
|
if (hosts.size >= maxHosts) {
|
|
6516
6664
|
break;
|
|
@@ -6524,8 +6672,8 @@ function carriedLinkHosts(mail, {
|
|
|
6524
6672
|
return {
|
|
6525
6673
|
hosts: [...hosts],
|
|
6526
6674
|
more,
|
|
6527
|
-
tooComplex,
|
|
6528
|
-
incomplete
|
|
6675
|
+
tooComplex: state.tooComplex,
|
|
6676
|
+
incomplete: state.incomplete
|
|
6529
6677
|
};
|
|
6530
6678
|
}
|
|
6531
6679
|
|
|
@@ -8171,7 +8319,7 @@ function scoreResults(results, options) {
|
|
|
8171
8319
|
var package_default = {
|
|
8172
8320
|
name: "spamscanner",
|
|
8173
8321
|
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.",
|
|
8174
|
-
version: "7.0.
|
|
8322
|
+
version: "7.0.13",
|
|
8175
8323
|
author: "Forward Email (https://forwardemail.net)",
|
|
8176
8324
|
bin: {
|
|
8177
8325
|
spamscanner: "./dist/esm/cli.js"
|
|
@@ -8436,9 +8584,12 @@ function rewriteMessage(source, headers = [], options = {}) {
|
|
|
8436
8584
|
}
|
|
8437
8585
|
}
|
|
8438
8586
|
const added = headers.map(([name, value]) => foldHeader(name, Buffer9.from(String(value), "utf8").toString("latin1"), newline));
|
|
8439
|
-
const mbox = kept.length > 0 && kept[0][0].startsWith("From ") ? kept.shift() : [];
|
|
8587
|
+
const mbox = kept.length > 0 && kept[0][0].startsWith("From ") ? kept.shift().slice(0, 1) : [];
|
|
8588
|
+
if (kept.length > 0 && /^[ \t]/.test(kept[0][0])) {
|
|
8589
|
+
kept.shift();
|
|
8590
|
+
}
|
|
8440
8591
|
const head = [...mbox, ...added, ...kept.map((field) => field.join(newline))].join(newline);
|
|
8441
|
-
return Buffer9.concat([Buffer9.from(head === ""
|
|
8592
|
+
return Buffer9.concat([Buffer9.from(head === "" ? newline : `${head}${newline}${newline}`, "latin1"), body]);
|
|
8442
8593
|
}
|
|
8443
8594
|
|
|
8444
8595
|
// src/train.js
|
|
@@ -8787,7 +8938,12 @@ import net2 from "node:net";
|
|
|
8787
8938
|
var MAX_MESSAGE_SIZE = 25 * 1024 * 1024;
|
|
8788
8939
|
var MAX_CONNECTIONS = 200;
|
|
8789
8940
|
function serializeResult(result, options = {}) {
|
|
8790
|
-
const {
|
|
8941
|
+
const {
|
|
8942
|
+
mail,
|
|
8943
|
+
tokens,
|
|
8944
|
+
attached,
|
|
8945
|
+
...rest
|
|
8946
|
+
} = result;
|
|
8791
8947
|
const output = {
|
|
8792
8948
|
...rest,
|
|
8793
8949
|
results: { ...result.results }
|
|
@@ -9448,6 +9604,7 @@ function reply(code, text, headers = [], body = null) {
|
|
|
9448
9604
|
lines2.push(...headers.map(([name, value]) => `${name}: ${value}`), "", "");
|
|
9449
9605
|
return body === null ? Buffer13.from(lines2.join("\r\n")) : Buffer13.concat([Buffer13.from(lines2.join("\r\n")), body]);
|
|
9450
9606
|
}
|
|
9607
|
+
var oneLine2 = (text) => String(text ?? "").replaceAll(/[\p{Cc}\u061C\u200E\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]+/gu, " ");
|
|
9451
9608
|
function report(result) {
|
|
9452
9609
|
const lines2 = [
|
|
9453
9610
|
`Spam detection software, running on the system "spamscanner", has ${result.isSpam ? "identified this incoming email as possible spam" : "scanned this incoming email"}.`,
|
|
@@ -9456,7 +9613,7 @@ function report(result) {
|
|
|
9456
9613
|
"",
|
|
9457
9614
|
" pts rule name description",
|
|
9458
9615
|
"---- ---------------------- --------------------------------------------------",
|
|
9459
|
-
...result.tests.map((test) => `${test.score.toFixed(1).padStart(4)} ${test.name.padEnd(22)} ${test.description}`)
|
|
9616
|
+
...result.tests.map((test) => `${test.score.toFixed(1).padStart(4)} ${oneLine2(test.name).padEnd(22)} ${oneLine2(test.description)}`)
|
|
9460
9617
|
];
|
|
9461
9618
|
return `${lines2.join("\r\n")}\r
|
|
9462
9619
|
`;
|
|
@@ -10091,7 +10248,7 @@ var SpamScanner = class {
|
|
|
10091
10248
|
// (`from`: the sender's domain; `carried`: the link hosts of the whole
|
|
10092
10249
|
// message and of what it carries, see carriedLinkHosts)
|
|
10093
10250
|
async checkLinks(links, config, hosts = [], options = {}) {
|
|
10094
|
-
const { from = "", carried = null } = typeof options === "string" ? { from: options } : options;
|
|
10251
|
+
const { from = "", carried = null } = typeof options === "string" ? { from: options } : options ?? {};
|
|
10095
10252
|
const phishing = [];
|
|
10096
10253
|
const homographs = [];
|
|
10097
10254
|
const seenHosts = /* @__PURE__ */ new Set();
|
|
@@ -10151,15 +10308,15 @@ var SpamScanner = class {
|
|
|
10151
10308
|
for (const host of carriedHosts) {
|
|
10152
10309
|
seenHosts.add(host);
|
|
10153
10310
|
}
|
|
10154
|
-
const
|
|
10311
|
+
const own = [...found.filter((link) => link.text), ...found.filter((link) => !link.text), ...bare].map((link) => lookupName(destination(link))).filter(Boolean);
|
|
10155
10312
|
const firsts = /* @__PURE__ */ new Map();
|
|
10156
|
-
for (const host of
|
|
10313
|
+
for (const host of [...own, ...carriedHosts]) {
|
|
10157
10314
|
const domain = registrableDomain(host);
|
|
10158
10315
|
if (!firsts.has(domain)) {
|
|
10159
10316
|
firsts.set(domain, host);
|
|
10160
10317
|
}
|
|
10161
10318
|
}
|
|
10162
|
-
const checked = [.../* @__PURE__ */ new Set([...firsts.values(), ...
|
|
10319
|
+
const checked = [.../* @__PURE__ */ new Set([...firsts.values(), ...own, ...carriedHosts])].slice(0, config.phishing.maxHosts);
|
|
10163
10320
|
const domains = [...new Set(checked.map((host) => registrableDomain(host)))];
|
|
10164
10321
|
const blocked = [];
|
|
10165
10322
|
let answered = 0;
|