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/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
|
|
5698
|
+
var MAX_CARD_BYTES = 4 * 1024 * 1024;
|
|
5699
|
+
var MAX_SHOWN_BYTES = 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,13 @@ 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 LONG_VALUE = 64 * 1024;
|
|
5820
|
+
function linksOf(name, value, { add, embed, cut }) {
|
|
5821
|
+
if (value.length > MAX_VALUE2) {
|
|
5822
|
+
cut();
|
|
5823
|
+
value = value.slice(0, MAX_VALUE2);
|
|
5824
|
+
}
|
|
5784
5825
|
const decoded = decodeEntities(value, { attribute: true });
|
|
5785
5826
|
switch (name) {
|
|
5786
5827
|
case "srcdoc": {
|
|
@@ -5821,7 +5862,7 @@ function linksOf(name, value, add, embed) {
|
|
|
5821
5862
|
}
|
|
5822
5863
|
var MAX_LENIENT_VALUE = 4096;
|
|
5823
5864
|
var UNQUOTED_END = /* @__PURE__ */ new Set(["<", ">", '"', "'", "`", "="]);
|
|
5824
|
-
function lenientLinks(html,
|
|
5865
|
+
function lenientLinks(html, done, reader) {
|
|
5825
5866
|
LENIENT.lastIndex = 0;
|
|
5826
5867
|
let match;
|
|
5827
5868
|
while (!done() && (match = LENIENT.exec(html)) !== null) {
|
|
@@ -5837,8 +5878,8 @@ function lenientLinks(html, add, done, embed) {
|
|
|
5837
5878
|
}
|
|
5838
5879
|
if (end < html.length && html[end] !== quote) {
|
|
5839
5880
|
const close = html.indexOf(quote, end);
|
|
5840
|
-
if (close !== -1 && close - i <= MAX_LENIENT_VALUE) {
|
|
5841
|
-
linksOf(name, html.slice(i, close),
|
|
5881
|
+
if (close !== -1 && close - i <= MAX_LENIENT_VALUE && !reader.spend(close - i)) {
|
|
5882
|
+
linksOf(name, html.slice(i, close), reader);
|
|
5842
5883
|
}
|
|
5843
5884
|
}
|
|
5844
5885
|
} else {
|
|
@@ -5846,7 +5887,10 @@ function lenientLinks(html, add, done, embed) {
|
|
|
5846
5887
|
end++;
|
|
5847
5888
|
}
|
|
5848
5889
|
}
|
|
5849
|
-
|
|
5890
|
+
if (reader.spend(end - i)) {
|
|
5891
|
+
break;
|
|
5892
|
+
}
|
|
5893
|
+
linksOf(name, html.slice(i, end), reader);
|
|
5850
5894
|
}
|
|
5851
5895
|
}
|
|
5852
5896
|
function endsLink2(text, i) {
|
|
@@ -6017,8 +6061,17 @@ function* chunksOf(html) {
|
|
|
6017
6061
|
cutStart = false;
|
|
6018
6062
|
continue;
|
|
6019
6063
|
}
|
|
6020
|
-
|
|
6021
|
-
start
|
|
6064
|
+
let hard = end;
|
|
6065
|
+
while (hard > start + CHUNK_OVERLAP + 1 && /[\dA-Za-z#]/.test(html[hard - 1])) {
|
|
6066
|
+
hard--;
|
|
6067
|
+
}
|
|
6068
|
+
if (hard < end && html[hard - 1] === "&" && hard - 1 > start + CHUNK_OVERLAP) {
|
|
6069
|
+
hard--;
|
|
6070
|
+
} else {
|
|
6071
|
+
hard = end;
|
|
6072
|
+
}
|
|
6073
|
+
yield { text: html.slice(start, hard), cutStart, cutEnd: true };
|
|
6074
|
+
start = hard - CHUNK_OVERLAP;
|
|
6022
6075
|
cutStart = true;
|
|
6023
6076
|
continue;
|
|
6024
6077
|
}
|
|
@@ -6188,27 +6241,14 @@ function quotedPrintableValues(text) {
|
|
|
6188
6241
|
}
|
|
6189
6242
|
return out.join("\n");
|
|
6190
6243
|
}
|
|
6191
|
-
function
|
|
6192
|
-
const unfolded = text.replaceAll(FOLD, "");
|
|
6193
|
-
if (!/quoted-printable/i.test(unfolded)) {
|
|
6194
|
-
return false;
|
|
6195
|
-
}
|
|
6196
|
-
return unfolded.split(LINE_BREAK).some((line) => {
|
|
6197
|
-
const colon = valueColon(line);
|
|
6198
|
-
return colon !== -1 && QUOTED_PRINTABLE.test(line.slice(0, colon));
|
|
6199
|
-
});
|
|
6200
|
-
}
|
|
6201
|
-
function shownText(text, full = true) {
|
|
6244
|
+
function shownText(text) {
|
|
6202
6245
|
const unfolded = text.replaceAll(FOLD, "");
|
|
6203
6246
|
const shown = /* @__PURE__ */ new Set([text, readEscapes(unfolded)]);
|
|
6204
|
-
|
|
6205
|
-
if (
|
|
6206
|
-
decoded
|
|
6207
|
-
if (
|
|
6208
|
-
shown.add(
|
|
6209
|
-
if (unfolded !== text) {
|
|
6210
|
-
shown.add(quotedPrintableValues(text));
|
|
6211
|
-
}
|
|
6247
|
+
const decoded = quotedPrintableValues(unfolded);
|
|
6248
|
+
if (decoded !== unfolded) {
|
|
6249
|
+
shown.add(readEscapes(decoded));
|
|
6250
|
+
if (unfolded !== text) {
|
|
6251
|
+
shown.add(quotedPrintableValues(text));
|
|
6212
6252
|
}
|
|
6213
6253
|
}
|
|
6214
6254
|
if (/\\[^nN,;\\]/.test(decoded)) {
|
|
@@ -6313,59 +6353,73 @@ function charsetOf(attachment) {
|
|
|
6313
6353
|
const charset = headers && typeof headers.get === "function" ? headers.get("content-type")?.params?.charset : null;
|
|
6314
6354
|
return typeof charset === "string" ? charset.toLowerCase() : "";
|
|
6315
6355
|
}
|
|
6316
|
-
function
|
|
6317
|
-
const
|
|
6356
|
+
function kindOf(attachment) {
|
|
6357
|
+
const { content } = attachment ?? {};
|
|
6358
|
+
if (!Buffer7.isBuffer(content) || content.length === 0) {
|
|
6359
|
+
return null;
|
|
6360
|
+
}
|
|
6361
|
+
const type = typeof attachment.contentType === "string" ? attachment.contentType.toLowerCase() : "";
|
|
6362
|
+
const extensions = extensionsOf(attachment);
|
|
6363
|
+
const has = (set) => [...extensions].some((name) => set.has(name));
|
|
6364
|
+
if (MESSAGE_TYPES.has(type) || extensions.has("eml") || extensions.has("u8msg")) {
|
|
6365
|
+
return null;
|
|
6366
|
+
}
|
|
6367
|
+
const isHtml = HTML_TYPES.has(type) || has(HTML_EXTENSIONS);
|
|
6368
|
+
const isCard = !isHtml && (CARD_TYPES.has(type) || has(CARD_EXTENSIONS));
|
|
6369
|
+
const isText = isCard || !isHtml && type === "text/plain";
|
|
6370
|
+
return isHtml || isText ? { isHtml, isText, isCard } : null;
|
|
6371
|
+
}
|
|
6372
|
+
function* documentsOf(attachments, state, late) {
|
|
6373
|
+
const read = [];
|
|
6318
6374
|
let bytes = 0;
|
|
6319
6375
|
let opened = 0;
|
|
6320
6376
|
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")) {
|
|
6377
|
+
const kind = kindOf(attachment);
|
|
6378
|
+
if (!kind) {
|
|
6329
6379
|
continue;
|
|
6330
6380
|
}
|
|
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;
|
|
6381
|
+
if (!kind.isText && ++opened > MAX_ATTACHED || bytes + attachment.content.length > MAX_ATTACHED_BYTES) {
|
|
6382
|
+
state.tooComplex = true;
|
|
6383
|
+
break;
|
|
6340
6384
|
}
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6385
|
+
bytes += attachment.content.length;
|
|
6386
|
+
read.push({ attachment, kind });
|
|
6387
|
+
}
|
|
6388
|
+
for (const { attachment, kind: { isHtml, isCard } } of read) {
|
|
6389
|
+
if (late()) {
|
|
6390
|
+
state.incomplete = true;
|
|
6391
|
+
return;
|
|
6344
6392
|
}
|
|
6345
|
-
|
|
6346
|
-
const text = decodeText(content, charsetOf(attachment));
|
|
6393
|
+
const text = decodeText(attachment.content, charsetOf(attachment));
|
|
6347
6394
|
if (isHtml) {
|
|
6348
|
-
|
|
6395
|
+
yield { html: text };
|
|
6349
6396
|
continue;
|
|
6350
6397
|
}
|
|
6351
6398
|
if (!isCard) {
|
|
6352
|
-
|
|
6399
|
+
yield { text };
|
|
6353
6400
|
continue;
|
|
6354
6401
|
}
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6402
|
+
let readable = withoutBinaryValues(text.length > MAX_CARD_BYTES ? text.slice(0, MAX_CARD_BYTES) : text);
|
|
6403
|
+
if (text.length > MAX_CARD_BYTES || readable.length > MAX_SHOWN_BYTES) {
|
|
6404
|
+
state.tooComplex = true;
|
|
6405
|
+
readable = readable.slice(0, MAX_SHOWN_BYTES);
|
|
6406
|
+
}
|
|
6407
|
+
if (late()) {
|
|
6408
|
+
state.incomplete = true;
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
yield { text: shownText(readable) };
|
|
6412
|
+
if (late()) {
|
|
6413
|
+
state.incomplete = true;
|
|
6414
|
+
return;
|
|
6360
6415
|
}
|
|
6361
|
-
documents.push({ text: shownText(readable, full) });
|
|
6362
6416
|
const html = htmlDescriptions(readable);
|
|
6363
6417
|
if (html !== "") {
|
|
6364
|
-
|
|
6418
|
+
yield { html };
|
|
6365
6419
|
}
|
|
6366
6420
|
}
|
|
6367
|
-
return documents;
|
|
6368
6421
|
}
|
|
6422
|
+
var LATE_EVERY = 1024;
|
|
6369
6423
|
function carriedLinkHosts(mail, {
|
|
6370
6424
|
mails = [],
|
|
6371
6425
|
attachments = [],
|
|
@@ -6378,40 +6432,45 @@ function carriedLinkHosts(mail, {
|
|
|
6378
6432
|
const seenBare = /* @__PURE__ */ new Set();
|
|
6379
6433
|
const seenLoose = /* @__PURE__ */ new Set();
|
|
6380
6434
|
let more = false;
|
|
6381
|
-
let
|
|
6382
|
-
const
|
|
6383
|
-
const
|
|
6435
|
+
let full = false;
|
|
6436
|
+
const domains = /* @__PURE__ */ new Set();
|
|
6437
|
+
const state = { tooComplex: false, incomplete: false };
|
|
6438
|
+
const until = Date.now() + MAX_READ_TIME;
|
|
6439
|
+
const late = () => {
|
|
6440
|
+
if (!state.incomplete && Date.now() > until) {
|
|
6441
|
+
state.incomplete = true;
|
|
6442
|
+
}
|
|
6443
|
+
return state.incomplete;
|
|
6444
|
+
};
|
|
6445
|
+
let calls = 0;
|
|
6446
|
+
const done = () => full || state.incomplete || ++calls % LATE_EVERY === 0 && late();
|
|
6447
|
+
const looseDone = () => done() || loose.size > maxHosts;
|
|
6384
6448
|
const addLoose = (host) => {
|
|
6385
6449
|
if (!looseDone()) {
|
|
6386
6450
|
loose.add(host);
|
|
6387
6451
|
}
|
|
6388
6452
|
};
|
|
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 }));
|
|
6453
|
+
const queue = [mail, ...mails].map((document) => ({ document, depth: 0 }));
|
|
6454
|
+
const carried = documentsOf(attachments, state, late);
|
|
6401
6455
|
let depth = 0;
|
|
6402
6456
|
let embedded = 0;
|
|
6403
6457
|
let embeddedLength = 0;
|
|
6458
|
+
const embeddedSeen = /* @__PURE__ */ new Set();
|
|
6404
6459
|
const embed = (html) => {
|
|
6460
|
+
if (html === "" || embeddedSeen.has(html)) {
|
|
6461
|
+
return;
|
|
6462
|
+
}
|
|
6405
6463
|
if (depth >= MAX_EMBED_DEPTH || embedded >= MAX_EMBEDDED || embeddedLength + html.length > MAX_EMBEDDED_LENGTH) {
|
|
6406
|
-
tooComplex = true;
|
|
6464
|
+
state.tooComplex = true;
|
|
6407
6465
|
return;
|
|
6408
6466
|
}
|
|
6467
|
+
embeddedSeen.add(html);
|
|
6409
6468
|
embedded++;
|
|
6410
6469
|
embeddedLength += html.length;
|
|
6411
6470
|
queue.push({ document: { html }, depth: depth + 1 });
|
|
6412
6471
|
};
|
|
6413
6472
|
const add = (value) => {
|
|
6414
|
-
if (
|
|
6473
|
+
if (full || typeof value !== "string" || value === "" || seen.has(value)) {
|
|
6415
6474
|
return;
|
|
6416
6475
|
}
|
|
6417
6476
|
if (seen.size < MAX_SEEN) {
|
|
@@ -6426,34 +6485,64 @@ function carriedLinkHosts(mail, {
|
|
|
6426
6485
|
}
|
|
6427
6486
|
const found = hostsOf(value);
|
|
6428
6487
|
if (found.unresolved) {
|
|
6429
|
-
tooComplex = true;
|
|
6488
|
+
state.tooComplex = true;
|
|
6430
6489
|
}
|
|
6431
6490
|
for (const host of found) {
|
|
6432
6491
|
if (hosts.has(host)) {
|
|
6433
6492
|
continue;
|
|
6434
6493
|
}
|
|
6435
|
-
|
|
6436
|
-
|
|
6494
|
+
const domain = registrableDomain(host);
|
|
6495
|
+
if (hosts.size < maxHosts || !domains.has(domain) && domains.size < maxHosts) {
|
|
6496
|
+
hosts.add(host);
|
|
6497
|
+
domains.add(domain);
|
|
6498
|
+
continue;
|
|
6499
|
+
}
|
|
6500
|
+
more = true;
|
|
6501
|
+
if (domains.size >= maxHosts) {
|
|
6502
|
+
full = true;
|
|
6437
6503
|
return;
|
|
6438
6504
|
}
|
|
6439
|
-
hosts.add(host);
|
|
6440
6505
|
}
|
|
6441
6506
|
};
|
|
6442
|
-
|
|
6507
|
+
let spent = 0;
|
|
6508
|
+
const spend = (length) => {
|
|
6509
|
+
spent += length;
|
|
6510
|
+
if (spent < LONG_VALUE) {
|
|
6511
|
+
return false;
|
|
6512
|
+
}
|
|
6513
|
+
spent = 0;
|
|
6514
|
+
return late();
|
|
6515
|
+
};
|
|
6516
|
+
const reader = {
|
|
6517
|
+
add,
|
|
6518
|
+
embed,
|
|
6519
|
+
spend,
|
|
6520
|
+
cut() {
|
|
6521
|
+
state.tooComplex = true;
|
|
6522
|
+
}
|
|
6523
|
+
};
|
|
6524
|
+
const readText = (source, { cutStart = false, cutEnd = false, exact = false } = {}) => {
|
|
6443
6525
|
for (const match of source.matchAll(URL_START)) {
|
|
6444
|
-
if (
|
|
6526
|
+
if (done()) {
|
|
6445
6527
|
break;
|
|
6446
6528
|
}
|
|
6447
6529
|
if (cutStart && match.index === 0) {
|
|
6448
6530
|
continue;
|
|
6449
6531
|
}
|
|
6450
6532
|
if (cutEnd && match.index + match[0].length === source.length) {
|
|
6533
|
+
if (exact && source.length - match.index < CHUNK_OVERLAP) {
|
|
6534
|
+
continue;
|
|
6535
|
+
}
|
|
6451
6536
|
const value = match[0];
|
|
6452
6537
|
const at = /^www\./i.test(value) ? 0 : value.indexOf("//") + 2;
|
|
6453
|
-
if (
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6538
|
+
if (!/[/?#\\]/.test(value.slice(at))) {
|
|
6539
|
+
state.tooComplex = true;
|
|
6540
|
+
continue;
|
|
6541
|
+
}
|
|
6542
|
+
add(value);
|
|
6543
|
+
const url = urlOf(value);
|
|
6544
|
+
if (url && isClickTracker(url)) {
|
|
6545
|
+
state.tooComplex = true;
|
|
6457
6546
|
}
|
|
6458
6547
|
continue;
|
|
6459
6548
|
}
|
|
@@ -6475,42 +6564,60 @@ function carriedLinkHosts(mail, {
|
|
|
6475
6564
|
});
|
|
6476
6565
|
}
|
|
6477
6566
|
};
|
|
6478
|
-
for (
|
|
6479
|
-
if (
|
|
6567
|
+
for (let index = 0; ; index++) {
|
|
6568
|
+
if (full || late()) {
|
|
6480
6569
|
break;
|
|
6481
6570
|
}
|
|
6482
|
-
(
|
|
6483
|
-
|
|
6571
|
+
if (index >= queue.length) {
|
|
6572
|
+
const next = carried.next();
|
|
6573
|
+
if (next.done) {
|
|
6574
|
+
break;
|
|
6575
|
+
}
|
|
6576
|
+
queue.push({ document: next.value, depth: 0 });
|
|
6577
|
+
}
|
|
6578
|
+
({ depth } = queue[index]);
|
|
6579
|
+
const { document } = queue[index];
|
|
6484
6580
|
const html = withoutNul(typeof document?.html === "string" ? document.html : "");
|
|
6485
6581
|
const text = withoutNul(typeof document?.text === "string" ? document.text : "");
|
|
6486
6582
|
for (const [name, value] of attributesOf(html)) {
|
|
6487
|
-
if (
|
|
6583
|
+
if (done()) {
|
|
6488
6584
|
break;
|
|
6489
6585
|
}
|
|
6490
|
-
if (LINK_ATTRIBUTES.has(name)) {
|
|
6491
|
-
|
|
6586
|
+
if (!LINK_ATTRIBUTES.has(name)) {
|
|
6587
|
+
continue;
|
|
6492
6588
|
}
|
|
6589
|
+
if (spend(value.length)) {
|
|
6590
|
+
break;
|
|
6591
|
+
}
|
|
6592
|
+
linksOf(name, value, reader);
|
|
6493
6593
|
}
|
|
6494
|
-
lenientLinks(html,
|
|
6594
|
+
lenientLinks(html, done, reader);
|
|
6495
6595
|
for (const pattern of [CSS_URL, CSS_IMPORT]) {
|
|
6496
6596
|
for (const [, link] of html.matchAll(pattern)) {
|
|
6497
|
-
if (
|
|
6597
|
+
if (done() || spend(link.length)) {
|
|
6498
6598
|
break;
|
|
6499
6599
|
}
|
|
6500
|
-
|
|
6600
|
+
if (link.length > MAX_VALUE2) {
|
|
6601
|
+
state.tooComplex = true;
|
|
6602
|
+
}
|
|
6603
|
+
add(decodeEntities(link.slice(0, MAX_VALUE2), { attribute: true }));
|
|
6501
6604
|
}
|
|
6502
6605
|
}
|
|
6503
|
-
|
|
6504
|
-
|
|
6606
|
+
for (const chunk2 of chunksOf(text)) {
|
|
6607
|
+
if (full || late()) {
|
|
6608
|
+
break;
|
|
6609
|
+
}
|
|
6610
|
+
readText(chunk2.text, { ...chunk2, exact: true });
|
|
6505
6611
|
}
|
|
6506
6612
|
for (const chunk2 of chunksOf(html)) {
|
|
6507
|
-
if (
|
|
6613
|
+
if (full || late()) {
|
|
6508
6614
|
break;
|
|
6509
6615
|
}
|
|
6510
|
-
|
|
6616
|
+
const decoded = decodeEntities(chunk2.text);
|
|
6617
|
+
readText(decoded, { ...chunk2, exact: decoded.endsWith(chunk2.text.slice(-CHUNK_OVERLAP)) });
|
|
6511
6618
|
}
|
|
6512
6619
|
}
|
|
6513
|
-
if (!more && !incomplete) {
|
|
6620
|
+
if (!more && !state.incomplete) {
|
|
6514
6621
|
for (const name of loose) {
|
|
6515
6622
|
if (hosts.size >= maxHosts) {
|
|
6516
6623
|
break;
|
|
@@ -6524,8 +6631,8 @@ function carriedLinkHosts(mail, {
|
|
|
6524
6631
|
return {
|
|
6525
6632
|
hosts: [...hosts],
|
|
6526
6633
|
more,
|
|
6527
|
-
tooComplex,
|
|
6528
|
-
incomplete
|
|
6634
|
+
tooComplex: state.tooComplex,
|
|
6635
|
+
incomplete: state.incomplete
|
|
6529
6636
|
};
|
|
6530
6637
|
}
|
|
6531
6638
|
|
|
@@ -8171,7 +8278,7 @@ function scoreResults(results, options) {
|
|
|
8171
8278
|
var package_default = {
|
|
8172
8279
|
name: "spamscanner",
|
|
8173
8280
|
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.
|
|
8281
|
+
version: "7.0.12",
|
|
8175
8282
|
author: "Forward Email (https://forwardemail.net)",
|
|
8176
8283
|
bin: {
|
|
8177
8284
|
spamscanner: "./dist/esm/cli.js"
|
|
@@ -8436,9 +8543,12 @@ function rewriteMessage(source, headers = [], options = {}) {
|
|
|
8436
8543
|
}
|
|
8437
8544
|
}
|
|
8438
8545
|
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() : [];
|
|
8546
|
+
const mbox = kept.length > 0 && kept[0][0].startsWith("From ") ? kept.shift().slice(0, 1) : [];
|
|
8547
|
+
if (kept.length > 0 && /^[ \t]/.test(kept[0][0])) {
|
|
8548
|
+
kept.shift();
|
|
8549
|
+
}
|
|
8440
8550
|
const head = [...mbox, ...added, ...kept.map((field) => field.join(newline))].join(newline);
|
|
8441
|
-
return Buffer9.concat([Buffer9.from(head === ""
|
|
8551
|
+
return Buffer9.concat([Buffer9.from(head === "" ? newline : `${head}${newline}${newline}`, "latin1"), body]);
|
|
8442
8552
|
}
|
|
8443
8553
|
|
|
8444
8554
|
// src/train.js
|
|
@@ -8787,7 +8897,12 @@ import net2 from "node:net";
|
|
|
8787
8897
|
var MAX_MESSAGE_SIZE = 25 * 1024 * 1024;
|
|
8788
8898
|
var MAX_CONNECTIONS = 200;
|
|
8789
8899
|
function serializeResult(result, options = {}) {
|
|
8790
|
-
const {
|
|
8900
|
+
const {
|
|
8901
|
+
mail,
|
|
8902
|
+
tokens,
|
|
8903
|
+
attached,
|
|
8904
|
+
...rest
|
|
8905
|
+
} = result;
|
|
8791
8906
|
const output = {
|
|
8792
8907
|
...rest,
|
|
8793
8908
|
results: { ...result.results }
|
|
@@ -9448,6 +9563,7 @@ function reply(code, text, headers = [], body = null) {
|
|
|
9448
9563
|
lines2.push(...headers.map(([name, value]) => `${name}: ${value}`), "", "");
|
|
9449
9564
|
return body === null ? Buffer13.from(lines2.join("\r\n")) : Buffer13.concat([Buffer13.from(lines2.join("\r\n")), body]);
|
|
9450
9565
|
}
|
|
9566
|
+
var oneLine2 = (text) => String(text ?? "").replaceAll(/[\p{Cc}\u061C\u200E\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]+/gu, " ");
|
|
9451
9567
|
function report(result) {
|
|
9452
9568
|
const lines2 = [
|
|
9453
9569
|
`Spam detection software, running on the system "spamscanner", has ${result.isSpam ? "identified this incoming email as possible spam" : "scanned this incoming email"}.`,
|
|
@@ -9456,7 +9572,7 @@ function report(result) {
|
|
|
9456
9572
|
"",
|
|
9457
9573
|
" pts rule name description",
|
|
9458
9574
|
"---- ---------------------- --------------------------------------------------",
|
|
9459
|
-
...result.tests.map((test) => `${test.score.toFixed(1).padStart(4)} ${test.name.padEnd(22)} ${test.description}`)
|
|
9575
|
+
...result.tests.map((test) => `${test.score.toFixed(1).padStart(4)} ${oneLine2(test.name).padEnd(22)} ${oneLine2(test.description)}`)
|
|
9460
9576
|
];
|
|
9461
9577
|
return `${lines2.join("\r\n")}\r
|
|
9462
9578
|
`;
|
|
@@ -10091,7 +10207,7 @@ var SpamScanner = class {
|
|
|
10091
10207
|
// (`from`: the sender's domain; `carried`: the link hosts of the whole
|
|
10092
10208
|
// message and of what it carries, see carriedLinkHosts)
|
|
10093
10209
|
async checkLinks(links, config, hosts = [], options = {}) {
|
|
10094
|
-
const { from = "", carried = null } = typeof options === "string" ? { from: options } : options;
|
|
10210
|
+
const { from = "", carried = null } = typeof options === "string" ? { from: options } : options ?? {};
|
|
10095
10211
|
const phishing = [];
|
|
10096
10212
|
const homographs = [];
|
|
10097
10213
|
const seenHosts = /* @__PURE__ */ new Set();
|
|
@@ -10151,15 +10267,15 @@ var SpamScanner = class {
|
|
|
10151
10267
|
for (const host of carriedHosts) {
|
|
10152
10268
|
seenHosts.add(host);
|
|
10153
10269
|
}
|
|
10154
|
-
const
|
|
10270
|
+
const own = [...found.filter((link) => link.text), ...found.filter((link) => !link.text), ...bare].map((link) => lookupName(destination(link))).filter(Boolean);
|
|
10155
10271
|
const firsts = /* @__PURE__ */ new Map();
|
|
10156
|
-
for (const host of
|
|
10272
|
+
for (const host of [...own, ...carriedHosts]) {
|
|
10157
10273
|
const domain = registrableDomain(host);
|
|
10158
10274
|
if (!firsts.has(domain)) {
|
|
10159
10275
|
firsts.set(domain, host);
|
|
10160
10276
|
}
|
|
10161
10277
|
}
|
|
10162
|
-
const checked = [.../* @__PURE__ */ new Set([...firsts.values(), ...
|
|
10278
|
+
const checked = [.../* @__PURE__ */ new Set([...firsts.values(), ...own, ...carriedHosts])].slice(0, config.phishing.maxHosts);
|
|
10163
10279
|
const domains = [...new Set(checked.map((host) => registrableDomain(host)))];
|
|
10164
10280
|
const blocked = [];
|
|
10165
10281
|
let answered = 0;
|