spamscanner 7.0.14 → 7.0.15

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.
@@ -4022,6 +4022,13 @@ function extensionOf(name) {
4022
4022
  }
4023
4023
  return whole;
4024
4024
  }
4025
+ function uncompressedName(name) {
4026
+ if (!COMPRESSED_EXTENSIONS.has(extensionOf(name))) {
4027
+ return String(name);
4028
+ }
4029
+ const trimmed = trimEndOf(String(name).replace(ALL_BIDI_CONTROLS, ""), ". ");
4030
+ return trimmed.slice(0, trimmed.lastIndexOf("."));
4031
+ }
4025
4032
  function lastExtension(base) {
4026
4033
  const dot = base.lastIndexOf(".");
4027
4034
  return dot === -1 ? "" : base.slice(dot + 1).trim();
@@ -4065,7 +4072,20 @@ function inspectAttachments(attachments = []) {
4065
4072
  } else if (RISKY_EXTENSIONS.has(extension)) {
4066
4073
  add("risky_attachment", `Attachment "${name}" is a risky file type (.${extension})`, { extension });
4067
4074
  }
4075
+ const held = COMPRESSED_EXTENSIONS.has(extension) ? extensionOf(uncompressedName(filename)) : "";
4076
+ if (EXECUTABLE_EXTENSIONS.has(held)) {
4077
+ const script2 = SCRIPT_EXTENSIONS.has(held);
4078
+ add(script2 ? "script_in_archive" : "executable_in_archive", `Archive "${name}" contains ${script2 ? "a script" : "a program"} (${shorten(uncompressedName(filename).split(/[/\\]/).pop())})`, { extension: held, extensions: [held] });
4079
+ }
4080
+ const webPage = detected?.type === "html" || detected?.type === "svg" || ["htm", "html", "mht", "mhtml", "shtml", "xht", "xhtml", "svg"].includes(extension);
4081
+ const addWebPage = () => {
4082
+ const active = content !== null && /<script\b|<form\b|\bon(?:load|error|click)\s*=|javascript:|window\.location|document\.location|atob\(/i.test(content.subarray(0, 2e5).toString("utf8"));
4083
+ add("html_attachment", `Attachment "${name}" is ${detected?.type === "svg" || extension === "svg" ? "an SVG image" : "a web page"}${active ? " with scripts or forms" : ""}`, { extension: extension || detected?.type, active });
4084
+ };
4068
4085
  if (!content) {
4086
+ if (webPage) {
4087
+ addWebPage();
4088
+ }
4069
4089
  continue;
4070
4090
  }
4071
4091
  const zipFamily = ZIP_FAMILY.has(detected?.type) || extension === "zip" && !detected?.executable && entriesOf(content).length > 0;
@@ -4077,10 +4097,10 @@ function inspectAttachments(attachments = []) {
4077
4097
  } else if (office && entries.some((entry) => MACRO_EXTENSIONS.has(extensionOf(entry.name)))) {
4078
4098
  add("macro", `Attachment "${name}" is an Office document holding another with macros`, { extension });
4079
4099
  }
4080
- const inner = entries.filter((entry) => EXECUTABLE_EXTENSIONS.has(extensionOf(entry.name)) && !(office && OFFICE_EMBEDDED.has(extensionOf(entry.name))));
4081
- const details = { extension: extensionOf(inner[0]?.name ?? ""), extensions: [...new Set(inner.map((entry) => extensionOf(entry.name)))] };
4100
+ const inner = entries.filter((entry) => EXECUTABLE_EXTENSIONS.has(extensionOf(uncompressedName(entry.name))) && !(office && OFFICE_EMBEDDED.has(extensionOf(entry.name))));
4101
+ const details = { extension: extensionOf(uncompressedName(inner[0]?.name ?? "")), extensions: [...new Set(inner.map((entry) => extensionOf(uncompressedName(entry.name))))] };
4082
4102
  const listed = inner.slice(0, 3).map((entry) => shorten(entry.name.split("/").pop())).join(", ");
4083
- if (inner.some((entry) => !SCRIPT_EXTENSIONS.has(extensionOf(entry.name)))) {
4103
+ if (inner.some((entry) => !SCRIPT_EXTENSIONS.has(extensionOf(uncompressedName(entry.name))))) {
4084
4104
  add("executable_in_archive", `Archive "${name}" contains ${inner.length === 1 ? "a program" : `${inner.length} programs`} (${listed})`, details);
4085
4105
  } else if (inner.length > 0) {
4086
4106
  add("script_in_archive", `Archive "${name}" contains ${inner.length === 1 ? "a script" : `${inner.length} scripts`} (${listed})`, details);
@@ -4099,15 +4119,13 @@ function inspectAttachments(attachments = []) {
4099
4119
  } else if (detected?.type === "rtf" && /\\object\b|\\objdata\b|\\objupdate\b/.test(content.toString("latin1"))) {
4100
4120
  add("rtf_object", `RTF document "${name}" embeds an OLE object`, { extension: "rtf" });
4101
4121
  }
4102
- if (detected?.type === "html" || detected?.type === "svg" || ["htm", "html", "shtml", "xhtml", "svg"].includes(extension)) {
4103
- const text = content.subarray(0, 2e5).toString("utf8");
4104
- const active = /<script\b|<form\b|\bon(?:load|error|click)\s*=|javascript:|window\.location|document\.location|atob\(/i.test(text);
4105
- add("html_attachment", `Attachment "${name}" is ${detected?.type === "svg" || extension === "svg" ? "an SVG image" : "a web page"}${active ? " with scripts or forms" : ""}`, { extension: extension || detected?.type, active });
4122
+ if (webPage) {
4123
+ addWebPage();
4106
4124
  }
4107
4125
  }
4108
4126
  return findings;
4109
4127
  }
4110
- var import_node_buffer, EXECUTABLE_EXTENSIONS, RISKY_EXTENSIONS, SCRIPT_EXTENSIONS, MACRO_EXTENSIONS, DOCUMENT_EXTENSIONS, TEMPORARY_EXTENSIONS, BIDI_CONTROLS, ALL_BIDI_CONTROLS, ARCHIVE_TYPES, ZIP_FAMILY, OFFICE_TYPES, OFFICE_EMBEDDED, ENTRIES, MAX_NAME_LENGTH, MAX_NAME_BYTES, DIRECTORY, isDirectory, VBA_NAMES, PDF_ACTIVE, escaped, WORD_ESCAPE, PDF_ACTIVE_ESCAPED, pdfActive;
4128
+ var import_node_buffer, EXECUTABLE_EXTENSIONS, RISKY_EXTENSIONS, SCRIPT_EXTENSIONS, MACRO_EXTENSIONS, DOCUMENT_EXTENSIONS, TEMPORARY_EXTENSIONS, BIDI_CONTROLS, ALL_BIDI_CONTROLS, ARCHIVE_TYPES, ZIP_FAMILY, OFFICE_TYPES, OFFICE_EMBEDDED, ENTRIES, MAX_NAME_LENGTH, MAX_NAME_BYTES, DIRECTORY, isDirectory, COMPRESSED_EXTENSIONS, VBA_NAMES, PDF_ACTIVE, escaped, WORD_ESCAPE, PDF_ACTIVE_ESCAPED, pdfActive;
4111
4129
  var init_attachments = __esm({
4112
4130
  "src/attachments.js"() {
4113
4131
  import_node_buffer = require("node:buffer");
@@ -4119,6 +4137,7 @@ var init_attachments = __esm({
4119
4137
  "apk",
4120
4138
  "app",
4121
4139
  "appimage",
4140
+ "appinstaller",
4122
4141
  "application",
4123
4142
  "appref-ms",
4124
4143
  "appx",
@@ -4155,6 +4174,7 @@ var init_attachments = __esm({
4155
4174
  "inf",
4156
4175
  "ins",
4157
4176
  "ipa",
4177
+ "iqy",
4158
4178
  "iso",
4159
4179
  "isp",
4160
4180
  "its",
@@ -4163,6 +4183,7 @@ var init_attachments = __esm({
4163
4183
  "js",
4164
4184
  "jse",
4165
4185
  "ksh",
4186
+ "library-ms",
4166
4187
  "lnk",
4167
4188
  "mad",
4168
4189
  "maf",
@@ -4194,6 +4215,8 @@ var init_attachments = __esm({
4194
4215
  "mst",
4195
4216
  "msu",
4196
4217
  "nsh",
4218
+ "one",
4219
+ "onepkg",
4197
4220
  "ops",
4198
4221
  "osd",
4199
4222
  "pcd",
@@ -4224,6 +4247,8 @@ var init_attachments = __esm({
4224
4247
  "scpt",
4225
4248
  "scr",
4226
4249
  "sct",
4250
+ "search-ms",
4251
+ "searchconnector-ms",
4227
4252
  "settingcontent-ms",
4228
4253
  "sh",
4229
4254
  "shb",
@@ -4268,6 +4293,7 @@ var init_attachments = __esm({
4268
4293
  MAX_NAME_BYTES = 1024 * 1024;
4269
4294
  DIRECTORY = 33639248;
4270
4295
  isDirectory = (buffer, offset) => offset >= 0 && offset + 46 <= buffer.length && buffer.readUInt32LE(offset) === DIRECTORY;
4296
+ COMPRESSED_EXTENSIONS = /* @__PURE__ */ new Set(["gz", "bz2", "xz", "zst"]);
4271
4297
  VBA_NAMES = /* @__PURE__ */ new Set(["vba", "_vba_project", "_vba_project_cur", "macros"]);
4272
4298
  PDF_ACTIVE = linear(String.raw`\/(?:JavaScript|JS|Launch)\b`);
4273
4299
  escaped = (character) => {
@@ -4394,18 +4420,100 @@ function isAttachedMessage(attachment) {
4394
4420
  const type = String(attachment.contentType).toLowerCase();
4395
4421
  return type === "message/rfc822" || type === "message/global" || /\.(?:eml|u8msg)$/i.test(String(attachment.filename));
4396
4422
  }
4423
+ function addFinding(list, part) {
4424
+ const found = foundIn.get(list) ?? /* @__PURE__ */ new Set();
4425
+ foundIn.set(list, found);
4426
+ const types = inspectAttachments([part]).map((finding2) => finding2.type);
4427
+ if (types.some((type) => !found.has(type))) {
4428
+ list.push(part);
4429
+ for (const type of types) {
4430
+ found.add(type);
4431
+ }
4432
+ }
4433
+ }
4434
+ function parametersOf(value) {
4435
+ const parameters = [];
4436
+ let start = -1;
4437
+ let quoted = false;
4438
+ for (let i = 0; i < value.length; i++) {
4439
+ if (quoted && value[i] === "\\") {
4440
+ i++;
4441
+ } else if (value[i] === '"') {
4442
+ quoted = !quoted;
4443
+ } else if (value[i] === ";" && !quoted) {
4444
+ if (start !== -1) {
4445
+ parameters.push(value.slice(start, i));
4446
+ }
4447
+ start = i + 1;
4448
+ }
4449
+ }
4450
+ if (start !== -1) {
4451
+ parameters.push(value.slice(start));
4452
+ }
4453
+ return parameters;
4454
+ }
4455
+ function repeatedNames(node) {
4456
+ const names = [];
4457
+ const { headers, libmime } = node ?? {};
4458
+ if (typeof headers?.get !== "function" || typeof libmime?.parseHeaderValue !== "function") {
4459
+ return names;
4460
+ }
4461
+ const read = (value, parameter) => {
4462
+ let name = "";
4463
+ try {
4464
+ name = libmime.decodeWords(String(libmime.parseHeaderValue(value, { stripComments: true }).params[parameter] ?? "")).trim();
4465
+ } catch {
4466
+ }
4467
+ if (name !== "") {
4468
+ names.push(name);
4469
+ }
4470
+ };
4471
+ for (const [field, parameter] of [["content-disposition", "filename"], ["content-type", "name"]]) {
4472
+ const values = headers.get(field).slice(0, MAX_NAME_FIELDS);
4473
+ for (const value of values) {
4474
+ const parameters = parametersOf(value).slice(0, MAX_NAME_PARAMETERS).map((text) => ({ text, key: text.slice(0, Math.max(0, text.indexOf("="))).trim().toLowerCase() }));
4475
+ const repeated = parameters.filter(({ key }) => (key === parameter || key.startsWith(`${parameter}*`)) && parameters.filter((other) => other.key === key).length > 1);
4476
+ if (values.length > 1) {
4477
+ read(value, parameter);
4478
+ }
4479
+ for (const { text } of repeated) {
4480
+ read(`x;${text}`, text.slice(0, text.indexOf("=")).trim().toLowerCase().replace(/\*.*$/, ""));
4481
+ }
4482
+ }
4483
+ }
4484
+ return names;
4485
+ }
4486
+ function namedTextParts(mail) {
4487
+ return mail[NAMED_TEXT] ?? [];
4488
+ }
4397
4489
  async function simpleParse(raw, { timeout = PARSE_TIMEOUT, parser = import_mailparser.simpleParser } = {}) {
4398
4490
  const input = import_node_stream.Readable.from([finishable(unfoldHeaders(raw))], { objectMode: false, autoDestroy: false });
4399
4491
  input.on("error", () => {
4400
4492
  });
4493
+ const named = [];
4401
4494
  const { pipe } = input;
4402
4495
  input.pipe = function(parser2, options) {
4403
4496
  parser2.splitter?.on("error", (error) => input.destroy(error));
4497
+ const { createNode } = parser2;
4498
+ if (typeof createNode === "function") {
4499
+ parser2.createNode = function(node) {
4500
+ const created = createNode.call(this, node);
4501
+ if (created?.isAttachment === false && node?.filename) {
4502
+ addFinding(named, { filename: String(node.filename), contentType: created.contentType, content: null });
4503
+ }
4504
+ for (const filename of repeatedNames(node)) {
4505
+ if (filename !== node?.filename) {
4506
+ addFinding(named, { filename, contentType: created?.contentType || "application/octet-stream", content: null });
4507
+ }
4508
+ }
4509
+ return created;
4510
+ };
4511
+ }
4404
4512
  return pipe.call(this, parser2, options);
4405
4513
  };
4406
4514
  let timer;
4407
4515
  try {
4408
- return await Promise.race([
4516
+ const mail = await Promise.race([
4409
4517
  parser(input, PARSER_OPTIONS),
4410
4518
  new Promise((resolve, reject) => {
4411
4519
  timer = setTimeout(() => {
@@ -4415,6 +4523,17 @@ async function simpleParse(raw, { timeout = PARSE_TIMEOUT, parser = import_mailp
4415
4523
  }, timeout);
4416
4524
  })
4417
4525
  ]);
4526
+ if (mail && typeof mail === "object") {
4527
+ let count = 0;
4528
+ for (const [, filename] of String(mail.text ?? "").matchAll(ENCODED_FILES)) {
4529
+ addFinding(named, { filename: filename.trim(), contentType: "application/octet-stream", content: null });
4530
+ if (++count >= MAX_ENCODED_FILES) {
4531
+ break;
4532
+ }
4533
+ }
4534
+ Object.defineProperty(mail, NAMED_TEXT, { value: named });
4535
+ }
4536
+ return mail;
4418
4537
  } finally {
4419
4538
  clearTimeout(timer);
4420
4539
  input.destroy();
@@ -4546,6 +4665,9 @@ function unfoldHeaders(raw) {
4546
4665
  chunks.push(raw.subarray(copied));
4547
4666
  return import_node_buffer2.Buffer.concat(chunks);
4548
4667
  }
4668
+ function lineCost(line) {
4669
+ return Math.max(line.length, MIN_LINE_COST);
4670
+ }
4549
4671
  function upToLine(raw, offset) {
4550
4672
  return raw.subarray(0, Math.max(raw.lastIndexOf(10, offset - 1) + 1, 0));
4551
4673
  }
@@ -4572,9 +4694,9 @@ function smallerHeader(raw) {
4572
4694
  const whole = text.slice(start, stop).replace(/\r$/, "");
4573
4695
  if (last && (whole.startsWith(" ") || whole.startsWith(" "))) {
4574
4696
  const line = last.address ? whole : whole.slice(0, MAX_LINE);
4575
- if (last.address || last.size + line.length <= MAX_FIELD) {
4697
+ if (last.address || last.size + lineCost(line) <= MAX_FIELD) {
4576
4698
  last.lines.push(line);
4577
- last.size += line.length;
4699
+ last.size += lineCost(line);
4578
4700
  }
4579
4701
  } else {
4580
4702
  const name = whole.slice(0, whole.indexOf(":")).trim().toLowerCase();
@@ -4584,7 +4706,7 @@ function smallerHeader(raw) {
4584
4706
  name,
4585
4707
  address: address2,
4586
4708
  lines: [line],
4587
- size: line.length
4709
+ size: lineCost(line)
4588
4710
  });
4589
4711
  }
4590
4712
  start = stop + 1;
@@ -4595,7 +4717,7 @@ function smallerHeader(raw) {
4595
4717
  const bounded = boundAddressField(line, Number.POSITIVE_INFINITY);
4596
4718
  fromCut ||= field.name === "from" && bounded !== line;
4597
4719
  field.lines = [bounded];
4598
- field.size = bounded.length;
4720
+ field.size = lineCost(bounded);
4599
4721
  }
4600
4722
  const total = fields.reduce((sum, field) => sum + field.size, 0);
4601
4723
  const counts = /* @__PURE__ */ new Map();
@@ -4628,7 +4750,7 @@ async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PA
4628
4750
  shorter = fewerParts(input);
4629
4751
  } else if (limit === HEADER_TOO_LARGE) {
4630
4752
  const smaller = smallerHeader(input);
4631
- shorter = smaller.raw.length < input.length ? smaller.raw : upToLine(input, MAX_PREFIX_BYTES);
4753
+ shorter = smaller.raw.length < input.length ? smaller.raw : upToLine(input, input.length > MAX_PREFIX_BYTES ? MAX_PREFIX_BYTES : Math.floor(input.length / 2));
4632
4754
  fromCut ||= smaller.raw === shorter && smaller.fromCut;
4633
4755
  }
4634
4756
  if (shorter.length >= input.length) {
@@ -4640,7 +4762,13 @@ async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PA
4640
4762
  }
4641
4763
  }
4642
4764
  async function expandAttachments(attachments = [], errors = [], parse = parseMessage, gets = {}) {
4643
- const { unopened = [], mails = [] } = Array.isArray(gets) ? { unopened: gets } : gets ?? {};
4765
+ const { unopened = [], mails = [], named = [] } = Array.isArray(gets) ? { unopened: gets } : gets ?? {};
4766
+ const addNamed = (...parts) => {
4767
+ for (const part of parts) {
4768
+ addFinding(named, part);
4769
+ }
4770
+ };
4771
+ const nameOf = (value) => trimEndOf(String(value ?? "").trim().toLowerCase(), ". ");
4644
4772
  const all = [];
4645
4773
  const deadline = Date.now() + PARSE_TIMEOUT;
4646
4774
  let budget = MAX_NESTED_BYTES;
@@ -4662,6 +4790,10 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
4662
4790
  const name = attachment.filename || (prefix ? "unnamed attachment" : "");
4663
4791
  const filename = prefix ? `${prefix}/${name}` : name || "attached message";
4664
4792
  all.push(prefix ? { ...attachment, filename: `${prefix}/${name}` } : attachment);
4793
+ const typeName = String(attachment.headers?.get?.("content-type")?.params?.name ?? "").trim();
4794
+ if (typeName && nameOf(typeName) !== nameOf(attachment.filename)) {
4795
+ addNamed({ filename: prefix ? `${prefix}/${typeName}` : typeName, contentType: attachment.contentType, content: null });
4796
+ }
4665
4797
  const { content } = attachment;
4666
4798
  const attached = isAttachedMessage(attachment);
4667
4799
  if (attached && import_node_buffer2.Buffer.isBuffer(content) && depth >= MAX_NESTED_DEPTH) {
@@ -4690,20 +4822,24 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
4690
4822
  errors.push(inner.error);
4691
4823
  }
4692
4824
  mails.push(inner.mail);
4693
- await visit(inner.mail.attachments, prefix ? `${prefix}/${name}` : name || "attached message", depth + 1);
4825
+ const carrier = prefix ? `${prefix}/${name}` : name || "attached message";
4826
+ addNamed(...namedTextParts(inner.mail).map((part) => ({ ...part, filename: `${carrier}/${part.filename}` })));
4827
+ await visit(inner.mail.attachments, carrier, depth + 1);
4694
4828
  }
4695
4829
  }
4696
4830
  };
4697
4831
  await visit(attachments, "", 0);
4698
4832
  return all;
4699
4833
  }
4700
- var import_node_buffer2, import_node_crypto, import_node_stream, import_mailparser, ADDRESS_FIELDS, MAX_ADDRESS_FIELD, MAX_ADDRESS_SEPARATORS, MAX_ADDRESS_GROUPS, MAX_ADDRESS_TOTAL, AUTH_ADDRESS_FIELDS, ADDRESS_OPERATORS, SINGLE_FIELDS, PRIORITY_FIELDS, BOUNDED2, ROOM, PARSER_OPTIONS, processHeaders, TOO_MANY_PARTS, HEADER_TOO_LARGE, MAX_PART_LINES, MAX_PREFIX_BYTES, MAX_LINE, MAX_FIELD, MAX_HEADER, MAX_REPEATED, KEPT_FIELDS, MAX_AUTH_WORK, SIGNATURE_FIELDS, AUTH_FIELDS, MAX_SIGNATURES, MAX_DKIM_SIGNATURES, MAX_NESTED_DEPTH, MAX_ATTACHMENTS, MAX_NESTED_BYTES, MAX_ATTACHED_MESSAGES, PARSE_TIMEOUT, MAX_FOLDS;
4834
+ var import_node_buffer2, import_node_crypto, import_node_stream, import_mailparser, ADDRESS_FIELDS, MAX_ADDRESS_FIELD, MAX_ADDRESS_SEPARATORS, MAX_ADDRESS_GROUPS, MAX_ADDRESS_TOTAL, AUTH_ADDRESS_FIELDS, ADDRESS_OPERATORS, SINGLE_FIELDS, PRIORITY_FIELDS, BOUNDED2, ROOM, PARSER_OPTIONS, processHeaders, TOO_MANY_PARTS, HEADER_TOO_LARGE, MAX_PART_LINES, MAX_PREFIX_BYTES, MAX_LINE, MAX_FIELD, MAX_HEADER, MAX_REPEATED, MIN_LINE_COST, KEPT_FIELDS, MAX_AUTH_WORK, SIGNATURE_FIELDS, AUTH_FIELDS, MAX_SIGNATURES, MAX_DKIM_SIGNATURES, MAX_NESTED_DEPTH, MAX_ATTACHMENTS, MAX_NESTED_BYTES, MAX_ATTACHED_MESSAGES, PARSE_TIMEOUT, NAMED_TEXT, ENCODED_FILES, MAX_ENCODED_FILES, foundIn, MAX_NAME_FIELDS, MAX_NAME_PARAMETERS, MAX_FOLDS;
4701
4835
  var init_parse = __esm({
4702
4836
  "src/parse.js"() {
4703
4837
  import_node_buffer2 = require("node:buffer");
4704
4838
  import_node_crypto = require("node:crypto");
4705
4839
  import_node_stream = require("node:stream");
4706
4840
  import_mailparser = require("mailparser");
4841
+ init_attachments();
4842
+ init_linear();
4707
4843
  ADDRESS_FIELDS = /* @__PURE__ */ new Set(["from", "to", "cc", "bcc", "sender", "reply-to", "delivered-to", "return-path", "disposition-notification-to"]);
4708
4844
  MAX_ADDRESS_FIELD = 16 * 1024;
4709
4845
  MAX_ADDRESS_SEPARATORS = 2e3;
@@ -4754,6 +4890,7 @@ var init_parse = __esm({
4754
4890
  MAX_FIELD = 8192;
4755
4891
  MAX_HEADER = 512 * 1024;
4756
4892
  MAX_REPEATED = 100;
4893
+ MIN_LINE_COST = 64;
4757
4894
  KEPT_FIELDS = /* @__PURE__ */ new Set([
4758
4895
  "from",
4759
4896
  "sender",
@@ -4784,6 +4921,12 @@ var init_parse = __esm({
4784
4921
  MAX_NESTED_BYTES = 25 * 1024 * 1024;
4785
4922
  MAX_ATTACHED_MESSAGES = 100;
4786
4923
  PARSE_TIMEOUT = 6e4;
4924
+ NAMED_TEXT = /* @__PURE__ */ Symbol("named text parts");
4925
+ ENCODED_FILES = /^(?:begin [0-7]{3,4} |=ybegin\b[^\r\n\u2028\u2029]*?\bname=)([^\r\n\u2028\u2029]+)\r?$/gm;
4926
+ MAX_ENCODED_FILES = 1e3;
4927
+ foundIn = /* @__PURE__ */ new WeakMap();
4928
+ MAX_NAME_FIELDS = 8;
4929
+ MAX_NAME_PARAMETERS = 32;
4787
4930
  MAX_FOLDS = 256;
4788
4931
  }
4789
4932
  });
@@ -7262,7 +7405,7 @@ var init_links = __esm({
7262
7405
  MEDIA_PROPERTIES = /* @__PURE__ */ new Set(["photo", "logo", "sound", "key", "attach"]);
7263
7406
  HTML_PROPERTIES = /^(?:x-alt-desc|styled-description|description|comment)[;:]/i;
7264
7407
  BINARY = /;\s*(?:(?:encoding\s*=\s*)?"?(?:b|base64)"?|value\s*=\s*"?binary"?)\s*(?:;|$)/i;
7265
- HTML_TYPES = /* @__PURE__ */ new Set(["text/html", "application/xhtml+xml", "image/svg+xml", "text/xml", "application/xml"]);
7408
+ HTML_TYPES = /* @__PURE__ */ new Set(["text/html", "text/x-amp-html", "application/xhtml+xml", "image/svg+xml", "text/xml", "application/xml"]);
7266
7409
  HTML_EXTENSIONS = /* @__PURE__ */ new Set(["html", "htm", "shtml", "xhtml", "xht", "svg", "xml"]);
7267
7410
  CARD_TYPES = /* @__PURE__ */ new Set(["text/calendar", "text/x-vcalendar", "application/ics", "text/vcard", "text/x-vcard", "text/directory"]);
7268
7411
  CARD_EXTENSIONS = /* @__PURE__ */ new Set(["ics", "ical", "ifb", "vcs", "vcf", "vcard"]);
@@ -7954,18 +8097,27 @@ function parseVerdict(text) {
7954
8097
  sources.push([text.slice(0, 2e4), answered]);
7955
8098
  }
7956
8099
  for (const [source, wanted] of sources) {
8100
+ let found = null;
8101
+ let end = -1;
7957
8102
  for (const candidate of jsonObjects(source)) {
7958
- if (!wanted(candidate)) {
8103
+ if (!wanted(candidate) || candidate[0] < end) {
7959
8104
  continue;
7960
8105
  }
7961
8106
  try {
7962
8107
  const result = normalizeVerdict(JSON.parse(candidate[1]));
7963
8108
  if (result) {
7964
- return result;
8109
+ if (found && found.verdict !== result.verdict) {
8110
+ return null;
8111
+ }
8112
+ found ??= result;
8113
+ end = candidate[0] + candidate[1].length;
7965
8114
  }
7966
8115
  } catch {
7967
8116
  }
7968
8117
  }
8118
+ if (found) {
8119
+ return found;
8120
+ }
7969
8121
  }
7970
8122
  const prose = visible.toLowerCase();
7971
8123
  let deniedEnd = -1;
@@ -8932,7 +9084,7 @@ var init_package = __esm({
8932
9084
  package_default = {
8933
9085
  name: "spamscanner",
8934
9086
  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.",
8935
- version: "7.0.14",
9087
+ version: "7.0.15",
8936
9088
  author: "Forward Email (https://forwardemail.net)",
8937
9089
  bin: {
8938
9090
  spamscanner: "./dist/esm/cli.js"
@@ -11256,14 +11408,15 @@ ${visibleText(html2.slice(0, config.maxLength * 2)).text}`, { maxLength: config.
11256
11408
  };
11257
11409
  const unopened = [];
11258
11410
  const opened = [];
11259
- const attached = await expandAttachments(mail.attachments, parseErrors, void 0, { unopened, mails: opened });
11411
+ const named = [];
11412
+ const attached = await expandAttachments(mail.attachments, parseErrors, void 0, { unopened, mails: opened, named });
11260
11413
  const carried = config.phishing.attachments === true && (config.phishing.cloudflare || config.dnsbl.domain?.length > 0) ? carriedLinkHosts(mail, {
11261
11414
  mails: opened,
11262
11415
  attachments: attached,
11263
11416
  maxHosts: config.phishing.maxHosts,
11264
11417
  hostsInText: config.phishing.hostsInText === true
11265
11418
  }) : null;
11266
- const attachments = config.attachments === false ? [] : inspectAttachments(attached).filter((item) => config.macros !== false || !["macro", "pdf_active", "rtf_object"].includes(item.type)).map((item) => finding(item));
11419
+ const attachments = config.attachments === false ? [] : inspectAttachments([...attached, ...namedTextParts(mail), ...named]).filter((item) => config.macros !== false || !["macro", "pdf_active", "rtf_object"].includes(item.type)).map((item) => finding(item));
11267
11420
  const [links, authentication, checkedReputation, dnsbl, virusScan, toxicity, nsfw] = await Promise.all([
11268
11421
  this.checkLinks(checkedLinks, config, extracted.hosts, { from: fromDomain(mail), carried }),
11269
11422
  config.authenticationResults && typeof config.authenticationResults === "object" ? normalizeAuthOutput(config.authenticationResults) : config.authentication && session.remoteAddress ? withTimeout(authenticate(raw, {