spamscanner 7.0.8 → 7.0.10

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;
@@ -3233,6 +3234,7 @@ var MACRO_EXTENSIONS = /* @__PURE__ */ new Set(["docm", "dotm", "xlsm", "xltm",
3233
3234
  var DOCUMENT_EXTENSIONS = /* @__PURE__ */ new Set(["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "rtf", "jpg", "jpeg", "png", "gif", "zip", "csv", "odt", "ods", "html", "htm", "mp3", "mp4", "mov", "wav", "eml", "msg", "xml", "json"]);
3234
3235
  var TEMPORARY_EXTENSIONS = /* @__PURE__ */ new Set(["tmp"]);
3235
3236
  var BIDI_CONTROLS = characterClass([[8234, 8238], [8294, 8297]], "u");
3237
+ var ALL_BIDI_CONTROLS = characterClass([[8234, 8238], [8294, 8297]], "gu");
3236
3238
  var ARCHIVE_TYPES = /* @__PURE__ */ new Set(["zip", "rar", "7z", "gz", "bz2", "xz", "cab", "iso", "tar", "zst", "lzh", "ace", "arj"]);
3237
3239
  function sniff(buffer) {
3238
3240
  if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
@@ -3416,7 +3418,16 @@ function shorten(name, max = 100) {
3416
3418
  return characters.length <= max ? name : `${characters.slice(0, max - 41).join("")}\u2026${characters.slice(-40).join("")}`;
3417
3419
  }
3418
3420
  function extensionOf(name) {
3419
- const base = trimEndOf(String(name).toLowerCase().split(/[/\\]/).pop(), ". ");
3421
+ const base = trimEndOf(String(name).toLowerCase().replace(ALL_BIDI_CONTROLS, "").split(/[/\\]/).pop(), ". ");
3422
+ const whole = lastExtension(base);
3423
+ const file = base.replace(/^[a-z]:/, "");
3424
+ const colon = file.indexOf(":");
3425
+ if (colon !== -1 && /^(?::?\$data)?$/.test(file.slice(colon + 1))) {
3426
+ return lastExtension(trimEndOf(file.slice(0, colon), ". "));
3427
+ }
3428
+ return whole;
3429
+ }
3430
+ function lastExtension(base) {
3420
3431
  const dot = base.lastIndexOf(".");
3421
3432
  return dot === -1 ? "" : base.slice(dot + 1).trim();
3422
3433
  }
@@ -3432,8 +3443,13 @@ function oleHasMacros(buffer) {
3432
3443
  return false;
3433
3444
  }
3434
3445
  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))));
3446
+ var escaped = (character) => {
3447
+ const hex = character.codePointAt(0).toString(16);
3448
+ return `(?:${character}|#${hex[0]}[${hex[1].toLowerCase()}${hex[1].toUpperCase()}])`;
3449
+ };
3450
+ var WORD_ESCAPE = String.raw`#(?:3\d|4[1-9a-fA-F]|5[\daA]|5[fF]|6[1-9a-fA-F]|7[\daA])`;
3451
+ var PDF_ACTIVE_ESCAPED = new RegExp(`/(?:${["JavaScript", "JS", "Launch"].map((name) => [...name].map((character) => escaped(character)).join("")).join("|")})(?![\\w]|${WORD_ESCAPE})`);
3452
+ var pdfActive = (text) => PDF_ACTIVE.test(text) || PDF_ACTIVE_ESCAPED.test(text);
3437
3453
  function inspectAttachments(attachments = []) {
3438
3454
  const findings = [];
3439
3455
  for (const attachment of attachments) {
@@ -3491,7 +3507,7 @@ function inspectAttachments(attachments = []) {
3491
3507
  add("macro", `Attachment "${name}" is an Office document with a VBA macro project`, { extension });
3492
3508
  }
3493
3509
  } else if (detected?.type === "pdf" || extension === "pdf") {
3494
- if (PDF_ACTIVE.test(decodePdfNames(content.toString("latin1")))) {
3510
+ if (pdfActive(content.toString("latin1"))) {
3495
3511
  add("pdf_active", `PDF "${name}" contains JavaScript or a launch action`, { extension: "pdf" });
3496
3512
  }
3497
3513
  } else if (detected?.type === "rtf" && /\\object\b|\\objdata\b|\\objupdate\b/.test(content.toString("latin1"))) {
@@ -3539,7 +3555,7 @@ function boundAddressField(line, room) {
3539
3555
  const limit = Math.min(line.length, MAX_ADDRESS_FIELD, room);
3540
3556
  let end = start;
3541
3557
  let expecting = "";
3542
- let escaped = false;
3558
+ let escaped2 = false;
3543
3559
  let separators = 0;
3544
3560
  let groups = 0;
3545
3561
  let index = start;
@@ -3548,8 +3564,8 @@ function boundAddressField(line, room) {
3548
3564
  if ((chr === "," || chr === ";") && ++separators > MAX_ADDRESS_SEPARATORS) {
3549
3565
  break;
3550
3566
  }
3551
- if (escaped) {
3552
- escaped = false;
3567
+ if (escaped2) {
3568
+ escaped2 = false;
3553
3569
  } else if (expecting !== "" && chr === expecting) {
3554
3570
  expecting = "";
3555
3571
  end = chr === ";" ? index + 1 : end;
@@ -3557,7 +3573,7 @@ function boundAddressField(line, room) {
3557
3573
  expecting = ADDRESS_OPERATORS.get(chr);
3558
3574
  end = chr === "," || chr === ";" ? index : end;
3559
3575
  } else if (expecting === '"' && chr === "\\") {
3560
- escaped = true;
3576
+ escaped2 = true;
3561
3577
  }
3562
3578
  if (chr === ":" && expecting === ";" && ++groups > MAX_ADDRESS_GROUPS) {
3563
3579
  break;
@@ -3700,9 +3716,14 @@ var SIGNATURE_FIELDS = /* @__PURE__ */ new Set(["dkim-signature", "arc-message-s
3700
3716
  var AUTH_FIELDS = /* @__PURE__ */ new Set([...KEPT_FIELDS, "arc-seal", "arc-message-signature"]);
3701
3717
  var MAX_SIGNATURES = 10;
3702
3718
  var MAX_DKIM_SIGNATURES = 8;
3719
+ function isAttachedMessage(attachment) {
3720
+ const type = String(attachment.contentType).toLowerCase();
3721
+ return type === "message/rfc822" || type === "message/global" || /\.(?:eml|u8msg)$/i.test(String(attachment.filename));
3722
+ }
3703
3723
  var MAX_NESTED_DEPTH = 3;
3704
3724
  var MAX_ATTACHMENTS = 100;
3705
3725
  var MAX_NESTED_BYTES = 25 * 1024 * 1024;
3726
+ var MAX_ATTACHED_MESSAGES = 100;
3706
3727
  var PARSE_TIMEOUT = 6e4;
3707
3728
  async function simpleParse(raw, { timeout = PARSE_TIMEOUT, parser = simpleParser } = {}) {
3708
3729
  const input = Readable.from([finishable(unfoldHeaders(raw))], { objectMode: false, autoDestroy: false });
@@ -3760,18 +3781,18 @@ function headerValue2(line) {
3760
3781
  let key = null;
3761
3782
  let inKey = false;
3762
3783
  let quoted = false;
3763
- let escaped = false;
3784
+ let escaped2 = false;
3764
3785
  let value = "";
3765
3786
  for (const chr of line.slice(line.indexOf(":") + 1).replaceAll(/\r?\n[ \t]*/g, " ")) {
3766
3787
  if (inKey && chr === "=") {
3767
3788
  key = value.trim().toLowerCase();
3768
3789
  inKey = false;
3769
3790
  value = "";
3770
- } else if (inKey || escaped) {
3791
+ } else if (inKey || escaped2) {
3771
3792
  value += chr;
3772
- escaped = false;
3793
+ escaped2 = false;
3773
3794
  } else if (chr === "\\") {
3774
- escaped = true;
3795
+ escaped2 = true;
3775
3796
  } else if (chr === '"') {
3776
3797
  quoted = !quoted;
3777
3798
  } else if (chr === ";" && !quoted) {
@@ -3819,7 +3840,7 @@ function unfoldHeaders(raw) {
3819
3840
  if (blank && type.boundary) {
3820
3841
  boundaries.push(Buffer2.from(type.boundary));
3821
3842
  }
3822
- head = blank && type.value === "message/rfc822" && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
3843
+ head = blank && ["message/rfc822", "message/global"].includes(type.value) && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
3823
3844
  };
3824
3845
  for (let start = 0; start < raw.length; ) {
3825
3846
  const lf = raw.indexOf(10, start);
@@ -3956,6 +3977,7 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
3956
3977
  let budget = MAX_NESTED_BYTES;
3957
3978
  let nested = 0;
3958
3979
  let tooMany = false;
3980
+ let opened = 0;
3959
3981
  const visit = async (list, prefix, depth) => {
3960
3982
  for (const attachment of list) {
3961
3983
  if (prefix) {
@@ -3972,17 +3994,22 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
3972
3994
  const filename = prefix ? `${prefix}/${name}` : name || "attached message";
3973
3995
  all.push(prefix ? { ...attachment, filename: `${prefix}/${name}` } : attachment);
3974
3996
  const { content } = attachment;
3975
- const attached = attachment.contentType === "message/rfc822" || /\.eml$/i.test(attachment.filename || "");
3997
+ const attached = isAttachedMessage(attachment);
3976
3998
  if (attached && Buffer2.isBuffer(content) && depth >= MAX_NESTED_DEPTH) {
3977
3999
  unopened.push({ filename, reason: `nested more than ${MAX_NESTED_DEPTH} deep` });
3978
4000
  continue;
3979
4001
  }
4002
+ if (attached && Buffer2.isBuffer(content) && opened >= MAX_ATTACHED_MESSAGES) {
4003
+ unopened.push({ filename, reason: `more than ${MAX_ATTACHED_MESSAGES} attached messages` });
4004
+ continue;
4005
+ }
3980
4006
  if (attached && Buffer2.isBuffer(content) && content.length > budget) {
3981
4007
  unopened.push({ filename, reason: `past ${MAX_NESTED_BYTES / 1024 / 1024} MB of attached messages` });
3982
4008
  continue;
3983
4009
  }
3984
4010
  if (attached && Buffer2.isBuffer(content)) {
3985
4011
  budget -= content.length;
4012
+ opened++;
3986
4013
  let inner;
3987
4014
  try {
3988
4015
  inner = await parseWithin(content, parse, deadline);
@@ -5668,9 +5695,12 @@ ${visibleText(html.slice(0, 1e5)).text}`)) {
5668
5695
  hit("SEXTORTION_SUBJECT", 2, "Subject used by sextortion and account takeover scams, without a demand for payment", "SCAM");
5669
5696
  }
5670
5697
  const markup = html.slice(0, 1e5);
5698
+ const readable = readableText(`${subject}
5699
+ ${text.slice(0, 5e4)}
5700
+ ${visibleText(withoutHelp(markup)).text}`);
5671
5701
  if (isInjection(`${subject}
5672
5702
  ${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)))) {
5703
+ ${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
5704
  hit("PROMPT_INJECTION", 3, "Contains instructions addressed to an AI filter", "SPAM");
5675
5705
  }
5676
5706
  if (session.resolvedClientHostname?.endsWith(".outbound.protection.outlook.com")) {
@@ -7048,7 +7078,7 @@ function scoreResults(results, options) {
7048
7078
  var package_default = {
7049
7079
  name: "spamscanner",
7050
7080
  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",
7081
+ version: "7.0.10",
7052
7082
  author: "Forward Email (https://forwardemail.net)",
7053
7083
  bin: {
7054
7084
  spamscanner: "./dist/esm/cli.js"
@@ -9098,7 +9128,7 @@ var SpamScanner = class {
9098
9128
  message: `Attachment "${filename}" contains ${result.viruses.join(", ")}`
9099
9129
  });
9100
9130
  viruses.push(found);
9101
- if (attachment.contentType === "message/rfc822" || /\.eml$/i.test(attachment.filename || "")) {
9131
+ if (isAttachedMessage(attachment)) {
9102
9132
  carriers.set(found, attachment.filename || "attached message");
9103
9133
  }
9104
9134
  }