spamscanner 7.0.9 → 7.0.11
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.map +1 -1
- package/dist/cjs/index.cjs +1338 -179
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/esm/arf.js.map +1 -1
- package/dist/esm/cli.js +1266 -123
- package/dist/esm/cli.js.map +4 -4
- package/dist/esm/index.js +1306 -163
- package/dist/esm/index.js.map +4 -4
- package/dist/types/attachments.d.ts +1 -0
- package/dist/types/entities.d.ts +2 -0
- package/dist/types/html.d.ts +2 -2
- package/dist/types/index.d.ts +26 -6
- package/dist/types/links.d.ts +24 -0
- package/dist/types/parse.d.ts +1 -1
- package/package.json +1 -1
package/dist/cjs/arf.cjs.map
CHANGED
|
@@ -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/**\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"],
|
|
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`: gets the attached messages not opened, past the limits above,\n// as {filename, reason}, whose attachments were not inspected; `mails`:\n// gets the attached messages opened, as mailparser parsed them; or an array,\n// `unopened`)\nexport async function expandAttachments(attachments = [], errors = [], parse = parseMessage, gets = {}) {\n\tconst {unopened = [], mails = []} = Array.isArray(gets) ? {unopened: gets} : gets;\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\tmails.push(inner.mail);\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
5
|
"mappings": "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",
|
|
6
6
|
"names": ["import_node_buffer", "import_node_crypto", "key", "parser"]
|
|
7
7
|
}
|