spamscanner 7.0.8 → 7.0.9

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/esm/index.js CHANGED
@@ -2102,11 +2102,12 @@ function gatewayTarget(url) {
2102
2102
  }
2103
2103
  if (host === "urldefense.com" && url.pathname.startsWith("/v3/__")) {
2104
2104
  const rest = url.href.slice(url.href.indexOf("/v3/__") + 6);
2105
- const end = rest.indexOf("__;");
2105
+ const end = rest.lastIndexOf("__;");
2106
2106
  return end === -1 ? null : rest.slice(0, end);
2107
2107
  }
2108
2108
  return null;
2109
2109
  }
2110
+ var MAX_GATEWAYS = 16;
2110
2111
  function unwrapLink(address2) {
2111
2112
  let url;
2112
2113
  try {
@@ -2114,7 +2115,7 @@ function unwrapLink(address2) {
2114
2115
  } catch {
2115
2116
  return null;
2116
2117
  }
2117
- for (let depth = 0; depth < 3; depth++) {
2118
+ for (let depth = 0; depth < MAX_GATEWAYS; depth++) {
2118
2119
  const target = gatewayTarget(url);
2119
2120
  if (!target) {
2120
2121
  break;
@@ -3432,8 +3433,13 @@ function oleHasMacros(buffer) {
3432
3433
  return false;
3433
3434
  }
3434
3435
  var PDF_ACTIVE = linear(String.raw`\/(?:JavaScript|JS|Launch)\b`);
3435
- var PDF_ESCAPED_NAME = /\/[^\s/<>[\]()%{}]*#[\dA-Fa-f]{2}[^\s/<>[\]()%{}]*/g;
3436
- var decodePdfNames = (text) => text.replaceAll(PDF_ESCAPED_NAME, (name) => name.replaceAll(/#([\dA-Fa-f]{2})/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))));
3436
+ var escaped = (character) => {
3437
+ const hex = character.codePointAt(0).toString(16);
3438
+ return `(?:${character}|#${hex[0]}[${hex[1].toLowerCase()}${hex[1].toUpperCase()}])`;
3439
+ };
3440
+ var WORD_ESCAPE = String.raw`#(?:3\d|4[1-9a-fA-F]|5[\daA]|5[fF]|6[1-9a-fA-F]|7[\daA])`;
3441
+ var PDF_ACTIVE_ESCAPED = new RegExp(`/(?:${["JavaScript", "JS", "Launch"].map((name) => [...name].map((character) => escaped(character)).join("")).join("|")})(?![\\w]|${WORD_ESCAPE})`);
3442
+ var pdfActive = (text) => PDF_ACTIVE.test(text) || PDF_ACTIVE_ESCAPED.test(text);
3437
3443
  function inspectAttachments(attachments = []) {
3438
3444
  const findings = [];
3439
3445
  for (const attachment of attachments) {
@@ -3491,7 +3497,7 @@ function inspectAttachments(attachments = []) {
3491
3497
  add("macro", `Attachment "${name}" is an Office document with a VBA macro project`, { extension });
3492
3498
  }
3493
3499
  } else if (detected?.type === "pdf" || extension === "pdf") {
3494
- if (PDF_ACTIVE.test(decodePdfNames(content.toString("latin1")))) {
3500
+ if (pdfActive(content.toString("latin1"))) {
3495
3501
  add("pdf_active", `PDF "${name}" contains JavaScript or a launch action`, { extension: "pdf" });
3496
3502
  }
3497
3503
  } else if (detected?.type === "rtf" && /\\object\b|\\objdata\b|\\objupdate\b/.test(content.toString("latin1"))) {
@@ -3539,7 +3545,7 @@ function boundAddressField(line, room) {
3539
3545
  const limit = Math.min(line.length, MAX_ADDRESS_FIELD, room);
3540
3546
  let end = start;
3541
3547
  let expecting = "";
3542
- let escaped = false;
3548
+ let escaped2 = false;
3543
3549
  let separators = 0;
3544
3550
  let groups = 0;
3545
3551
  let index = start;
@@ -3548,8 +3554,8 @@ function boundAddressField(line, room) {
3548
3554
  if ((chr === "," || chr === ";") && ++separators > MAX_ADDRESS_SEPARATORS) {
3549
3555
  break;
3550
3556
  }
3551
- if (escaped) {
3552
- escaped = false;
3557
+ if (escaped2) {
3558
+ escaped2 = false;
3553
3559
  } else if (expecting !== "" && chr === expecting) {
3554
3560
  expecting = "";
3555
3561
  end = chr === ";" ? index + 1 : end;
@@ -3557,7 +3563,7 @@ function boundAddressField(line, room) {
3557
3563
  expecting = ADDRESS_OPERATORS.get(chr);
3558
3564
  end = chr === "," || chr === ";" ? index : end;
3559
3565
  } else if (expecting === '"' && chr === "\\") {
3560
- escaped = true;
3566
+ escaped2 = true;
3561
3567
  }
3562
3568
  if (chr === ":" && expecting === ";" && ++groups > MAX_ADDRESS_GROUPS) {
3563
3569
  break;
@@ -3700,9 +3706,14 @@ var SIGNATURE_FIELDS = /* @__PURE__ */ new Set(["dkim-signature", "arc-message-s
3700
3706
  var AUTH_FIELDS = /* @__PURE__ */ new Set([...KEPT_FIELDS, "arc-seal", "arc-message-signature"]);
3701
3707
  var MAX_SIGNATURES = 10;
3702
3708
  var MAX_DKIM_SIGNATURES = 8;
3709
+ function isAttachedMessage(attachment) {
3710
+ const type = String(attachment.contentType).toLowerCase();
3711
+ return type === "message/rfc822" || type === "message/global" || /\.(?:eml|u8msg)$/i.test(String(attachment.filename));
3712
+ }
3703
3713
  var MAX_NESTED_DEPTH = 3;
3704
3714
  var MAX_ATTACHMENTS = 100;
3705
3715
  var MAX_NESTED_BYTES = 25 * 1024 * 1024;
3716
+ var MAX_ATTACHED_MESSAGES = 100;
3706
3717
  var PARSE_TIMEOUT = 6e4;
3707
3718
  async function simpleParse(raw, { timeout = PARSE_TIMEOUT, parser = simpleParser } = {}) {
3708
3719
  const input = Readable.from([finishable(unfoldHeaders(raw))], { objectMode: false, autoDestroy: false });
@@ -3760,18 +3771,18 @@ function headerValue2(line) {
3760
3771
  let key = null;
3761
3772
  let inKey = false;
3762
3773
  let quoted = false;
3763
- let escaped = false;
3774
+ let escaped2 = false;
3764
3775
  let value = "";
3765
3776
  for (const chr of line.slice(line.indexOf(":") + 1).replaceAll(/\r?\n[ \t]*/g, " ")) {
3766
3777
  if (inKey && chr === "=") {
3767
3778
  key = value.trim().toLowerCase();
3768
3779
  inKey = false;
3769
3780
  value = "";
3770
- } else if (inKey || escaped) {
3781
+ } else if (inKey || escaped2) {
3771
3782
  value += chr;
3772
- escaped = false;
3783
+ escaped2 = false;
3773
3784
  } else if (chr === "\\") {
3774
- escaped = true;
3785
+ escaped2 = true;
3775
3786
  } else if (chr === '"') {
3776
3787
  quoted = !quoted;
3777
3788
  } else if (chr === ";" && !quoted) {
@@ -3819,7 +3830,7 @@ function unfoldHeaders(raw) {
3819
3830
  if (blank && type.boundary) {
3820
3831
  boundaries.push(Buffer2.from(type.boundary));
3821
3832
  }
3822
- head = blank && type.value === "message/rfc822" && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
3833
+ head = blank && ["message/rfc822", "message/global"].includes(type.value) && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
3823
3834
  };
3824
3835
  for (let start = 0; start < raw.length; ) {
3825
3836
  const lf = raw.indexOf(10, start);
@@ -3956,6 +3967,7 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
3956
3967
  let budget = MAX_NESTED_BYTES;
3957
3968
  let nested = 0;
3958
3969
  let tooMany = false;
3970
+ let opened = 0;
3959
3971
  const visit = async (list, prefix, depth) => {
3960
3972
  for (const attachment of list) {
3961
3973
  if (prefix) {
@@ -3972,17 +3984,22 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
3972
3984
  const filename = prefix ? `${prefix}/${name}` : name || "attached message";
3973
3985
  all.push(prefix ? { ...attachment, filename: `${prefix}/${name}` } : attachment);
3974
3986
  const { content } = attachment;
3975
- const attached = attachment.contentType === "message/rfc822" || /\.eml$/i.test(attachment.filename || "");
3987
+ const attached = isAttachedMessage(attachment);
3976
3988
  if (attached && Buffer2.isBuffer(content) && depth >= MAX_NESTED_DEPTH) {
3977
3989
  unopened.push({ filename, reason: `nested more than ${MAX_NESTED_DEPTH} deep` });
3978
3990
  continue;
3979
3991
  }
3992
+ if (attached && Buffer2.isBuffer(content) && opened >= MAX_ATTACHED_MESSAGES) {
3993
+ unopened.push({ filename, reason: `more than ${MAX_ATTACHED_MESSAGES} attached messages` });
3994
+ continue;
3995
+ }
3980
3996
  if (attached && Buffer2.isBuffer(content) && content.length > budget) {
3981
3997
  unopened.push({ filename, reason: `past ${MAX_NESTED_BYTES / 1024 / 1024} MB of attached messages` });
3982
3998
  continue;
3983
3999
  }
3984
4000
  if (attached && Buffer2.isBuffer(content)) {
3985
4001
  budget -= content.length;
4002
+ opened++;
3986
4003
  let inner;
3987
4004
  try {
3988
4005
  inner = await parseWithin(content, parse, deadline);
@@ -5668,9 +5685,12 @@ ${visibleText(html.slice(0, 1e5)).text}`)) {
5668
5685
  hit("SEXTORTION_SUBJECT", 2, "Subject used by sextortion and account takeover scams, without a demand for payment", "SCAM");
5669
5686
  }
5670
5687
  const markup = html.slice(0, 1e5);
5688
+ const readable = readableText(`${subject}
5689
+ ${text.slice(0, 5e4)}
5690
+ ${visibleText(withoutHelp(markup)).text}`);
5671
5691
  if (isInjection(`${subject}
5672
5692
  ${text.slice(0, 5e4)}
5673
- ${withoutHelp(markup)}`) || attributeText(markup).some(({ text: text2, start, end, help }) => (!help || isOrder(text2)) && isInjection(unquoted(text2), AI_ACTION_OBJECT, () => shownNear(markup, start, end)))) {
5693
+ ${withoutHelp(markup)}`) || isInjection(readable) || attributeText(markup).some(({ text: text2, start, end, help }) => (!help || isOrder(text2)) && isInjection(unquoted(text2), AI_ACTION_OBJECT, () => shownNear(markup, start, end)))) {
5674
5694
  hit("PROMPT_INJECTION", 3, "Contains instructions addressed to an AI filter", "SPAM");
5675
5695
  }
5676
5696
  if (session.resolvedClientHostname?.endsWith(".outbound.protection.outlook.com")) {
@@ -7048,7 +7068,7 @@ function scoreResults(results, options) {
7048
7068
  var package_default = {
7049
7069
  name: "spamscanner",
7050
7070
  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.",
7051
- version: "7.0.8",
7071
+ version: "7.0.9",
7052
7072
  author: "Forward Email (https://forwardemail.net)",
7053
7073
  bin: {
7054
7074
  spamscanner: "./dist/esm/cli.js"
@@ -9098,7 +9118,7 @@ var SpamScanner = class {
9098
9118
  message: `Attachment "${filename}" contains ${result.viruses.join(", ")}`
9099
9119
  });
9100
9120
  viruses.push(found);
9101
- if (attachment.contentType === "message/rfc822" || /\.eml$/i.test(attachment.filename || "")) {
9121
+ if (isAttachedMessage(attachment)) {
9102
9122
  carriers.set(found, attachment.filename || "attached message");
9103
9123
  }
9104
9124
  }