spamscanner 7.0.6 → 7.0.8
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 +197 -45
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/esm/arf.js.map +1 -1
- package/dist/esm/cli.js +184 -33
- package/dist/esm/cli.js.map +3 -3
- package/dist/esm/index.js +186 -34
- package/dist/esm/index.js.map +3 -3
- package/dist/types/dnsbl.d.ts +19 -3
- package/dist/types/index.d.ts +13 -3
- package/dist/types/parse.d.ts +1 -1
- package/dist/types/tokenizer.d.ts +2 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -2116,6 +2116,9 @@ function unwrapLink(address2) {
|
|
|
2116
2116
|
}
|
|
2117
2117
|
for (let depth = 0; depth < 3; depth++) {
|
|
2118
2118
|
const target = gatewayTarget(url);
|
|
2119
|
+
if (!target) {
|
|
2120
|
+
break;
|
|
2121
|
+
}
|
|
2119
2122
|
let next;
|
|
2120
2123
|
try {
|
|
2121
2124
|
next = new URL(target);
|
|
@@ -2378,13 +2381,15 @@ function endsInsideTag(html, end, link) {
|
|
|
2378
2381
|
return joined !== host && (!isHost(host) || isHost(joined) && registrableDomain(joined) !== registrableDomain(host));
|
|
2379
2382
|
}
|
|
2380
2383
|
var MAX_SHOWN_HTML = 2e5;
|
|
2381
|
-
function extractLinks(text, html, { maxLength = 5e5 } = {}) {
|
|
2384
|
+
function extractLinks(text, html, { maxLength = 5e5, bareHosts = false } = {}) {
|
|
2382
2385
|
text = typeof text === "string" ? foldToAscii(text) : text;
|
|
2383
2386
|
html = typeof html === "string" ? foldToAscii(html) : html;
|
|
2384
2387
|
const links = [];
|
|
2385
2388
|
const seen = /* @__PURE__ */ new Set();
|
|
2389
|
+
const linkHosts = /* @__PURE__ */ new Set();
|
|
2390
|
+
const bareFound = [];
|
|
2386
2391
|
const names = /* @__PURE__ */ new Map();
|
|
2387
|
-
const add = (raw, shown = null) => {
|
|
2392
|
+
const add = (raw, shown = null, bare = false) => {
|
|
2388
2393
|
let href = trimEndOf(trimControls(raw.replaceAll(/[\t\n\r]/g, "")).trim(), `).,;:!?'"]`);
|
|
2389
2394
|
if (/^www\./i.test(href)) {
|
|
2390
2395
|
href = `http://${href}`;
|
|
@@ -2408,11 +2413,13 @@ ${shown || ""}`;
|
|
|
2408
2413
|
}
|
|
2409
2414
|
seen.add(key);
|
|
2410
2415
|
const host = url.hostname.toLowerCase().replace(/\.$/, "").replace(/^\[(.*)]$/, "$1");
|
|
2416
|
+
linkHosts.add(host);
|
|
2411
2417
|
links.push({
|
|
2412
2418
|
url: url.href.slice(0, MAX_URL),
|
|
2413
2419
|
host,
|
|
2414
2420
|
text: shown,
|
|
2415
|
-
userinfo: Boolean(url.username || url.password)
|
|
2421
|
+
userinfo: Boolean(url.username || url.password),
|
|
2422
|
+
...bare && { bare: true }
|
|
2416
2423
|
});
|
|
2417
2424
|
};
|
|
2418
2425
|
if (typeof html === "string" && html) {
|
|
@@ -2441,10 +2448,92 @@ ${shown || ""}`;
|
|
|
2441
2448
|
}
|
|
2442
2449
|
add(link);
|
|
2443
2450
|
}
|
|
2451
|
+
if (bareHosts && index < 2) {
|
|
2452
|
+
bareLinkHosts(read, (host) => {
|
|
2453
|
+
bareFound.push(host);
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
for (const host of bareFound) {
|
|
2459
|
+
if (!linkHosts.has(host)) {
|
|
2460
|
+
add(`http://${host}/`, null, true);
|
|
2444
2461
|
}
|
|
2445
2462
|
}
|
|
2446
2463
|
return links;
|
|
2447
2464
|
}
|
|
2465
|
+
var IN_WORD = /[\p{L}\p{N}\p{M}_%]/u;
|
|
2466
|
+
var MAX_BARE_NAME = 760;
|
|
2467
|
+
function isHostCode(code) {
|
|
2468
|
+
return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 45 || code === 46;
|
|
2469
|
+
}
|
|
2470
|
+
function endsLink(char) {
|
|
2471
|
+
return /[\s<>"'\u0060]/u.test(char);
|
|
2472
|
+
}
|
|
2473
|
+
function bareLinkHosts(text, add) {
|
|
2474
|
+
const found = /* @__PURE__ */ new Set();
|
|
2475
|
+
let previous = -1;
|
|
2476
|
+
let read = 0;
|
|
2477
|
+
let space = -1;
|
|
2478
|
+
let scheme = -1;
|
|
2479
|
+
for (let slash = text.indexOf("/"); slash !== -1; slash = text.indexOf("/", slash + 1)) {
|
|
2480
|
+
const floor = Math.max(previous + 1, slash - MAX_BARE_NAME);
|
|
2481
|
+
previous = slash;
|
|
2482
|
+
let end = slash;
|
|
2483
|
+
while (end > floor && text.codePointAt(end - 1) >= 48 && text.codePointAt(end - 1) <= 57) {
|
|
2484
|
+
end--;
|
|
2485
|
+
}
|
|
2486
|
+
end = end > floor && text[end - 1] === ":" ? end - 1 : slash;
|
|
2487
|
+
let start = end;
|
|
2488
|
+
while (start > floor && isHostCode(text.codePointAt(start - 1))) {
|
|
2489
|
+
start--;
|
|
2490
|
+
}
|
|
2491
|
+
if (start === end || start > 0 && (text[start - 1] === "/" || IN_WORD.test(text[start - 1]))) {
|
|
2492
|
+
continue;
|
|
2493
|
+
}
|
|
2494
|
+
for (; read < start; read++) {
|
|
2495
|
+
if (endsLink(text[read])) {
|
|
2496
|
+
space = read;
|
|
2497
|
+
} else if (text[read] === "/" && text[read - 1] === "/" && text[read - 2] === ":") {
|
|
2498
|
+
scheme = read;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if (scheme > space) {
|
|
2502
|
+
continue;
|
|
2503
|
+
}
|
|
2504
|
+
while (end > start && text[end - 1] === ".") {
|
|
2505
|
+
end--;
|
|
2506
|
+
}
|
|
2507
|
+
const labels = text.slice(start, end).split(".");
|
|
2508
|
+
const tld = labels.pop();
|
|
2509
|
+
if (!/^(?:[a-z]{2,24}|xn--[a-z\d-]{1,59})$/i.test(tld)) {
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
const parts = [];
|
|
2513
|
+
while (labels.length > 0 && parts.length < 11) {
|
|
2514
|
+
const label = labels.pop();
|
|
2515
|
+
if (!/^[a-z\d-]{1,63}$/i.test(label)) {
|
|
2516
|
+
break;
|
|
2517
|
+
}
|
|
2518
|
+
parts.unshift(label);
|
|
2519
|
+
}
|
|
2520
|
+
if (parts.length === 0) {
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
parts[0] = parts[0].replace(/^-+/, "");
|
|
2524
|
+
if (!parts[0]) {
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
const host = `${parts.join(".")}.${tld}`.toLowerCase();
|
|
2528
|
+
if (found.has(host)) {
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
found.add(host);
|
|
2532
|
+
if (isHost(host)) {
|
|
2533
|
+
add(host);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2448
2537
|
function hostOf(link) {
|
|
2449
2538
|
const authority = link.replace(/^https?:\/\//i, "").split(/[/?#\\]/)[0];
|
|
2450
2539
|
return trimEndOf(authority.slice(authority.lastIndexOf("@") + 1).replace(/:\d*$/, ""), `).,;:!?'"]`);
|
|
@@ -3861,24 +3950,38 @@ async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PA
|
|
|
3861
3950
|
}
|
|
3862
3951
|
}
|
|
3863
3952
|
}
|
|
3864
|
-
async function expandAttachments(attachments = [], errors = [], parse = parseMessage) {
|
|
3953
|
+
async function expandAttachments(attachments = [], errors = [], parse = parseMessage, unopened = []) {
|
|
3865
3954
|
const all = [];
|
|
3866
3955
|
const deadline = Date.now() + PARSE_TIMEOUT;
|
|
3867
3956
|
let budget = MAX_NESTED_BYTES;
|
|
3868
3957
|
let nested = 0;
|
|
3958
|
+
let tooMany = false;
|
|
3869
3959
|
const visit = async (list, prefix, depth) => {
|
|
3870
3960
|
for (const attachment of list) {
|
|
3871
3961
|
if (prefix) {
|
|
3872
3962
|
if (nested >= MAX_ATTACHMENTS) {
|
|
3963
|
+
if (!tooMany) {
|
|
3964
|
+
tooMany = true;
|
|
3965
|
+
unopened.push({ filename: prefix, reason: `more than ${MAX_ATTACHMENTS} attachments in attached messages` });
|
|
3966
|
+
}
|
|
3873
3967
|
return;
|
|
3874
3968
|
}
|
|
3875
3969
|
nested++;
|
|
3876
3970
|
}
|
|
3877
3971
|
const name = attachment.filename || (prefix ? "unnamed attachment" : "");
|
|
3972
|
+
const filename = prefix ? `${prefix}/${name}` : name || "attached message";
|
|
3878
3973
|
all.push(prefix ? { ...attachment, filename: `${prefix}/${name}` } : attachment);
|
|
3879
3974
|
const { content } = attachment;
|
|
3880
3975
|
const attached = attachment.contentType === "message/rfc822" || /\.eml$/i.test(attachment.filename || "");
|
|
3881
|
-
if (attached &&
|
|
3976
|
+
if (attached && Buffer2.isBuffer(content) && depth >= MAX_NESTED_DEPTH) {
|
|
3977
|
+
unopened.push({ filename, reason: `nested more than ${MAX_NESTED_DEPTH} deep` });
|
|
3978
|
+
continue;
|
|
3979
|
+
}
|
|
3980
|
+
if (attached && Buffer2.isBuffer(content) && content.length > budget) {
|
|
3981
|
+
unopened.push({ filename, reason: `past ${MAX_NESTED_BYTES / 1024 / 1024} MB of attached messages` });
|
|
3982
|
+
continue;
|
|
3983
|
+
}
|
|
3984
|
+
if (attached && Buffer2.isBuffer(content)) {
|
|
3882
3985
|
budget -= content.length;
|
|
3883
3986
|
let inner;
|
|
3884
3987
|
try {
|
|
@@ -3900,17 +4003,47 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
|
|
|
3900
4003
|
|
|
3901
4004
|
// src/reputation.js
|
|
3902
4005
|
import { isIP as isIP2 } from "node:net";
|
|
3903
|
-
import { domainToASCII } from "node:url";
|
|
4006
|
+
import { domainToASCII as domainToASCII2 } from "node:url";
|
|
3904
4007
|
|
|
3905
4008
|
// src/dnsbl.js
|
|
3906
4009
|
import dns from "node:dns";
|
|
3907
4010
|
import { isIP } from "node:net";
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
4011
|
+
import { domainToASCII } from "node:url";
|
|
4012
|
+
import { getPublicSuffix } from "tldts";
|
|
4013
|
+
var LOOKUP_LABEL = /^[a-z\d_-]{1,63}$/;
|
|
4014
|
+
function lookupName(host) {
|
|
4015
|
+
if (typeof host !== "string") {
|
|
4016
|
+
return null;
|
|
4017
|
+
}
|
|
4018
|
+
let end = host.length;
|
|
4019
|
+
while (end > 0 && host[end - 1] === ".") {
|
|
4020
|
+
end--;
|
|
4021
|
+
}
|
|
4022
|
+
let name = host.slice(0, end).toLowerCase();
|
|
4023
|
+
if (/[\u0080-\u{10FFFF}]/u.test(name)) {
|
|
4024
|
+
name = domainToASCII(name) || name;
|
|
3911
4025
|
}
|
|
3912
|
-
|
|
3913
|
-
|
|
4026
|
+
if (isIP(name.replaceAll(/^\[|]$/g, "")) !== 0) {
|
|
4027
|
+
return null;
|
|
4028
|
+
}
|
|
4029
|
+
const labels = name.split(".");
|
|
4030
|
+
let start = labels.length;
|
|
4031
|
+
let length = -1;
|
|
4032
|
+
while (start > 0 && LOOKUP_LABEL.test(labels[start - 1]) && length + 1 + labels[start - 1].length <= 253) {
|
|
4033
|
+
start--;
|
|
4034
|
+
length += 1 + labels[start].length;
|
|
4035
|
+
}
|
|
4036
|
+
if (start === labels.length) {
|
|
4037
|
+
return null;
|
|
4038
|
+
}
|
|
4039
|
+
if (start === 0) {
|
|
4040
|
+
return name;
|
|
4041
|
+
}
|
|
4042
|
+
const suffix = labels.slice(start).join(".");
|
|
4043
|
+
if (start === labels.length - 1 || getPublicSuffix(suffix, { allowPrivateDomains: true }) === suffix) {
|
|
4044
|
+
return null;
|
|
4045
|
+
}
|
|
4046
|
+
return suffix;
|
|
3914
4047
|
}
|
|
3915
4048
|
var CLOUDFLARE_MALWARE = ["1.1.1.2", "1.0.0.2"];
|
|
3916
4049
|
var CLOUDFLARE_FAMILY = ["1.1.1.3", "1.0.0.3"];
|
|
@@ -4109,9 +4242,10 @@ var DnsChecker = class {
|
|
|
4109
4242
|
return results.filter(Boolean);
|
|
4110
4243
|
}
|
|
4111
4244
|
/**
|
|
4112
|
-
* Ask Cloudflare's filtering resolvers about host names
|
|
4113
|
-
*
|
|
4114
|
-
* resolver is "
|
|
4245
|
+
* Ask Cloudflare's filtering resolvers about host names, each by the name
|
|
4246
|
+
* it is looked up as (see lookupName), once; the findings name it. A name
|
|
4247
|
+
* blocked by the malware resolver is "malware"; one blocked only by the
|
|
4248
|
+
* family resolver is "adult". The hosts that got no answer (from the malware
|
|
4115
4249
|
* resolver, or from the family resolver when asked) are listed in the
|
|
4116
4250
|
* result's (non-enumerable) `unanswered` property.
|
|
4117
4251
|
* @param {string[]} hosts
|
|
@@ -4124,10 +4258,8 @@ var DnsChecker = class {
|
|
|
4124
4258
|
async checkCloudflare(hosts = [], options = {}) {
|
|
4125
4259
|
const { adult = true, malwareServers = CLOUDFLARE_MALWARE, familyServers = CLOUDFLARE_FAMILY } = options;
|
|
4126
4260
|
const unanswered = [];
|
|
4127
|
-
const
|
|
4128
|
-
|
|
4129
|
-
return null;
|
|
4130
|
-
}
|
|
4261
|
+
const names = [...new Set(hosts.map((host) => lookupName(host)).filter(Boolean))];
|
|
4262
|
+
const results = await Promise.all(names.map(async (host) => {
|
|
4131
4263
|
const malware = await this.answer(host, malwareServers);
|
|
4132
4264
|
if (malware?.includes("0.0.0.0")) {
|
|
4133
4265
|
return { host, category: "malware" };
|
|
@@ -4231,7 +4363,7 @@ function asciiHostname(value) {
|
|
|
4231
4363
|
if (lower === "" || /[\0-,/:-^`{-\u007F]/.test(lower)) {
|
|
4232
4364
|
return null;
|
|
4233
4365
|
}
|
|
4234
|
-
const ascii =
|
|
4366
|
+
const ascii = domainToASCII2(lower);
|
|
4235
4367
|
return ascii.length <= 253 && /^[\w-]{1,63}(?:\.[\w-]{1,63})*$/.test(ascii) ? ascii : null;
|
|
4236
4368
|
}
|
|
4237
4369
|
function normalizeEntry(value) {
|
|
@@ -6916,7 +7048,7 @@ function scoreResults(results, options) {
|
|
|
6916
7048
|
var package_default = {
|
|
6917
7049
|
name: "spamscanner",
|
|
6918
7050
|
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.",
|
|
6919
|
-
version: "7.0.
|
|
7051
|
+
version: "7.0.8",
|
|
6920
7052
|
author: "Forward Email (https://forwardemail.net)",
|
|
6921
7053
|
bin: {
|
|
6922
7054
|
spamscanner: "./dist/esm/cli.js"
|
|
@@ -8718,14 +8850,16 @@ var SpamScanner = class {
|
|
|
8718
8850
|
* a header field ("Name: value", or an mbox "From " line) is read as the
|
|
8719
8851
|
* body of a message with no header, so its text is the input; other
|
|
8720
8852
|
* input mailparser cannot read gives a message with no text. Other errors
|
|
8721
|
-
* are thrown.
|
|
8853
|
+
* are thrown. `error` is the parser's limit the message exceeds, so that
|
|
8854
|
+
* only its first parts were read (as scan() reports it, see parseError),
|
|
8855
|
+
* or null.
|
|
8722
8856
|
* @param {Buffer|string|Uint8Array|AsyncIterable} source
|
|
8723
|
-
* @returns {Promise<{raw: Buffer, mail: object}>}
|
|
8857
|
+
* @returns {Promise<{raw: Buffer, mail: object, error: string|null}>}
|
|
8724
8858
|
*/
|
|
8725
8859
|
async parse(source) {
|
|
8726
8860
|
const raw = await toBuffer(source);
|
|
8727
|
-
const { mail } = await parseWithin(withHeader(raw));
|
|
8728
|
-
return { raw, mail };
|
|
8861
|
+
const { mail, error } = await parseWithin(withHeader(raw));
|
|
8862
|
+
return { raw, mail, error: error ?? null };
|
|
8729
8863
|
}
|
|
8730
8864
|
/**
|
|
8731
8865
|
* Classifier features of a parsed message.
|
|
@@ -8858,15 +8992,23 @@ var SpamScanner = class {
|
|
|
8858
8992
|
message: `A link shows "${link.text}" but goes to ${link.host}`
|
|
8859
8993
|
}));
|
|
8860
8994
|
}
|
|
8861
|
-
|
|
8995
|
+
const name = lookupName(destination(link));
|
|
8996
|
+
if (name) {
|
|
8997
|
+
seenHosts.add(name);
|
|
8998
|
+
}
|
|
8862
8999
|
}
|
|
8863
|
-
|
|
9000
|
+
const found = links.filter((link) => !link.bare);
|
|
9001
|
+
const bare = links.filter((link) => link.bare);
|
|
9002
|
+
for (const link of [...found.filter((link2) => link2.text), ...found.filter((link2) => !link2.text)]) {
|
|
8864
9003
|
lookalike(destination(link), { url: link.url });
|
|
8865
9004
|
}
|
|
9005
|
+
for (const link of bare) {
|
|
9006
|
+
lookalike(link.host, { source: "text" });
|
|
9007
|
+
}
|
|
8866
9008
|
for (const host of hosts) {
|
|
8867
9009
|
lookalike(host, { source: "text" });
|
|
8868
9010
|
}
|
|
8869
|
-
const ordered = [...new Set([...
|
|
9011
|
+
const ordered = [...new Set([...found.filter((link) => link.text), ...found.filter((link) => !link.text), ...bare].map((link) => lookupName(destination(link))).filter(Boolean))];
|
|
8870
9012
|
const firsts = /* @__PURE__ */ new Map();
|
|
8871
9013
|
for (const host of ordered) {
|
|
8872
9014
|
const domain = registrableDomain(host);
|
|
@@ -8880,9 +9022,9 @@ var SpamScanner = class {
|
|
|
8880
9022
|
let answered = 0;
|
|
8881
9023
|
const cloudflare = async () => {
|
|
8882
9024
|
await withTimeout(Promise.all(checked.map(async (host) => {
|
|
8883
|
-
const
|
|
8884
|
-
blocked.push(...
|
|
8885
|
-
if (!
|
|
9025
|
+
const found2 = await this.dns.checkCloudflare([host], { adult: config.phishing.adult });
|
|
9026
|
+
blocked.push(...found2);
|
|
9027
|
+
if (!found2.unanswered?.length) {
|
|
8886
9028
|
answered++;
|
|
8887
9029
|
}
|
|
8888
9030
|
})), config.timeout, null);
|
|
@@ -9066,7 +9208,7 @@ var SpamScanner = class {
|
|
|
9066
9208
|
const linkLength = Number.isFinite(config.phishing.maxLength) && config.phishing.maxLength > 0 ? config.phishing.maxLength : 1e6;
|
|
9067
9209
|
const text = typeof mail.text === "string" ? mail.text : "";
|
|
9068
9210
|
const html = typeof mail.html === "string" ? mail.html : "";
|
|
9069
|
-
const checkedLinks = extractLinks(text.slice(0, linkLength), html.slice(0, linkLength * 2), { maxLength: linkLength * 2 });
|
|
9211
|
+
const checkedLinks = extractLinks(text.slice(0, linkLength), html.slice(0, linkLength * 2), { maxLength: linkLength * 2, bareHosts: true });
|
|
9070
9212
|
const linksTruncated = text.length > linkLength || html.length > linkLength * 2;
|
|
9071
9213
|
const plainObfuscation = obfuscationOf(`${mail.subject || ""}
|
|
9072
9214
|
${typeof mail.text === "string" ? mail.text : ""}`, { maxLength: config.maxLength });
|
|
@@ -9077,7 +9219,8 @@ ${visibleText(html.slice(0, config.maxLength * 2)).text}`, { maxLength: config.m
|
|
|
9077
9219
|
mixed: Math.max(plainObfuscation.mixed, shownObfuscation.mixed),
|
|
9078
9220
|
styled: plainObfuscation.styled || shownObfuscation.styled
|
|
9079
9221
|
};
|
|
9080
|
-
const
|
|
9222
|
+
const unopened = [];
|
|
9223
|
+
const attached = await expandAttachments(mail.attachments, parseErrors, void 0, unopened);
|
|
9081
9224
|
const attachments = config.attachments === false ? [] : inspectAttachments(attached).filter((item) => config.macros !== false || !["macro", "pdf_active", "rtf_object"].includes(item.type)).map((item) => finding(item));
|
|
9082
9225
|
const [links, authentication, checkedReputation, dnsbl, virusScan, toxicity, nsfw] = await Promise.all([
|
|
9083
9226
|
this.checkLinks(checkedLinks, config, extracted.hosts, fromDomain(mail)),
|
|
@@ -9198,6 +9341,13 @@ ${visibleText(html.slice(0, config.maxLength * 2)).text}`, { maxLength: config.m
|
|
|
9198
9341
|
// scanned, as {filename, reason}: the same on every try, so not a
|
|
9199
9342
|
// failure.
|
|
9200
9343
|
virusScanSkipped: virusScan.skipped,
|
|
9344
|
+
// Attached messages not opened, past the limits attached messages
|
|
9345
|
+
// are read within (nested more than three deep, more than 100
|
|
9346
|
+
// attachments in them, or more than 25 MB of them), as
|
|
9347
|
+
// {filename, reason}: their attachments were not inspected. Not
|
|
9348
|
+
// scored (forwarded messages can be nested so), for a caller that
|
|
9349
|
+
// needs a message read whole, e.g. to block what it carries.
|
|
9350
|
+
unopenedAttachments: unopened,
|
|
9201
9351
|
// Whether the message has more text than phishing.maxLength, whose
|
|
9202
9352
|
// links were not read.
|
|
9203
9353
|
linksTruncated,
|
|
@@ -9207,7 +9357,8 @@ ${visibleText(html.slice(0, config.maxLength * 2)).text}`, { maxLength: config.m
|
|
|
9207
9357
|
riskScore: Math.max(0, ...links.homographs.map((item) => item.riskScore))
|
|
9208
9358
|
}
|
|
9209
9359
|
},
|
|
9210
|
-
|
|
9360
|
+
// (the links of the message, not hosts found without one)
|
|
9361
|
+
links: checkedLinks.filter((link) => !link.bare).map((link) => link.url),
|
|
9211
9362
|
language: extracted.language,
|
|
9212
9363
|
tokens: extracted.words,
|
|
9213
9364
|
mail,
|
|
@@ -9245,6 +9396,7 @@ export {
|
|
|
9245
9396
|
scoreResults,
|
|
9246
9397
|
segmentWords,
|
|
9247
9398
|
spamHeaders,
|
|
9248
|
-
train
|
|
9399
|
+
train,
|
|
9400
|
+
unwrapLink
|
|
9249
9401
|
};
|
|
9250
9402
|
//# sourceMappingURL=index.js.map
|