spamscanner 7.0.6 → 7.0.7
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/index.cjs +161 -37
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/esm/cli.js +150 -26
- package/dist/esm/cli.js.map +3 -3
- package/dist/esm/index.js +150 -26
- package/dist/esm/index.js.map +3 -3
- package/dist/types/dnsbl.d.ts +19 -3
- package/dist/types/tokenizer.d.ts +2 -1
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -2787,13 +2787,15 @@ function endsInsideTag(html2, end, link) {
|
|
|
2787
2787
|
const joined = trimEndOf(link.replace(/^https?:\/\//i, "") + after, ".-");
|
|
2788
2788
|
return joined !== host && (!isHost(host) || isHost(joined) && registrableDomain(joined) !== registrableDomain(host));
|
|
2789
2789
|
}
|
|
2790
|
-
function extractLinks(text, html2, { maxLength = 5e5 } = {}) {
|
|
2790
|
+
function extractLinks(text, html2, { maxLength = 5e5, bareHosts = false } = {}) {
|
|
2791
2791
|
text = typeof text === "string" ? foldToAscii(text) : text;
|
|
2792
2792
|
html2 = typeof html2 === "string" ? foldToAscii(html2) : html2;
|
|
2793
2793
|
const links = [];
|
|
2794
2794
|
const seen = /* @__PURE__ */ new Set();
|
|
2795
|
+
const linkHosts = /* @__PURE__ */ new Set();
|
|
2796
|
+
const bareFound = [];
|
|
2795
2797
|
const names = /* @__PURE__ */ new Map();
|
|
2796
|
-
const add = (raw, shown = null) => {
|
|
2798
|
+
const add = (raw, shown = null, bare = false) => {
|
|
2797
2799
|
let href = trimEndOf(trimControls(raw.replaceAll(/[\t\n\r]/g, "")).trim(), `).,;:!?'"]`);
|
|
2798
2800
|
if (/^www\./i.test(href)) {
|
|
2799
2801
|
href = `http://${href}`;
|
|
@@ -2817,11 +2819,13 @@ ${shown || ""}`;
|
|
|
2817
2819
|
}
|
|
2818
2820
|
seen.add(key);
|
|
2819
2821
|
const host = url.hostname.toLowerCase().replace(/\.$/, "").replace(/^\[(.*)]$/, "$1");
|
|
2822
|
+
linkHosts.add(host);
|
|
2820
2823
|
links.push({
|
|
2821
2824
|
url: url.href.slice(0, MAX_URL),
|
|
2822
2825
|
host,
|
|
2823
2826
|
text: shown,
|
|
2824
|
-
userinfo: Boolean(url.username || url.password)
|
|
2827
|
+
userinfo: Boolean(url.username || url.password),
|
|
2828
|
+
...bare && { bare: true }
|
|
2825
2829
|
});
|
|
2826
2830
|
};
|
|
2827
2831
|
if (typeof html2 === "string" && html2) {
|
|
@@ -2850,10 +2854,90 @@ ${shown || ""}`;
|
|
|
2850
2854
|
}
|
|
2851
2855
|
add(link);
|
|
2852
2856
|
}
|
|
2857
|
+
if (bareHosts && index < 2) {
|
|
2858
|
+
bareLinkHosts(read, (host) => {
|
|
2859
|
+
bareFound.push(host);
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
for (const host of bareFound) {
|
|
2865
|
+
if (!linkHosts.has(host)) {
|
|
2866
|
+
add(`http://${host}/`, null, true);
|
|
2853
2867
|
}
|
|
2854
2868
|
}
|
|
2855
2869
|
return links;
|
|
2856
2870
|
}
|
|
2871
|
+
function isHostCode(code) {
|
|
2872
|
+
return code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 45 || code === 46;
|
|
2873
|
+
}
|
|
2874
|
+
function endsLink(char) {
|
|
2875
|
+
return /[\s<>"'\u0060]/u.test(char);
|
|
2876
|
+
}
|
|
2877
|
+
function bareLinkHosts(text, add) {
|
|
2878
|
+
const found = /* @__PURE__ */ new Set();
|
|
2879
|
+
let previous = -1;
|
|
2880
|
+
let read = 0;
|
|
2881
|
+
let space = -1;
|
|
2882
|
+
let scheme = -1;
|
|
2883
|
+
for (let slash = text.indexOf("/"); slash !== -1; slash = text.indexOf("/", slash + 1)) {
|
|
2884
|
+
const floor = Math.max(previous + 1, slash - MAX_BARE_NAME);
|
|
2885
|
+
previous = slash;
|
|
2886
|
+
let end = slash;
|
|
2887
|
+
while (end > floor && text.codePointAt(end - 1) >= 48 && text.codePointAt(end - 1) <= 57) {
|
|
2888
|
+
end--;
|
|
2889
|
+
}
|
|
2890
|
+
end = end > floor && text[end - 1] === ":" ? end - 1 : slash;
|
|
2891
|
+
let start = end;
|
|
2892
|
+
while (start > floor && isHostCode(text.codePointAt(start - 1))) {
|
|
2893
|
+
start--;
|
|
2894
|
+
}
|
|
2895
|
+
if (start === end || start > 0 && (text[start - 1] === "/" || IN_WORD.test(text[start - 1]))) {
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
for (; read < start; read++) {
|
|
2899
|
+
if (endsLink(text[read])) {
|
|
2900
|
+
space = read;
|
|
2901
|
+
} else if (text[read] === "/" && text[read - 1] === "/" && text[read - 2] === ":") {
|
|
2902
|
+
scheme = read;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
if (scheme > space) {
|
|
2906
|
+
continue;
|
|
2907
|
+
}
|
|
2908
|
+
while (end > start && text[end - 1] === ".") {
|
|
2909
|
+
end--;
|
|
2910
|
+
}
|
|
2911
|
+
const labels = text.slice(start, end).split(".");
|
|
2912
|
+
const tld = labels.pop();
|
|
2913
|
+
if (!/^(?:[a-z]{2,24}|xn--[a-z\d-]{1,59})$/i.test(tld)) {
|
|
2914
|
+
continue;
|
|
2915
|
+
}
|
|
2916
|
+
const parts = [];
|
|
2917
|
+
while (labels.length > 0 && parts.length < 11) {
|
|
2918
|
+
const label = labels.pop();
|
|
2919
|
+
if (!/^[a-z\d-]{1,63}$/i.test(label)) {
|
|
2920
|
+
break;
|
|
2921
|
+
}
|
|
2922
|
+
parts.unshift(label);
|
|
2923
|
+
}
|
|
2924
|
+
if (parts.length === 0) {
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
parts[0] = parts[0].replace(/^-+/, "");
|
|
2928
|
+
if (!parts[0]) {
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2931
|
+
const host = `${parts.join(".")}.${tld}`.toLowerCase();
|
|
2932
|
+
if (found.has(host)) {
|
|
2933
|
+
continue;
|
|
2934
|
+
}
|
|
2935
|
+
found.add(host);
|
|
2936
|
+
if (isHost(host)) {
|
|
2937
|
+
add(host);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2857
2941
|
function hostOf(link) {
|
|
2858
2942
|
const authority = link.replace(/^https?:\/\//i, "").split(/[/?#\\]/)[0];
|
|
2859
2943
|
return trimEndOf(authority.slice(authority.lastIndexOf("@") + 1).replace(/:\d*$/, ""), `).,;:!?'"]`);
|
|
@@ -3392,7 +3476,7 @@ ${body}`);
|
|
|
3392
3476
|
hidden: visible.hidden
|
|
3393
3477
|
};
|
|
3394
3478
|
}
|
|
3395
|
-
var import_tldts2, INVISIBLE, OBFUSCATING, JOINERS, INVISIBLE_OR_JOINER, ZERO_WIDTH_SPACE, ZERO_WIDTH_JOINERS, LATIN_END, LATIN_START, ZWSP_SCRIPTS, ZWSP_SCRIPT_END, ZWSP_SCRIPT_START, SOFT_HYPHEN, BIDI, RIGHT_TO_LEFT, LETTER_END, LETTER_START, DIGIT_END, DIGIT_START, STYLED_LETTERS, WIDE_LETTERS, CJK, LOOKALIKES, UNIT_LETTERS, UNIT_WORD, LEET, LEET_DIGITS, UNSPACED, URL_PATTERN, EMAIL_SOURCE, EMAIL_PATTERN, PHONE_PATTERN, IP_PATTERN, MONEY_PATTERN, BITCOIN_PATTERN, CARD_PATTERN, ANCHOR_START, MAX_LINK_TEXT, MAX_URL, SHOWN_URL, IMG_PIXEL, IMG, BARE_HOST, MAX_BARE_HOSTS, NAME_END, FILE_TLDS, LINK_FILE_TLDS, CLICK_TRACKERS, isUnder, BRAND_NAMES, SHORTENERS, FREEMAIL, segmenters, LATIN_LOOKALIKES, LATIN, CYRILLIC_OR_GREEK, INLINE_TAG, HOST_CHAR, MAX_SHOWN_HTML, LINKED_BRANDS, SIGN_IN_PART, signInPath, SENDING_DOMAINS, ADDRESS_SOURCE, NAMED_ADDRESS, MIN_SHOWN_WORDS, PART_WORD;
|
|
3479
|
+
var import_tldts2, INVISIBLE, OBFUSCATING, JOINERS, INVISIBLE_OR_JOINER, ZERO_WIDTH_SPACE, ZERO_WIDTH_JOINERS, LATIN_END, LATIN_START, ZWSP_SCRIPTS, ZWSP_SCRIPT_END, ZWSP_SCRIPT_START, SOFT_HYPHEN, BIDI, RIGHT_TO_LEFT, LETTER_END, LETTER_START, DIGIT_END, DIGIT_START, STYLED_LETTERS, WIDE_LETTERS, CJK, LOOKALIKES, UNIT_LETTERS, UNIT_WORD, LEET, LEET_DIGITS, UNSPACED, URL_PATTERN, EMAIL_SOURCE, EMAIL_PATTERN, PHONE_PATTERN, IP_PATTERN, MONEY_PATTERN, BITCOIN_PATTERN, CARD_PATTERN, ANCHOR_START, MAX_LINK_TEXT, MAX_URL, SHOWN_URL, IMG_PIXEL, IMG, BARE_HOST, MAX_BARE_HOSTS, NAME_END, FILE_TLDS, LINK_FILE_TLDS, CLICK_TRACKERS, isUnder, BRAND_NAMES, SHORTENERS, FREEMAIL, segmenters, LATIN_LOOKALIKES, LATIN, CYRILLIC_OR_GREEK, INLINE_TAG, HOST_CHAR, MAX_SHOWN_HTML, IN_WORD, MAX_BARE_NAME, LINKED_BRANDS, SIGN_IN_PART, signInPath, SENDING_DOMAINS, ADDRESS_SOURCE, NAMED_ADDRESS, MIN_SHOWN_WORDS, PART_WORD;
|
|
3396
3480
|
var init_tokenizer = __esm({
|
|
3397
3481
|
"src/tokenizer.js"() {
|
|
3398
3482
|
import_tldts2 = require("tldts");
|
|
@@ -3515,6 +3599,8 @@ var init_tokenizer = __esm({
|
|
|
3515
3599
|
INLINE_TAG = /^<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nobr|q|s|samp|small|span|strike|strong|sub|sup|time|tt|u|var|wbr)(?=[\t\n\f\r />])|^<!/i;
|
|
3516
3600
|
HOST_CHAR = /[\p{L}\p{N}.-]/u;
|
|
3517
3601
|
MAX_SHOWN_HTML = 2e5;
|
|
3602
|
+
IN_WORD = /[\p{L}\p{N}\p{M}_%]/u;
|
|
3603
|
+
MAX_BARE_NAME = 760;
|
|
3518
3604
|
LINKED_BRANDS = /* @__PURE__ */ new Set(["airbnb", "aliexpress", "alibaba", "bestbuy", "booking", "costco", "discord", "ebay", "facebook", "github", "gitlab", "instagram", "linkedin", "roblox", "shopify", "skype", "spotify", "steam", "telegram", "tiktok", "twitter", "uber", "walmart", "whatsapp", "zoom"]);
|
|
3519
3605
|
SIGN_IN_PART = /^(?:your|my|service|secure)?(?:login|logon|signin|signon|accounts?|verify|verification|security|secure|auth|authenticate|authorize|oauth2?|billing|password|recover|recovery|unlock|confirm|update|wallet|webscr)(?:verification|payment|info|help|page|\d{1,2})?$/;
|
|
3520
3606
|
signInPath = (path4) => path4.toLowerCase().replaceAll(/\b(log|sign)-(in|on)\b/g, "$1$2").split(/[/?&=._-]/).some((part) => SIGN_IN_PART.test(part));
|
|
@@ -4441,12 +4527,39 @@ var init_parse = __esm({
|
|
|
4441
4527
|
});
|
|
4442
4528
|
|
|
4443
4529
|
// src/dnsbl.js
|
|
4444
|
-
function
|
|
4445
|
-
if (typeof
|
|
4446
|
-
return
|
|
4530
|
+
function lookupName(host) {
|
|
4531
|
+
if (typeof host !== "string") {
|
|
4532
|
+
return null;
|
|
4533
|
+
}
|
|
4534
|
+
let end = host.length;
|
|
4535
|
+
while (end > 0 && host[end - 1] === ".") {
|
|
4536
|
+
end--;
|
|
4537
|
+
}
|
|
4538
|
+
let name = host.slice(0, end).toLowerCase();
|
|
4539
|
+
if (/[\u0080-\u{10FFFF}]/u.test(name)) {
|
|
4540
|
+
name = (0, import_node_url2.domainToASCII)(name) || name;
|
|
4541
|
+
}
|
|
4542
|
+
if ((0, import_node_net.isIP)(name.replaceAll(/^\[|]$/g, "")) !== 0) {
|
|
4543
|
+
return null;
|
|
4544
|
+
}
|
|
4545
|
+
const labels = name.split(".");
|
|
4546
|
+
let start = labels.length;
|
|
4547
|
+
let length = -1;
|
|
4548
|
+
while (start > 0 && LOOKUP_LABEL.test(labels[start - 1]) && length + 1 + labels[start - 1].length <= 253) {
|
|
4549
|
+
start--;
|
|
4550
|
+
length += 1 + labels[start].length;
|
|
4551
|
+
}
|
|
4552
|
+
if (start === labels.length) {
|
|
4553
|
+
return null;
|
|
4554
|
+
}
|
|
4555
|
+
if (start === 0) {
|
|
4556
|
+
return name;
|
|
4447
4557
|
}
|
|
4448
|
-
const
|
|
4449
|
-
|
|
4558
|
+
const suffix = labels.slice(start).join(".");
|
|
4559
|
+
if (start === labels.length - 1 || (0, import_tldts3.getPublicSuffix)(suffix, { allowPrivateDomains: true }) === suffix) {
|
|
4560
|
+
return null;
|
|
4561
|
+
}
|
|
4562
|
+
return suffix;
|
|
4450
4563
|
}
|
|
4451
4564
|
function ipv6Groups(address2) {
|
|
4452
4565
|
const expanded = address2.replace(/(\d+)\.(\d+)\.(\d+)\.(\d+)$/, (...match) => {
|
|
@@ -4523,11 +4636,14 @@ function isListedAnswer(address2, zone) {
|
|
|
4523
4636
|
}
|
|
4524
4637
|
return true;
|
|
4525
4638
|
}
|
|
4526
|
-
var import_node_dns, import_node_net, CLOUDFLARE_MALWARE, CLOUDFLARE_FAMILY, DnsChecker;
|
|
4639
|
+
var import_node_dns, import_node_net, import_node_url2, import_tldts3, LOOKUP_LABEL, CLOUDFLARE_MALWARE, CLOUDFLARE_FAMILY, DnsChecker;
|
|
4527
4640
|
var init_dnsbl = __esm({
|
|
4528
4641
|
"src/dnsbl.js"() {
|
|
4529
4642
|
import_node_dns = __toESM(require("node:dns"), 1);
|
|
4530
4643
|
import_node_net = require("node:net");
|
|
4644
|
+
import_node_url2 = require("node:url");
|
|
4645
|
+
import_tldts3 = require("tldts");
|
|
4646
|
+
LOOKUP_LABEL = /^[a-z\d_-]{1,63}$/;
|
|
4531
4647
|
CLOUDFLARE_MALWARE = ["1.1.1.2", "1.0.0.2"];
|
|
4532
4648
|
CLOUDFLARE_FAMILY = ["1.1.1.3", "1.0.0.3"];
|
|
4533
4649
|
DnsChecker = class {
|
|
@@ -4650,9 +4766,10 @@ var init_dnsbl = __esm({
|
|
|
4650
4766
|
return results.filter(Boolean);
|
|
4651
4767
|
}
|
|
4652
4768
|
/**
|
|
4653
|
-
* Ask Cloudflare's filtering resolvers about host names
|
|
4654
|
-
*
|
|
4655
|
-
* resolver is "
|
|
4769
|
+
* Ask Cloudflare's filtering resolvers about host names, each by the name
|
|
4770
|
+
* it is looked up as (see lookupName), once; the findings name it. A name
|
|
4771
|
+
* blocked by the malware resolver is "malware"; one blocked only by the
|
|
4772
|
+
* family resolver is "adult". The hosts that got no answer (from the malware
|
|
4656
4773
|
* resolver, or from the family resolver when asked) are listed in the
|
|
4657
4774
|
* result's (non-enumerable) `unanswered` property.
|
|
4658
4775
|
* @param {string[]} hosts
|
|
@@ -4665,10 +4782,8 @@ var init_dnsbl = __esm({
|
|
|
4665
4782
|
async checkCloudflare(hosts = [], options = {}) {
|
|
4666
4783
|
const { adult = true, malwareServers = CLOUDFLARE_MALWARE, familyServers = CLOUDFLARE_FAMILY } = options;
|
|
4667
4784
|
const unanswered = [];
|
|
4668
|
-
const
|
|
4669
|
-
|
|
4670
|
-
return null;
|
|
4671
|
-
}
|
|
4785
|
+
const names = [...new Set(hosts.map((host) => lookupName(host)).filter(Boolean))];
|
|
4786
|
+
const results = await Promise.all(names.map(async (host) => {
|
|
4672
4787
|
const malware = await this.answer(host, malwareServers);
|
|
4673
4788
|
if (malware?.includes("0.0.0.0")) {
|
|
4674
4789
|
return { host, category: "malware" };
|
|
@@ -4771,7 +4886,7 @@ function asciiHostname(value) {
|
|
|
4771
4886
|
if (lower === "" || /[\0-,/:-^`{-\u007F]/.test(lower)) {
|
|
4772
4887
|
return null;
|
|
4773
4888
|
}
|
|
4774
|
-
const ascii = (0,
|
|
4889
|
+
const ascii = (0, import_node_url3.domainToASCII)(lower);
|
|
4775
4890
|
return ascii.length <= 253 && /^[\w-]{1,63}(?:\.[\w-]{1,63})*$/.test(ascii) ? ascii : null;
|
|
4776
4891
|
}
|
|
4777
4892
|
function normalizeEntry(value) {
|
|
@@ -4920,11 +5035,11 @@ function trustedReputationValues(mail = {}, session = {}, authentication = null)
|
|
|
4920
5035
|
envelope: trustedSender ? { mailFrom: { address: sender } } : void 0
|
|
4921
5036
|
}), ...fromValues];
|
|
4922
5037
|
}
|
|
4923
|
-
var import_node_net2,
|
|
5038
|
+
var import_node_net2, import_node_url3, EMPTY, ReputationChecker, domainOf, sameDomain;
|
|
4924
5039
|
var init_reputation = __esm({
|
|
4925
5040
|
"src/reputation.js"() {
|
|
4926
5041
|
import_node_net2 = require("node:net");
|
|
4927
|
-
|
|
5042
|
+
import_node_url3 = require("node:url");
|
|
4928
5043
|
init_dnsbl();
|
|
4929
5044
|
init_http();
|
|
4930
5045
|
init_tokenizer();
|
|
@@ -5920,13 +6035,13 @@ function joinsDisplay(brand, display, name) {
|
|
|
5920
6035
|
return false;
|
|
5921
6036
|
}
|
|
5922
6037
|
function isOfficial({ brand, official }, domain, display, shared = true) {
|
|
5923
|
-
const parsed = (0,
|
|
6038
|
+
const parsed = (0, import_tldts4.parse)(domain);
|
|
5924
6039
|
const name = parsed.domainWithoutSuffix ?? "";
|
|
5925
6040
|
const compact = brand.replaceAll(" ", "");
|
|
5926
6041
|
if (parsed.publicSuffix === compact || compact.length >= 5 && joinsDisplay(brand, display, name) && !JOINED_WORDS.some((word) => name.slice(compact.length).includes(word))) {
|
|
5927
6042
|
return true;
|
|
5928
6043
|
}
|
|
5929
|
-
return official.some((entry) => entry instanceof RegExp ? entry.test(registrableDomain(domain)) : entry.includes(".") ? (shared || (0,
|
|
6044
|
+
return official.some((entry) => entry instanceof RegExp ? entry.test(registrableDomain(domain)) : entry.includes(".") ? (shared || (0, import_tldts4.parse)(entry).domain !== null) && (domain === entry || domain.endsWith(`.${entry}`)) : name === entry);
|
|
5930
6045
|
}
|
|
5931
6046
|
function headerString(mail, name) {
|
|
5932
6047
|
const value = mail.headers?.get?.(name);
|
|
@@ -6025,7 +6140,7 @@ ${withoutHelp(markup)}`) || attributeText(markup).some(({ text: text2, start, en
|
|
|
6025
6140
|
}
|
|
6026
6141
|
} else if (claimed && isWholeName(name, claimed)) {
|
|
6027
6142
|
const domain = claimed[1];
|
|
6028
|
-
const brand = MAILBOXES.has((0,
|
|
6143
|
+
const brand = MAILBOXES.has((0, import_tldts4.parse)(domain).domainWithoutSuffix) ? null : DISPLAY_BRANDS.find((entry) => isOfficial(entry, domain, "", false) && !isOfficial(entry, session.originalFromAddressDomain, ""));
|
|
6029
6144
|
if (brand) {
|
|
6030
6145
|
hit("FROM_NAME_BRAND", 2, `Display name is an address of "${brand.brand}" but the message is from ${fromRoot}`, "PHISHING");
|
|
6031
6146
|
}
|
|
@@ -6058,10 +6173,10 @@ ${withoutHelp(markup)}`) || attributeText(markup).some(({ text: text2, start, en
|
|
|
6058
6173
|
category
|
|
6059
6174
|
};
|
|
6060
6175
|
}
|
|
6061
|
-
var
|
|
6176
|
+
var import_tldts4, GTUBE, BLOCKED_SUBJECT, HACKED_SUBJECT, COIN, CODE, CURRENCY, MONEY, CRYPTOCURRENCY, AMOUNT_DEMAND, AMOUNT_GIVEN, HARM_VERB, HARMED, RECORDING, THREAT, HOURS, COUNT, DEADLINE, isPaymentDemand, BITCOIN_ADDRESS, IGNORE_INSTRUCTIONS, AI_ACTION, AI_OBJECT, AI_ACTION_OBJECT, AI_NEAR, VERDICT, VERDICT_WORD, VERDICT_ORDER, VERDICT_CLAIM, CLASSIFY, SYSTEM_PROMPT, YOU_ARE, QUOTE, CONDITION, DISCUSSED, DISCUSSED_SHOWN, DISCUSSED_NEAR, isDiscussed, isInjection, QUOTE_OR_SPACE, SHOWN_NEAR, isOrder, PAYPAL_DOMAIN, PAYPAL_SPAM_TYPE_IDS, PAYPAL_OWN_REQUEST, PAYPAL_INVOICE_SUBJECT, MS_SPAM_VERDICT, MS_SPAM_CATEGORY, APPLE_NEXT, APPLE_FILLER, APPLE_STRONG, APPLE_BUSINESS, APPLE_ALONE, APPLE_BESIDE, APPLE_PART, NEXT_WORD, MAX_AFTER, APPLE, DISPLAY_BRANDS, ADDRESSES, LIST_HOSTS, VIA_LIST, MAILBOXES, BEFORE_ADDRESS, AFTER_ADDRESS, isWholeName, JOINED_WORDS, MAX_DISPLAY;
|
|
6062
6177
|
var init_is_arbitrary = __esm({
|
|
6063
6178
|
"src/is-arbitrary.js"() {
|
|
6064
|
-
|
|
6179
|
+
import_tldts4 = require("tldts");
|
|
6065
6180
|
init_homograph();
|
|
6066
6181
|
init_html();
|
|
6067
6182
|
init_linear();
|
|
@@ -7195,7 +7310,7 @@ Answer with exactly one word: ham, spam, phishing, scam or malware.`;
|
|
|
7195
7310
|
|
|
7196
7311
|
// src/model.js
|
|
7197
7312
|
function moduleDirectory() {
|
|
7198
|
-
return import_node_path.default.dirname((0,
|
|
7313
|
+
return import_node_path.default.dirname((0, import_node_url4.fileURLToPath)(__importMetaUrl));
|
|
7199
7314
|
}
|
|
7200
7315
|
function defaultModelPath(here = moduleDirectory()) {
|
|
7201
7316
|
if (import_node_process2.default.env.SPAMSCANNER_MODEL) {
|
|
@@ -7276,7 +7391,7 @@ function saveModel(classifier, file, options = {}) {
|
|
|
7276
7391
|
}
|
|
7277
7392
|
loaded.delete(resolved);
|
|
7278
7393
|
}
|
|
7279
|
-
var import_node_fs2, import_node_crypto3, import_node_module2, import_node_path, import_node_process2,
|
|
7394
|
+
var import_node_fs2, import_node_crypto3, import_node_module2, import_node_path, import_node_process2, import_node_url4, loaded;
|
|
7280
7395
|
var init_model = __esm({
|
|
7281
7396
|
"src/model.js"() {
|
|
7282
7397
|
import_node_fs2 = require("node:fs");
|
|
@@ -7284,7 +7399,7 @@ var init_model = __esm({
|
|
|
7284
7399
|
import_node_module2 = require("node:module");
|
|
7285
7400
|
import_node_path = __toESM(require("node:path"), 1);
|
|
7286
7401
|
import_node_process2 = __toESM(require("node:process"), 1);
|
|
7287
|
-
|
|
7402
|
+
import_node_url4 = require("node:url");
|
|
7288
7403
|
init_classifier();
|
|
7289
7404
|
loaded = /* @__PURE__ */ new Map();
|
|
7290
7405
|
}
|
|
@@ -7514,7 +7629,7 @@ var init_package = __esm({
|
|
|
7514
7629
|
package_default = {
|
|
7515
7630
|
name: "spamscanner",
|
|
7516
7631
|
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.",
|
|
7517
|
-
version: "7.0.
|
|
7632
|
+
version: "7.0.7",
|
|
7518
7633
|
author: "Forward Email (https://forwardemail.net)",
|
|
7519
7634
|
bin: {
|
|
7520
7635
|
spamscanner: "./dist/esm/cli.js"
|
|
@@ -9574,15 +9689,23 @@ var init_src = __esm({
|
|
|
9574
9689
|
message: `A link shows "${link.text}" but goes to ${link.host}`
|
|
9575
9690
|
}));
|
|
9576
9691
|
}
|
|
9577
|
-
|
|
9692
|
+
const name = lookupName(destination(link));
|
|
9693
|
+
if (name) {
|
|
9694
|
+
seenHosts.add(name);
|
|
9695
|
+
}
|
|
9578
9696
|
}
|
|
9579
|
-
|
|
9697
|
+
const found = links.filter((link) => !link.bare);
|
|
9698
|
+
const bare = links.filter((link) => link.bare);
|
|
9699
|
+
for (const link of [...found.filter((link2) => link2.text), ...found.filter((link2) => !link2.text)]) {
|
|
9580
9700
|
lookalike(destination(link), { url: link.url });
|
|
9581
9701
|
}
|
|
9702
|
+
for (const link of bare) {
|
|
9703
|
+
lookalike(link.host, { source: "text" });
|
|
9704
|
+
}
|
|
9582
9705
|
for (const host of hosts) {
|
|
9583
9706
|
lookalike(host, { source: "text" });
|
|
9584
9707
|
}
|
|
9585
|
-
const ordered = [...new Set([...
|
|
9708
|
+
const ordered = [...new Set([...found.filter((link) => link.text), ...found.filter((link) => !link.text), ...bare].map((link) => lookupName(destination(link))).filter(Boolean))];
|
|
9586
9709
|
const firsts = /* @__PURE__ */ new Map();
|
|
9587
9710
|
for (const host of ordered) {
|
|
9588
9711
|
const domain = registrableDomain(host);
|
|
@@ -9596,9 +9719,9 @@ var init_src = __esm({
|
|
|
9596
9719
|
let answered = 0;
|
|
9597
9720
|
const cloudflare = async () => {
|
|
9598
9721
|
await withTimeout(Promise.all(checked.map(async (host) => {
|
|
9599
|
-
const
|
|
9600
|
-
blocked.push(...
|
|
9601
|
-
if (!
|
|
9722
|
+
const found2 = await this.dns.checkCloudflare([host], { adult: config.phishing.adult });
|
|
9723
|
+
blocked.push(...found2);
|
|
9724
|
+
if (!found2.unanswered?.length) {
|
|
9602
9725
|
answered++;
|
|
9603
9726
|
}
|
|
9604
9727
|
})), config.timeout, null);
|
|
@@ -9782,7 +9905,7 @@ var init_src = __esm({
|
|
|
9782
9905
|
const linkLength = Number.isFinite(config.phishing.maxLength) && config.phishing.maxLength > 0 ? config.phishing.maxLength : 1e6;
|
|
9783
9906
|
const text = typeof mail.text === "string" ? mail.text : "";
|
|
9784
9907
|
const html2 = typeof mail.html === "string" ? mail.html : "";
|
|
9785
|
-
const checkedLinks = extractLinks(text.slice(0, linkLength), html2.slice(0, linkLength * 2), { maxLength: linkLength * 2 });
|
|
9908
|
+
const checkedLinks = extractLinks(text.slice(0, linkLength), html2.slice(0, linkLength * 2), { maxLength: linkLength * 2, bareHosts: true });
|
|
9786
9909
|
const linksTruncated = text.length > linkLength || html2.length > linkLength * 2;
|
|
9787
9910
|
const plainObfuscation = obfuscationOf(`${mail.subject || ""}
|
|
9788
9911
|
${typeof mail.text === "string" ? mail.text : ""}`, { maxLength: config.maxLength });
|
|
@@ -9923,7 +10046,8 @@ ${visibleText(html2.slice(0, config.maxLength * 2)).text}`, { maxLength: config.
|
|
|
9923
10046
|
riskScore: Math.max(0, ...links.homographs.map((item) => item.riskScore))
|
|
9924
10047
|
}
|
|
9925
10048
|
},
|
|
9926
|
-
|
|
10049
|
+
// (the links of the message, not hosts found without one)
|
|
10050
|
+
links: checkedLinks.filter((link) => !link.bare).map((link) => link.url),
|
|
9927
10051
|
language: extracted.language,
|
|
9928
10052
|
tokens: extracted.words,
|
|
9929
10053
|
mail,
|