spamscanner 7.0.7 → 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/cjs/arf.cjs CHANGED
@@ -247,7 +247,7 @@ function unfoldHeaders(raw) {
247
247
  if (blank && type.boundary) {
248
248
  boundaries.push(import_node_buffer.Buffer.from(type.boundary));
249
249
  }
250
- head = blank && type.value === "message/rfc822" && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
250
+ head = blank && ["message/rfc822", "message/global"].includes(type.value) && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
251
251
  };
252
252
  for (let start = 0; start < raw.length; ) {
253
253
  const lf = raw.indexOf(10, start);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/arf.js", "../../src/parse.js"],
4
- "sourcesContent": ["import {Buffer} from 'node:buffer';\nimport {randomBytes} from 'node:crypto';\nimport {isIP} from 'node:net';\nimport {simpleParse} from './parse.js';\n\n/**\n * Feedback types from RFC 5965, RFC 6591 (auth-failure) and RFC 7489 (dmarc).\n */\nexport const VALID_FEEDBACK_TYPES = new Set(['abuse', 'fraud', 'virus', 'other', 'not-spam', 'auth-failure', 'dmarc']);\n\n// Fields that may appear more than once (RFC 5965 section 3.2).\nconst MULTIPLE = new Set(['original_rcpt_to', 'authentication_results', 'reported_domain', 'reported_uri']);\n\n/**\n * Parse the fields of a message/feedback-report part. Folded lines are\n * unfolded. Fields that may repeat are arrays; for other fields the first\n * occurrence wins.\n * @param {string} content\n * @returns {Record<string, string|string[]>}\n */\nexport function parseReportFields(content) {\n\tconst fields = {};\n\tconst unfolded = String(content).replaceAll(/\\r?\\n[ \\t]+/g, ' ');\n\tfor (const line of unfolded.split(/\\r?\\n/)) {\n\t\t// (no \"$\": a line with a line break of its own fails, as before,\n\t\t// without \"\\s*\" and \".*\" trying every split of it)\n\t\tconst match = /^([\\w-]+):(\\s*)(.*)/.exec(line);\n\t\tif (!match || match[0].length !== line.length) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst name = match[1].toLowerCase().replaceAll('-', '_');\n\t\tconst value = match[3].trim();\n\t\tif (MULTIPLE.has(name)) {\n\t\t\t(fields[name] ||= []).push(value);\n\t\t} else if (!(name in fields)) {\n\t\t\tfields[name] = value;\n\t\t}\n\t}\n\n\treturn fields;\n}\n\nfunction extractEmail(value) {\n\tif (!value) {\n\t\treturn null;\n\t}\n\n\t// (an address is short: what follows the first 1024 characters is not read)\n\tconst head = value.slice(0, 1024);\n\tconst angle = /<([^<>]*)>/.exec(head);\n\tif (angle) {\n\t\treturn angle[1].trim() || null;\n\t}\n\n\tconst plain = /[^\\s@<>]{1,256}@[^\\s@<>]+/.exec(head);\n\treturn plain ? plain[0] : head.trim();\n}\n\nfunction parseIp(value) {\n\tif (!value) {\n\t\treturn null;\n\t}\n\n\tconst candidate = value.trim().replaceAll(/^\\[|]$/g, '');\n\treturn isIP(candidate) ? candidate : null;\n}\n\nfunction parseDate(value) {\n\tif (!value) {\n\t\treturn null;\n\t}\n\n\tconst date = new Date(value);\n\treturn Number.isNaN(date.getTime()) ? null : date;\n}\n\nfunction parseReportingMta(value) {\n\tif (!value) {\n\t\treturn null;\n\t}\n\n\tconst trimmed = value.trim();\n\tconst match = /^([\\w-]+);\\s*(.+)/.exec(trimmed);\n\treturn match && match[0].length === trimmed.length ? {type: match[1].toLowerCase(), name: match[2].trim()} : {type: 'unknown', name: value.trim()};\n}\n\n// (written into a Buffer: an array of numbers would take many times the\n// memory of a large original message)\nfunction decodeQuotedPrintable(text) {\n\tconst soft = text.replaceAll(/=\\r?\\n/g, '');\n\tconst bytes = Buffer.allocUnsafe(soft.length);\n\tlet length = 0;\n\tfor (let i = 0; i < soft.length; i++) {\n\t\tif (soft[i] === '=' && /^[\\da-f]{2}$/i.test(soft.slice(i + 1, i + 3))) {\n\t\t\tbytes[length++] = Number.parseInt(soft.slice(i + 1, i + 3), 16);\n\t\t\ti += 2;\n\t\t} else {\n\t\t\tbytes[length++] = soft.codePointAt(i) % 256;\n\t\t}\n\t}\n\n\treturn bytes.subarray(0, length);\n}\n\n/**\n * Find a top-level MIME part by content type in a raw multipart message,\n * splitting only at boundary lines. mailparser merges inline message/rfc822\n * parts into the text, so the original message of a report is read here.\n * @param {string} raw\n * @param {string} boundary\n * @param {string[]} types\n * @returns {{type: string, content: Buffer}|null}\n */\nexport function findPart(raw, boundary, types) {\n\tconst escaped = boundary.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\n\tconst parts = raw.split(new RegExp(String.raw`\\r?\\n--${escaped}(?:--)?[ \\t]*(?=\\r?\\n|$)`));\n\tfor (const part of parts.slice(1)) {\n\t\t// A part is a header block (possibly empty), a blank line and a body.\n\t\tconst match = /^\\r?\\n((?:[^\\r\\n]+\\r?\\n)*)\\r?\\n([\\s\\S]*)$/.exec(part);\n\t\tif (!match) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst headers = match[1].replaceAll(/\\r?\\n[ \\t]+/g, ' ');\n\t\tconst type = (/^content-type:\\s*([^;\\s]+)/im.exec(headers)?.[1] || 'text/plain').toLowerCase();\n\t\tif (!types.includes(type)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst encoding = (/^content-transfer-encoding:\\s*(\\S+)/im.exec(headers)?.[1] || '').toLowerCase();\n\t\tlet content;\n\t\tif (encoding === 'base64') {\n\t\t\tcontent = Buffer.from(match[2], 'base64');\n\t\t} else if (encoding === 'quoted-printable') {\n\t\t\tcontent = decodeQuotedPrintable(match[2]);\n\t\t} else {\n\t\t\tcontent = Buffer.from(match[2], 'latin1');\n\t\t}\n\n\t\treturn {type, content};\n\t}\n\n\treturn null;\n}\n\n/**\n * Whether a message parsed by mailparser is an ARF report\n * (multipart/report; report-type=feedback-report).\n * @param {object} parsed\n * @returns {boolean}\n */\nexport function isArfMessage(parsed) {\n\tconst header = typeof parsed?.headers?.get === 'function' ? parsed.headers.get('content-type') : null;\n\tif (!header) {\n\t\treturn false;\n\t}\n\n\tconst value = typeof header === 'string' ? header : header.value;\n\tconst type = typeof header === 'string' ? (/report-type\\s*=\\s*\"?([\\w-]+)/i.exec(header)?.[1] || '') : (header.params?.['report-type'] || '');\n\treturn /^multipart\\/report\\b/i.test(String(value)) && type.toLowerCase() === 'feedback-report';\n}\n\n/**\n * Parse an ARF (RFC 5965) abuse report.\n * @param {Buffer|Uint8Array|string} source - the raw report message\n * @returns {Promise<object>} feedbackType, userAgent, version, arrivalDate,\n * sourceIp, originalMailFrom, originalRcptTo, reportingMta,\n * originalEnvelopeId, authenticationResults, reportedDomain, reportedUri,\n * incidents, humanReadable, originalMessage, originalHeaders (an object of\n * lowercase header name to value, or null when the report has no original\n * message) and rawFeedbackReport\n * @throws {Error} when the message is not a valid ARF report\n */\nexport async function parse(message) {\n\t// (a Uint8Array as the bytes it holds, as a Buffer)\n\tconst source = message instanceof Uint8Array && !Buffer.isBuffer(message) ? Buffer.from(message.buffer, message.byteOffset, message.byteLength) : message;\n\tconst parsed = await simpleParse(source);\n\tif (!isArfMessage(parsed)) {\n\t\tthrow new Error('Not an ARF report: expected multipart/report with report-type=feedback-report');\n\t}\n\n\tconst report = parsed.attachments.find(part => String(part.contentType).toLowerCase() === 'message/feedback-report');\n\tif (!report) {\n\t\tthrow new Error('Invalid ARF report: missing the message/feedback-report part');\n\t}\n\n\tconst raw = report.content.toString('utf8');\n\tconst fields = parseReportFields(raw);\n\tif (!fields.feedback_type) {\n\t\tthrow new Error('Invalid ARF report: missing the Feedback-Type field');\n\t}\n\n\tif (!fields.user_agent) {\n\t\tthrow new Error('Invalid ARF report: missing the User-Agent field');\n\t}\n\n\tlet feedbackType = fields.feedback_type.toLowerCase();\n\tlet feedbackTypeOriginal = null;\n\tif (!VALID_FEEDBACK_TYPES.has(feedbackType)) {\n\t\tfeedbackTypeOriginal = feedbackType;\n\t\tfeedbackType = 'other';\n\t}\n\n\tconst incidents = Number.parseInt(fields.incidents, 10);\n\tconst result = {\n\t\tisArf: true,\n\t\tfeedbackType,\n\t\tfeedbackTypeOriginal,\n\t\tuserAgent: fields.user_agent,\n\t\tversion: fields.version || '1',\n\t\tarrivalDate: parseDate(fields.arrival_date || fields.received_date),\n\t\tsourceIp: parseIp(fields.source_ip),\n\t\toriginalMailFrom: extractEmail(fields.original_mail_from),\n\t\toriginalRcptTo: fields.original_rcpt_to ? fields.original_rcpt_to.map(value => extractEmail(value)).filter(Boolean) : [],\n\t\treportingMta: parseReportingMta(fields.reporting_mta),\n\t\toriginalEnvelopeId: fields.original_envelope_id || null,\n\t\tauthenticationResults: fields.authentication_results || [],\n\t\treportedDomain: fields.reported_domain || [],\n\t\treportedUri: fields.reported_uri || [],\n\t\tincidents: Number.isFinite(incidents) && incidents > 0 ? incidents : 1,\n\t\thumanReadable: null,\n\t\toriginalMessage: null,\n\t\toriginalHeaders: null,\n\t\trawFeedbackReport: raw,\n\t};\n\n\tconst text = (Buffer.isBuffer(source) ? source : Buffer.from(String(source))).toString('latin1');\n\tconst {boundary} = parsed.headers.get('content-type').params;\n\tconst human = findPart(text, boundary, ['text/plain']);\n\tresult.humanReadable = human ? human.content.toString('utf8').trim() || null : null;\n\tconst original = findPart(text, boundary, ['message/rfc822', 'text/rfc822-headers', 'message/rfc822-headers']);\n\tif (original) {\n\t\tconst content = original.content.toString('utf8');\n\t\tresult.originalMessage = content;\n\t\tconst inner = await simpleParse(original.type === 'message/rfc822' ? content : `${content.trimEnd()}\\r\\n\\r\\n`);\n\t\tresult.originalHeaders = Object.fromEntries([...inner.headers].map(([key, value]) => [key, value]));\n\t}\n\n\treturn result;\n}\n\n/**\n * Parse an ARF report, or return null when the message is not one.\n * @param {Buffer|Uint8Array|string} source\n * @returns {Promise<object|null>}\n */\nexport async function tryParse(source) {\n\ttry {\n\t\treturn await parse(source);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction clean(value, name) {\n\tconst text = String(value);\n\tif (/[\\r\\n]/.test(text)) {\n\t\tthrow new TypeError(`${name} must not contain line breaks`);\n\t}\n\n\treturn text;\n}\n\n/**\n * Write an ARF report. (A string message gives a string report, and a\n * Buffer a Buffer, its bytes kept as they are, whatever their character set.)\n * @template {string|Buffer} M\n * @param {object} options\n * @param {string} options.feedbackType - abuse, fraud, virus, other, not-spam, ...\n * @param {string} options.userAgent\n * @param {string} options.from\n * @param {string} options.to\n * @param {M} options.originalMessage\n * @param {string} [options.humanReadable]\n * @param {string} [options.subject]\n * @param {string} [options.sourceIp]\n * @param {string} [options.originalMailFrom]\n * @param {string|string[]} [options.originalRcptTo]\n * @param {Date|string|number} [options.arrivalDate]\n * @param {string} [options.reportingMta]\n * @returns {M extends Buffer ? Buffer : string} the report\n */\nexport function create(options = {}) {\n\tconst {\n\t\tfeedbackType, userAgent, from, to, originalMessage, humanReadable = 'This is an abuse report for a message received from your network.', subject = 'Abuse report', sourceIp, originalMailFrom, originalRcptTo = [], arrivalDate, reportingMta,\n\t} = options;\n\tif (!feedbackType || !userAgent || !from || !to || !originalMessage) {\n\t\tthrow new TypeError('An ARF report needs feedbackType, userAgent, from, to and originalMessage');\n\t}\n\n\tconst boundary = `arf-${randomBytes(12).toString('hex')}`;\n\tconst report = [\n\t\t`Feedback-Type: ${clean(feedbackType, 'feedbackType')}`,\n\t\t`User-Agent: ${clean(userAgent, 'userAgent')}`,\n\t\t'Version: 1',\n\t];\n\tif (sourceIp) {\n\t\treport.push(`Source-IP: ${clean(sourceIp, 'sourceIp')}`);\n\t}\n\n\tif (originalMailFrom) {\n\t\treport.push(`Original-Mail-From: <${clean(originalMailFrom, 'originalMailFrom')}>`);\n\t}\n\n\tfor (const rcpt of [originalRcptTo].flat()) {\n\t\treport.push(`Original-Rcpt-To: <${clean(rcpt, 'originalRcptTo')}>`);\n\t}\n\n\tif (arrivalDate) {\n\t\tconst date = new Date(arrivalDate);\n\t\tif (Number.isNaN(date.getTime())) {\n\t\t\tthrow new TypeError('arrivalDate must be a date');\n\t\t}\n\n\t\treport.push(`Arrival-Date: ${date.toUTCString()}`);\n\t}\n\n\tif (reportingMta) {\n\t\treport.push(`Reporting-MTA: dns; ${clean(reportingMta, 'reportingMta')}`);\n\t}\n\n\tconst head = [\n\t\t`From: ${clean(from, 'from')}`,\n\t\t`To: ${clean(to, 'to')}`,\n\t\t`Date: ${new Date().toUTCString()}`,\n\t\t`Subject: ${encodeWords(clean(subject, 'subject'))}`,\n\t\t'MIME-Version: 1.0',\n\t\t`Content-Type: multipart/report; report-type=feedback-report; boundary=\"${boundary}\"`,\n\t\t'',\n\t\t`--${boundary}`,\n\t\t'Content-Type: text/plain; charset=utf-8',\n\t\t'Content-Transfer-Encoding: 8bit',\n\t\t'',\n\t\thumanReadable,\n\t\t'',\n\t\t`--${boundary}`,\n\t\t'Content-Type: message/feedback-report',\n\t\t'',\n\t\t...report,\n\t\t'',\n\t\t`--${boundary}`,\n\t\t'Content-Type: message/rfc822',\n\t\t'Content-Disposition: inline',\n\t\t'',\n\t\t'',\n\t].join('\\r\\n');\n\tconst tail = `\\r\\n--${boundary}--\\r\\n`;\n\tif (Buffer.isBuffer(originalMessage)) {\n\t\tconst end = originalMessage.at(-1) === 0x0A ? (originalMessage.at(-2) === 0x0D ? 2 : 1) : 0;\n\t\treturn Buffer.concat([Buffer.from(head), originalMessage.subarray(0, originalMessage.length - end), Buffer.from(tail)]);\n\t}\n\n\treturn head + String(originalMessage).replace(/\\r?\\n$/, '') + tail;\n}\n\n// A header value in RFC 2047 encoded words when it is not plain ASCII, in\n// words of at most 45 bytes (whole characters each), so lines stay short\nfunction encodeWords(text) {\n\tif (/^[\\u0020-\\u007E]*$/.test(text)) {\n\t\treturn text;\n\t}\n\n\tconst words = [];\n\tlet word = '';\n\tfor (const character of text) {\n\t\tif (Buffer.byteLength(word + character) > 45) {\n\t\t\twords.push(word);\n\t\t\tword = '';\n\t\t}\n\n\t\tword += character;\n\t}\n\n\twords.push(word);\n\treturn words.map(part => `=?UTF-8?B?${Buffer.from(part).toString('base64')}?=`).join('\\r\\n ');\n}\n\n/**\n * The ARF parser as an object, as in earlier versions.\n */\nexport const ArfParser = {\n\tisArfMessage, parse, tryParse, create,\n};\n\nexport default ArfParser;\n", "import {Buffer} from 'node:buffer';\nimport {createHash} from 'node:crypto';\nimport {Readable} from 'node:stream';\nimport {MailParser, simpleParser} from 'mailparser';\n\n// Address fields, which mailparser (of every MIME part's header) and mailauth\n// (From and Return-Path of the message's header) read with nodemailer's\n// addressparser: its time grows with the square of the number of addresses\n// in a field, and with the length of the field times the number of groups\n// nested in it (\"g:g:g:...\"), so a field of a megabyte took seconds, and\n// minutes in mailauth. Each is read up to this many characters, this many\n// \",\" and \";\" and this many \":\" in a group, and a message's (each attached\n// message's) fields up to this many characters in all, always to the end of\n// a whole address: what is cut is never part of an address, so no other\n// address than one the field gives is read. A field cut gets `truncated`\n// (mail.from.truncated), and a From field cut gives no credit for the\n// sender's authentication (see trustedReputationValues and authenticate).\nconst ADDRESS_FIELDS = new Set(['from', 'to', 'cc', 'bcc', 'sender', 'reply-to', 'delivered-to', 'return-path', 'disposition-notification-to']);\nconst MAX_ADDRESS_FIELD = 16 * 1024;\nconst MAX_ADDRESS_SEPARATORS = 2000;\nconst MAX_ADDRESS_GROUPS = 16;\nconst MAX_ADDRESS_TOTAL = 64 * 1024;\n// (the address fields mailauth reads with addressparser)\nconst AUTH_ADDRESS_FIELDS = new Set(['from', 'return-path']);\n// (the characters addressparser reads as operators, with the one that ends\n// each: in a group, up to its \";\", everything else is text)\nconst ADDRESS_OPERATORS = new Map([['\"', '\"'], ['(', ')'], ['<', '>'], [',', ''], [':', ';'], [';', '']]);\n// (the fields mailparser reads one value of, the priority of any of four)\nconst SINGLE_FIELDS = new Set(['message-id', 'content-id', 'from', 'sender', 'in-reply-to', 'reply-to', 'subject', 'date', 'content-disposition', 'content-type', 'content-transfer-encoding', 'priority', 'mime-version', 'content-description', 'precedence', 'errors-to', 'disposition-notification-to']);\nconst PRIORITY_FIELDS = new Set(['priority', 'x-priority', 'x-msmail-priority', 'importance']);\nconst BOUNDED = Symbol('bounded address fields');\nconst ROOM = Symbol('room left for address fields');\n\n// (no text made from HTML by mailparser: html-to-text takes quadratic time on\n// deeply nested HTML, and fails on HTML nested a few thousand levels deep;\n// getFeatures reads the text HTML shows instead)\nconst PARSER_OPTIONS = {\n\tskipHtmlToText: true, skipImageLinks: true, skipTextToHtml: true, skipTextLinks: true, [BOUNDED]: true,\n};\n\n// A header field \"Name: value\" (folded or not) up to the end of the last\n// whole address within the limits, and at most `room` characters (but its\n// name), as addressparser reads it: an address ends at a \",\" or \";\" outside\n// quotes, a comment, angle brackets and a group, or at the \";\" ending a\n// group. The field itself when it is within them.\nfunction boundAddressField(line, room) {\n\tconst start = line.indexOf(':') + 1;\n\tconst limit = Math.min(line.length, MAX_ADDRESS_FIELD, room);\n\tlet end = start;\n\tlet expecting = '';\n\tlet escaped = false;\n\tlet separators = 0;\n\tlet groups = 0;\n\tlet index = start;\n\tfor (; index < limit; index++) {\n\t\tconst chr = line[index];\n\t\tif ((chr === ',' || chr === ';') && ++separators > MAX_ADDRESS_SEPARATORS) {\n\t\t\tbreak;\n\t\t}\n\n\t\tif (escaped) {\n\t\t\tescaped = false;\n\t\t} else if (expecting !== '' && chr === expecting) {\n\t\t\texpecting = '';\n\t\t\tend = chr === ';' ? index + 1 : end;\n\t\t} else if (expecting === '' && ADDRESS_OPERATORS.has(chr)) {\n\t\t\texpecting = ADDRESS_OPERATORS.get(chr);\n\t\t\tend = chr === ',' || chr === ';' ? index : end;\n\t\t} else if (expecting === '\"' && chr === '\\\\') {\n\t\t\tescaped = true;\n\t\t}\n\n\t\t// (a group in a group is read again, and again, for each \":\")\n\t\tif (chr === ':' && expecting === ';' && ++groups > MAX_ADDRESS_GROUPS) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn index === line.length ? line : line.slice(0, end).trimEnd();\n}\n\n// Header fields ({key, line}, key the name in lower case) with their address\n// fields bounded (see boundAddressField), `budget.room` characters of them in\n// all, and the names of the fields cut. `only`: just these fields (by\n// default every address field and List-* field)\nfunction boundAddressLines(lines, budget, only = null) {\n\tconst cut = new Set();\n\tconst bounded = lines.map(entry => {\n\t\tif (only ? !only.has(entry.key) : !ADDRESS_FIELDS.has(entry.key) && !entry.key.startsWith('list-')) {\n\t\t\treturn entry;\n\t\t}\n\n\t\tconst line = boundAddressField(entry.line, budget.room);\n\t\tbudget.room = Math.max(budget.room - line.length, 0);\n\t\tif (line === entry.line) {\n\t\t\treturn entry;\n\t\t}\n\n\t\tcut.add(entry.key);\n\t\treturn {...entry, line};\n\t});\n\treturn {lines: bounded, cut};\n}\n\n// Mailparser reads every header (the message's and each part's) with\n// processHeaders: for a parser of parseMessage (only: others are given what\n// they always were), with address fields bounded, a budget for each message,\n// and those cut marked `truncated`. (simpleParser makes its own MailParser,\n// so this is how its headers are reached.)\nconst {processHeaders} = MailParser.prototype;\nMailParser.prototype.processHeaders = function (lines) {\n\tif (!this.options[BOUNDED]) {\n\t\treturn processHeaders.call(this, lines);\n\t}\n\n\tthis[ROOM] ??= {room: MAX_ADDRESS_TOTAL};\n\tconst bounded = boundAddressLines(lines, this[ROOM]);\n\tconst headers = processHeaders.call(this, bounded.lines);\n\t// (of a field given more than once that is read once, the last with a\n\t// value, as mailparser read it before 3.9.40, which reads the first)\n\tconst keyOf = entry => PRIORITY_FIELDS.has(entry.key) ? 'priority' : entry.key;\n\tconst counts = new Map();\n\tfor (const key of bounded.lines.map(entry => keyOf(entry)).filter(key => SINGLE_FIELDS.has(key))) {\n\t\tcounts.set(key, (counts.get(key) || 0) + 1);\n\t}\n\n\tfor (const key of [...counts.keys()].filter(key => counts.get(key) > 1)) {\n\t\tconst last = bounded.lines.findLast(entry => keyOf(entry) === key && processHeaders.call(this, [entry]).has(key));\n\t\tif (last) {\n\t\t\theaders.set(key, processHeaders.call(this, [last]).get(key));\n\t\t}\n\t}\n\n\t// (List-* fields are read into \"list\")\n\tfor (const value of [...bounded.cut].flatMap(key => headers.get(key) ?? [])) {\n\t\tvalue.truncated = true;\n\t}\n\n\treturn headers;\n};\n\n/**\n * The message as mailauth is to read it: its address fields bounded as\n * parseMessage bounds them (mailauth reads From and Return-Path with the same\n * parser), and, when the header would take mailauth longer than linear time\n * (it joins folded lines, and finds the fields a DKIM signature names, in\n * time that grows with the square of their number), the header cut down:\n * its folded lines unfolded (which relaxed canonicalization of DKIM does\n * too) and only the fields that say who sent the message and how, as many\n * as smallerHeader keeps, each up to MAX_FIELD characters (address fields as\n * bounded), and at most MAX_SIGNATURES signatures. Only the first\n * MAX_DKIM_SIGNATURES DKIM-Signature fields are ever kept (with their folded\n * lines; the body is untouched). `fromCut` is whether a\n * From field was cut or left out: then mailauth did not read the From\n * address the message gives.\n * @param {Buffer} raw\n * @returns {{raw: Buffer, fromCut: boolean}}\n */\nexport function boundedForAuth(raw) {\n\t// (where mailauth finds the end of the header, and its fields: a line\n\t// starting with white space continues the one before, and the name is\n\t// up to the first \":\")\n\tconst ends = [raw.indexOf('\\n\\n'), raw.indexOf('\\n\\r\\n')].filter(index => index !== -1);\n\tconst end = ends.length > 0 ? Math.min(...ends) + 1 : raw.length;\n\tconst body = raw.subarray(ends.length > 0 ? raw.indexOf(0x0A, end) + 1 : raw.length);\n\tconst text = raw.subarray(0, end).toString('latin1');\n\tlet {length} = text;\n\twhile (length > 0 && (text[length - 1] === '\\n' || text[length - 1] === '\\r')) {\n\t\tlength--;\n\t}\n\n\tconst fields = [];\n\tlet folds = 0;\n\tlet work = 0;\n\tfor (let start = 0; start <= length;) {\n\t\tlet stop = text.indexOf('\\n', start);\n\t\tstop = stop === -1 || stop > length ? length : stop;\n\t\tconst lineEnd = stop > start && text[stop - 1] === '\\r' ? stop - 1 : stop;\n\t\tconst last = fields.at(-1);\n\t\tif (last && /\\s/.test(text[start])) {\n\t\t\tlast.end = lineEnd;\n\t\t\tlast.folds++;\n\t\t\tfolds++;\n\t\t\t// (each folded line is moved past the fields after it, and joined\n\t\t\t// to the lines after it of its field)\n\t\t\twork += last.folds;\n\t\t} else {\n\t\t\tfields.push({start, end: lineEnd, folds: 0});\n\t\t}\n\n\t\tstart = stop + 1;\n\t}\n\n\tlet signatures = 0;\n\tlet dkim = 0;\n\tfor (const field of fields) {\n\t\tfield.line = text.slice(field.start, field.end);\n\t\tconst colon = field.line.indexOf(':');\n\t\tfield.key = (colon === -1 ? field.line : field.line.slice(0, colon)).trim().toLowerCase();\n\t\tfield.dropped = field.key === 'dkim-signature' && ++dkim > MAX_DKIM_SIGNATURES;\n\t\tif (SIGNATURE_FIELDS.has(field.key) && !field.dropped) {\n\t\t\t// (each field a signature names is looked for among all the fields)\n\t\t\tsignatures += field.line.split(':').length - 1;\n\t\t}\n\t}\n\n\tconst read = dkim > MAX_DKIM_SIGNATURES ? fields.filter(field => !field.dropped) : fields;\n\twork += (folds + signatures) * read.length;\n\t// (only From and Return-Path, the address fields mailauth reads: others,\n\t// To and Cc among them, are often signed, and cutting them would fail a\n\t// signature that holds)\n\tconst bounded = boundAddressLines(read, {room: MAX_ADDRESS_TOTAL}, AUTH_ADDRESS_FIELDS);\n\tlet kept = bounded.lines;\n\tlet fromCut = bounded.cut.has('from');\n\tif (work > MAX_AUTH_WORK) {\n\t\tconst counts = new Map();\n\t\tkept = kept.filter(field => {\n\t\t\tcounts.set(field.key, (counts.get(field.key) || 0) + 1);\n\t\t\tconst keep = AUTH_FIELDS.has(field.key) && counts.get(field.key) <= (SIGNATURE_FIELDS.has(field.key) ? MAX_SIGNATURES : MAX_REPEATED);\n\t\t\tfromCut ||= field.key === 'from' && !keep;\n\t\t\treturn keep;\n\t\t}).map(field => {\n\t\t\tconst line = field.line.replaceAll(/\\r?\\n(?=\\s)/g, '');\n\t\t\treturn {...field, line: ADDRESS_FIELDS.has(field.key) ? line : line.slice(0, MAX_FIELD)};\n\t\t});\n\t} else if (bounded.cut.size === 0 && read === fields) {\n\t\treturn {raw, fromCut};\n\t}\n\n\tconst header = kept.map(field => field.line).join('\\r\\n');\n\treturn {raw: Buffer.concat([Buffer.from(`${header}\\r\\n\\r\\n`, 'latin1'), body]), fromCut};\n}\n\n// The limits of mailparser (of mailsplit, which it splits messages with) that\n// make it fail: more than 1000 MIME parts (an inline attached message is a\n// part too), and a part's header over 1 MB.\nconst TOO_MANY_PARTS = 'Max allowed child nodes exceeded';\nconst HEADER_TOO_LARGE = 'Max header size for a MIME node exceeded';\n// A message beyond them is read up to this many lines that may start a part\n// (\"--boundary\"), and this many bytes of it if a part's header is too large.\nconst MAX_PART_LINES = 990;\nconst MAX_PREFIX_BYTES = 1_000_000;\n// Of a message header too large, each line is read up to this many\n// characters, each field up to this many, and if the whole is still too\n// large, only the fields that say who sent the message and how.\nconst MAX_LINE = 998;\nconst MAX_FIELD = 8192;\nconst MAX_HEADER = 512 * 1024;\nconst MAX_REPEATED = 100;\nconst KEPT_FIELDS = new Set([\n\t'from', 'sender', 'reply-to', 'to', 'cc', 'subject', 'date', 'message-id', 'return-path', 'received', 'authentication-results', 'arc-authentication-results', 'dkim-signature', 'mime-version', 'content-type', 'content-transfer-encoding', 'list-unsubscribe', 'list-id',\n]);\n// Of a header that would take mailauth too long (more than this much work,\n// in its steps: see boundedForAuth), it reads these fields, and at most this\n// many signatures.\nconst MAX_AUTH_WORK = 10_000_000;\nconst SIGNATURE_FIELDS = new Set(['dkim-signature', 'arc-message-signature']);\nconst AUTH_FIELDS = new Set([...KEPT_FIELDS, 'arc-seal', 'arc-message-signature']);\nconst MAX_SIGNATURES = 10;\n/**\n * DKIM-Signature fields mailauth reads of a message (see boundedForAuth): it\n * checks every signature, hashing the body once for each body length (l=)\n * and canonicalization and looking up each signer's key, so hundreds of them\n * would hold up the process and send as many DNS queries. Real mail carries\n * one to three.\n */\nexport const MAX_DKIM_SIGNATURES = 8;\n\n// Attached messages (message/rfc822, .eml) are opened this many levels deep,\n// for at most this many attachments of theirs and this many bytes of attached\n// messages (the message's own attachments are always all read).\nconst MAX_NESTED_DEPTH = 3;\nconst MAX_ATTACHMENTS = 100;\nconst MAX_NESTED_BYTES = 25 * 1024 * 1024;\n\n// Milliseconds a message (with the tries of parseWithin), or all the messages\n// attached to one, may take to parse. mailparser is driven by its own stream\n// events alone, so a parse it never finishes leaves nothing pending: a scan\n// that would never settle, or a process that exits with it unsettled.\nexport const PARSE_TIMEOUT = 60_000;\n\n/**\n * The simpleParser of mailparser on a message it can finish (see finishable),\n * settled within `timeout`: a parse that has not settled by then is stopped\n * (its parser destroyed) and rejected with an ETIMEDOUT error.\n * @param {Buffer|string} raw\n * @param {object} [options]\n * @param {number} [options.timeout] - milliseconds (PARSE_TIMEOUT)\n * @param {typeof simpleParser} [options.parser] - for tests\n * @returns {Promise<object>} the parsed message\n */\nexport async function simpleParse(raw, {timeout = PARSE_TIMEOUT, parser = simpleParser} = {}) {\n\t// (a stream, not the buffer, so a parse that stalls can be stopped:\n\t// simpleParser destroys its parser when its input fails; the input is\n\t// not destroyed when it ends, so it can still fail)\n\tconst input = Readable.from([finishable(unfoldHeaders(raw))], {objectMode: false, autoDestroy: false});\n\tinput.on('error', () => {});\n\t// (and fails it on an error of the splitter that reads its parts, as\n\t// where its limits are met: mailparser before 3.9.30 finishes instead on\n\t// one that comes after the end of its input, as each one does with the\n\t// mailsplit it uses, which splits a line at a time: with what it read so\n\t// far, or never, as a parse that never finishes. Node.js 18 gets 3.9.21\n\t// at most.)\n\tconst {pipe} = input;\n\tinput.pipe = function (parser, options) {\n\t\tparser.splitter?.on('error', error => input.destroy(error));\n\t\treturn pipe.call(this, parser, options);\n\t};\n\n\tlet timer;\n\ttry {\n\t\treturn await Promise.race([\n\t\t\tparser(input, PARSER_OPTIONS),\n\t\t\tnew Promise((resolve, reject) => {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tconst error = Object.assign(new Error(`Message not parsed within ${timeout} ms`), {code: 'ETIMEDOUT'});\n\t\t\t\t\tinput.destroy(error);\n\t\t\t\t\treject(error);\n\t\t\t\t}, timeout);\n\t\t\t}),\n\t\t]);\n\t} finally {\n\t\tclearTimeout(timer);\n\t\tinput.destroy();\n\t}\n}\n\n/**\n * Parse a message with mailparser, which does not make text of HTML here:\n * `text` is that of the plain text parts only (empty for a message of HTML\n * alone), and the text HTML shows is read by getFeatures.\n * @param {Buffer|string} raw\n * @param {{timeout?: number}} [options] - milliseconds for the parse (PARSE_TIMEOUT)\n * @returns {Promise<object>} the parsed message\n */\nexport async function parseMessage(raw, options = {}) {\n\tconst buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n\tconst mail = await simpleParse(buffer, options);\n\t// (without the line breaks that adds to the last part)\n\tconst added = buffer.at(-1) === 0x0A ? 2 : 3;\n\tfor (const key of ['text', 'html']) {\n\t\tif (typeof mail[key] === 'string') {\n\t\t\tmail[key] = mail[key].replace(new RegExp(`\\\\n{0,${added}}$`), '');\n\t\t}\n\t}\n\n\t// (and from an attachment left open at the end, as ClamAV and checksums\n\t// read it: only one whose own bytes end the message, so not one closed\n\t// by a boundary, nor one followed by another part or an epilogue, and\n\t// only one not encoded, whose bytes are those of the message)\n\tconst tail = Buffer.from('\\r\\n'.repeat(added));\n\tconst last = mail.attachments?.at(-1);\n\tconst encoding = String(last?.headers?.get?.('content-transfer-encoding') ?? '7bit').trim().toLowerCase();\n\tconst own = Buffer.isBuffer(last?.content) && last.content.length >= tail.length && last.content.subarray(-tail.length).equals(tail) ? last.content.subarray(0, -tail.length) : null;\n\tif (own && ['7bit', '8bit', 'binary'].includes(encoding) && own.length <= buffer.length && buffer.subarray(buffer.length - own.length).equals(own)) {\n\t\tlast.content = own;\n\t\tlast.size = own.length;\n\t\tlast.checksum = createHash('md5').update(own).digest('hex');\n\t}\n\n\treturn mail;\n}\n\n/**\n * A message mailparser can finish: it never finishes one that ends in the\n * header of an attached message (however that header names or folds its\n * type, or only a file name like \"fw.eml\") without the blank line after it,\n * so every message gets one.\n * @param {Buffer|string} raw\n * @returns {Buffer}\n */\nexport function finishable(raw) {\n\tconst buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n\treturn Buffer.concat([buffer, Buffer.from(buffer.at(-1) === 0x0A ? '\\r\\n\\r\\n' : '\\r\\n\\r\\n\\r\\n')]);\n}\n\n// A header field folded on more lines than this is read unfolded: mailsplit\n// before 5.4.18 (of mailparser before 3.9.30, as Node.js 18 gets) joins the\n// lines of a field in time that grows with their number times its length,\n// seconds for a field of a megabyte. Real fields are folded on a few lines; a\n// list of a thousand recipients on a thousand.\nconst MAX_FOLDS = 256;\n\n// The value and parameters of a header field (\"Name: value; key=value\"), as\n// libmime (parseHeaderValue) reads them for mailsplit: `value` lower case.\nfunction headerValue(line) {\n\tconst parameters = new Map();\n\tlet key = null;\n\tlet inKey = false;\n\tlet quoted = false;\n\tlet escaped = false;\n\tlet value = '';\n\tfor (const chr of line.slice(line.indexOf(':') + 1).replaceAll(/\\r?\\n[ \\t]*/g, ' ')) {\n\t\tif (inKey && chr === '=') {\n\t\t\tkey = value.trim().toLowerCase();\n\t\t\tinKey = false;\n\t\t\tvalue = '';\n\t\t} else if (inKey || escaped) {\n\t\t\tvalue += chr;\n\t\t\tescaped = false;\n\t\t} else if (chr === '\\\\') {\n\t\t\tescaped = true;\n\t\t} else if (chr === '\"') {\n\t\t\tquoted = !quoted;\n\t\t} else if (chr === ';' && !quoted) {\n\t\t\tparameters.set(key, value.trim());\n\t\t\tinKey = true;\n\t\t\tvalue = '';\n\t\t} else {\n\t\t\tvalue += chr;\n\t\t}\n\t}\n\n\tif (!inKey) {\n\t\tparameters.set(key, value.trim());\n\t}\n\n\treturn {value: parameters.get(null).toLowerCase(), boundary: parameters.get('boundary') ?? ''};\n}\n\n// Whether the line at raw[start, stop) is a delimiter of `boundary` as\n// mailsplit reads one: 1 \"--boundary\", 2 \"--boundary--\", 0 neither.\nfunction delimiter(raw, start, stop, boundary) {\n\tconst rest = start + 2 + boundary.length;\n\tif (stop - start < boundary.length + 2 || stop - rest > 4 || raw[start] !== 0x2D || raw[start + 1] !== 0x2D || !raw.subarray(start + 2, rest).equals(boundary)) {\n\t\treturn 0;\n\t}\n\n\tif (raw[rest] === 0x0D || raw[rest] === 0x0A) {\n\t\treturn 1;\n\t}\n\n\t// (\"--\", then <CR><LF>, <LF> or nothing)\n\tconst tail = raw.subarray(rest, stop).toString('latin1');\n\treturn /^-{0,2}$|^--\\r?\\n$|^--\\r$/.test(tail) ? 2 : 0;\n}\n\n/**\n * The message with each header field folded on more than MAX_FOLDS lines\n * unfolded: its line breaks and the white space after each made one space,\n * as mailparser reads a field (libmime's decodeHeader), so what it reads is\n * the same. Header fields are those of the message and, as mailsplit finds\n * them, of each MIME part and each attached message inline; everything else\n * is left as it is. The message itself when nothing is unfolded.\n * @param {Buffer} raw\n * @returns {Buffer}\n */\nexport function unfoldHeaders(raw) {\n\tconst boundaries = [];\n\tconst unfold = [];\n\tlet head = true;\n\tlet fields = [];\n\t// (the end of a header: by a blank line, a part follows)\n\tconst endHead = blank => {\n\t\tunfold.push(...fields.filter(field => field.folds > MAX_FOLDS));\n\t\tconst first = new Map();\n\t\tfor (const field of fields) {\n\t\t\tconst name = raw.toString('latin1', field.start, Math.min(field.end, field.start + 32));\n\t\t\tconst key = name.slice(0, name.indexOf(':')).trim().toLowerCase();\n\t\t\tif (['content-type', 'content-transfer-encoding', 'content-disposition'].includes(key) && !first.has(key)) {\n\t\t\t\tfirst.set(key, headerValue(raw.toString('latin1', field.start, field.end)));\n\t\t\t}\n\t\t}\n\n\t\tfields = [];\n\t\tconst type = first.get('content-type') ?? {value: '', boundary: ''};\n\t\tconst encoding = (first.get('content-transfer-encoding')?.value ?? '').replaceAll(/\\(.*\\)/g, '').trim();\n\t\tif (blank && type.boundary) {\n\t\t\tboundaries.push(Buffer.from(type.boundary));\n\t\t}\n\n\t\t// (an attached message inline: its header follows)\n\t\thead = blank && type.value === 'message/rfc822' && ['', '7bit', '8bit', 'binary'].includes(encoding) && first.get('content-disposition')?.value === 'inline';\n\t};\n\n\tfor (let start = 0; start < raw.length;) {\n\t\tconst lf = raw.indexOf(0x0A, start);\n\t\tconst stop = lf === -1 ? raw.length : lf + 1;\n\t\tconst end = lf === -1 ? stop : (raw[lf - 1] === 0x0D ? lf - 1 : lf);\n\t\tconst kind = boundaries.length > 0 ? delimiter(raw, start, stop, boundaries.at(-1)) : 0;\n\t\tif (kind !== 0) {\n\t\t\tif (head) {\n\t\t\t\tendHead(false);\n\t\t\t}\n\n\t\t\t// (a part, or the end of the parts)\n\t\t\thead = kind === 1;\n\t\t\tif (kind === 2) {\n\t\t\t\tboundaries.pop();\n\t\t\t}\n\t\t} else if (head && end === start) {\n\t\t\tendHead(true);\n\t\t} else if (head && fields.length > 0 && (raw[start] === 0x20 || raw[start] === 0x09)) {\n\t\t\tfields.at(-1).end = end;\n\t\t\tfields.at(-1).folds++;\n\t\t} else if (head) {\n\t\t\tfields.push({start, end, folds: 0});\n\t\t}\n\n\t\tstart = stop;\n\t}\n\n\tunfold.push(...fields.filter(field => field.folds > MAX_FOLDS));\n\tif (unfold.length === 0) {\n\t\treturn raw;\n\t}\n\n\tconst chunks = [];\n\tlet copied = 0;\n\tfor (const field of unfold) {\n\t\tchunks.push(raw.subarray(copied, field.start), Buffer.from(raw.toString('latin1', field.start, field.end).replaceAll(/\\r?\\n[ \\t]*/g, ' '), 'latin1'));\n\t\tcopied = field.end;\n\t}\n\n\tchunks.push(raw.subarray(copied));\n\treturn Buffer.concat(chunks);\n}\n\n// The message up to the start of the line at `offset` or before it.\nfunction upToLine(raw, offset) {\n\treturn raw.subarray(0, Math.max(raw.lastIndexOf(0x0A, offset - 1) + 1, 0));\n}\n\n// The message before its MAX_PART_LINES-th line that starts with \"--\", or,\n// with fewer such lines (attached messages inline are parts without one),\n// its first half.\nfunction fewerParts(raw) {\n\tlet offset = -1;\n\tfor (let count = 0; count < MAX_PART_LINES; count++) {\n\t\toffset = raw.indexOf('\\n--', offset + 1);\n\t\tif (offset === -1) {\n\t\t\treturn upToLine(raw, Math.floor(raw.length / 2));\n\t\t}\n\t}\n\n\treturn raw.subarray(0, offset + 1);\n}\n\n// The message with its header (up to the first blank line) cut down to\n// MAX_LINE characters a line, MAX_FIELD a field, and the KEPT_FIELDS when\n// the whole is over MAX_HEADER, so the sender is read whatever comes before;\n// address fields are cut to whole addresses instead (see boundAddressField).\n// `fromCut` is whether a From field was cut or left out.\nfunction smallerHeader(raw) {\n\tconst text = raw.toString('latin1');\n\tconst blank = /\\r?\\n\\r?\\n/.exec(text);\n\tconst end = blank ? blank.index : text.length;\n\tconst fields = [];\n\tlet start = 0;\n\twhile (start < end) {\n\t\tlet stop = text.indexOf('\\n', start);\n\t\tstop = stop === -1 || stop > end ? end : stop;\n\t\tconst last = fields.at(-1);\n\t\tconst whole = text.slice(start, stop).replace(/\\r$/, '');\n\t\tif (last && (whole.startsWith(' ') || whole.startsWith('\\t'))) {\n\t\t\tconst line = last.address ? whole : whole.slice(0, MAX_LINE);\n\t\t\tif (last.address || last.size + line.length <= MAX_FIELD) {\n\t\t\t\tlast.lines.push(line);\n\t\t\t\tlast.size += line.length;\n\t\t\t}\n\t\t} else {\n\t\t\tconst name = whole.slice(0, whole.indexOf(':')).trim().toLowerCase();\n\t\t\tconst address = ADDRESS_FIELDS.has(name) || name.startsWith('list-');\n\t\t\tconst line = address ? whole : whole.slice(0, MAX_LINE);\n\t\t\tfields.push({\n\t\t\t\tname, address, lines: [line], size: line.length,\n\t\t\t});\n\t\t}\n\n\t\tstart = stop + 1;\n\t}\n\n\tlet fromCut = false;\n\tfor (const field of fields.filter(field => field.address)) {\n\t\tconst line = field.lines.join('\\r\\n');\n\t\tconst bounded = boundAddressField(line, Number.POSITIVE_INFINITY);\n\t\tfromCut ||= field.name === 'from' && bounded !== line;\n\t\tfield.lines = [bounded];\n\t\tfield.size = bounded.length;\n\t}\n\n\tconst total = fields.reduce((sum, field) => sum + field.size, 0);\n\t// (and at most MAX_REPEATED of each, as hundreds of Received fields could\n\t// still be too many)\n\tconst counts = new Map();\n\tconst kept = total > MAX_HEADER\n\t\t? fields.filter(field => {\n\t\t\tcounts.set(field.name, (counts.get(field.name) || 0) + 1);\n\t\t\tconst keep = KEPT_FIELDS.has(field.name) && counts.get(field.name) <= MAX_REPEATED;\n\t\t\tfromCut ||= field.name === 'from' && !keep;\n\t\t\treturn keep;\n\t\t})\n\t\t: fields;\n\tconst header = kept.flatMap(field => field.lines).join('\\r\\n');\n\treturn {raw: Buffer.concat([Buffer.from(`${header}\\r\\n\\r\\n`, 'latin1'), raw.subarray(blank ? blank.index + blank[0].length : raw.length)]), fromCut};\n}\n\n/**\n * Parse a message, reading what can be read of one beyond mailparser's\n * limits (more than 1000 MIME parts, a part's header over 1 MB): its parts\n * before the 990th line that may start one (or its first half, again and\n * again, when attached messages inline make the parts), and, when a header\n * is too large, the message with its header lines cut down, or its first\n * megabyte when the header is that of a later part. `error` is the limit,\n * or null. Other errors are thrown, as earlier versions did (servers defer\n * the message), and so is a parse not finished by `deadline` (ETIMEDOUT).\n * @param {Buffer} raw\n * @param {(raw: Buffer, options: {timeout: number}) => Promise<object>} [parse] - for tests\n * @param {number} [deadline] - Date.now() milliseconds by which every try\n * ends (PARSE_TIMEOUT from now)\n * @returns {Promise<{mail: object, error: string|null}>}\n */\nexport async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PARSE_TIMEOUT) {\n\tlet input = raw;\n\tlet reason = null;\n\tlet fromCut = false;\n\tfor (;;) {\n\t\ttry {\n\t\t\t// eslint-disable-next-line no-await-in-loop\n\t\t\tconst mail = await parse(input, {timeout: Math.max(1, deadline - Date.now())});\n\t\t\t// (a From field cut down is marked as parseMessage marks one)\n\t\t\tif (fromCut && mail.from) {\n\t\t\t\tmail.from.truncated = true;\n\t\t\t}\n\n\t\t\treturn {mail, error: reason};\n\t\t} catch (error) {\n\t\t\tconst limit = error.code === 'EMAXLEN' && [TOO_MANY_PARTS, HEADER_TOO_LARGE].includes(error.message) ? error.message : null;\n\t\t\tlet shorter = input;\n\t\t\tif (limit === TOO_MANY_PARTS) {\n\t\t\t\tshorter = fewerParts(input);\n\t\t\t} else if (limit === HEADER_TOO_LARGE) {\n\t\t\t\tconst smaller = smallerHeader(input);\n\t\t\t\tshorter = smaller.raw.length < input.length ? smaller.raw : upToLine(input, MAX_PREFIX_BYTES);\n\t\t\t\tfromCut ||= smaller.raw === shorter && smaller.fromCut;\n\t\t\t}\n\n\t\t\t// (each try reads less, so this ends)\n\t\t\tif (shorter.length >= input.length) {\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\treason ??= limit;\n\t\t\tinput = shorter;\n\t\t}\n\t}\n}\n\n/**\n * A message's attachments, with those of the messages attached to it (as a\n * forwarded message is), named after the message that carries them:\n * \"fw.eml/invoice.exe\". An attached message that cannot be read (beyond the\n * parser's limits, or failing it) never fails the scan: why goes to `errors`.\n * @param {Array<object>} attachments - mailparser attachments\n * @param {string[]} [errors] - gets why attached messages were not read whole\n * @param {(raw: Buffer) => Promise<object>} [parse] - for tests\n * @returns {Promise<Array<object>>}\n */\nexport async function expandAttachments(attachments = [], errors = [], parse = parseMessage) {\n\tconst all = [];\n\t// (one time limit for all of them)\n\tconst deadline = Date.now() + PARSE_TIMEOUT;\n\tlet budget = MAX_NESTED_BYTES;\n\tlet nested = 0;\n\tconst visit = async (list, prefix, depth) => {\n\t\tfor (const attachment of list) {\n\t\t\tif (prefix) {\n\t\t\t\tif (nested >= MAX_ATTACHMENTS) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tnested++;\n\t\t\t}\n\n\t\t\tconst name = attachment.filename || (prefix ? 'unnamed attachment' : '');\n\t\t\tall.push(prefix ? {...attachment, filename: `${prefix}/${name}`} : attachment);\n\t\t\tconst {content} = attachment;\n\t\t\tconst attached = attachment.contentType === 'message/rfc822' || /\\.eml$/i.test(attachment.filename || '');\n\t\t\tif (attached && depth < MAX_NESTED_DEPTH && Buffer.isBuffer(content) && content.length <= budget) {\n\t\t\t\tbudget -= content.length;\n\t\t\t\tlet inner;\n\t\t\t\ttry {\n\t\t\t\t\t// eslint-disable-next-line no-await-in-loop\n\t\t\t\t\tinner = await parseWithin(content, parse, deadline);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// (7.0.3 did not open attached messages, and one never failed it)\n\t\t\t\t\terrors.push(`${name || 'attached message'}: ${error.message}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (inner.error) {\n\t\t\t\t\terrors.push(inner.error);\n\t\t\t\t}\n\n\t\t\t\t// eslint-disable-next-line no-await-in-loop\n\t\t\t\tawait visit(inner.mail.attachments, prefix ? `${prefix}/${name}` : (name || 'attached message'), depth + 1);\n\t\t\t}\n\t\t}\n\t};\n\n\tawait visit(attachments, '', 0);\n\treturn all;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {Buffer} from 'node:buffer';\nimport {randomBytes} from 'node:crypto';\nimport {isIP} from 'node:net';\nimport {simpleParse} from './parse.js';\n\n/**\n * Feedback types from RFC 5965, RFC 6591 (auth-failure) and RFC 7489 (dmarc).\n */\nexport const VALID_FEEDBACK_TYPES = new Set(['abuse', 'fraud', 'virus', 'other', 'not-spam', 'auth-failure', 'dmarc']);\n\n// Fields that may appear more than once (RFC 5965 section 3.2).\nconst MULTIPLE = new Set(['original_rcpt_to', 'authentication_results', 'reported_domain', 'reported_uri']);\n\n/**\n * Parse the fields of a message/feedback-report part. Folded lines are\n * unfolded. Fields that may repeat are arrays; for other fields the first\n * occurrence wins.\n * @param {string} content\n * @returns {Record<string, string|string[]>}\n */\nexport function parseReportFields(content) {\n\tconst fields = {};\n\tconst unfolded = String(content).replaceAll(/\\r?\\n[ \\t]+/g, ' ');\n\tfor (const line of unfolded.split(/\\r?\\n/)) {\n\t\t// (no \"$\": a line with a line break of its own fails, as before,\n\t\t// without \"\\s*\" and \".*\" trying every split of it)\n\t\tconst match = /^([\\w-]+):(\\s*)(.*)/.exec(line);\n\t\tif (!match || match[0].length !== line.length) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst name = match[1].toLowerCase().replaceAll('-', '_');\n\t\tconst value = match[3].trim();\n\t\tif (MULTIPLE.has(name)) {\n\t\t\t(fields[name] ||= []).push(value);\n\t\t} else if (!(name in fields)) {\n\t\t\tfields[name] = value;\n\t\t}\n\t}\n\n\treturn fields;\n}\n\nfunction extractEmail(value) {\n\tif (!value) {\n\t\treturn null;\n\t}\n\n\t// (an address is short: what follows the first 1024 characters is not read)\n\tconst head = value.slice(0, 1024);\n\tconst angle = /<([^<>]*)>/.exec(head);\n\tif (angle) {\n\t\treturn angle[1].trim() || null;\n\t}\n\n\tconst plain = /[^\\s@<>]{1,256}@[^\\s@<>]+/.exec(head);\n\treturn plain ? plain[0] : head.trim();\n}\n\nfunction parseIp(value) {\n\tif (!value) {\n\t\treturn null;\n\t}\n\n\tconst candidate = value.trim().replaceAll(/^\\[|]$/g, '');\n\treturn isIP(candidate) ? candidate : null;\n}\n\nfunction parseDate(value) {\n\tif (!value) {\n\t\treturn null;\n\t}\n\n\tconst date = new Date(value);\n\treturn Number.isNaN(date.getTime()) ? null : date;\n}\n\nfunction parseReportingMta(value) {\n\tif (!value) {\n\t\treturn null;\n\t}\n\n\tconst trimmed = value.trim();\n\tconst match = /^([\\w-]+);\\s*(.+)/.exec(trimmed);\n\treturn match && match[0].length === trimmed.length ? {type: match[1].toLowerCase(), name: match[2].trim()} : {type: 'unknown', name: value.trim()};\n}\n\n// (written into a Buffer: an array of numbers would take many times the\n// memory of a large original message)\nfunction decodeQuotedPrintable(text) {\n\tconst soft = text.replaceAll(/=\\r?\\n/g, '');\n\tconst bytes = Buffer.allocUnsafe(soft.length);\n\tlet length = 0;\n\tfor (let i = 0; i < soft.length; i++) {\n\t\tif (soft[i] === '=' && /^[\\da-f]{2}$/i.test(soft.slice(i + 1, i + 3))) {\n\t\t\tbytes[length++] = Number.parseInt(soft.slice(i + 1, i + 3), 16);\n\t\t\ti += 2;\n\t\t} else {\n\t\t\tbytes[length++] = soft.codePointAt(i) % 256;\n\t\t}\n\t}\n\n\treturn bytes.subarray(0, length);\n}\n\n/**\n * Find a top-level MIME part by content type in a raw multipart message,\n * splitting only at boundary lines. mailparser merges inline message/rfc822\n * parts into the text, so the original message of a report is read here.\n * @param {string} raw\n * @param {string} boundary\n * @param {string[]} types\n * @returns {{type: string, content: Buffer}|null}\n */\nexport function findPart(raw, boundary, types) {\n\tconst escaped = boundary.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\n\tconst parts = raw.split(new RegExp(String.raw`\\r?\\n--${escaped}(?:--)?[ \\t]*(?=\\r?\\n|$)`));\n\tfor (const part of parts.slice(1)) {\n\t\t// A part is a header block (possibly empty), a blank line and a body.\n\t\tconst match = /^\\r?\\n((?:[^\\r\\n]+\\r?\\n)*)\\r?\\n([\\s\\S]*)$/.exec(part);\n\t\tif (!match) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst headers = match[1].replaceAll(/\\r?\\n[ \\t]+/g, ' ');\n\t\tconst type = (/^content-type:\\s*([^;\\s]+)/im.exec(headers)?.[1] || 'text/plain').toLowerCase();\n\t\tif (!types.includes(type)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst encoding = (/^content-transfer-encoding:\\s*(\\S+)/im.exec(headers)?.[1] || '').toLowerCase();\n\t\tlet content;\n\t\tif (encoding === 'base64') {\n\t\t\tcontent = Buffer.from(match[2], 'base64');\n\t\t} else if (encoding === 'quoted-printable') {\n\t\t\tcontent = decodeQuotedPrintable(match[2]);\n\t\t} else {\n\t\t\tcontent = Buffer.from(match[2], 'latin1');\n\t\t}\n\n\t\treturn {type, content};\n\t}\n\n\treturn null;\n}\n\n/**\n * Whether a message parsed by mailparser is an ARF report\n * (multipart/report; report-type=feedback-report).\n * @param {object} parsed\n * @returns {boolean}\n */\nexport function isArfMessage(parsed) {\n\tconst header = typeof parsed?.headers?.get === 'function' ? parsed.headers.get('content-type') : null;\n\tif (!header) {\n\t\treturn false;\n\t}\n\n\tconst value = typeof header === 'string' ? header : header.value;\n\tconst type = typeof header === 'string' ? (/report-type\\s*=\\s*\"?([\\w-]+)/i.exec(header)?.[1] || '') : (header.params?.['report-type'] || '');\n\treturn /^multipart\\/report\\b/i.test(String(value)) && type.toLowerCase() === 'feedback-report';\n}\n\n/**\n * Parse an ARF (RFC 5965) abuse report.\n * @param {Buffer|Uint8Array|string} source - the raw report message\n * @returns {Promise<object>} feedbackType, userAgent, version, arrivalDate,\n * sourceIp, originalMailFrom, originalRcptTo, reportingMta,\n * originalEnvelopeId, authenticationResults, reportedDomain, reportedUri,\n * incidents, humanReadable, originalMessage, originalHeaders (an object of\n * lowercase header name to value, or null when the report has no original\n * message) and rawFeedbackReport\n * @throws {Error} when the message is not a valid ARF report\n */\nexport async function parse(message) {\n\t// (a Uint8Array as the bytes it holds, as a Buffer)\n\tconst source = message instanceof Uint8Array && !Buffer.isBuffer(message) ? Buffer.from(message.buffer, message.byteOffset, message.byteLength) : message;\n\tconst parsed = await simpleParse(source);\n\tif (!isArfMessage(parsed)) {\n\t\tthrow new Error('Not an ARF report: expected multipart/report with report-type=feedback-report');\n\t}\n\n\tconst report = parsed.attachments.find(part => String(part.contentType).toLowerCase() === 'message/feedback-report');\n\tif (!report) {\n\t\tthrow new Error('Invalid ARF report: missing the message/feedback-report part');\n\t}\n\n\tconst raw = report.content.toString('utf8');\n\tconst fields = parseReportFields(raw);\n\tif (!fields.feedback_type) {\n\t\tthrow new Error('Invalid ARF report: missing the Feedback-Type field');\n\t}\n\n\tif (!fields.user_agent) {\n\t\tthrow new Error('Invalid ARF report: missing the User-Agent field');\n\t}\n\n\tlet feedbackType = fields.feedback_type.toLowerCase();\n\tlet feedbackTypeOriginal = null;\n\tif (!VALID_FEEDBACK_TYPES.has(feedbackType)) {\n\t\tfeedbackTypeOriginal = feedbackType;\n\t\tfeedbackType = 'other';\n\t}\n\n\tconst incidents = Number.parseInt(fields.incidents, 10);\n\tconst result = {\n\t\tisArf: true,\n\t\tfeedbackType,\n\t\tfeedbackTypeOriginal,\n\t\tuserAgent: fields.user_agent,\n\t\tversion: fields.version || '1',\n\t\tarrivalDate: parseDate(fields.arrival_date || fields.received_date),\n\t\tsourceIp: parseIp(fields.source_ip),\n\t\toriginalMailFrom: extractEmail(fields.original_mail_from),\n\t\toriginalRcptTo: fields.original_rcpt_to ? fields.original_rcpt_to.map(value => extractEmail(value)).filter(Boolean) : [],\n\t\treportingMta: parseReportingMta(fields.reporting_mta),\n\t\toriginalEnvelopeId: fields.original_envelope_id || null,\n\t\tauthenticationResults: fields.authentication_results || [],\n\t\treportedDomain: fields.reported_domain || [],\n\t\treportedUri: fields.reported_uri || [],\n\t\tincidents: Number.isFinite(incidents) && incidents > 0 ? incidents : 1,\n\t\thumanReadable: null,\n\t\toriginalMessage: null,\n\t\toriginalHeaders: null,\n\t\trawFeedbackReport: raw,\n\t};\n\n\tconst text = (Buffer.isBuffer(source) ? source : Buffer.from(String(source))).toString('latin1');\n\tconst {boundary} = parsed.headers.get('content-type').params;\n\tconst human = findPart(text, boundary, ['text/plain']);\n\tresult.humanReadable = human ? human.content.toString('utf8').trim() || null : null;\n\tconst original = findPart(text, boundary, ['message/rfc822', 'text/rfc822-headers', 'message/rfc822-headers']);\n\tif (original) {\n\t\tconst content = original.content.toString('utf8');\n\t\tresult.originalMessage = content;\n\t\tconst inner = await simpleParse(original.type === 'message/rfc822' ? content : `${content.trimEnd()}\\r\\n\\r\\n`);\n\t\tresult.originalHeaders = Object.fromEntries([...inner.headers].map(([key, value]) => [key, value]));\n\t}\n\n\treturn result;\n}\n\n/**\n * Parse an ARF report, or return null when the message is not one.\n * @param {Buffer|Uint8Array|string} source\n * @returns {Promise<object|null>}\n */\nexport async function tryParse(source) {\n\ttry {\n\t\treturn await parse(source);\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction clean(value, name) {\n\tconst text = String(value);\n\tif (/[\\r\\n]/.test(text)) {\n\t\tthrow new TypeError(`${name} must not contain line breaks`);\n\t}\n\n\treturn text;\n}\n\n/**\n * Write an ARF report. (A string message gives a string report, and a\n * Buffer a Buffer, its bytes kept as they are, whatever their character set.)\n * @template {string|Buffer} M\n * @param {object} options\n * @param {string} options.feedbackType - abuse, fraud, virus, other, not-spam, ...\n * @param {string} options.userAgent\n * @param {string} options.from\n * @param {string} options.to\n * @param {M} options.originalMessage\n * @param {string} [options.humanReadable]\n * @param {string} [options.subject]\n * @param {string} [options.sourceIp]\n * @param {string} [options.originalMailFrom]\n * @param {string|string[]} [options.originalRcptTo]\n * @param {Date|string|number} [options.arrivalDate]\n * @param {string} [options.reportingMta]\n * @returns {M extends Buffer ? Buffer : string} the report\n */\nexport function create(options = {}) {\n\tconst {\n\t\tfeedbackType, userAgent, from, to, originalMessage, humanReadable = 'This is an abuse report for a message received from your network.', subject = 'Abuse report', sourceIp, originalMailFrom, originalRcptTo = [], arrivalDate, reportingMta,\n\t} = options;\n\tif (!feedbackType || !userAgent || !from || !to || !originalMessage) {\n\t\tthrow new TypeError('An ARF report needs feedbackType, userAgent, from, to and originalMessage');\n\t}\n\n\tconst boundary = `arf-${randomBytes(12).toString('hex')}`;\n\tconst report = [\n\t\t`Feedback-Type: ${clean(feedbackType, 'feedbackType')}`,\n\t\t`User-Agent: ${clean(userAgent, 'userAgent')}`,\n\t\t'Version: 1',\n\t];\n\tif (sourceIp) {\n\t\treport.push(`Source-IP: ${clean(sourceIp, 'sourceIp')}`);\n\t}\n\n\tif (originalMailFrom) {\n\t\treport.push(`Original-Mail-From: <${clean(originalMailFrom, 'originalMailFrom')}>`);\n\t}\n\n\tfor (const rcpt of [originalRcptTo].flat()) {\n\t\treport.push(`Original-Rcpt-To: <${clean(rcpt, 'originalRcptTo')}>`);\n\t}\n\n\tif (arrivalDate) {\n\t\tconst date = new Date(arrivalDate);\n\t\tif (Number.isNaN(date.getTime())) {\n\t\t\tthrow new TypeError('arrivalDate must be a date');\n\t\t}\n\n\t\treport.push(`Arrival-Date: ${date.toUTCString()}`);\n\t}\n\n\tif (reportingMta) {\n\t\treport.push(`Reporting-MTA: dns; ${clean(reportingMta, 'reportingMta')}`);\n\t}\n\n\tconst head = [\n\t\t`From: ${clean(from, 'from')}`,\n\t\t`To: ${clean(to, 'to')}`,\n\t\t`Date: ${new Date().toUTCString()}`,\n\t\t`Subject: ${encodeWords(clean(subject, 'subject'))}`,\n\t\t'MIME-Version: 1.0',\n\t\t`Content-Type: multipart/report; report-type=feedback-report; boundary=\"${boundary}\"`,\n\t\t'',\n\t\t`--${boundary}`,\n\t\t'Content-Type: text/plain; charset=utf-8',\n\t\t'Content-Transfer-Encoding: 8bit',\n\t\t'',\n\t\thumanReadable,\n\t\t'',\n\t\t`--${boundary}`,\n\t\t'Content-Type: message/feedback-report',\n\t\t'',\n\t\t...report,\n\t\t'',\n\t\t`--${boundary}`,\n\t\t'Content-Type: message/rfc822',\n\t\t'Content-Disposition: inline',\n\t\t'',\n\t\t'',\n\t].join('\\r\\n');\n\tconst tail = `\\r\\n--${boundary}--\\r\\n`;\n\tif (Buffer.isBuffer(originalMessage)) {\n\t\tconst end = originalMessage.at(-1) === 0x0A ? (originalMessage.at(-2) === 0x0D ? 2 : 1) : 0;\n\t\treturn Buffer.concat([Buffer.from(head), originalMessage.subarray(0, originalMessage.length - end), Buffer.from(tail)]);\n\t}\n\n\treturn head + String(originalMessage).replace(/\\r?\\n$/, '') + tail;\n}\n\n// A header value in RFC 2047 encoded words when it is not plain ASCII, in\n// words of at most 45 bytes (whole characters each), so lines stay short\nfunction encodeWords(text) {\n\tif (/^[\\u0020-\\u007E]*$/.test(text)) {\n\t\treturn text;\n\t}\n\n\tconst words = [];\n\tlet word = '';\n\tfor (const character of text) {\n\t\tif (Buffer.byteLength(word + character) > 45) {\n\t\t\twords.push(word);\n\t\t\tword = '';\n\t\t}\n\n\t\tword += character;\n\t}\n\n\twords.push(word);\n\treturn words.map(part => `=?UTF-8?B?${Buffer.from(part).toString('base64')}?=`).join('\\r\\n ');\n}\n\n/**\n * The ARF parser as an object, as in earlier versions.\n */\nexport const ArfParser = {\n\tisArfMessage, parse, tryParse, create,\n};\n\nexport default ArfParser;\n", "import {Buffer} from 'node:buffer';\nimport {createHash} from 'node:crypto';\nimport {Readable} from 'node:stream';\nimport {MailParser, simpleParser} from 'mailparser';\n\n// Address fields, which mailparser (of every MIME part's header) and mailauth\n// (From and Return-Path of the message's header) read with nodemailer's\n// addressparser: its time grows with the square of the number of addresses\n// in a field, and with the length of the field times the number of groups\n// nested in it (\"g:g:g:...\"), so a field of a megabyte took seconds, and\n// minutes in mailauth. Each is read up to this many characters, this many\n// \",\" and \";\" and this many \":\" in a group, and a message's (each attached\n// message's) fields up to this many characters in all, always to the end of\n// a whole address: what is cut is never part of an address, so no other\n// address than one the field gives is read. A field cut gets `truncated`\n// (mail.from.truncated), and a From field cut gives no credit for the\n// sender's authentication (see trustedReputationValues and authenticate).\nconst ADDRESS_FIELDS = new Set(['from', 'to', 'cc', 'bcc', 'sender', 'reply-to', 'delivered-to', 'return-path', 'disposition-notification-to']);\nconst MAX_ADDRESS_FIELD = 16 * 1024;\nconst MAX_ADDRESS_SEPARATORS = 2000;\nconst MAX_ADDRESS_GROUPS = 16;\nconst MAX_ADDRESS_TOTAL = 64 * 1024;\n// (the address fields mailauth reads with addressparser)\nconst AUTH_ADDRESS_FIELDS = new Set(['from', 'return-path']);\n// (the characters addressparser reads as operators, with the one that ends\n// each: in a group, up to its \";\", everything else is text)\nconst ADDRESS_OPERATORS = new Map([['\"', '\"'], ['(', ')'], ['<', '>'], [',', ''], [':', ';'], [';', '']]);\n// (the fields mailparser reads one value of, the priority of any of four)\nconst SINGLE_FIELDS = new Set(['message-id', 'content-id', 'from', 'sender', 'in-reply-to', 'reply-to', 'subject', 'date', 'content-disposition', 'content-type', 'content-transfer-encoding', 'priority', 'mime-version', 'content-description', 'precedence', 'errors-to', 'disposition-notification-to']);\nconst PRIORITY_FIELDS = new Set(['priority', 'x-priority', 'x-msmail-priority', 'importance']);\nconst BOUNDED = Symbol('bounded address fields');\nconst ROOM = Symbol('room left for address fields');\n\n// (no text made from HTML by mailparser: html-to-text takes quadratic time on\n// deeply nested HTML, and fails on HTML nested a few thousand levels deep;\n// getFeatures reads the text HTML shows instead)\nconst PARSER_OPTIONS = {\n\tskipHtmlToText: true, skipImageLinks: true, skipTextToHtml: true, skipTextLinks: true, [BOUNDED]: true,\n};\n\n// A header field \"Name: value\" (folded or not) up to the end of the last\n// whole address within the limits, and at most `room` characters (but its\n// name), as addressparser reads it: an address ends at a \",\" or \";\" outside\n// quotes, a comment, angle brackets and a group, or at the \";\" ending a\n// group. The field itself when it is within them.\nfunction boundAddressField(line, room) {\n\tconst start = line.indexOf(':') + 1;\n\tconst limit = Math.min(line.length, MAX_ADDRESS_FIELD, room);\n\tlet end = start;\n\tlet expecting = '';\n\tlet escaped = false;\n\tlet separators = 0;\n\tlet groups = 0;\n\tlet index = start;\n\tfor (; index < limit; index++) {\n\t\tconst chr = line[index];\n\t\tif ((chr === ',' || chr === ';') && ++separators > MAX_ADDRESS_SEPARATORS) {\n\t\t\tbreak;\n\t\t}\n\n\t\tif (escaped) {\n\t\t\tescaped = false;\n\t\t} else if (expecting !== '' && chr === expecting) {\n\t\t\texpecting = '';\n\t\t\tend = chr === ';' ? index + 1 : end;\n\t\t} else if (expecting === '' && ADDRESS_OPERATORS.has(chr)) {\n\t\t\texpecting = ADDRESS_OPERATORS.get(chr);\n\t\t\tend = chr === ',' || chr === ';' ? index : end;\n\t\t} else if (expecting === '\"' && chr === '\\\\') {\n\t\t\tescaped = true;\n\t\t}\n\n\t\t// (a group in a group is read again, and again, for each \":\")\n\t\tif (chr === ':' && expecting === ';' && ++groups > MAX_ADDRESS_GROUPS) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn index === line.length ? line : line.slice(0, end).trimEnd();\n}\n\n// Header fields ({key, line}, key the name in lower case) with their address\n// fields bounded (see boundAddressField), `budget.room` characters of them in\n// all, and the names of the fields cut. `only`: just these fields (by\n// default every address field and List-* field)\nfunction boundAddressLines(lines, budget, only = null) {\n\tconst cut = new Set();\n\tconst bounded = lines.map(entry => {\n\t\tif (only ? !only.has(entry.key) : !ADDRESS_FIELDS.has(entry.key) && !entry.key.startsWith('list-')) {\n\t\t\treturn entry;\n\t\t}\n\n\t\tconst line = boundAddressField(entry.line, budget.room);\n\t\tbudget.room = Math.max(budget.room - line.length, 0);\n\t\tif (line === entry.line) {\n\t\t\treturn entry;\n\t\t}\n\n\t\tcut.add(entry.key);\n\t\treturn {...entry, line};\n\t});\n\treturn {lines: bounded, cut};\n}\n\n// Mailparser reads every header (the message's and each part's) with\n// processHeaders: for a parser of parseMessage (only: others are given what\n// they always were), with address fields bounded, a budget for each message,\n// and those cut marked `truncated`. (simpleParser makes its own MailParser,\n// so this is how its headers are reached.)\nconst {processHeaders} = MailParser.prototype;\nMailParser.prototype.processHeaders = function (lines) {\n\tif (!this.options[BOUNDED]) {\n\t\treturn processHeaders.call(this, lines);\n\t}\n\n\tthis[ROOM] ??= {room: MAX_ADDRESS_TOTAL};\n\tconst bounded = boundAddressLines(lines, this[ROOM]);\n\tconst headers = processHeaders.call(this, bounded.lines);\n\t// (of a field given more than once that is read once, the last with a\n\t// value, as mailparser read it before 3.9.40, which reads the first)\n\tconst keyOf = entry => PRIORITY_FIELDS.has(entry.key) ? 'priority' : entry.key;\n\tconst counts = new Map();\n\tfor (const key of bounded.lines.map(entry => keyOf(entry)).filter(key => SINGLE_FIELDS.has(key))) {\n\t\tcounts.set(key, (counts.get(key) || 0) + 1);\n\t}\n\n\tfor (const key of [...counts.keys()].filter(key => counts.get(key) > 1)) {\n\t\tconst last = bounded.lines.findLast(entry => keyOf(entry) === key && processHeaders.call(this, [entry]).has(key));\n\t\tif (last) {\n\t\t\theaders.set(key, processHeaders.call(this, [last]).get(key));\n\t\t}\n\t}\n\n\t// (List-* fields are read into \"list\")\n\tfor (const value of [...bounded.cut].flatMap(key => headers.get(key) ?? [])) {\n\t\tvalue.truncated = true;\n\t}\n\n\treturn headers;\n};\n\n/**\n * The message as mailauth is to read it: its address fields bounded as\n * parseMessage bounds them (mailauth reads From and Return-Path with the same\n * parser), and, when the header would take mailauth longer than linear time\n * (it joins folded lines, and finds the fields a DKIM signature names, in\n * time that grows with the square of their number), the header cut down:\n * its folded lines unfolded (which relaxed canonicalization of DKIM does\n * too) and only the fields that say who sent the message and how, as many\n * as smallerHeader keeps, each up to MAX_FIELD characters (address fields as\n * bounded), and at most MAX_SIGNATURES signatures. Only the first\n * MAX_DKIM_SIGNATURES DKIM-Signature fields are ever kept (with their folded\n * lines; the body is untouched). `fromCut` is whether a\n * From field was cut or left out: then mailauth did not read the From\n * address the message gives.\n * @param {Buffer} raw\n * @returns {{raw: Buffer, fromCut: boolean}}\n */\nexport function boundedForAuth(raw) {\n\t// (where mailauth finds the end of the header, and its fields: a line\n\t// starting with white space continues the one before, and the name is\n\t// up to the first \":\")\n\tconst ends = [raw.indexOf('\\n\\n'), raw.indexOf('\\n\\r\\n')].filter(index => index !== -1);\n\tconst end = ends.length > 0 ? Math.min(...ends) + 1 : raw.length;\n\tconst body = raw.subarray(ends.length > 0 ? raw.indexOf(0x0A, end) + 1 : raw.length);\n\tconst text = raw.subarray(0, end).toString('latin1');\n\tlet {length} = text;\n\twhile (length > 0 && (text[length - 1] === '\\n' || text[length - 1] === '\\r')) {\n\t\tlength--;\n\t}\n\n\tconst fields = [];\n\tlet folds = 0;\n\tlet work = 0;\n\tfor (let start = 0; start <= length;) {\n\t\tlet stop = text.indexOf('\\n', start);\n\t\tstop = stop === -1 || stop > length ? length : stop;\n\t\tconst lineEnd = stop > start && text[stop - 1] === '\\r' ? stop - 1 : stop;\n\t\tconst last = fields.at(-1);\n\t\tif (last && /\\s/.test(text[start])) {\n\t\t\tlast.end = lineEnd;\n\t\t\tlast.folds++;\n\t\t\tfolds++;\n\t\t\t// (each folded line is moved past the fields after it, and joined\n\t\t\t// to the lines after it of its field)\n\t\t\twork += last.folds;\n\t\t} else {\n\t\t\tfields.push({start, end: lineEnd, folds: 0});\n\t\t}\n\n\t\tstart = stop + 1;\n\t}\n\n\tlet signatures = 0;\n\tlet dkim = 0;\n\tfor (const field of fields) {\n\t\tfield.line = text.slice(field.start, field.end);\n\t\tconst colon = field.line.indexOf(':');\n\t\tfield.key = (colon === -1 ? field.line : field.line.slice(0, colon)).trim().toLowerCase();\n\t\tfield.dropped = field.key === 'dkim-signature' && ++dkim > MAX_DKIM_SIGNATURES;\n\t\tif (SIGNATURE_FIELDS.has(field.key) && !field.dropped) {\n\t\t\t// (each field a signature names is looked for among all the fields)\n\t\t\tsignatures += field.line.split(':').length - 1;\n\t\t}\n\t}\n\n\tconst read = dkim > MAX_DKIM_SIGNATURES ? fields.filter(field => !field.dropped) : fields;\n\twork += (folds + signatures) * read.length;\n\t// (only From and Return-Path, the address fields mailauth reads: others,\n\t// To and Cc among them, are often signed, and cutting them would fail a\n\t// signature that holds)\n\tconst bounded = boundAddressLines(read, {room: MAX_ADDRESS_TOTAL}, AUTH_ADDRESS_FIELDS);\n\tlet kept = bounded.lines;\n\tlet fromCut = bounded.cut.has('from');\n\tif (work > MAX_AUTH_WORK) {\n\t\tconst counts = new Map();\n\t\tkept = kept.filter(field => {\n\t\t\tcounts.set(field.key, (counts.get(field.key) || 0) + 1);\n\t\t\tconst keep = AUTH_FIELDS.has(field.key) && counts.get(field.key) <= (SIGNATURE_FIELDS.has(field.key) ? MAX_SIGNATURES : MAX_REPEATED);\n\t\t\tfromCut ||= field.key === 'from' && !keep;\n\t\t\treturn keep;\n\t\t}).map(field => {\n\t\t\tconst line = field.line.replaceAll(/\\r?\\n(?=\\s)/g, '');\n\t\t\treturn {...field, line: ADDRESS_FIELDS.has(field.key) ? line : line.slice(0, MAX_FIELD)};\n\t\t});\n\t} else if (bounded.cut.size === 0 && read === fields) {\n\t\treturn {raw, fromCut};\n\t}\n\n\tconst header = kept.map(field => field.line).join('\\r\\n');\n\treturn {raw: Buffer.concat([Buffer.from(`${header}\\r\\n\\r\\n`, 'latin1'), body]), fromCut};\n}\n\n// The limits of mailparser (of mailsplit, which it splits messages with) that\n// make it fail: more than 1000 MIME parts (an inline attached message is a\n// part too), and a part's header over 1 MB.\nconst TOO_MANY_PARTS = 'Max allowed child nodes exceeded';\nconst HEADER_TOO_LARGE = 'Max header size for a MIME node exceeded';\n// A message beyond them is read up to this many lines that may start a part\n// (\"--boundary\"), and this many bytes of it if a part's header is too large.\nconst MAX_PART_LINES = 990;\nconst MAX_PREFIX_BYTES = 1_000_000;\n// Of a message header too large, each line is read up to this many\n// characters, each field up to this many, and if the whole is still too\n// large, only the fields that say who sent the message and how.\nconst MAX_LINE = 998;\nconst MAX_FIELD = 8192;\nconst MAX_HEADER = 512 * 1024;\nconst MAX_REPEATED = 100;\nconst KEPT_FIELDS = new Set([\n\t'from', 'sender', 'reply-to', 'to', 'cc', 'subject', 'date', 'message-id', 'return-path', 'received', 'authentication-results', 'arc-authentication-results', 'dkim-signature', 'mime-version', 'content-type', 'content-transfer-encoding', 'list-unsubscribe', 'list-id',\n]);\n// Of a header that would take mailauth too long (more than this much work,\n// in its steps: see boundedForAuth), it reads these fields, and at most this\n// many signatures.\nconst MAX_AUTH_WORK = 10_000_000;\nconst SIGNATURE_FIELDS = new Set(['dkim-signature', 'arc-message-signature']);\nconst AUTH_FIELDS = new Set([...KEPT_FIELDS, 'arc-seal', 'arc-message-signature']);\nconst MAX_SIGNATURES = 10;\n/**\n * DKIM-Signature fields mailauth reads of a message (see boundedForAuth): it\n * checks every signature, hashing the body once for each body length (l=)\n * and canonicalization and looking up each signer's key, so hundreds of them\n * would hold up the process and send as many DNS queries. Real mail carries\n * one to three.\n */\nexport const MAX_DKIM_SIGNATURES = 8;\n\n/**\n * Whether an attachment is an attached message: message/rfc822, or\n * message/global (RFC 6532, the same with UTF-8 in its header), by type or\n * by file name (.eml, .u8msg).\n * @param {{contentType?: string, filename?: string}} attachment - as mailparser gives it\n * @returns {boolean}\n */\nexport function isAttachedMessage(attachment) {\n\tconst type = String(attachment.contentType).toLowerCase();\n\treturn type === 'message/rfc822' || type === 'message/global' || /\\.(?:eml|u8msg)$/i.test(String(attachment.filename));\n}\n\n// Attached messages (see isAttachedMessage) are opened this many levels deep,\n// for at most this many attachments of theirs and this many bytes of attached\n// messages (the message's own attachments are always all read).\nconst MAX_NESTED_DEPTH = 3;\nconst MAX_ATTACHMENTS = 100;\nconst MAX_NESTED_BYTES = 25 * 1024 * 1024;\n// (and at most this many attached messages opened in all, at every level:\n// each is parsed on its own, with its own MIME part limit, so many small\n// ones of many parts each would take seconds each)\nconst MAX_ATTACHED_MESSAGES = 100;\n\n// Milliseconds a message (with the tries of parseWithin), or all the messages\n// attached to one, may take to parse. mailparser is driven by its own stream\n// events alone, so a parse it never finishes leaves nothing pending: a scan\n// that would never settle, or a process that exits with it unsettled.\nexport const PARSE_TIMEOUT = 60_000;\n\n/**\n * The simpleParser of mailparser on a message it can finish (see finishable),\n * settled within `timeout`: a parse that has not settled by then is stopped\n * (its parser destroyed) and rejected with an ETIMEDOUT error.\n * @param {Buffer|string} raw\n * @param {object} [options]\n * @param {number} [options.timeout] - milliseconds (PARSE_TIMEOUT)\n * @param {typeof simpleParser} [options.parser] - for tests\n * @returns {Promise<object>} the parsed message\n */\nexport async function simpleParse(raw, {timeout = PARSE_TIMEOUT, parser = simpleParser} = {}) {\n\t// (a stream, not the buffer, so a parse that stalls can be stopped:\n\t// simpleParser destroys its parser when its input fails; the input is\n\t// not destroyed when it ends, so it can still fail)\n\tconst input = Readable.from([finishable(unfoldHeaders(raw))], {objectMode: false, autoDestroy: false});\n\tinput.on('error', () => {});\n\t// (and fails it on an error of the splitter that reads its parts, as\n\t// where its limits are met: mailparser before 3.9.30 finishes instead on\n\t// one that comes after the end of its input, as each one does with the\n\t// mailsplit it uses, which splits a line at a time: with what it read so\n\t// far, or never, as a parse that never finishes. Node.js 18 gets 3.9.21\n\t// at most.)\n\tconst {pipe} = input;\n\tinput.pipe = function (parser, options) {\n\t\tparser.splitter?.on('error', error => input.destroy(error));\n\t\treturn pipe.call(this, parser, options);\n\t};\n\n\tlet timer;\n\ttry {\n\t\treturn await Promise.race([\n\t\t\tparser(input, PARSER_OPTIONS),\n\t\t\tnew Promise((resolve, reject) => {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tconst error = Object.assign(new Error(`Message not parsed within ${timeout} ms`), {code: 'ETIMEDOUT'});\n\t\t\t\t\tinput.destroy(error);\n\t\t\t\t\treject(error);\n\t\t\t\t}, timeout);\n\t\t\t}),\n\t\t]);\n\t} finally {\n\t\tclearTimeout(timer);\n\t\tinput.destroy();\n\t}\n}\n\n/**\n * Parse a message with mailparser, which does not make text of HTML here:\n * `text` is that of the plain text parts only (empty for a message of HTML\n * alone), and the text HTML shows is read by getFeatures.\n * @param {Buffer|string} raw\n * @param {{timeout?: number}} [options] - milliseconds for the parse (PARSE_TIMEOUT)\n * @returns {Promise<object>} the parsed message\n */\nexport async function parseMessage(raw, options = {}) {\n\tconst buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n\tconst mail = await simpleParse(buffer, options);\n\t// (without the line breaks that adds to the last part)\n\tconst added = buffer.at(-1) === 0x0A ? 2 : 3;\n\tfor (const key of ['text', 'html']) {\n\t\tif (typeof mail[key] === 'string') {\n\t\t\tmail[key] = mail[key].replace(new RegExp(`\\\\n{0,${added}}$`), '');\n\t\t}\n\t}\n\n\t// (and from an attachment left open at the end, as ClamAV and checksums\n\t// read it: only one whose own bytes end the message, so not one closed\n\t// by a boundary, nor one followed by another part or an epilogue, and\n\t// only one not encoded, whose bytes are those of the message)\n\tconst tail = Buffer.from('\\r\\n'.repeat(added));\n\tconst last = mail.attachments?.at(-1);\n\tconst encoding = String(last?.headers?.get?.('content-transfer-encoding') ?? '7bit').trim().toLowerCase();\n\tconst own = Buffer.isBuffer(last?.content) && last.content.length >= tail.length && last.content.subarray(-tail.length).equals(tail) ? last.content.subarray(0, -tail.length) : null;\n\tif (own && ['7bit', '8bit', 'binary'].includes(encoding) && own.length <= buffer.length && buffer.subarray(buffer.length - own.length).equals(own)) {\n\t\tlast.content = own;\n\t\tlast.size = own.length;\n\t\tlast.checksum = createHash('md5').update(own).digest('hex');\n\t}\n\n\treturn mail;\n}\n\n/**\n * A message mailparser can finish: it never finishes one that ends in the\n * header of an attached message (however that header names or folds its\n * type, or only a file name like \"fw.eml\") without the blank line after it,\n * so every message gets one.\n * @param {Buffer|string} raw\n * @returns {Buffer}\n */\nexport function finishable(raw) {\n\tconst buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);\n\treturn Buffer.concat([buffer, Buffer.from(buffer.at(-1) === 0x0A ? '\\r\\n\\r\\n' : '\\r\\n\\r\\n\\r\\n')]);\n}\n\n// A header field folded on more lines than this is read unfolded: mailsplit\n// before 5.4.18 (of mailparser before 3.9.30, as Node.js 18 gets) joins the\n// lines of a field in time that grows with their number times its length,\n// seconds for a field of a megabyte. Real fields are folded on a few lines; a\n// list of a thousand recipients on a thousand.\nconst MAX_FOLDS = 256;\n\n// The value and parameters of a header field (\"Name: value; key=value\"), as\n// libmime (parseHeaderValue) reads them for mailsplit: `value` lower case.\nfunction headerValue(line) {\n\tconst parameters = new Map();\n\tlet key = null;\n\tlet inKey = false;\n\tlet quoted = false;\n\tlet escaped = false;\n\tlet value = '';\n\tfor (const chr of line.slice(line.indexOf(':') + 1).replaceAll(/\\r?\\n[ \\t]*/g, ' ')) {\n\t\tif (inKey && chr === '=') {\n\t\t\tkey = value.trim().toLowerCase();\n\t\t\tinKey = false;\n\t\t\tvalue = '';\n\t\t} else if (inKey || escaped) {\n\t\t\tvalue += chr;\n\t\t\tescaped = false;\n\t\t} else if (chr === '\\\\') {\n\t\t\tescaped = true;\n\t\t} else if (chr === '\"') {\n\t\t\tquoted = !quoted;\n\t\t} else if (chr === ';' && !quoted) {\n\t\t\tparameters.set(key, value.trim());\n\t\t\tinKey = true;\n\t\t\tvalue = '';\n\t\t} else {\n\t\t\tvalue += chr;\n\t\t}\n\t}\n\n\tif (!inKey) {\n\t\tparameters.set(key, value.trim());\n\t}\n\n\treturn {value: parameters.get(null).toLowerCase(), boundary: parameters.get('boundary') ?? ''};\n}\n\n// Whether the line at raw[start, stop) is a delimiter of `boundary` as\n// mailsplit reads one: 1 \"--boundary\", 2 \"--boundary--\", 0 neither.\nfunction delimiter(raw, start, stop, boundary) {\n\tconst rest = start + 2 + boundary.length;\n\tif (stop - start < boundary.length + 2 || stop - rest > 4 || raw[start] !== 0x2D || raw[start + 1] !== 0x2D || !raw.subarray(start + 2, rest).equals(boundary)) {\n\t\treturn 0;\n\t}\n\n\tif (raw[rest] === 0x0D || raw[rest] === 0x0A) {\n\t\treturn 1;\n\t}\n\n\t// (\"--\", then <CR><LF>, <LF> or nothing)\n\tconst tail = raw.subarray(rest, stop).toString('latin1');\n\treturn /^-{0,2}$|^--\\r?\\n$|^--\\r$/.test(tail) ? 2 : 0;\n}\n\n/**\n * The message with each header field folded on more than MAX_FOLDS lines\n * unfolded: its line breaks and the white space after each made one space,\n * as mailparser reads a field (libmime's decodeHeader), so what it reads is\n * the same. Header fields are those of the message and, as mailsplit finds\n * them, of each MIME part and each attached message inline; everything else\n * is left as it is. The message itself when nothing is unfolded.\n * @param {Buffer} raw\n * @returns {Buffer}\n */\nexport function unfoldHeaders(raw) {\n\tconst boundaries = [];\n\tconst unfold = [];\n\tlet head = true;\n\tlet fields = [];\n\t// (the end of a header: by a blank line, a part follows)\n\tconst endHead = blank => {\n\t\tunfold.push(...fields.filter(field => field.folds > MAX_FOLDS));\n\t\tconst first = new Map();\n\t\tfor (const field of fields) {\n\t\t\tconst name = raw.toString('latin1', field.start, Math.min(field.end, field.start + 32));\n\t\t\tconst key = name.slice(0, name.indexOf(':')).trim().toLowerCase();\n\t\t\tif (['content-type', 'content-transfer-encoding', 'content-disposition'].includes(key) && !first.has(key)) {\n\t\t\t\tfirst.set(key, headerValue(raw.toString('latin1', field.start, field.end)));\n\t\t\t}\n\t\t}\n\n\t\tfields = [];\n\t\tconst type = first.get('content-type') ?? {value: '', boundary: ''};\n\t\tconst encoding = (first.get('content-transfer-encoding')?.value ?? '').replaceAll(/\\(.*\\)/g, '').trim();\n\t\tif (blank && type.boundary) {\n\t\t\tboundaries.push(Buffer.from(type.boundary));\n\t\t}\n\n\t\t// (an attached message inline: its header follows)\n\t\thead = blank && ['message/rfc822', 'message/global'].includes(type.value) && ['', '7bit', '8bit', 'binary'].includes(encoding) && first.get('content-disposition')?.value === 'inline';\n\t};\n\n\tfor (let start = 0; start < raw.length;) {\n\t\tconst lf = raw.indexOf(0x0A, start);\n\t\tconst stop = lf === -1 ? raw.length : lf + 1;\n\t\tconst end = lf === -1 ? stop : (raw[lf - 1] === 0x0D ? lf - 1 : lf);\n\t\tconst kind = boundaries.length > 0 ? delimiter(raw, start, stop, boundaries.at(-1)) : 0;\n\t\tif (kind !== 0) {\n\t\t\tif (head) {\n\t\t\t\tendHead(false);\n\t\t\t}\n\n\t\t\t// (a part, or the end of the parts)\n\t\t\thead = kind === 1;\n\t\t\tif (kind === 2) {\n\t\t\t\tboundaries.pop();\n\t\t\t}\n\t\t} else if (head && end === start) {\n\t\t\tendHead(true);\n\t\t} else if (head && fields.length > 0 && (raw[start] === 0x20 || raw[start] === 0x09)) {\n\t\t\tfields.at(-1).end = end;\n\t\t\tfields.at(-1).folds++;\n\t\t} else if (head) {\n\t\t\tfields.push({start, end, folds: 0});\n\t\t}\n\n\t\tstart = stop;\n\t}\n\n\tunfold.push(...fields.filter(field => field.folds > MAX_FOLDS));\n\tif (unfold.length === 0) {\n\t\treturn raw;\n\t}\n\n\tconst chunks = [];\n\tlet copied = 0;\n\tfor (const field of unfold) {\n\t\tchunks.push(raw.subarray(copied, field.start), Buffer.from(raw.toString('latin1', field.start, field.end).replaceAll(/\\r?\\n[ \\t]*/g, ' '), 'latin1'));\n\t\tcopied = field.end;\n\t}\n\n\tchunks.push(raw.subarray(copied));\n\treturn Buffer.concat(chunks);\n}\n\n// The message up to the start of the line at `offset` or before it.\nfunction upToLine(raw, offset) {\n\treturn raw.subarray(0, Math.max(raw.lastIndexOf(0x0A, offset - 1) + 1, 0));\n}\n\n// The message before its MAX_PART_LINES-th line that starts with \"--\", or,\n// with fewer such lines (attached messages inline are parts without one),\n// its first half.\nfunction fewerParts(raw) {\n\tlet offset = -1;\n\tfor (let count = 0; count < MAX_PART_LINES; count++) {\n\t\toffset = raw.indexOf('\\n--', offset + 1);\n\t\tif (offset === -1) {\n\t\t\treturn upToLine(raw, Math.floor(raw.length / 2));\n\t\t}\n\t}\n\n\treturn raw.subarray(0, offset + 1);\n}\n\n// The message with its header (up to the first blank line) cut down to\n// MAX_LINE characters a line, MAX_FIELD a field, and the KEPT_FIELDS when\n// the whole is over MAX_HEADER, so the sender is read whatever comes before;\n// address fields are cut to whole addresses instead (see boundAddressField).\n// `fromCut` is whether a From field was cut or left out.\nfunction smallerHeader(raw) {\n\tconst text = raw.toString('latin1');\n\tconst blank = /\\r?\\n\\r?\\n/.exec(text);\n\tconst end = blank ? blank.index : text.length;\n\tconst fields = [];\n\tlet start = 0;\n\twhile (start < end) {\n\t\tlet stop = text.indexOf('\\n', start);\n\t\tstop = stop === -1 || stop > end ? end : stop;\n\t\tconst last = fields.at(-1);\n\t\tconst whole = text.slice(start, stop).replace(/\\r$/, '');\n\t\tif (last && (whole.startsWith(' ') || whole.startsWith('\\t'))) {\n\t\t\tconst line = last.address ? whole : whole.slice(0, MAX_LINE);\n\t\t\tif (last.address || last.size + line.length <= MAX_FIELD) {\n\t\t\t\tlast.lines.push(line);\n\t\t\t\tlast.size += line.length;\n\t\t\t}\n\t\t} else {\n\t\t\tconst name = whole.slice(0, whole.indexOf(':')).trim().toLowerCase();\n\t\t\tconst address = ADDRESS_FIELDS.has(name) || name.startsWith('list-');\n\t\t\tconst line = address ? whole : whole.slice(0, MAX_LINE);\n\t\t\tfields.push({\n\t\t\t\tname, address, lines: [line], size: line.length,\n\t\t\t});\n\t\t}\n\n\t\tstart = stop + 1;\n\t}\n\n\tlet fromCut = false;\n\tfor (const field of fields.filter(field => field.address)) {\n\t\tconst line = field.lines.join('\\r\\n');\n\t\tconst bounded = boundAddressField(line, Number.POSITIVE_INFINITY);\n\t\tfromCut ||= field.name === 'from' && bounded !== line;\n\t\tfield.lines = [bounded];\n\t\tfield.size = bounded.length;\n\t}\n\n\tconst total = fields.reduce((sum, field) => sum + field.size, 0);\n\t// (and at most MAX_REPEATED of each, as hundreds of Received fields could\n\t// still be too many)\n\tconst counts = new Map();\n\tconst kept = total > MAX_HEADER\n\t\t? fields.filter(field => {\n\t\t\tcounts.set(field.name, (counts.get(field.name) || 0) + 1);\n\t\t\tconst keep = KEPT_FIELDS.has(field.name) && counts.get(field.name) <= MAX_REPEATED;\n\t\t\tfromCut ||= field.name === 'from' && !keep;\n\t\t\treturn keep;\n\t\t})\n\t\t: fields;\n\tconst header = kept.flatMap(field => field.lines).join('\\r\\n');\n\treturn {raw: Buffer.concat([Buffer.from(`${header}\\r\\n\\r\\n`, 'latin1'), raw.subarray(blank ? blank.index + blank[0].length : raw.length)]), fromCut};\n}\n\n/**\n * Parse a message, reading what can be read of one beyond mailparser's\n * limits (more than 1000 MIME parts, a part's header over 1 MB): its parts\n * before the 990th line that may start one (or its first half, again and\n * again, when attached messages inline make the parts), and, when a header\n * is too large, the message with its header lines cut down, or its first\n * megabyte when the header is that of a later part. `error` is the limit,\n * or null. Other errors are thrown, as earlier versions did (servers defer\n * the message), and so is a parse not finished by `deadline` (ETIMEDOUT).\n * @param {Buffer} raw\n * @param {(raw: Buffer, options: {timeout: number}) => Promise<object>} [parse] - for tests\n * @param {number} [deadline] - Date.now() milliseconds by which every try\n * ends (PARSE_TIMEOUT from now)\n * @returns {Promise<{mail: object, error: string|null}>}\n */\nexport async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PARSE_TIMEOUT) {\n\tlet input = raw;\n\tlet reason = null;\n\tlet fromCut = false;\n\tfor (;;) {\n\t\ttry {\n\t\t\t// eslint-disable-next-line no-await-in-loop\n\t\t\tconst mail = await parse(input, {timeout: Math.max(1, deadline - Date.now())});\n\t\t\t// (a From field cut down is marked as parseMessage marks one)\n\t\t\tif (fromCut && mail.from) {\n\t\t\t\tmail.from.truncated = true;\n\t\t\t}\n\n\t\t\treturn {mail, error: reason};\n\t\t} catch (error) {\n\t\t\tconst limit = error.code === 'EMAXLEN' && [TOO_MANY_PARTS, HEADER_TOO_LARGE].includes(error.message) ? error.message : null;\n\t\t\tlet shorter = input;\n\t\t\tif (limit === TOO_MANY_PARTS) {\n\t\t\t\tshorter = fewerParts(input);\n\t\t\t} else if (limit === HEADER_TOO_LARGE) {\n\t\t\t\tconst smaller = smallerHeader(input);\n\t\t\t\tshorter = smaller.raw.length < input.length ? smaller.raw : upToLine(input, MAX_PREFIX_BYTES);\n\t\t\t\tfromCut ||= smaller.raw === shorter && smaller.fromCut;\n\t\t\t}\n\n\t\t\t// (each try reads less, so this ends)\n\t\t\tif (shorter.length >= input.length) {\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\treason ??= limit;\n\t\t\tinput = shorter;\n\t\t}\n\t}\n}\n\n/**\n * A message's attachments, with those of the messages attached to it (as a\n * forwarded message is), named after the message that carries them:\n * \"fw.eml/invoice.exe\". An attached message that cannot be read (beyond the\n * parser's limits, or failing it) never fails the scan: why goes to `errors`.\n * @param {Array<object>} attachments - mailparser attachments\n * @param {string[]} [errors] - gets why attached messages were not read whole\n * @param {(raw: Buffer) => Promise<object>} [parse] - for tests\n * @returns {Promise<Array<object>>}\n */\n// (`unopened`: the attached messages not opened, past the limits above, as\n// {filename, reason}, whose attachments were not inspected)\nexport async function expandAttachments(attachments = [], errors = [], parse = parseMessage, unopened = []) {\n\tconst all = [];\n\t// (one time limit for all of them)\n\tconst deadline = Date.now() + PARSE_TIMEOUT;\n\tlet budget = MAX_NESTED_BYTES;\n\tlet nested = 0;\n\tlet tooMany = false;\n\tlet opened = 0;\n\tconst visit = async (list, prefix, depth) => {\n\t\tfor (const attachment of list) {\n\t\t\tif (prefix) {\n\t\t\t\tif (nested >= MAX_ATTACHMENTS) {\n\t\t\t\t\t// (once: the rest of them are not listed either)\n\t\t\t\t\tif (!tooMany) {\n\t\t\t\t\t\ttooMany = true;\n\t\t\t\t\t\tunopened.push({filename: prefix, reason: `more than ${MAX_ATTACHMENTS} attachments in attached messages`});\n\t\t\t\t\t}\n\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tnested++;\n\t\t\t}\n\n\t\t\tconst name = attachment.filename || (prefix ? 'unnamed attachment' : '');\n\t\t\tconst filename = prefix ? `${prefix}/${name}` : (name || 'attached message');\n\t\t\tall.push(prefix ? {...attachment, filename: `${prefix}/${name}`} : attachment);\n\t\t\tconst {content} = attachment;\n\t\t\tconst attached = isAttachedMessage(attachment);\n\t\t\tif (attached && Buffer.isBuffer(content) && depth >= MAX_NESTED_DEPTH) {\n\t\t\t\tunopened.push({filename, reason: `nested more than ${MAX_NESTED_DEPTH} deep`});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (attached && Buffer.isBuffer(content) && opened >= MAX_ATTACHED_MESSAGES) {\n\t\t\t\tunopened.push({filename, reason: `more than ${MAX_ATTACHED_MESSAGES} attached messages`});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (attached && Buffer.isBuffer(content) && content.length > budget) {\n\t\t\t\tunopened.push({filename, reason: `past ${MAX_NESTED_BYTES / 1024 / 1024} MB of attached messages`});\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (attached && Buffer.isBuffer(content)) {\n\t\t\t\tbudget -= content.length;\n\t\t\t\topened++;\n\t\t\t\tlet inner;\n\t\t\t\ttry {\n\t\t\t\t\t// eslint-disable-next-line no-await-in-loop\n\t\t\t\t\tinner = await parseWithin(content, parse, deadline);\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// (7.0.3 did not open attached messages, and one never failed it)\n\t\t\t\t\terrors.push(`${name || 'attached message'}: ${error.message}`);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (inner.error) {\n\t\t\t\t\terrors.push(inner.error);\n\t\t\t\t}\n\n\t\t\t\t// eslint-disable-next-line no-await-in-loop\n\t\t\t\tawait visit(inner.mail.attachments, prefix ? `${prefix}/${name}` : (name || 'attached message'), depth + 1);\n\t\t\t}\n\t\t}\n\t};\n\n\tawait visit(attachments, '', 0);\n\treturn all;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["import_node_buffer", "import_node_crypto", "key", "parser"]
7
7
  }
@@ -2527,7 +2527,7 @@ function gatewayTarget(url) {
2527
2527
  }
2528
2528
  if (host === "urldefense.com" && url.pathname.startsWith("/v3/__")) {
2529
2529
  const rest = url.href.slice(url.href.indexOf("/v3/__") + 6);
2530
- const end = rest.indexOf("__;");
2530
+ const end = rest.lastIndexOf("__;");
2531
2531
  return end === -1 ? null : rest.slice(0, end);
2532
2532
  }
2533
2533
  return null;
@@ -2539,8 +2539,11 @@ function unwrapLink(address2) {
2539
2539
  } catch {
2540
2540
  return null;
2541
2541
  }
2542
- for (let depth = 0; depth < 3; depth++) {
2542
+ for (let depth = 0; depth < MAX_GATEWAYS; depth++) {
2543
2543
  const target = gatewayTarget(url);
2544
+ if (!target) {
2545
+ break;
2546
+ }
2544
2547
  let next;
2545
2548
  try {
2546
2549
  next = new URL(target);
@@ -3476,7 +3479,7 @@ ${body}`);
3476
3479
  hidden: visible.hidden
3477
3480
  };
3478
3481
  }
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;
3482
+ 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;
3480
3483
  var init_tokenizer = __esm({
3481
3484
  "src/tokenizer.js"() {
3482
3485
  import_tldts2 = require("tldts");
@@ -3583,6 +3586,7 @@ var init_tokenizer = __esm({
3583
3586
  LINK_FILE_TLDS = /* @__PURE__ */ new Set(["md", "mov", "pm", "ps", "py", "rs", "sh", "so", "zip"]);
3584
3587
  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"];
3585
3588
  isUnder = (host, domain) => host === domain || host.endsWith(`.${domain}`);
3589
+ MAX_GATEWAYS = 16;
3586
3590
  BRAND_NAMES = new Set(BRANDS);
3587
3591
  SHORTENERS = /* @__PURE__ */ new Set(["bit.ly", "tinyurl.com", "goo.gl", "t.co", "ow.ly", "is.gd", "buff.ly", "rebrand.ly", "cutt.ly", "shorturl.at", "tiny.cc", "rb.gy", "bit.do", "v.gd", "s.id", "t.ly", "lnkd.in", "qrco.de", "shorturl.asia", "urlz.fr"]);
3588
3592
  FREEMAIL = /* @__PURE__ */ new Set(["gmail.com", "googlemail.com", "yahoo.com", "ymail.com", "hotmail.com", "outlook.com", "live.com", "msn.com", "aol.com", "icloud.com", "me.com", "mail.com", "gmx.com", "gmx.de", "gmx.net", "web.de", "yandex.ru", "yandex.com", "mail.ru", "qq.com", "163.com", "126.com", "proton.me", "protonmail.com", "zoho.com", "naver.com", "daum.net", "rediffmail.com", "libero.it", "orange.fr", "laposte.net", "wp.pl", "o2.pl", "seznam.cz", "rambler.ru", "tutanota.com", "tuta.com"]);
@@ -3864,7 +3868,7 @@ function inspectAttachments(attachments = []) {
3864
3868
  add("macro", `Attachment "${name}" is an Office document with a VBA macro project`, { extension });
3865
3869
  }
3866
3870
  } else if (detected?.type === "pdf" || extension === "pdf") {
3867
- if (PDF_ACTIVE.test(decodePdfNames(content.toString("latin1")))) {
3871
+ if (pdfActive(content.toString("latin1"))) {
3868
3872
  add("pdf_active", `PDF "${name}" contains JavaScript or a launch action`, { extension: "pdf" });
3869
3873
  }
3870
3874
  } else if (detected?.type === "rtf" && /\\object\b|\\objdata\b|\\objupdate\b/.test(content.toString("latin1"))) {
@@ -3878,7 +3882,7 @@ function inspectAttachments(attachments = []) {
3878
3882
  }
3879
3883
  return findings;
3880
3884
  }
3881
- var import_node_buffer, EXECUTABLE_EXTENSIONS, RISKY_EXTENSIONS, SCRIPT_EXTENSIONS, MACRO_EXTENSIONS, DOCUMENT_EXTENSIONS, TEMPORARY_EXTENSIONS, BIDI_CONTROLS, ARCHIVE_TYPES, ZIP_FAMILY, OFFICE_TYPES, OFFICE_EMBEDDED, ENTRIES, MAX_NAME_LENGTH, MAX_NAME_BYTES, DIRECTORY, isDirectory, VBA_NAMES, PDF_ACTIVE, PDF_ESCAPED_NAME, decodePdfNames;
3885
+ var import_node_buffer, EXECUTABLE_EXTENSIONS, RISKY_EXTENSIONS, SCRIPT_EXTENSIONS, MACRO_EXTENSIONS, DOCUMENT_EXTENSIONS, TEMPORARY_EXTENSIONS, 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;
3882
3886
  var init_attachments = __esm({
3883
3887
  "src/attachments.js"() {
3884
3888
  import_node_buffer = require("node:buffer");
@@ -4040,8 +4044,13 @@ var init_attachments = __esm({
4040
4044
  isDirectory = (buffer, offset) => offset >= 0 && offset + 46 <= buffer.length && buffer.readUInt32LE(offset) === DIRECTORY;
4041
4045
  VBA_NAMES = /* @__PURE__ */ new Set(["vba", "_vba_project", "_vba_project_cur", "macros"]);
4042
4046
  PDF_ACTIVE = linear(String.raw`\/(?:JavaScript|JS|Launch)\b`);
4043
- PDF_ESCAPED_NAME = /\/[^\s/<>[\]()%{}]*#[\dA-Fa-f]{2}[^\s/<>[\]()%{}]*/g;
4044
- decodePdfNames = (text) => text.replaceAll(PDF_ESCAPED_NAME, (name) => name.replaceAll(/#([\dA-Fa-f]{2})/g, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))));
4047
+ escaped = (character) => {
4048
+ const hex = character.codePointAt(0).toString(16);
4049
+ return `(?:${character}|#${hex[0]}[${hex[1].toLowerCase()}${hex[1].toUpperCase()}])`;
4050
+ };
4051
+ WORD_ESCAPE = String.raw`#(?:3\d|4[1-9a-fA-F]|5[\daA]|5[fF]|6[1-9a-fA-F]|7[\daA])`;
4052
+ PDF_ACTIVE_ESCAPED = new RegExp(`/(?:${["JavaScript", "JS", "Launch"].map((name) => [...name].map((character) => escaped(character)).join("")).join("|")})(?![\\w]|${WORD_ESCAPE})`);
4053
+ pdfActive = (text) => PDF_ACTIVE.test(text) || PDF_ACTIVE_ESCAPED.test(text);
4045
4054
  }
4046
4055
  });
4047
4056
 
@@ -4051,7 +4060,7 @@ function boundAddressField(line, room) {
4051
4060
  const limit = Math.min(line.length, MAX_ADDRESS_FIELD, room);
4052
4061
  let end = start;
4053
4062
  let expecting = "";
4054
- let escaped = false;
4063
+ let escaped2 = false;
4055
4064
  let separators = 0;
4056
4065
  let groups = 0;
4057
4066
  let index = start;
@@ -4060,8 +4069,8 @@ function boundAddressField(line, room) {
4060
4069
  if ((chr === "," || chr === ";") && ++separators > MAX_ADDRESS_SEPARATORS) {
4061
4070
  break;
4062
4071
  }
4063
- if (escaped) {
4064
- escaped = false;
4072
+ if (escaped2) {
4073
+ escaped2 = false;
4065
4074
  } else if (expecting !== "" && chr === expecting) {
4066
4075
  expecting = "";
4067
4076
  end = chr === ";" ? index + 1 : end;
@@ -4069,7 +4078,7 @@ function boundAddressField(line, room) {
4069
4078
  expecting = ADDRESS_OPERATORS.get(chr);
4070
4079
  end = chr === "," || chr === ";" ? index : end;
4071
4080
  } else if (expecting === '"' && chr === "\\") {
4072
- escaped = true;
4081
+ escaped2 = true;
4073
4082
  }
4074
4083
  if (chr === ":" && expecting === ";" && ++groups > MAX_ADDRESS_GROUPS) {
4075
4084
  break;
@@ -4155,6 +4164,10 @@ function boundedForAuth(raw) {
4155
4164
  \r
4156
4165
  `, "latin1"), body]), fromCut };
4157
4166
  }
4167
+ function isAttachedMessage(attachment) {
4168
+ const type = String(attachment.contentType).toLowerCase();
4169
+ return type === "message/rfc822" || type === "message/global" || /\.(?:eml|u8msg)$/i.test(String(attachment.filename));
4170
+ }
4158
4171
  async function simpleParse(raw, { timeout = PARSE_TIMEOUT, parser = import_mailparser.simpleParser } = {}) {
4159
4172
  const input = import_node_stream.Readable.from([finishable(unfoldHeaders(raw))], { objectMode: false, autoDestroy: false });
4160
4173
  input.on("error", () => {
@@ -4210,18 +4223,18 @@ function headerValue2(line) {
4210
4223
  let key = null;
4211
4224
  let inKey = false;
4212
4225
  let quoted = false;
4213
- let escaped = false;
4226
+ let escaped2 = false;
4214
4227
  let value = "";
4215
4228
  for (const chr of line.slice(line.indexOf(":") + 1).replaceAll(/\r?\n[ \t]*/g, " ")) {
4216
4229
  if (inKey && chr === "=") {
4217
4230
  key = value.trim().toLowerCase();
4218
4231
  inKey = false;
4219
4232
  value = "";
4220
- } else if (inKey || escaped) {
4233
+ } else if (inKey || escaped2) {
4221
4234
  value += chr;
4222
- escaped = false;
4235
+ escaped2 = false;
4223
4236
  } else if (chr === "\\") {
4224
- escaped = true;
4237
+ escaped2 = true;
4225
4238
  } else if (chr === '"') {
4226
4239
  quoted = !quoted;
4227
4240
  } else if (chr === ";" && !quoted) {
@@ -4269,7 +4282,7 @@ function unfoldHeaders(raw) {
4269
4282
  if (blank && type.boundary) {
4270
4283
  boundaries.push(import_node_buffer2.Buffer.from(type.boundary));
4271
4284
  }
4272
- head = blank && type.value === "message/rfc822" && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
4285
+ head = blank && ["message/rfc822", "message/global"].includes(type.value) && ["", "7bit", "8bit", "binary"].includes(encoding) && first.get("content-disposition")?.value === "inline";
4273
4286
  };
4274
4287
  for (let start = 0; start < raw.length; ) {
4275
4288
  const lf = raw.indexOf(10, start);
@@ -4400,25 +4413,45 @@ async function parseWithin(raw, parse = parseMessage, deadline = Date.now() + PA
4400
4413
  }
4401
4414
  }
4402
4415
  }
4403
- async function expandAttachments(attachments = [], errors = [], parse = parseMessage) {
4416
+ async function expandAttachments(attachments = [], errors = [], parse = parseMessage, unopened = []) {
4404
4417
  const all = [];
4405
4418
  const deadline = Date.now() + PARSE_TIMEOUT;
4406
4419
  let budget = MAX_NESTED_BYTES;
4407
4420
  let nested = 0;
4421
+ let tooMany = false;
4422
+ let opened = 0;
4408
4423
  const visit = async (list, prefix, depth) => {
4409
4424
  for (const attachment of list) {
4410
4425
  if (prefix) {
4411
4426
  if (nested >= MAX_ATTACHMENTS) {
4427
+ if (!tooMany) {
4428
+ tooMany = true;
4429
+ unopened.push({ filename: prefix, reason: `more than ${MAX_ATTACHMENTS} attachments in attached messages` });
4430
+ }
4412
4431
  return;
4413
4432
  }
4414
4433
  nested++;
4415
4434
  }
4416
4435
  const name = attachment.filename || (prefix ? "unnamed attachment" : "");
4436
+ const filename = prefix ? `${prefix}/${name}` : name || "attached message";
4417
4437
  all.push(prefix ? { ...attachment, filename: `${prefix}/${name}` } : attachment);
4418
4438
  const { content } = attachment;
4419
- const attached = attachment.contentType === "message/rfc822" || /\.eml$/i.test(attachment.filename || "");
4420
- if (attached && depth < MAX_NESTED_DEPTH && import_node_buffer2.Buffer.isBuffer(content) && content.length <= budget) {
4439
+ const attached = isAttachedMessage(attachment);
4440
+ if (attached && import_node_buffer2.Buffer.isBuffer(content) && depth >= MAX_NESTED_DEPTH) {
4441
+ unopened.push({ filename, reason: `nested more than ${MAX_NESTED_DEPTH} deep` });
4442
+ continue;
4443
+ }
4444
+ if (attached && import_node_buffer2.Buffer.isBuffer(content) && opened >= MAX_ATTACHED_MESSAGES) {
4445
+ unopened.push({ filename, reason: `more than ${MAX_ATTACHED_MESSAGES} attached messages` });
4446
+ continue;
4447
+ }
4448
+ if (attached && import_node_buffer2.Buffer.isBuffer(content) && content.length > budget) {
4449
+ unopened.push({ filename, reason: `past ${MAX_NESTED_BYTES / 1024 / 1024} MB of attached messages` });
4450
+ continue;
4451
+ }
4452
+ if (attached && import_node_buffer2.Buffer.isBuffer(content)) {
4421
4453
  budget -= content.length;
4454
+ opened++;
4422
4455
  let inner;
4423
4456
  try {
4424
4457
  inner = await parseWithin(content, parse, deadline);
@@ -4436,7 +4469,7 @@ async function expandAttachments(attachments = [], errors = [], parse = parseMes
4436
4469
  await visit(attachments, "", 0);
4437
4470
  return all;
4438
4471
  }
4439
- 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, PARSE_TIMEOUT, MAX_FOLDS;
4472
+ 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;
4440
4473
  var init_parse = __esm({
4441
4474
  "src/parse.js"() {
4442
4475
  import_node_buffer2 = require("node:buffer");
@@ -4521,6 +4554,7 @@ var init_parse = __esm({
4521
4554
  MAX_NESTED_DEPTH = 3;
4522
4555
  MAX_ATTACHMENTS = 100;
4523
4556
  MAX_NESTED_BYTES = 25 * 1024 * 1024;
4557
+ MAX_ATTACHED_MESSAGES = 100;
4524
4558
  PARSE_TIMEOUT = 6e4;
4525
4559
  MAX_FOLDS = 256;
4526
4560
  }
@@ -6102,9 +6136,12 @@ ${visibleText(html2.slice(0, 1e5)).text}`)) {
6102
6136
  hit("SEXTORTION_SUBJECT", 2, "Subject used by sextortion and account takeover scams, without a demand for payment", "SCAM");
6103
6137
  }
6104
6138
  const markup = html2.slice(0, 1e5);
6139
+ const readable = readableText(`${subject}
6140
+ ${text.slice(0, 5e4)}
6141
+ ${visibleText(withoutHelp(markup)).text}`);
6105
6142
  if (isInjection(`${subject}
6106
6143
  ${text.slice(0, 5e4)}
6107
- ${withoutHelp(markup)}`) || attributeText(markup).some(({ text: text2, start, end, help }) => (!help || isOrder(text2)) && isInjection(unquoted(text2), AI_ACTION_OBJECT, () => shownNear(markup, start, end)))) {
6144
+ ${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)))) {
6108
6145
  hit("PROMPT_INJECTION", 3, "Contains instructions addressed to an AI filter", "SPAM");
6109
6146
  }
6110
6147
  if (session.resolvedClientHostname?.endsWith(".outbound.protection.outlook.com")) {
@@ -7629,7 +7666,7 @@ var init_package = __esm({
7629
7666
  package_default = {
7630
7667
  name: "spamscanner",
7631
7668
  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.",
7632
- version: "7.0.7",
7669
+ version: "7.0.9",
7633
7670
  author: "Forward Email (https://forwardemail.net)",
7634
7671
  bin: {
7635
7672
  spamscanner: "./dist/esm/cli.js"
@@ -9280,7 +9317,8 @@ __export(src_exports, {
9280
9317
  scoreResults: () => scoreResults,
9281
9318
  segmentWords: () => segmentWords,
9282
9319
  spamHeaders: () => spamHeaders,
9283
- train: () => train
9320
+ train: () => train,
9321
+ unwrapLink: () => unwrapLink
9284
9322
  });
9285
9323
  function finding(fields) {
9286
9324
  Object.defineProperty(fields, "toString", {
@@ -9549,14 +9587,16 @@ var init_src = __esm({
9549
9587
  * a header field ("Name: value", or an mbox "From " line) is read as the
9550
9588
  * body of a message with no header, so its text is the input; other
9551
9589
  * input mailparser cannot read gives a message with no text. Other errors
9552
- * are thrown.
9590
+ * are thrown. `error` is the parser's limit the message exceeds, so that
9591
+ * only its first parts were read (as scan() reports it, see parseError),
9592
+ * or null.
9553
9593
  * @param {Buffer|string|Uint8Array|AsyncIterable} source
9554
- * @returns {Promise<{raw: Buffer, mail: object}>}
9594
+ * @returns {Promise<{raw: Buffer, mail: object, error: string|null}>}
9555
9595
  */
9556
9596
  async parse(source) {
9557
9597
  const raw = await toBuffer(source);
9558
- const { mail } = await parseWithin(withHeader(raw));
9559
- return { raw, mail };
9598
+ const { mail, error } = await parseWithin(withHeader(raw));
9599
+ return { raw, mail, error: error ?? null };
9560
9600
  }
9561
9601
  /**
9562
9602
  * Classifier features of a parsed message.
@@ -9795,7 +9835,7 @@ var init_src = __esm({
9795
9835
  message: `Attachment "${filename}" contains ${result.viruses.join(", ")}`
9796
9836
  });
9797
9837
  viruses.push(found);
9798
- if (attachment.contentType === "message/rfc822" || /\.eml$/i.test(attachment.filename || "")) {
9838
+ if (isAttachedMessage(attachment)) {
9799
9839
  carriers.set(found, attachment.filename || "attached message");
9800
9840
  }
9801
9841
  }
@@ -9916,7 +9956,8 @@ ${visibleText(html2.slice(0, config.maxLength * 2)).text}`, { maxLength: config.
9916
9956
  mixed: Math.max(plainObfuscation.mixed, shownObfuscation.mixed),
9917
9957
  styled: plainObfuscation.styled || shownObfuscation.styled
9918
9958
  };
9919
- const attached = await expandAttachments(mail.attachments, parseErrors);
9959
+ const unopened = [];
9960
+ const attached = await expandAttachments(mail.attachments, parseErrors, void 0, unopened);
9920
9961
  const attachments = config.attachments === false ? [] : inspectAttachments(attached).filter((item) => config.macros !== false || !["macro", "pdf_active", "rtf_object"].includes(item.type)).map((item) => finding(item));
9921
9962
  const [links, authentication, checkedReputation, dnsbl, virusScan, toxicity, nsfw] = await Promise.all([
9922
9963
  this.checkLinks(checkedLinks, config, extracted.hosts, fromDomain(mail)),
@@ -10037,6 +10078,13 @@ ${visibleText(html2.slice(0, config.maxLength * 2)).text}`, { maxLength: config.
10037
10078
  // scanned, as {filename, reason}: the same on every try, so not a
10038
10079
  // failure.
10039
10080
  virusScanSkipped: virusScan.skipped,
10081
+ // Attached messages not opened, past the limits attached messages
10082
+ // are read within (nested more than three deep, more than 100
10083
+ // attachments in them, or more than 25 MB of them), as
10084
+ // {filename, reason}: their attachments were not inspected. Not
10085
+ // scored (forwarded messages can be nested so), for a caller that
10086
+ // needs a message read whole, e.g. to block what it carries.
10087
+ unopenedAttachments: unopened,
10040
10088
  // Whether the message has more text than phishing.maxLength, whose
10041
10089
  // links were not read.
10042
10090
  linksTruncated,