spamscanner 7.0.5 → 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.
@@ -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 isLookupName(name) {
4445
- if (typeof name !== "string") {
4446
- return false;
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 host = name.replaceAll(/^\[|]$/g, "").replace(/\.$/, "");
4449
- return host !== "" && (0, import_node_net.isIP)(host) === 0 && host.length <= 253 && host.split(".").every((label) => label.length > 0 && label.length <= 63);
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. A name blocked by
4654
- * the malware resolver is "malware"; one blocked only by the family
4655
- * resolver is "adult". The hosts that got no answer (from the malware
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 results = await Promise.all(hosts.map(async (host) => {
4669
- if (!isLookupName(host)) {
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, import_node_url2.domainToASCII)(lower);
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, import_node_url2, EMPTY, ReputationChecker, domainOf, sameDomain;
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
- import_node_url2 = require("node:url");
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, import_tldts3.parse)(domain);
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, import_tldts3.parse)(entry).domain !== null) && (domain === entry || domain.endsWith(`.${entry}`)) : name === entry);
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, import_tldts3.parse)(domain).domainWithoutSuffix) ? null : DISPLAY_BRANDS.find((entry) => isOfficial(entry, domain, "", false) && !isOfficial(entry, session.originalFromAddressDomain, ""));
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 import_tldts3, 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;
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
- import_tldts3 = require("tldts");
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, import_node_url3.fileURLToPath)(__importMetaUrl));
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, import_node_url3, loaded;
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
- import_node_url3 = require("node:url");
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.5",
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
- seenHosts.add(destination(link));
9692
+ const name = lookupName(destination(link));
9693
+ if (name) {
9694
+ seenHosts.add(name);
9695
+ }
9578
9696
  }
9579
- for (const link of [...links.filter((link2) => link2.text), ...links.filter((link2) => !link2.text)]) {
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([...links.filter((link) => link.text), ...links.filter((link) => !link.text)].map((link) => destination(link)))];
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 found = await this.dns.checkCloudflare([host], { adult: config.phishing.adult });
9600
- blocked.push(...found);
9601
- if (!found.unanswered?.length) {
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
- links: checkedLinks.map((link) => link.url),
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,