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.
@@ -1784,7 +1784,7 @@ var init_entities = __esm({
1784
1784
  ",itilde=129,iukcy=456,iuml=ef,jcirc=135,jcy=439,jfr=1d527,jmath=237,jopf=1d55b,jscr=1d4bf,jsercy=458",
1785
1785
  ",jukcy=454,kappa=3ba,kappav=3f0,kcedil=137,kcy=43a,kfr=1d528,kgreen=138,khcy=445,kjcy=45c,kopf=1d55c",
1786
1786
  ",kscr=1d4c0,lAarr=21da,lArr=21d0,lAtail=291b,lBarr=290e,lE=2266,lEg=2a8b,lHar=2962,lacute=13a,laempt",
1787
- "yv=29b4,lagran=2112,lambda=3bb,lang=2329,langd=2991,langle=27e8,lap=2a85,laquo=ab,larr=2190,larrb=21",
1787
+ "yv=29b4,lagran=2112,lambda=3bb,lang=27e8,langd=2991,langle=27e8,lap=2a85,laquo=ab,larr=2190,larrb=21",
1788
1788
  "e4,larrbfs=291f,larrfs=291d,larrhk=21a9,larrlp=21ab,larrpl=2939,larrsim=2973,larrtl=21a2,lat=2aab,la",
1789
1789
  "tail=2919,late=2aad,lates=2aad.fe00,lbarr=290c,lbbrk=2772,lbrace=7b,lbrack=5b,lbrke=298b,lbrksld=298",
1790
1790
  "f,lbrkslu=298d,lcaron=13e,lcedil=13c,lceil=2308,lcub=7b,lcy=43b,ldca=2936,ldquo=201c,ldquor=201e,ldr",
@@ -1842,7 +1842,7 @@ var init_entities = __esm({
1842
1842
  "line=2312,profsurf=2313,prop=221d,propto=221d,prsim=227e,prurel=22b0,pscr=1d4c5,psi=3c8,puncsp=2008,",
1843
1843
  "qfr=1d52e,qint=2a0c,qopf=1d562,qprime=2057,qscr=1d4c6,quaternions=210d,quatint=2a16,quest=3f,questeq",
1844
1844
  "=225f,quot=22,rAarr=21db,rArr=21d2,rAtail=291c,rBarr=290f,rHar=2964,race=223d.331,racute=155,radic=2",
1845
- "21a,raemptyv=29b3,rang=232a,rangd=2992,range=29a5,rangle=27e9,raquo=bb,rarr=2192,rarrap=2975,rarrb=2",
1845
+ "21a,raemptyv=29b3,rang=27e9,rangd=2992,range=29a5,rangle=27e9,raquo=bb,rarr=2192,rarrap=2975,rarrb=2",
1846
1846
  "1e5,rarrbfs=2920,rarrc=2933,rarrfs=291e,rarrhk=21aa,rarrlp=21ac,rarrpl=2945,rarrsim=2974,rarrtl=21a3",
1847
1847
  ",rarrw=219d,ratail=291a,ratio=2236,rationals=211a,rbarr=290d,rbbrk=2773,rbrace=7d,rbrack=5d,rbrke=29",
1848
1848
  "8c,rbrksld=298e,rbrkslu=2990,rcaron=159,rcedil=157,rceil=2309,rcub=7d,rcy=440,rdca=2937,rdldhar=2969",
@@ -2697,6 +2697,31 @@ function continuesName(text, index) {
2697
2697
  }
2698
2698
  return (text[index - 1] === "." || text[index - 1] === "-") && NAME_END.test(text.slice(Math.max(0, index - 3), index - 1));
2699
2699
  }
2700
+ function boundedUrl(address2) {
2701
+ const text = String(address2).replaceAll(/[\t\n\r]/g, "");
2702
+ const colon = text.indexOf(":");
2703
+ let start = colon === -1 ? 0 : colon + 1;
2704
+ while (start < text.length && (text[start] === "/" || text[start] === "\\")) {
2705
+ start++;
2706
+ }
2707
+ AUTHORITY_END.lastIndex = start;
2708
+ const end = AUTHORITY_END.exec(text)?.index ?? text.length;
2709
+ const file = /^[\0- ]*file:/i.test(text);
2710
+ const at = file ? start : Math.max(start, text.lastIndexOf("@", end - 1) + 1);
2711
+ const bracket = text.lastIndexOf("]", end - 1);
2712
+ const port = text.lastIndexOf(":", end - 1);
2713
+ let host = text.slice(at, !file && port > Math.max(at - 1, bracket) ? port : end);
2714
+ if (host.length > MAX_URL_HOST && host.includes("%")) {
2715
+ try {
2716
+ host = decodeURIComponent(host);
2717
+ } catch {
2718
+ }
2719
+ }
2720
+ if (host.length > MAX_URL_HOST && host.replaceAll(IGNORED_IN_HOST, "").length > MAX_URL_HOST) {
2721
+ throw new TypeError("Invalid URL: host too long");
2722
+ }
2723
+ return new URL(text);
2724
+ }
2700
2725
  function isClickTracker(url) {
2701
2726
  const host = url.hostname.toLowerCase();
2702
2727
  return CLICK_TRACKERS.some((domain) => isUnder(host, domain)) || host === "substack.com" && url.pathname.startsWith("/redirect/");
@@ -2723,7 +2748,7 @@ function gatewayTarget(url) {
2723
2748
  function unwrapLink(address2) {
2724
2749
  let url;
2725
2750
  try {
2726
- url = new URL(address2);
2751
+ url = boundedUrl(address2);
2727
2752
  } catch {
2728
2753
  return null;
2729
2754
  }
@@ -2734,7 +2759,7 @@ function unwrapLink(address2) {
2734
2759
  }
2735
2760
  let next;
2736
2761
  try {
2737
- next = new URL(target);
2762
+ next = boundedUrl(target);
2738
2763
  } catch {
2739
2764
  break;
2740
2765
  }
@@ -2996,7 +3021,7 @@ function extractLinks(text, html2, { maxLength = 5e5, bareHosts: bareHosts2 = fa
2996
3021
  }
2997
3022
  let url;
2998
3023
  try {
2999
- url = new URL(href);
3024
+ url = boundedUrl(href);
3000
3025
  } catch {
3001
3026
  return;
3002
3027
  }
@@ -3309,7 +3334,7 @@ function isDeceptiveLink(link, { from } = {}) {
3309
3334
  }
3310
3335
  let shownUrl;
3311
3336
  try {
3312
- shownUrl = new URL(/^https?:\/\//i.test(link.text) ? link.text : `http://${link.text}`);
3337
+ shownUrl = boundedUrl(/^https?:\/\//i.test(link.text) ? link.text : `http://${link.text}`);
3313
3338
  } catch {
3314
3339
  return false;
3315
3340
  }
@@ -3667,7 +3692,7 @@ ${body}`);
3667
3692
  hidden: visible.hidden
3668
3693
  };
3669
3694
  }
3670
- 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, MAX_GATEWAYS, 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;
3695
+ 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, MAX_URL_HOST, AUTHORITY_END, IGNORED_IN_HOST, isUnder, MAX_GATEWAYS, 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;
3671
3696
  var init_tokenizer = __esm({
3672
3697
  "src/tokenizer.js"() {
3673
3698
  import_tldts2 = require("tldts");
@@ -3773,6 +3798,9 @@ var init_tokenizer = __esm({
3773
3798
  FILE_TLDS = /* @__PURE__ */ new Set(["ai", "app", "md", "mov", "pm", "ps", "py", "rs", "sh", "so", "zip"]);
3774
3799
  LINK_FILE_TLDS = /* @__PURE__ */ new Set(["md", "mov", "pm", "ps", "py", "rs", "sh", "so", "zip"]);
3775
3800
  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"];
3801
+ MAX_URL_HOST = 4096;
3802
+ AUTHORITY_END = /[/\\?#]/g;
3803
+ IGNORED_IN_HOST = /[\u00AD\u034F\u180B-\u180D\u180F\u200B\u2060\u2064\uFE00-\uFE0F\uFEFF]|\uD82F[\uDCA0-\uDCA3]|\uDB40[\uDD00-\uDDEF]/g;
3776
3804
  isUnder = (host, domain) => host === domain || host.endsWith(`.${domain}`);
3777
3805
  MAX_GATEWAYS = 16;
3778
3806
  BRAND_NAMES = new Set(BRANDS);
@@ -4612,7 +4640,7 @@ async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PA
4612
4640
  }
4613
4641
  }
4614
4642
  async function expandAttachments(attachments = [], errors = [], parse = parseMessage, gets = {}) {
4615
- const { unopened = [], mails = [] } = Array.isArray(gets) ? { unopened: gets } : gets;
4643
+ const { unopened = [], mails = [] } = Array.isArray(gets) ? { unopened: gets } : gets ?? {};
4616
4644
  const all = [];
4617
4645
  const deadline = Date.now() + PARSE_TIMEOUT;
4618
4646
  let budget = MAX_NESTED_BYTES;
@@ -4761,6 +4789,10 @@ var init_parse = __esm({
4761
4789
  });
4762
4790
 
4763
4791
  // src/dnsbl.js
4792
+ function lastLabels(name) {
4793
+ const separators = [...name.matchAll(LABEL_SEPARATOR)];
4794
+ return separators.length < 128 ? 0 : separators.at(-128).index + 1;
4795
+ }
4764
4796
  function lookupName(host) {
4765
4797
  if (typeof host !== "string") {
4766
4798
  return null;
@@ -4771,6 +4803,7 @@ function lookupName(host) {
4771
4803
  }
4772
4804
  let name = host.slice(0, end).toLowerCase();
4773
4805
  if (/[\u0080-\u{10FFFF}]/u.test(name)) {
4806
+ name = name.slice(lastLabels(name));
4774
4807
  name = (0, import_node_url2.domainToASCII)(name) || name;
4775
4808
  }
4776
4809
  if ((0, import_node_net.isIP)(name.replaceAll(/^\[|]$/g, "")) !== 0) {
@@ -4870,7 +4903,7 @@ function isListedAnswer(address2, zone) {
4870
4903
  }
4871
4904
  return true;
4872
4905
  }
4873
- var import_node_dns, import_node_net, import_node_url2, import_tldts3, LOOKUP_LABEL, CLOUDFLARE_MALWARE, CLOUDFLARE_FAMILY, DnsChecker;
4906
+ var import_node_dns, import_node_net, import_node_url2, import_tldts3, LOOKUP_LABEL, LABEL_SEPARATOR, CLOUDFLARE_MALWARE, CLOUDFLARE_FAMILY, DnsChecker;
4874
4907
  var init_dnsbl = __esm({
4875
4908
  "src/dnsbl.js"() {
4876
4909
  import_node_dns = __toESM(require("node:dns"), 1);
@@ -4878,6 +4911,7 @@ var init_dnsbl = __esm({
4878
4911
  import_node_url2 = require("node:url");
4879
4912
  import_tldts3 = require("tldts");
4880
4913
  LOOKUP_LABEL = /^[a-z\d_-]{1,63}$/;
4914
+ LABEL_SEPARATOR = /[.\u3002\uFF0E\uFF61]/g;
4881
4915
  CLOUDFLARE_MALWARE = ["1.1.1.2", "1.0.0.2"];
4882
4916
  CLOUDFLARE_FAMILY = ["1.1.1.3", "1.0.0.3"];
4883
4917
  DnsChecker = class {
@@ -5117,7 +5151,7 @@ function asciiHostname(value) {
5117
5151
  return null;
5118
5152
  }
5119
5153
  const lower = value.normalize("NFKC").toLowerCase().replace(/\.$/, "");
5120
- if (lower === "" || /[\0-,/:-^`{-\u007F]/.test(lower)) {
5154
+ if (lower === "" || lower.length > 1024 || /[\0-,/:-^`{-\u007F]/.test(lower)) {
5121
5155
  return null;
5122
5156
  }
5123
5157
  const ascii = (0, import_node_url3.domainToASCII)(lower);
@@ -5190,8 +5224,8 @@ function trustReputation(result, trusted = []) {
5190
5224
  for (const value of result.checkedValues.filter((item) => allowed.has(item))) {
5191
5225
  const signers = allowed.get(value);
5192
5226
  const counts = (entry) => Boolean(entry) && (signers === null || signers.some((signer) => vouches(normalizeEntry(entry), signer)));
5193
- const detail = result.details[value];
5194
- const listed = result.allowlisted[value] || (detail?.isAllowlisted ? detail.allowlistValue : null);
5227
+ const detail = Object.hasOwn(result.details, value) ? result.details[value] : void 0;
5228
+ const listed = Object.hasOwn(result.allowlisted, value) && result.allowlisted[value] || (detail?.isAllowlisted ? detail.allowlistValue : null);
5195
5229
  if (counts(listed) && !out.isAllowlisted) {
5196
5230
  out.isAllowlisted = true;
5197
5231
  out.allowlistValue = listed;
@@ -6234,7 +6268,7 @@ function urlOf(value) {
6234
6268
  return null;
6235
6269
  }
6236
6270
  try {
6237
- return new URL(href);
6271
+ return boundedUrl(href);
6238
6272
  } catch {
6239
6273
  return null;
6240
6274
  }
@@ -6324,7 +6358,11 @@ function* attributesOf(html2) {
6324
6358
  TAG_START.lastIndex = Math.max(i, match.index + 1);
6325
6359
  }
6326
6360
  }
6327
- function linksOf(name, value, add, embed) {
6361
+ function linksOf(name, value, { add, embed, cut }) {
6362
+ if (value.length > MAX_VALUE2) {
6363
+ cut();
6364
+ value = value.slice(0, MAX_VALUE2);
6365
+ }
6328
6366
  const decoded = decodeEntities(value, { attribute: true });
6329
6367
  switch (name) {
6330
6368
  case "srcdoc": {
@@ -6363,7 +6401,7 @@ function linksOf(name, value, add, embed) {
6363
6401
  }
6364
6402
  }
6365
6403
  }
6366
- function lenientLinks(html2, add, done, embed) {
6404
+ function lenientLinks(html2, done, reader) {
6367
6405
  LENIENT.lastIndex = 0;
6368
6406
  let match;
6369
6407
  while (!done() && (match = LENIENT.exec(html2)) !== null) {
@@ -6379,8 +6417,8 @@ function lenientLinks(html2, add, done, embed) {
6379
6417
  }
6380
6418
  if (end < html2.length && html2[end] !== quote) {
6381
6419
  const close = html2.indexOf(quote, end);
6382
- if (close !== -1 && close - i <= MAX_LENIENT_VALUE) {
6383
- linksOf(name, html2.slice(i, close), add, embed);
6420
+ if (close !== -1 && close - i <= MAX_LENIENT_VALUE && !reader.spend(close - i)) {
6421
+ linksOf(name, html2.slice(i, close), reader);
6384
6422
  }
6385
6423
  }
6386
6424
  } else {
@@ -6388,7 +6426,10 @@ function lenientLinks(html2, add, done, embed) {
6388
6426
  end++;
6389
6427
  }
6390
6428
  }
6391
- linksOf(name, html2.slice(i, end), add, embed);
6429
+ if (reader.spend(end - i)) {
6430
+ break;
6431
+ }
6432
+ linksOf(name, html2.slice(i, end), reader);
6392
6433
  }
6393
6434
  }
6394
6435
  function endsLink2(text, i) {
@@ -6559,8 +6600,17 @@ function* chunksOf(html2) {
6559
6600
  cutStart = false;
6560
6601
  continue;
6561
6602
  }
6562
- yield { text: html2.slice(start, end), cutStart, cutEnd: true };
6563
- start = end - CHUNK_OVERLAP;
6603
+ let hard = end;
6604
+ while (hard > start + CHUNK_OVERLAP + 1 && /[\dA-Za-z#]/.test(html2[hard - 1])) {
6605
+ hard--;
6606
+ }
6607
+ if (hard < end && html2[hard - 1] === "&" && hard - 1 > start + CHUNK_OVERLAP) {
6608
+ hard--;
6609
+ } else {
6610
+ hard = end;
6611
+ }
6612
+ yield { text: html2.slice(start, hard), cutStart, cutEnd: true };
6613
+ start = hard - CHUNK_OVERLAP;
6564
6614
  cutStart = true;
6565
6615
  continue;
6566
6616
  }
@@ -6722,27 +6772,14 @@ function quotedPrintableValues(text) {
6722
6772
  }
6723
6773
  return out.join("\n");
6724
6774
  }
6725
- function hasQuotedPrintable(text) {
6726
- const unfolded = text.replaceAll(FOLD, "");
6727
- if (!/quoted-printable/i.test(unfolded)) {
6728
- return false;
6729
- }
6730
- return unfolded.split(LINE_BREAK).some((line) => {
6731
- const colon = valueColon(line);
6732
- return colon !== -1 && QUOTED_PRINTABLE.test(line.slice(0, colon));
6733
- });
6734
- }
6735
- function shownText(text, full = true) {
6775
+ function shownText(text) {
6736
6776
  const unfolded = text.replaceAll(FOLD, "");
6737
6777
  const shown = /* @__PURE__ */ new Set([text, readEscapes(unfolded)]);
6738
- let decoded = unfolded;
6739
- if (full) {
6740
- decoded = quotedPrintableValues(unfolded);
6741
- if (decoded !== unfolded) {
6742
- shown.add(readEscapes(decoded));
6743
- if (unfolded !== text) {
6744
- shown.add(quotedPrintableValues(text));
6745
- }
6778
+ const decoded = quotedPrintableValues(unfolded);
6779
+ if (decoded !== unfolded) {
6780
+ shown.add(readEscapes(decoded));
6781
+ if (unfolded !== text) {
6782
+ shown.add(quotedPrintableValues(text));
6746
6783
  }
6747
6784
  }
6748
6785
  if (/\\[^nN,;\\]/.test(decoded)) {
@@ -6841,58 +6878,71 @@ function charsetOf(attachment) {
6841
6878
  const charset = headers && typeof headers.get === "function" ? headers.get("content-type")?.params?.charset : null;
6842
6879
  return typeof charset === "string" ? charset.toLowerCase() : "";
6843
6880
  }
6844
- function documentsOf(attachments, until) {
6845
- const documents = [];
6881
+ function kindOf(attachment) {
6882
+ const { content } = attachment ?? {};
6883
+ if (!import_node_buffer7.Buffer.isBuffer(content) || content.length === 0) {
6884
+ return null;
6885
+ }
6886
+ const type = typeof attachment.contentType === "string" ? attachment.contentType.toLowerCase() : "";
6887
+ const extensions = extensionsOf(attachment);
6888
+ const has = (set) => [...extensions].some((name) => set.has(name));
6889
+ if (MESSAGE_TYPES.has(type) || extensions.has("eml") || extensions.has("u8msg")) {
6890
+ return null;
6891
+ }
6892
+ const isHtml = HTML_TYPES.has(type) || has(HTML_EXTENSIONS);
6893
+ const isCard = !isHtml && (CARD_TYPES.has(type) || has(CARD_EXTENSIONS));
6894
+ const isText = isCard || !isHtml && type === "text/plain";
6895
+ return isHtml || isText ? { isHtml, isText, isCard } : null;
6896
+ }
6897
+ function* documentsOf(attachments, state, late) {
6898
+ const read = [];
6846
6899
  let bytes = 0;
6847
6900
  let opened = 0;
6848
6901
  for (const attachment of attachments) {
6849
- const { content } = attachment ?? {};
6850
- if (!import_node_buffer7.Buffer.isBuffer(content) || content.length === 0) {
6851
- continue;
6852
- }
6853
- const type = typeof attachment.contentType === "string" ? attachment.contentType.toLowerCase() : "";
6854
- const extensions = extensionsOf(attachment);
6855
- const has = (set) => [...extensions].some((name) => set.has(name));
6856
- if (MESSAGE_TYPES.has(type) || extensions.has("eml") || extensions.has("u8msg")) {
6857
- continue;
6858
- }
6859
- const isHtml = HTML_TYPES.has(type) || has(HTML_EXTENSIONS);
6860
- const isCard = !isHtml && (CARD_TYPES.has(type) || has(CARD_EXTENSIONS));
6861
- const isText = isCard || !isHtml && type === "text/plain";
6862
- if (!isHtml && !isText) {
6902
+ const kind = kindOf(attachment);
6903
+ if (!kind) {
6863
6904
  continue;
6864
6905
  }
6865
- if (!isText && ++opened > MAX_ATTACHED || bytes + content.length > MAX_ATTACHED_BYTES) {
6866
- documents.tooComplex = true;
6867
- return documents;
6906
+ if (!kind.isText && ++opened > MAX_ATTACHED || bytes + attachment.content.length > MAX_ATTACHED_BYTES) {
6907
+ state.tooComplex = true;
6908
+ break;
6868
6909
  }
6869
- if (Date.now() > until) {
6870
- documents.incomplete = true;
6871
- return documents;
6910
+ bytes += attachment.content.length;
6911
+ read.push({ attachment, kind });
6912
+ }
6913
+ for (const { attachment, kind: { isHtml, isCard } } of read) {
6914
+ if (late()) {
6915
+ state.incomplete = true;
6916
+ return;
6872
6917
  }
6873
- bytes += content.length;
6874
- const text = decodeText(content, charsetOf(attachment));
6918
+ const text = decodeText(attachment.content, charsetOf(attachment));
6875
6919
  if (isHtml) {
6876
- documents.push({ html: text });
6920
+ yield { html: text };
6877
6921
  continue;
6878
6922
  }
6879
6923
  if (!isCard) {
6880
- documents.push({ text });
6924
+ yield { text };
6881
6925
  continue;
6882
6926
  }
6883
- const readable = withoutBinaryValues(text);
6884
- const full = readable.length <= MAX_SHOWN_BYTES;
6885
- if (!full && hasQuotedPrintable(readable)) {
6886
- documents.tooComplex = true;
6887
- continue;
6927
+ let readable = withoutBinaryValues(text.length > MAX_CARD_BYTES ? text.slice(0, MAX_CARD_BYTES) : text);
6928
+ if (text.length > MAX_CARD_BYTES || readable.length > MAX_SHOWN_BYTES) {
6929
+ state.tooComplex = true;
6930
+ readable = readable.slice(0, MAX_SHOWN_BYTES);
6931
+ }
6932
+ if (late()) {
6933
+ state.incomplete = true;
6934
+ return;
6935
+ }
6936
+ yield { text: shownText(readable) };
6937
+ if (late()) {
6938
+ state.incomplete = true;
6939
+ return;
6888
6940
  }
6889
- documents.push({ text: shownText(readable, full) });
6890
6941
  const html2 = htmlDescriptions(readable);
6891
6942
  if (html2 !== "") {
6892
- documents.push({ html: html2 });
6943
+ yield { html: html2 };
6893
6944
  }
6894
6945
  }
6895
- return documents;
6896
6946
  }
6897
6947
  function carriedLinkHosts(mail, {
6898
6948
  mails = [],
@@ -6906,40 +6956,45 @@ function carriedLinkHosts(mail, {
6906
6956
  const seenBare = /* @__PURE__ */ new Set();
6907
6957
  const seenLoose = /* @__PURE__ */ new Set();
6908
6958
  let more = false;
6909
- let tooComplex = false;
6910
- const done = () => more;
6911
- const looseDone = () => more || loose.size > maxHosts;
6959
+ let full = false;
6960
+ const domains = /* @__PURE__ */ new Set();
6961
+ const state = { tooComplex: false, incomplete: false };
6962
+ const until = Date.now() + MAX_READ_TIME;
6963
+ const late = () => {
6964
+ if (!state.incomplete && Date.now() > until) {
6965
+ state.incomplete = true;
6966
+ }
6967
+ return state.incomplete;
6968
+ };
6969
+ let calls = 0;
6970
+ const done = () => full || state.incomplete || ++calls % LATE_EVERY === 0 && late();
6971
+ const looseDone = () => done() || loose.size > maxHosts;
6912
6972
  const addLoose = (host) => {
6913
6973
  if (!looseDone()) {
6914
6974
  loose.add(host);
6915
6975
  }
6916
6976
  };
6917
- const until = Date.now() + MAX_READ_TIME;
6918
- let incomplete = false;
6919
- const late = () => {
6920
- if (!incomplete && Date.now() > until) {
6921
- incomplete = true;
6922
- }
6923
- return incomplete;
6924
- };
6925
- const carried = documentsOf(attachments, until);
6926
- tooComplex = Boolean(carried.tooComplex);
6927
- incomplete = Boolean(carried.incomplete);
6928
- const queue = [mail, ...mails, ...carried].map((document) => ({ document, depth: 0 }));
6977
+ const queue = [mail, ...mails].map((document) => ({ document, depth: 0 }));
6978
+ const carried = documentsOf(attachments, state, late);
6929
6979
  let depth = 0;
6930
6980
  let embedded = 0;
6931
6981
  let embeddedLength = 0;
6982
+ const embeddedSeen = /* @__PURE__ */ new Set();
6932
6983
  const embed = (html2) => {
6984
+ if (html2 === "" || embeddedSeen.has(html2)) {
6985
+ return;
6986
+ }
6933
6987
  if (depth >= MAX_EMBED_DEPTH || embedded >= MAX_EMBEDDED || embeddedLength + html2.length > MAX_EMBEDDED_LENGTH) {
6934
- tooComplex = true;
6988
+ state.tooComplex = true;
6935
6989
  return;
6936
6990
  }
6991
+ embeddedSeen.add(html2);
6937
6992
  embedded++;
6938
6993
  embeddedLength += html2.length;
6939
6994
  queue.push({ document: { html: html2 }, depth: depth + 1 });
6940
6995
  };
6941
6996
  const add = (value) => {
6942
- if (more || typeof value !== "string" || value === "" || seen.has(value)) {
6997
+ if (full || typeof value !== "string" || value === "" || seen.has(value)) {
6943
6998
  return;
6944
6999
  }
6945
7000
  if (seen.size < MAX_SEEN) {
@@ -6954,34 +7009,64 @@ function carriedLinkHosts(mail, {
6954
7009
  }
6955
7010
  const found = hostsOf(value);
6956
7011
  if (found.unresolved) {
6957
- tooComplex = true;
7012
+ state.tooComplex = true;
6958
7013
  }
6959
7014
  for (const host of found) {
6960
7015
  if (hosts.has(host)) {
6961
7016
  continue;
6962
7017
  }
6963
- if (hosts.size >= maxHosts) {
6964
- more = true;
7018
+ const domain = registrableDomain(host);
7019
+ if (hosts.size < maxHosts || !domains.has(domain) && domains.size < maxHosts) {
7020
+ hosts.add(host);
7021
+ domains.add(domain);
7022
+ continue;
7023
+ }
7024
+ more = true;
7025
+ if (domains.size >= maxHosts) {
7026
+ full = true;
6965
7027
  return;
6966
7028
  }
6967
- hosts.add(host);
6968
7029
  }
6969
7030
  };
6970
- const readText = (source, { cutStart = false, cutEnd = false } = {}) => {
7031
+ let spent = 0;
7032
+ const spend = (length) => {
7033
+ spent += length;
7034
+ if (spent < LONG_VALUE) {
7035
+ return false;
7036
+ }
7037
+ spent = 0;
7038
+ return late();
7039
+ };
7040
+ const reader = {
7041
+ add,
7042
+ embed,
7043
+ spend,
7044
+ cut() {
7045
+ state.tooComplex = true;
7046
+ }
7047
+ };
7048
+ const readText = (source, { cutStart = false, cutEnd = false, exact = false } = {}) => {
6971
7049
  for (const match of source.matchAll(URL_START)) {
6972
- if (more) {
7050
+ if (done()) {
6973
7051
  break;
6974
7052
  }
6975
7053
  if (cutStart && match.index === 0) {
6976
7054
  continue;
6977
7055
  }
6978
7056
  if (cutEnd && match.index + match[0].length === source.length) {
7057
+ if (exact && source.length - match.index < CHUNK_OVERLAP) {
7058
+ continue;
7059
+ }
6979
7060
  const value = match[0];
6980
7061
  const at = /^www\./i.test(value) ? 0 : value.indexOf("//") + 2;
6981
- if (/[/?#\\]/.test(value.slice(at))) {
6982
- add(value);
6983
- } else if (source.length - match.index > CHUNK_OVERLAP) {
6984
- tooComplex = true;
7062
+ if (!/[/?#\\]/.test(value.slice(at))) {
7063
+ state.tooComplex = true;
7064
+ continue;
7065
+ }
7066
+ add(value);
7067
+ const url = urlOf(value);
7068
+ if (url && isClickTracker(url)) {
7069
+ state.tooComplex = true;
6985
7070
  }
6986
7071
  continue;
6987
7072
  }
@@ -7003,42 +7088,60 @@ function carriedLinkHosts(mail, {
7003
7088
  });
7004
7089
  }
7005
7090
  };
7006
- for (const element of queue) {
7007
- if (more || late()) {
7091
+ for (let index = 0; ; index++) {
7092
+ if (full || late()) {
7008
7093
  break;
7009
7094
  }
7010
- ({ depth } = element);
7011
- const { document } = element;
7095
+ if (index >= queue.length) {
7096
+ const next = carried.next();
7097
+ if (next.done) {
7098
+ break;
7099
+ }
7100
+ queue.push({ document: next.value, depth: 0 });
7101
+ }
7102
+ ({ depth } = queue[index]);
7103
+ const { document } = queue[index];
7012
7104
  const html2 = withoutNul(typeof document?.html === "string" ? document.html : "");
7013
7105
  const text = withoutNul(typeof document?.text === "string" ? document.text : "");
7014
7106
  for (const [name, value] of attributesOf(html2)) {
7015
- if (more) {
7107
+ if (done()) {
7016
7108
  break;
7017
7109
  }
7018
- if (LINK_ATTRIBUTES.has(name)) {
7019
- linksOf(name, value, add, embed);
7110
+ if (!LINK_ATTRIBUTES.has(name)) {
7111
+ continue;
7020
7112
  }
7113
+ if (spend(value.length)) {
7114
+ break;
7115
+ }
7116
+ linksOf(name, value, reader);
7021
7117
  }
7022
- lenientLinks(html2, add, done, embed);
7118
+ lenientLinks(html2, done, reader);
7023
7119
  for (const pattern of [CSS_URL, CSS_IMPORT]) {
7024
7120
  for (const [, link] of html2.matchAll(pattern)) {
7025
- if (more) {
7121
+ if (done() || spend(link.length)) {
7026
7122
  break;
7027
7123
  }
7028
- add(decodeEntities(link, { attribute: true }));
7124
+ if (link.length > MAX_VALUE2) {
7125
+ state.tooComplex = true;
7126
+ }
7127
+ add(decodeEntities(link.slice(0, MAX_VALUE2), { attribute: true }));
7029
7128
  }
7030
7129
  }
7031
- if (!more) {
7032
- readText(text);
7130
+ for (const chunk2 of chunksOf(text)) {
7131
+ if (full || late()) {
7132
+ break;
7133
+ }
7134
+ readText(chunk2.text, { ...chunk2, exact: true });
7033
7135
  }
7034
7136
  for (const chunk2 of chunksOf(html2)) {
7035
- if (more || late()) {
7137
+ if (full || late()) {
7036
7138
  break;
7037
7139
  }
7038
- readText(decodeEntities(chunk2.text), chunk2);
7140
+ const decoded = decodeEntities(chunk2.text);
7141
+ readText(decoded, { ...chunk2, exact: decoded.endsWith(chunk2.text.slice(-CHUNK_OVERLAP)) });
7039
7142
  }
7040
7143
  }
7041
- if (!more && !incomplete) {
7144
+ if (!more && !state.incomplete) {
7042
7145
  for (const name of loose) {
7043
7146
  if (hosts.size >= maxHosts) {
7044
7147
  break;
@@ -7052,11 +7155,11 @@ function carriedLinkHosts(mail, {
7052
7155
  return {
7053
7156
  hosts: [...hosts],
7054
7157
  more,
7055
- tooComplex,
7056
- incomplete
7158
+ tooComplex: state.tooComplex,
7159
+ incomplete: state.incomplete
7057
7160
  };
7058
7161
  }
7059
- var import_node_buffer7, import_node_net4, import_tldts4, LINK_ATTRIBUTES, TAG_START, URL_START, CSS_URL, CSS_IMPORT, LENIENT, IN_WORD2, NAMESPACES, MAX_HOST, MAX_SEEN, TRAILING, DECODE_CHUNK, CHUNK_OVERLAP, MAX_EMBED_DEPTH, MAX_EMBEDDED, MAX_EMBEDDED_LENGTH, MAX_ATTACHED, MAX_ATTACHED_BYTES, MAX_READ_TIME, MAX_SHOWN_BYTES, MAX_LENIENT_VALUE, UNQUOTED_END, UTF16LE, UTF16BE, DATA_DOCUMENT, LINE_BREAK, FOLD, QUOTED_PRINTABLE, MEDIA_PROPERTIES, HTML_PROPERTIES, BINARY, HTML_TYPES, HTML_EXTENSIONS, CARD_TYPES, CARD_EXTENSIONS, MESSAGE_TYPES;
7162
+ var import_node_buffer7, import_node_net4, import_tldts4, LINK_ATTRIBUTES, TAG_START, URL_START, CSS_URL, CSS_IMPORT, LENIENT, IN_WORD2, NAMESPACES, MAX_HOST, MAX_SEEN, TRAILING, DECODE_CHUNK, CHUNK_OVERLAP, MAX_EMBED_DEPTH, MAX_EMBEDDED, MAX_EMBEDDED_LENGTH, MAX_ATTACHED, MAX_ATTACHED_BYTES, MAX_READ_TIME, MAX_CARD_BYTES, MAX_SHOWN_BYTES, MAX_VALUE2, LONG_VALUE, MAX_LENIENT_VALUE, UNQUOTED_END, UTF16LE, UTF16BE, DATA_DOCUMENT, LINE_BREAK, FOLD, QUOTED_PRINTABLE, MEDIA_PROPERTIES, HTML_PROPERTIES, BINARY, HTML_TYPES, HTML_EXTENSIONS, CARD_TYPES, CARD_EXTENSIONS, MESSAGE_TYPES, LATE_EVERY;
7060
7163
  var init_links = __esm({
7061
7164
  "src/links.js"() {
7062
7165
  import_node_buffer7 = require("node:buffer");
@@ -7103,7 +7206,10 @@ var init_links = __esm({
7103
7206
  MAX_ATTACHED = 100;
7104
7207
  MAX_ATTACHED_BYTES = 50 * 1024 * 1024;
7105
7208
  MAX_READ_TIME = 5e3;
7106
- MAX_SHOWN_BYTES = 4 * 1024 * 1024;
7209
+ MAX_CARD_BYTES = 4 * 1024 * 1024;
7210
+ MAX_SHOWN_BYTES = 1024 * 1024;
7211
+ MAX_VALUE2 = 1024 * 1024;
7212
+ LONG_VALUE = 64 * 1024;
7107
7213
  MAX_LENIENT_VALUE = 4096;
7108
7214
  UNQUOTED_END = /* @__PURE__ */ new Set(["<", ">", '"', "'", "`", "="]);
7109
7215
  UTF16LE = /* @__PURE__ */ new Set(["csunicode", "iso-10646-ucs-2", "ucs-2", "unicode", "unicodefeff", "utf-16", "utf-16le"]);
@@ -7120,6 +7226,7 @@ var init_links = __esm({
7120
7226
  CARD_TYPES = /* @__PURE__ */ new Set(["text/calendar", "text/x-vcalendar", "application/ics", "text/vcard", "text/x-vcard", "text/directory"]);
7121
7227
  CARD_EXTENSIONS = /* @__PURE__ */ new Set(["ics", "ical", "ifb", "vcs", "vcf", "vcard"]);
7122
7228
  MESSAGE_TYPES = /* @__PURE__ */ new Set(["message/rfc822", "message/global"]);
7229
+ LATE_EVERY = 1024;
7123
7230
  }
7124
7231
  });
7125
7232
 
@@ -8784,7 +8891,7 @@ var init_package = __esm({
8784
8891
  package_default = {
8785
8892
  name: "spamscanner",
8786
8893
  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.",
8787
- version: "7.0.11",
8894
+ version: "7.0.12",
8788
8895
  author: "Forward Email (https://forwardemail.net)",
8789
8896
  bin: {
8790
8897
  spamscanner: "./dist/esm/cli.js"
@@ -9050,9 +9157,12 @@ function rewriteMessage(source, headers = [], options = {}) {
9050
9157
  }
9051
9158
  }
9052
9159
  const added = headers.map(([name, value]) => foldHeader(name, import_node_buffer9.Buffer.from(String(value), "utf8").toString("latin1"), newline));
9053
- const mbox = kept.length > 0 && kept[0][0].startsWith("From ") ? kept.shift() : [];
9160
+ const mbox = kept.length > 0 && kept[0][0].startsWith("From ") ? kept.shift().slice(0, 1) : [];
9161
+ if (kept.length > 0 && /^[ \t]/.test(kept[0][0])) {
9162
+ kept.shift();
9163
+ }
9054
9164
  const head = [...mbox, ...added, ...kept.map((field) => field.join(newline))].join(newline);
9055
- return import_node_buffer9.Buffer.concat([import_node_buffer9.Buffer.from(head === "" && header === "" ? newline : `${head}${newline}${newline}`, "latin1"), body]);
9165
+ return import_node_buffer9.Buffer.concat([import_node_buffer9.Buffer.from(head === "" ? newline : `${head}${newline}${newline}`, "latin1"), body]);
9056
9166
  }
9057
9167
  var import_node_buffer9, ENCODED_BYTES, PLAIN_WORD, ENCODED_LINE, MAX_LINE2, ENCODED_WORD, ENCODED_START;
9058
9168
  var init_headers = __esm({
@@ -9414,7 +9524,12 @@ var init_train = __esm({
9414
9524
 
9415
9525
  // src/server.js
9416
9526
  function serializeResult(result, options = {}) {
9417
- const { mail, tokens, ...rest } = result;
9527
+ const {
9528
+ mail,
9529
+ tokens,
9530
+ attached,
9531
+ ...rest
9532
+ } = result;
9418
9533
  const output = {
9419
9534
  ...rest,
9420
9535
  results: { ...result.results }
@@ -10099,7 +10214,7 @@ function report(result) {
10099
10214
  "",
10100
10215
  " pts rule name description",
10101
10216
  "---- ---------------------- --------------------------------------------------",
10102
- ...result.tests.map((test) => `${test.score.toFixed(1).padStart(4)} ${test.name.padEnd(22)} ${test.description}`)
10217
+ ...result.tests.map((test) => `${test.score.toFixed(1).padStart(4)} ${oneLine2(test.name).padEnd(22)} ${oneLine2(test.description)}`)
10103
10218
  ];
10104
10219
  return `${lines2.join("\r\n")}\r
10105
10220
  `;
@@ -10245,7 +10360,7 @@ function createSpamdServer(scanner, options = {}) {
10245
10360
  server.maxConnections = settings.maxConnections;
10246
10361
  return server;
10247
10362
  }
10248
- var import_node_buffer13, import_node_net7, EX_OK, EX_USAGE, EX_DATAERR, EX_SOFTWARE, EX_PROTOCOL, EX_NOPERM, COMMANDS2;
10363
+ var import_node_buffer13, import_node_net7, EX_OK, EX_USAGE, EX_DATAERR, EX_SOFTWARE, EX_PROTOCOL, EX_NOPERM, COMMANDS2, oneLine2;
10249
10364
  var init_spamd = __esm({
10250
10365
  "src/spamd.js"() {
10251
10366
  import_node_buffer13 = require("node:buffer");
@@ -10260,6 +10375,7 @@ var init_spamd = __esm({
10260
10375
  EX_PROTOCOL = 76;
10261
10376
  EX_NOPERM = 77;
10262
10377
  COMMANDS2 = /* @__PURE__ */ new Set(["CHECK", "SYMBOLS", "REPORT", "REPORT_IFSPAM", "PROCESS", "HEADERS", "PING", "SKIP", "TELL"]);
10378
+ oneLine2 = (text) => String(text ?? "").replaceAll(/[\p{Cc}\u061C\u200E\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]+/gu, " ");
10263
10379
  }
10264
10380
  });
10265
10381
 
@@ -10824,7 +10940,7 @@ var init_src = __esm({
10824
10940
  // (`from`: the sender's domain; `carried`: the link hosts of the whole
10825
10941
  // message and of what it carries, see carriedLinkHosts)
10826
10942
  async checkLinks(links, config, hosts = [], options = {}) {
10827
- const { from = "", carried = null } = typeof options === "string" ? { from: options } : options;
10943
+ const { from = "", carried = null } = typeof options === "string" ? { from: options } : options ?? {};
10828
10944
  const phishing = [];
10829
10945
  const homographs = [];
10830
10946
  const seenHosts = /* @__PURE__ */ new Set();
@@ -10884,15 +11000,15 @@ var init_src = __esm({
10884
11000
  for (const host of carriedHosts) {
10885
11001
  seenHosts.add(host);
10886
11002
  }
10887
- const ordered = [.../* @__PURE__ */ new Set([...[...found.filter((link) => link.text), ...found.filter((link) => !link.text), ...bare].map((link) => lookupName(destination(link))).filter(Boolean), ...carriedHosts])];
11003
+ const own3 = [...found.filter((link) => link.text), ...found.filter((link) => !link.text), ...bare].map((link) => lookupName(destination(link))).filter(Boolean);
10888
11004
  const firsts = /* @__PURE__ */ new Map();
10889
- for (const host of ordered) {
11005
+ for (const host of [...own3, ...carriedHosts]) {
10890
11006
  const domain = registrableDomain(host);
10891
11007
  if (!firsts.has(domain)) {
10892
11008
  firsts.set(domain, host);
10893
11009
  }
10894
11010
  }
10895
- const checked = [.../* @__PURE__ */ new Set([...firsts.values(), ...ordered])].slice(0, config.phishing.maxHosts);
11011
+ const checked = [.../* @__PURE__ */ new Set([...firsts.values(), ...own3, ...carriedHosts])].slice(0, config.phishing.maxHosts);
10896
11012
  const domains = [...new Set(checked.map((host) => registrableDomain(host)))];
10897
11013
  const blocked = [];
10898
11014
  let answered = 0;