spamscanner 7.0.13 → 7.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 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`: 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
- "mappings": "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",
6
- "names": ["import_node_buffer", "import_node_crypto", "key", "parser"]
3
+ "sources": ["../../src/arf.js", "../../src/parse.js", "../../src/attachments.js", "../../src/linear.js", "../../src/tokenizer.js", "../../src/homograph.js", "../../src/language.js", "../../src/entities.js", "../../src/html.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';\nimport {inspectAttachments} from './attachments.js';\nimport {trimEndOf} from './linear.js';\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;\n// (the parser counts each header line as at least this many characters, so\n// many short lines are too large as well)\nconst MIN_LINE_COST = 64;\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\nconst NAMED_TEXT = Symbol('named text parts');\n\n// Files written into a plain text body, uuencoded or yEncoded (\"begin 644\n// invoice.exe\", \"=ybegin line=128 size=1024 name=invoice.exe\"), which mail\n// programs show as attachments: their names, the first MAX_ENCODED_FILES of\n// them (each line read once, in linear time).\n// (not across the line separators U+2028 and U+2029, which end a line for\n// \"^\" and \"$\" too: each line would be read again from every one)\nconst ENCODED_FILES = /^(?:begin [0-7]{3,4} |=ybegin\\b[^\\r\\n\\u2028\\u2029]*?\\bname=)([^\\r\\n\\u2028\\u2029]+)\\r?$/gm;\nconst MAX_ENCODED_FILES = 1000;\n\n// The kinds of findings of the attachments in each list addFinding adds to.\nconst foundIn = new WeakMap();\n\n// Adds an attachment inspected by name to `list` when inspecting it finds a\n// kind of finding none before it did (more of a kind add nothing to the\n// score): so `list` holds one for each kind at most, and names that are\n// harmless, or many of one kind, never take the place of another.\nfunction addFinding(list, part) {\n\tconst found = foundIn.get(list) ?? new Set();\n\tfoundIn.set(list, found);\n\tconst types = inspectAttachments([part]).map(finding => finding.type);\n\tif (types.some(type => !found.has(type))) {\n\t\tlist.push(part);\n\t\tfor (const type of types) {\n\t\t\tfound.add(type);\n\t\t}\n\t}\n}\n\n// The parameters of a header field's value, as written: split at the\n// semicolons outside quoted strings (each character read once).\nfunction parametersOf(value) {\n\tconst parameters = [];\n\tlet start = -1;\n\tlet quoted = false;\n\tfor (let i = 0; i < value.length; i++) {\n\t\tif (quoted && value[i] === '\\\\') {\n\t\t\ti++;\n\t\t} else if (value[i] === '\"') {\n\t\t\tquoted = !quoted;\n\t\t} else if (value[i] === ';' && !quoted) {\n\t\t\tif (start !== -1) {\n\t\t\t\tparameters.push(value.slice(start, i));\n\t\t\t}\n\n\t\t\tstart = i + 1;\n\t\t}\n\t}\n\n\tif (start !== -1) {\n\t\tparameters.push(value.slice(start));\n\t}\n\n\treturn parameters;\n}\n\n// The most header fields of a part, and parameters of one, read for names.\nconst MAX_NAME_FIELDS = 8;\nconst MAX_NAME_PARAMETERS = 32;\n\n/**\n * The names a part gives itself more than once: by a Content-Disposition or\n * Content-Type field that is there twice, or a \"filename\" or \"name\" parameter\n * given twice in one (which no mail program writes). Mail programs differ in\n * which they show, and mailparser reads one (the first or the last, by its\n * version), so each is a name the part may go by. None for a part that names\n * itself once.\n * @param {object} node - a part, as mailsplit read it\n * @returns {string[]}\n */\nfunction repeatedNames(node) {\n\tconst names = [];\n\tconst {headers, libmime} = node ?? {};\n\tif (typeof headers?.get !== 'function' || typeof libmime?.parseHeaderValue !== 'function') {\n\t\treturn names;\n\t}\n\n\t// (a value that cannot be read names nothing)\n\tconst read = (value, parameter) => {\n\t\tlet name = '';\n\t\ttry {\n\t\t\tname = libmime.decodeWords(String(libmime.parseHeaderValue(value, {stripComments: true}).params[parameter] ?? '')).trim();\n\t\t} catch {}\n\n\t\tif (name !== '') {\n\t\t\tnames.push(name);\n\t\t}\n\t};\n\n\tfor (const [field, parameter] of [['content-disposition', 'filename'], ['content-type', 'name']]) {\n\t\tconst values = headers.get(field).slice(0, MAX_NAME_FIELDS);\n\t\tfor (const value of values) {\n\t\t\t// (the key of each parameter as written, \"filename*0\" apart from\n\t\t\t// \"filename\": the pieces of one name are not names of their own)\n\t\t\tconst parameters = parametersOf(value).slice(0, MAX_NAME_PARAMETERS).map(text => ({text, key: text.slice(0, Math.max(0, text.indexOf('='))).trim().toLowerCase()}));\n\t\t\tconst repeated = parameters.filter(({key}) => (key === parameter || key.startsWith(`${parameter}*`)) && parameters.filter(other => other.key === key).length > 1);\n\t\t\tif (values.length > 1) {\n\t\t\t\tread(value, parameter);\n\t\t\t}\n\n\t\t\tfor (const {text} of repeated) {\n\t\t\t\tread(`x;${text}`, text.slice(0, text.indexOf('=')).trim().toLowerCase().replace(/\\*.*$/, ''));\n\t\t\t}\n\t\t}\n\t}\n\n\treturn names;\n}\n\n/**\n * A parsed message's plain text and HTML parts named as files (by a file name\n * or a name), and the files uuencoded or yEncoded into its text, which mail\n * programs list as attachments while mailparser reads them as the body, as attachments without content: {filename, contentType,\n * content: null}. Only those inspecting by name finds a kind of finding in\n * that none before them did (see addFinding).\n * @param {object} mail - as simpleParse parsed it\n * @returns {Array<{filename: string, contentType: string, content: null}>}\n */\nexport function namedTextParts(mail) {\n\treturn mail[NAMED_TEXT] ?? [];\n}\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\t// (and records the text parts named as files: see namedTextParts)\n\tconst named = [];\n\tconst {pipe} = input;\n\tinput.pipe = function (parser, options) {\n\t\tparser.splitter?.on('error', error => input.destroy(error));\n\t\tconst {createNode} = parser;\n\t\tif (typeof createNode === 'function') {\n\t\t\tparser.createNode = function (node) {\n\t\t\t\tconst created = createNode.call(this, node);\n\t\t\t\tif (created?.isAttachment === false && node?.filename) {\n\t\t\t\t\taddFinding(named, {filename: String(node.filename), contentType: created.contentType, content: null});\n\t\t\t\t}\n\n\t\t\t\t// (and by every name a part gives itself twice: see repeatedNames)\n\t\t\t\tfor (const filename of repeatedNames(node)) {\n\t\t\t\t\tif (filename !== node?.filename) {\n\t\t\t\t\t\taddFinding(named, {filename, contentType: created?.contentType || 'application/octet-stream', content: null});\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn created;\n\t\t\t};\n\t\t}\n\n\t\treturn pipe.call(this, parser, options);\n\t};\n\n\tlet timer;\n\ttry {\n\t\tconst mail = 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\tif (mail && typeof mail === 'object') {\n\t\t\t// (and files written into its text, which mailparser leaves there)\n\t\t\tlet count = 0;\n\t\t\tfor (const [, filename] of String(mail.text ?? '').matchAll(ENCODED_FILES)) {\n\t\t\t\taddFinding(named, {filename: filename.trim(), contentType: 'application/octet-stream', content: null});\n\t\t\t\tif (++count >= MAX_ENCODED_FILES) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tObject.defineProperty(mail, NAMED_TEXT, {value: named});\n\t\t}\n\n\t\treturn mail;\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// What a header line counts for against the parser's limit.\nfunction lineCost(line) {\n\treturn Math.max(line.length, MIN_LINE_COST);\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 + lineCost(line) <= MAX_FIELD) {\n\t\t\t\tlast.lines.push(line);\n\t\t\t\tlast.size += lineCost(line);\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: lineCost(line),\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 = lineCost(bounded);\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\t// (or its first megabyte, or its first half when it is no longer,\n\t\t\t\t// so each try reads less)\n\t\t\t\tshorter = smaller.raw.length < input.length ? smaller.raw : upToLine(input, input.length > MAX_PREFIX_BYTES ? MAX_PREFIX_BYTES : Math.floor(input.length / 2));\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; `named`:\n// gets their namedTextParts, named as their attachments are, and the other\n// names attachments give in their Content-Type, without content; or an\n// array, `unopened`)\nexport async function expandAttachments(attachments = [], errors = [], parse = parseMessage, gets = {}) {\n\tconst {unopened = [], mails = [], named = []} = Array.isArray(gets) ? {unopened: gets} : (gets ?? {});\n\t// (those inspection finds a kind of finding in that none before them did,\n\t// see addFinding)\n\tconst addNamed = (...parts) => {\n\t\tfor (const part of parts) {\n\t\t\taddFinding(named, part);\n\t\t}\n\t};\n\n\t// (as Windows saves a file: case, and dots and spaces at its end, left out)\n\tconst nameOf = value => trimEndOf(String(value ?? '').trim().toLowerCase(), '. ');\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\t// (and the name its Content-Type gives, when another, which some\n\t\t\t// mail programs show: in `named`, inspected by name)\n\t\t\tconst typeName = String(attachment.headers?.get?.('content-type')?.params?.name ?? '').trim();\n\t\t\tif (typeName && nameOf(typeName) !== nameOf(attachment.filename)) {\n\t\t\t\taddNamed({filename: prefix ? `${prefix}/${typeName}` : typeName, contentType: attachment.contentType, content: null});\n\t\t\t}\n\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\t\t\t\tconst carrier = prefix ? `${prefix}/${name}` : (name || 'attached message');\n\t\t\t\taddNamed(...namedTextParts(inner.mail).map(part => ({...part, filename: `${carrier}/${part.filename}`})));\n\n\t\t\t\t// eslint-disable-next-line no-await-in-loop\n\t\t\t\tawait visit(inner.mail.attachments, carrier, depth + 1);\n\t\t\t}\n\t\t}\n\t};\n\n\tawait visit(attachments, '', 0);\n\treturn all;\n}\n", "import {Buffer} from 'node:buffer';\nimport {linear, trimEndOf} from './linear.js';\nimport {characterClass} from './tokenizer.js';\n\n/**\n * File extensions that run code when opened, on Windows, macOS, Linux and\n * Android: programs, installers, scripts, shortcuts, and disk images that\n * mail filters are bypassed with. Based on the attachment types Outlook blocks\n * by default, plus the scripting and container formats used in malware\n * campaigns.\n */\nexport const EXECUTABLE_EXTENSIONS = new Set([\n\t'ade', 'adp', 'apk', 'app', 'appimage', 'appinstaller', 'application', 'appref-ms', 'appx', 'appxbundle', 'asp', 'aspx', 'asx', 'bas', 'bat', 'bgi', 'cab', 'chm', 'cmd', 'cnt', 'com', 'command', 'cpl', 'csh', 'diagcab', 'dll', 'dmg', 'docm', 'dotm', 'elf', 'exe', 'fxp', 'gadget', 'grp', 'hlp', 'hpj', 'hta', 'htc', 'img', 'inf', 'ins', 'ipa', 'iqy', 'iso', 'isp', 'its', 'jar', 'jnlp', 'js', 'jse', 'ksh', 'library-ms', 'lnk', 'mad', 'maf', 'mag', 'mam', 'maq', 'mar', 'mas', 'mau', 'mav', 'maw', 'mcf', 'mda', 'mde', 'mdt', 'mdw', 'mdz', 'msc', 'msh', 'msh1', 'msh1xml', 'msh2', 'msh2xml', 'mshxml', 'msi', 'msix', 'msixbundle', 'msp', 'mst', 'msu', 'nsh', 'one', 'onepkg', 'ops', 'osd', 'pcd', 'pif', 'pkg', 'pl', 'plg', 'potm', 'ppam', 'ppsm', 'pptm', 'prf', 'prg', 'ps1', 'ps1xml', 'ps2', 'ps2xml', 'psc1', 'psc2', 'py', 'pyc', 'pyo', 'pyw', 'pyz', 'pyzw', 'reg', 'scf', 'scpt', 'scr', 'sct', 'search-ms', 'searchconnector-ms', 'settingcontent-ms', 'sh', 'shb', 'shs', 'slk', 'sys', 'vb', 'vbe', 'vbp', 'vbs', 'vhd', 'vhdx', 'vsmacros', 'vsw', 'webpnp', 'website', 'ws', 'wsb', 'wsc', 'wsf', 'wsh', 'xbap', 'xlam', 'xll', 'xlsm', 'xltm', 'xnk',\n]);\n\n/**\n * File extensions Outlook blocks that are risky by name rather than programs:\n * certificates (installing one trusts whoever holds its key), Internet\n * shortcuts, Windows themes, and data files people send every day (MATLAB\n * data, Outlook data, temporary files). Reported as risky_attachment, with\n * few points.\n */\nexport const RISKY_EXTENSIONS = new Set(['cer', 'der', 'mat', 'pst', 'theme', 'tmp', 'url']);\n\n// Scripts that developers and students share (a zipped website or homework):\n// in an archive they count for fewer points than native programs.\nconst SCRIPT_EXTENSIONS = new Set(['asp', 'aspx', 'csh', 'js', 'ksh', 'pl', 'py', 'pyc', 'pyo', 'pyw', 'pyz', 'pyzw', 'sh']);\n\n// Office formats whose file name says they may contain macros.\nconst MACRO_EXTENSIONS = new Set(['docm', 'dotm', 'xlsm', 'xltm', 'xlam', 'pptm', 'potm', 'ppsm', 'ppam', 'sldm']);\n\n// Formats people send every day; one of these before an executable extension\n// (\"invoice.pdf.exe\") is a disguise.\nconst DOCUMENT_EXTENSIONS = new Set(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf', 'jpg', 'jpeg', 'png', 'gif', 'zip', 'csv', 'odt', 'ods', 'html', 'htm', 'mp3', 'mp4', 'mov', 'wav', 'eml', 'msg', 'xml', 'json']);\n\n// Suffixes Office and editors add to a document's name for a temporary copy\n// (\"report.xlsx.tmp\"): not a disguise.\nconst TEMPORARY_EXTENSIONS = new Set(['tmp']);\n\nconst BIDI_CONTROLS = characterClass([[0x20_2A, 0x20_2E], [0x20_66, 0x20_69]], 'u');\nconst ALL_BIDI_CONTROLS = characterClass([[0x20_2A, 0x20_2E], [0x20_66, 0x20_69]], 'gu');\n\nconst ARCHIVE_TYPES = new Set(['zip', 'rar', '7z', 'gz', 'bz2', 'xz', 'cab', 'iso', 'tar', 'zst', 'lzh', 'ace', 'arj']);\n\n/**\n * Identify a file from its first bytes. Returns null when unknown.\n *\n * Recognises programs (Windows PE, ELF, Mach-O), Windows shortcuts, OLE\n * compound files (old Office formats, MSI), ZIP-based formats (Office, Java,\n * Android), other archives, disk images, PDF, RTF, HTML, SVG and common images.\n *\n * @param {Buffer} buffer\n * @returns {{type: string, executable: boolean, archive: boolean}|null}\n */\nexport function sniff(buffer) {\n\tif (!Buffer.isBuffer(buffer) || buffer.length < 4) {\n\t\treturn null;\n\t}\n\n\tconst at = (offset, bytes) => buffer.length >= offset + bytes.length && bytes.every((byte, i) => buffer[offset + i] === byte);\n\tconst ascii = (offset, text) => at(offset, [...Buffer.from(text, 'latin1')]);\n\tconst result = (type, executable = false) => ({type, executable, archive: ARCHIVE_TYPES.has(type)});\n\n\t// A Windows program: \"MZ\", and \"PE\\0\\0\" where its header says (text can\n\t// start with \"MZ\" too, such as a list of country codes).\n\tif (ascii(0, 'MZ') && buffer.length >= 64 && at(buffer.readUInt32LE(0x3C), [0x50, 0x45, 0x00, 0x00])) {\n\t\treturn result('exe', true);\n\t}\n\n\tif (at(0, [0x7F, 0x45, 0x4C, 0x46])) {\n\t\treturn result('elf', true);\n\t}\n\n\tconst magic = buffer.readUInt32BE(0);\n\tif ([0xFE_ED_FA_CE, 0xFE_ED_FA_CF, 0xCE_FA_ED_FE, 0xCF_FA_ED_FE].includes(magic)) {\n\t\treturn result('macho', true);\n\t}\n\n\t// 0xCAFEBABE is both a Mach-O universal binary and a Java class file;\n\t// both run code.\n\tif (magic === 0xCA_FE_BA_BE) {\n\t\treturn result('macho', true);\n\t}\n\n\tif (at(0, [0x4C, 0x00, 0x00, 0x00, 0x01, 0x14, 0x02, 0x00])) {\n\t\treturn result('lnk', true);\n\t}\n\n\tif (at(0, [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1])) {\n\t\treturn result('ole');\n\t}\n\n\tif (at(0, [0x50, 0x4B, 0x03, 0x04]) || at(0, [0x50, 0x4B, 0x05, 0x06])) {\n\t\tconst names = entriesOf(buffer).map(entry => entry.name);\n\t\tif (names.includes('AndroidManifest.xml') || names.includes('classes.dex')) {\n\t\t\treturn result('apk', true);\n\t\t}\n\n\t\tif (names.includes('META-INF/MANIFEST.MF') && names.some(name => name.endsWith('.class'))) {\n\t\t\treturn result('jar', true);\n\t\t}\n\n\t\tif (names.some(name => name.startsWith('word/'))) {\n\t\t\treturn result('docx');\n\t\t}\n\n\t\tif (names.some(name => name.startsWith('xl/'))) {\n\t\t\treturn result('xlsx');\n\t\t}\n\n\t\tif (names.some(name => name.startsWith('ppt/'))) {\n\t\t\treturn result('pptx');\n\t\t}\n\n\t\treturn result('zip');\n\t}\n\n\tif (ascii(0, 'Rar!')) {\n\t\treturn result('rar');\n\t}\n\n\tif (at(0, [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])) {\n\t\treturn result('7z');\n\t}\n\n\tif (at(0, [0x1F, 0x8B])) {\n\t\treturn result('gz');\n\t}\n\n\tif (ascii(0, 'MSCF')) {\n\t\treturn result('cab');\n\t}\n\n\tif (ascii(0x80_01, 'CD001') || ascii(0x88_01, 'CD001') || ascii(0x90_01, 'CD001')) {\n\t\treturn result('iso');\n\t}\n\n\tif (ascii(0, 'conectix') || ascii(0, 'vhdxfile')) {\n\t\treturn result('vhd', true);\n\t}\n\n\tif (ascii(0, '%PDF')) {\n\t\treturn result('pdf');\n\t}\n\n\tif (ascii(0, String.raw`{\\rt`)) {\n\t\treturn result('rtf');\n\t}\n\n\tif (at(0, [0x89, 0x50, 0x4E, 0x47])) {\n\t\treturn result('png');\n\t}\n\n\tif (at(0, [0xFF, 0xD8, 0xFF])) {\n\t\treturn result('jpg');\n\t}\n\n\tif (ascii(0, 'GIF8')) {\n\t\treturn result('gif');\n\t}\n\n\tif (ascii(0, 'RIFF') && ascii(8, 'WEBP')) {\n\t\treturn result('webp');\n\t}\n\n\tif (ascii(0, '#!')) {\n\t\treturn result('script', true);\n\t}\n\n\tconst head = buffer.subarray(0, 1024).toString('utf8').replace(/^\\uFEFF/u, '').trimStart().toLowerCase();\n\tif (head.startsWith('<svg') || (head.startsWith('<?xml') && head.includes('<svg'))) {\n\t\treturn result('svg');\n\t}\n\n\tif (head.startsWith('<!doctype html') || head.startsWith('<html') || head.startsWith('<head') || head.startsWith('<script')) {\n\t\treturn result('html');\n\t}\n\n\treturn null;\n}\n\nconst ZIP_FAMILY = new Set(['zip', 'docx', 'xlsx', 'pptx']);\nconst OFFICE_TYPES = new Set(['docx', 'xlsx', 'pptx']);\n// What Office embeds in a document: other documents (with macros too, which\n// are reported as macros).\nconst OFFICE_EMBEDDED = MACRO_EXTENSIONS;\n\n// The entries of each ZIP attachment, read once (by sniff and by\n// inspectAttachments).\nconst ENTRIES = new WeakMap();\nfunction entriesOf(buffer) {\n\tlet entries = ENTRIES.get(buffer);\n\tif (!entries) {\n\t\tentries = zipEntries(buffer);\n\t\tENTRIES.set(buffer, entries);\n\t}\n\n\treturn entries;\n}\n\n/**\n * The entries of a ZIP file, read from its central directory without\n * decompressing anything: name, sizes, and whether the entry is encrypted.\n *\n * Every end record that ends the file (its comment running to the last byte)\n * is read, as well as the last one found: archive programs differ in which\n * they use, so a directory hidden behind another record is read too. ZIP64\n * archives, and archives after other data (self-extracting archives), are\n * read as well. Each entry is read once, however many directories overlap\n * it; a name longer than 1 KiB keeps its first and last 512 bytes (with \"\u2026\"\n * between them), and reading stops after 1 MiB of names.\n * @param {Buffer} buffer\n * @param {number} [limit] - most entries to read\n * @returns {Array<{name: string, encrypted: boolean, size: number}>}\n */\nexport function zipEntries(buffer, limit = 2000) {\n\t// End of central directory: within the last 64 KiB plus 22 bytes.\n\tconst start = Math.max(0, buffer.length - 65_557);\n\tconst records = [];\n\tfor (let i = buffer.length - 22; i >= start; i--) {\n\t\tif (buffer.readUInt32LE(i) === 0x06_05_4B_50 && (records.length === 0 || i + 22 + buffer.readUInt16LE(i + 20) === buffer.length)) {\n\t\t\trecords.push(i);\n\t\t}\n\t}\n\n\t// (each entry once, and at most `limit` entries and MAX_NAME_BYTES of\n\t// names in all)\n\tconst state = {\n\t\tentries: [], limit, read: new Set(), names: MAX_NAME_BYTES,\n\t};\n\tfor (const eocd of records) {\n\t\tconst offset = directoryOf(buffer, eocd);\n\t\tif (offset !== -1) {\n\t\t\treadDirectory(buffer, offset, state);\n\t\t}\n\t}\n\n\treturn state.entries;\n}\n\n// The longest entry name read whole, and the most bytes of names read from\n// one archive.\nconst MAX_NAME_LENGTH = 1024;\nconst MAX_NAME_BYTES = 1024 * 1024;\n\nconst DIRECTORY = 0x02_01_4B_50;\nconst isDirectory = (buffer, offset) => offset >= 0 && offset + 46 <= buffer.length && buffer.readUInt32LE(offset) === DIRECTORY;\n\n// Where the central directory of an end record starts, or -1.\nfunction directoryOf(buffer, eocd) {\n\tlet offset = buffer.readUInt32LE(eocd + 16);\n\tlet size = buffer.readUInt32LE(eocd + 12);\n\t// ZIP64: the offset and size are in the ZIP64 end record, which a locator\n\t// right before this record points to.\n\tconst locator = eocd - 20;\n\tif ((offset === 0xFF_FF_FF_FF || size === 0xFF_FF_FF_FF) && locator >= 0 && buffer.readUInt32LE(locator) === 0x07_06_4B_50) {\n\t\t// (or, after other data, right before the locator)\n\t\tconst stated = Number(buffer.readBigUInt64LE(locator + 8));\n\t\tconst isRecord = at => at >= 0 && at + 56 <= locator && buffer.readUInt32LE(at) === 0x06_06_4B_50;\n\t\tconst record = isRecord(stated) ? stated : locator - 56;\n\t\tif (isRecord(record)) {\n\t\t\tsize = Number(buffer.readBigUInt64LE(record + 40));\n\t\t\toffset = Number(buffer.readBigUInt64LE(record + 48));\n\t\t}\n\t}\n\n\tif (isDirectory(buffer, offset)) {\n\t\treturn offset;\n\t}\n\n\t// (after other data, as in a self-extracting archive, the directory is\n\t// right before its end records, whatever offset they give)\n\tfor (const end of [eocd, locator - 56]) {\n\t\tconst directory = end - size;\n\t\tif (directory < end && isDirectory(buffer, directory)) {\n\t\t\treturn directory;\n\t\t}\n\t}\n\n\treturn -1;\n}\n\n// Read directory entries from `offset` into state.entries, until it has\n// state.limit, an entry was read already (state.read: their offsets) or the\n// names read reach state.names bytes.\nfunction readDirectory(buffer, offset, state) {\n\twhile (state.entries.length < state.limit && isDirectory(buffer, offset) && !state.read.has(offset)) {\n\t\tstate.read.add(offset);\n\t\tconst flags = buffer.readUInt16LE(offset + 8);\n\t\tconst size = buffer.readUInt32LE(offset + 24);\n\t\tconst nameLength = buffer.readUInt16LE(offset + 28);\n\t\tconst extraLength = buffer.readUInt16LE(offset + 30);\n\t\tconst commentLength = buffer.readUInt16LE(offset + 32);\n\t\tconst encoding = flags & 0x8_00 ? 'utf8' : 'latin1';\n\t\tconst start = offset + 46;\n\t\tconst end = Math.min(buffer.length, start + nameLength);\n\t\tconst kept = Math.min(end - start, MAX_NAME_LENGTH);\n\t\tstate.names -= kept;\n\t\tif (state.names < 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst half = MAX_NAME_LENGTH / 2;\n\t\tconst name = end - start > MAX_NAME_LENGTH\n\t\t\t? `${buffer.toString(encoding, start, start + half)}\u2026${buffer.toString(encoding, end - half, end)}`\n\t\t\t: buffer.toString(encoding, start, end);\n\t\tstate.entries.push({name, encrypted: (flags & 1) === 1, size});\n\t\toffset += 46 + nameLength + extraLength + commentLength;\n\t}\n}\n\n// A name for a message: at most `max` characters, its start and end (with\n// its extension) around \"\u2026\".\nfunction shorten(name, max = 100) {\n\tconst characters = [...name];\n\treturn characters.length <= max ? name : `${characters.slice(0, max - 41).join('')}\u2026${characters.slice(-40).join('')}`;\n}\n\n// (Windows drops dots and spaces at the end of a file name: \"run.js.\" runs,\n// and the direction controls that hide its extension: \"macro.vb\\u2069s\")\nexport function extensionOf(name) {\n\tconst base = trimEndOf(String(name).toLowerCase().replace(ALL_BIDI_CONTROLS, '').split(/[/\\\\]/).pop(), '. ');\n\tconst whole = lastExtension(base);\n\t// (a name with the stream name of a file's own content by the name Windows\n\t// saves it as: without a drive, before \"::$DATA\", \"macro.vbs::$DATA\" is\n\t// \"macro.vbs\"; another stream name, \"Amazon.com:Order.pdf\", is a stream\n\t// of an empty file, nothing to run, so the name as other systems save\n\t// it, as \"Report: Q1.exe\")\n\tconst file = base.replace(/^[a-z]:/, '');\n\tconst colon = file.indexOf(':');\n\tif (colon !== -1 && /^(?::?\\$data)?$/.test(file.slice(colon + 1))) {\n\t\treturn lastExtension(trimEndOf(file.slice(0, colon), '. '));\n\t}\n\n\treturn whole;\n}\n\n// Formats that compress one file, named after it (\"invoice.exe.gz\").\nconst COMPRESSED_EXTENSIONS = new Set(['gz', 'bz2', 'xz', 'zst']);\n\n// The name of the one file a compressed file holds, by its name: \"invoice.exe\"\n// of \"invoice.exe.gz\"; the name itself otherwise.\nfunction uncompressedName(name) {\n\tif (!COMPRESSED_EXTENSIONS.has(extensionOf(name))) {\n\t\treturn String(name);\n\t}\n\n\t// (in linear time: without what Windows drops at its end, before its last dot)\n\tconst trimmed = trimEndOf(String(name).replace(ALL_BIDI_CONTROLS, ''), '. ');\n\treturn trimmed.slice(0, trimmed.lastIndexOf('.'));\n}\n\nfunction lastExtension(base) {\n\tconst dot = base.lastIndexOf('.');\n\treturn dot === -1 ? '' : base.slice(dot + 1).trim();\n}\n\n// The names of the storages and streams of a VBA project (case does not\n// matter in a compound file).\nconst VBA_NAMES = new Set(['vba', '_vba_project', '_vba_project_cur', 'macros']);\n\nfunction oleHasMacros(buffer) {\n\t// VBA projects live in storages named \"VBA\", \"_VBA_PROJECT_CUR\" (Excel)\n\t// or \"Macros\" (Word), with a \"_VBA_PROJECT\" stream. These are entries of\n\t// the compound file's directory: 128 bytes each, at 128-byte boundaries\n\t// after the header, with a UTF-16LE name, its length in bytes (with the\n\t// final null) at 64 and its type at 66 (1 storage, 2 stream). Text that\n\t// mentions these names, as an Outlook message may, is not an entry.\n\tfor (let offset = 512; offset + 128 <= buffer.length; offset += 128) {\n\t\tconst length = buffer.readUInt16LE(offset + 64);\n\t\tconst type = buffer[offset + 66];\n\t\tif ((type === 1 || type === 2) && length >= 4 && length <= 64 && length % 2 === 0 && VBA_NAMES.has(buffer.subarray(offset, offset + length - 2).toString('utf16le').toLowerCase())) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\n// Actions a PDF runs: script and launch actions. (An /OpenAction that runs\n// script names /JS or /JavaScript, which this matches wherever it is, so the\n// action itself is not looked for: \"<<[^>]*\" would read the rest of the file\n// again from every /OpenAction.) Embedded files are not active content: every\n// ZUGFeRD or Factur-X e-invoice carries one.\nconst PDF_ACTIVE = linear(String.raw`\\/(?:JavaScript|JS|Launch)\\b`);\n\n// A PDF name with a character written as #xx (\"/J#61vaScript\" is\n// \"/JavaScript\") is the same name: each character of the active names is\n// matched as itself or as its #xx form, and the name must end there (no\n// letter, digit or \"_\" after it, written either way), so the file is read\n// once, without a decoded copy of it.\nconst escaped = character => {\n\tconst hex = character.codePointAt(0).toString(16);\n\treturn `(?:${character}|#${hex[0]}[${hex[1].toLowerCase()}${hex[1].toUpperCase()}])`;\n};\n\nconst WORD_ESCAPE = String.raw`#(?:3\\d|4[1-9a-fA-F]|5[\\daA]|5[fF]|6[1-9a-fA-F]|7[\\daA])`;\nconst PDF_ACTIVE_ESCAPED = new RegExp(`/(?:${['JavaScript', 'JS', 'Launch'].map(name => [...name].map(character => escaped(character)).join('')).join('|')})(?![\\\\w]|${WORD_ESCAPE})`);\nconst pdfActive = text => PDF_ACTIVE.test(text) || PDF_ACTIVE_ESCAPED.test(text);\n\n/**\n * Inspect a message's attachments.\n *\n * Reports each finding as { type, filename, message, score }:\n * - executable: a program, script, installer or shortcut (by extension or content)\n * - disguised_executable: content is a program but the name says otherwise\n * - double_extension: \"invoice.pdf.exe\"\n * - rtl_override: a right-to-left override hides the real extension (a file named \"invoice\", U+202E, \"fdp.exe\" shows as \"invoiceexe.pdf\")\n * - risky_attachment: a type that is risky by name but not a program\n * (a certificate, an Internet shortcut, a theme; see RISKY_EXTENSIONS)\n * - executable_in_archive: a ZIP holds a program, read without unpacking, or\n * a file compressed on its own is named after one (\"invoice.exe.gz\")\n * (extension: the first program's, extensions: every program's)\n * - script_in_archive: a ZIP holds scripts (JavaScript, Python, shell, Perl)\n * but no other programs\n * - encrypted_archive: a password-protected ZIP, which virus scanners cannot open\n * - macro: an Office document with a VBA project\n * - pdf_active: a PDF with JavaScript or launch actions\n * - rtf_object: an RTF document with an embedded OLE object\n * - html_attachment: an HTML or SVG file, the usual carrier of fake login pages\n *\n * @param {Array<{filename?: string, contentType?: string, content?: Buffer}>} attachments\n * @returns {Array<{type: string, filename: string, message: string, extension?: string, extensions?: string[]}>}\n */\nexport function inspectAttachments(attachments = []) {\n\tconst findings = [];\n\tfor (const attachment of attachments) {\n\t\tconst filename = typeof attachment.filename === 'string' ? attachment.filename : '';\n\t\tconst name = filename || 'unnamed attachment';\n\t\tconst content = Buffer.isBuffer(attachment.content) ? attachment.content : null;\n\t\tconst extension = extensionOf(filename);\n\t\tconst detected = content ? sniff(content) : null;\n\t\tconst add = (type, message, extra = {}) => findings.push({\n\t\t\ttype, filename: name, message, ...extra,\n\t\t});\n\n\t\tif (BIDI_CONTROLS.test(filename)) {\n\t\t\tadd('rtl_override', `Attachment \"${name}\" uses a right-to-left override to hide its real extension`);\n\t\t}\n\n\t\t// (also \"invoice.pdf .exe\", a space before the last dot)\n\t\tconst parts = trimEndOf(filename.toLowerCase(), '. ').split('.');\n\t\tif (parts.length >= 3 && (EXECUTABLE_EXTENSIONS.has(extension) || RISKY_EXTENSIONS.has(extension)) && !TEMPORARY_EXTENSIONS.has(extension) && DOCUMENT_EXTENSIONS.has(parts.at(-2).trim())) {\n\t\t\tadd('double_extension', `Attachment \"${name}\" has a document extension followed by a program extension`, {extension});\n\t\t}\n\n\t\t// A program by its content, whatever it is named (also as a script, a\n\t\t// file type that runs code too, but is often allowed); a script by its\n\t\t// name, else by its content.\n\t\tconst byName = EXECUTABLE_EXTENSIONS.has(extension) && !MACRO_EXTENSIONS.has(extension);\n\t\tif (detected?.executable && (detected.type !== 'script' || !byName)) {\n\t\t\tadd(extension && !EXECUTABLE_EXTENSIONS.has(extension) ? 'disguised_executable' : 'executable', `Attachment \"${name}\" contains a program (${detected.type})${extension && extension !== detected.type ? ` but is named .${extension}` : ''}`, {extension: detected.type});\n\t\t} else if (byName) {\n\t\t\tadd('executable', `Attachment \"${name}\" is a file type that runs code (.${extension})`, {extension});\n\t\t} else if (RISKY_EXTENSIONS.has(extension)) {\n\t\t\tadd('risky_attachment', `Attachment \"${name}\" is a risky file type (.${extension})`, {extension});\n\t\t}\n\n\t\t// (a program compressed on its own, named after it: \"invoice.exe.gz\")\n\t\tconst held = COMPRESSED_EXTENSIONS.has(extension) ? extensionOf(uncompressedName(filename)) : '';\n\t\tif (EXECUTABLE_EXTENSIONS.has(held)) {\n\t\t\tconst script = SCRIPT_EXTENSIONS.has(held);\n\t\t\tadd(script ? 'script_in_archive' : 'executable_in_archive', `Archive \"${name}\" contains ${script ? 'a script' : 'a program'} (${shorten(uncompressedName(filename).split(/[/\\\\]/).pop())})`, {extension: held, extensions: [held]});\n\t\t}\n\n\t\tconst webPage = detected?.type === 'html' || detected?.type === 'svg' || ['htm', 'html', 'mht', 'mhtml', 'shtml', 'xht', 'xhtml', 'svg'].includes(extension);\n\t\tconst addWebPage = () => {\n\t\t\tconst active = content !== null && /<script\\b|<form\\b|\\bon(?:load|error|click)\\s*=|javascript:|window\\.location|document\\.location|atob\\(/i.test(content.subarray(0, 200_000).toString('utf8'));\n\t\t\tadd('html_attachment', `Attachment \"${name}\" is ${detected?.type === 'svg' || extension === 'svg' ? 'an SVG image' : 'a web page'}${active ? ' with scripts or forms' : ''}`, {extension: extension || detected?.type, active});\n\t\t};\n\n\t\t// (a document named as one that holds macros counts only with a VBA\n\t\t// project inside; a web page without content, a part named as one,\n\t\t// counts by name)\n\t\tif (!content) {\n\t\t\tif (webPage) {\n\t\t\t\taddWebPage();\n\t\t\t}\n\n\t\t\tcontinue;\n\t\t}\n\n\t\t// (a ZIP named .zip after other data, as self-extracting archives are,\n\t\t// opens in archive programs too)\n\t\tconst zipFamily = ZIP_FAMILY.has(detected?.type) || (extension === 'zip' && !detected?.executable && entriesOf(content).length > 0);\n\t\tif (zipFamily) {\n\t\t\tconst entries = entriesOf(content);\n\t\t\tconst office = OFFICE_TYPES.has(detected?.type);\n\t\t\tif (entries.some(entry => /(?:^|\\/)vbaproject\\.bin$/i.test(entry.name))) {\n\t\t\t\tadd('macro', `Attachment \"${name}\" is an Office document with a VBA macro project`, {extension});\n\t\t\t} else if (office && entries.some(entry => MACRO_EXTENSIONS.has(extensionOf(entry.name)))) {\n\t\t\t\t// (as Office names an embedded workbook or document with macros:\n\t\t\t\t// \"word/embeddings/Microsoft_Excel_Macro-Enabled_Worksheet.xlsm\")\n\t\t\t\tadd('macro', `Attachment \"${name}\" is an Office document holding another with macros`, {extension});\n\t\t\t}\n\n\t\t\t// (an Office document holding a program is an archive of programs\n\t\t\t// too; not the documents and certificates Office itself embeds)\n\t\t\t// (and one compressed on its own, by its name: \"setup.exe.gz\")\n\t\t\tconst inner = entries.filter(entry => EXECUTABLE_EXTENSIONS.has(extensionOf(uncompressedName(entry.name))) && !(office && OFFICE_EMBEDDED.has(extensionOf(entry.name))));\n\t\t\tconst details = {extension: extensionOf(uncompressedName(inner[0]?.name ?? '')), extensions: [...new Set(inner.map(entry => extensionOf(uncompressedName(entry.name))))]};\n\t\t\tconst listed = inner.slice(0, 3).map(entry => shorten(entry.name.split('/').pop())).join(', ');\n\t\t\tif (inner.some(entry => !SCRIPT_EXTENSIONS.has(extensionOf(uncompressedName(entry.name))))) {\n\t\t\t\tadd('executable_in_archive', `Archive \"${name}\" contains ${inner.length === 1 ? 'a program' : `${inner.length} programs`} (${listed})`, details);\n\t\t\t} else if (inner.length > 0) {\n\t\t\t\tadd('script_in_archive', `Archive \"${name}\" contains ${inner.length === 1 ? 'a script' : `${inner.length} scripts`} (${listed})`, details);\n\t\t\t}\n\n\t\t\t// (archives only: Office documents are encrypted otherwise)\n\t\t\tif ((detected?.type === 'zip' || !ZIP_FAMILY.has(detected?.type)) && entries.some(entry => entry.encrypted)) {\n\t\t\t\tadd('encrypted_archive', `Archive \"${name}\" is password protected, so its contents cannot be scanned`, {extension});\n\t\t\t}\n\t\t} else if (detected?.type === 'ole') {\n\t\t\tif (oleHasMacros(content)) {\n\t\t\t\tadd('macro', `Attachment \"${name}\" is an Office document with a VBA macro project`, {extension});\n\t\t\t}\n\t\t} else if (detected?.type === 'pdf' || extension === 'pdf') {\n\t\t\tif (pdfActive(content.toString('latin1'))) {\n\t\t\t\tadd('pdf_active', `PDF \"${name}\" contains JavaScript or a launch action`, {extension: 'pdf'});\n\t\t\t}\n\t\t} else if (detected?.type === 'rtf' && /\\\\object\\b|\\\\objdata\\b|\\\\objupdate\\b/.test(content.toString('latin1'))) {\n\t\t\tadd('rtf_object', `RTF document \"${name}\" embeds an OLE object`, {extension: 'rtf'});\n\t\t}\n\n\t\tif (webPage) {\n\t\t\taddWebPage();\n\t\t}\n\t}\n\n\treturn findings;\n}\n", "import {createRequire} from 'node:module';\n\n/**\n * Regular expressions for message content.\n *\n * A message can be made of anything, so every pattern used on its content is\n * written to match in linear time as a JavaScript regular expression: no two\n * quantifiers that can match the same text in a row, and repetition bounded\n * where a run of characters could otherwise be scanned again from every\n * position in it. When the optional re2 package is installed, the patterns\n * run on RE2, which guarantees linear time (and every pattern here is valid\n * RE2: no backreferences, no lookaround, repetition counts up to 1000). A\n * pattern finds the same text on either engine, but for a bounded run of a\n * class of Unicode properties, which is unbounded on RE2 (see BOUNDED).\n */\n\n/**\n * The RE2 class from the re2 package, or null when it is not installed (or\n * cannot load, as in a single executable binary).\n * @param {Function} [load] - require()\n * @returns {Function|null}\n */\nexport function loadRe2(load = createRequire(import.meta.url)) {\n\ttry {\n\t\treturn load('re2');\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nconst RE2 = loadRe2();\n\n/**\n * Whether patterns run on RE2.\n * @type {boolean}\n */\nexport const usesRe2 = RE2 !== null;\n\n// Repetition bounded so that JavaScript stays linear ({0,n} or {1,n}, n of 10\n// or more). RE2 compiles each repetition separately, so a bounded class of\n// Unicode properties (\"[\\p{L}]{1,64}\") can exceed its size limit: that one\n// is unbounded on RE2, which then finds a longer run of letters than\n// JavaScript does. The local parts and host labels of email addresses\n// (EMAIL_SOURCE and ADDRESS_SOURCE in tokenizer.js) are such classes: past 64\n// and 63 characters, RE2 reads an address JavaScript reads only the end of,\n// or not at all (in any script). Every other bound stays, so both engines\n// find the same links and host names outside addresses (bounds that are\n// those of DNS anyway).\nconst BOUNDED = /^{([01]),\\d{2,}}/;\n\n// JavaScript's \\s (RE2's is ASCII only, without the no-break space that\n// mailparser makes of \"&nbsp;\", or the ideographic space)\nconst WHITESPACE = String.raw`\\t\\n\\x0B\\f\\r \\x{A0}\\x{1680}\\x{2000}-\\x{200A}\\x{2028}\\x{2029}\\x{202F}\\x{205F}\\x{3000}\\x{FEFF}`;\n\n/**\n * A JavaScript pattern for RE2, matching the same text: \\s and \\S as in\n * JavaScript, and a bounded repetition of a class of Unicode properties made\n * unbounded (see BOUNDED).\n * @param {string} source\n * @returns {string}\n */\nexport function re2Source(source) {\n\tlet out = '';\n\tlet inClass = false;\n\t// (whether the class read last has a Unicode property, and whether it is\n\t// what a repetition after it repeats)\n\tlet unicodeClass = false;\n\tlet afterClass = false;\n\tfor (let i = 0; i < source.length; i++) {\n\t\tconst char = source[i];\n\t\tconst repeats = afterClass;\n\t\tafterClass = false;\n\t\tif (char === '{' && !inClass && repeats && unicodeClass) {\n\t\t\tconst bound = BOUNDED.exec(source.slice(i, i + 16));\n\t\t\tif (bound) {\n\t\t\t\tout += bound[1] === '0' ? '*' : '+';\n\t\t\t\ti += bound[0].length - 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\tif (char === '\\\\') {\n\t\t\tconst next = source[++i] ?? '';\n\t\t\tif (inClass && (next === 'p' || next === 'P')) {\n\t\t\t\tunicodeClass = true;\n\t\t\t}\n\n\t\t\tif (next === 's') {\n\t\t\t\tout += inClass ? WHITESPACE : `[${WHITESPACE}]`;\n\t\t\t} else if (next === 'S') {\n\t\t\t\tif (inClass) {\n\t\t\t\t\tthrow new TypeError(String.raw`\\S inside a character class has no RE2 equivalent here`);\n\t\t\t\t}\n\n\t\t\t\tout += `[^${WHITESPACE}]`;\n\t\t\t} else {\n\t\t\t\tout += `\\\\${next}`;\n\t\t\t}\n\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (char === '[' && !inClass) {\n\t\t\tinClass = true;\n\t\t\tunicodeClass = false;\n\t\t} else if (char === ']' && inClass) {\n\t\t\tinClass = false;\n\t\t\tafterClass = true;\n\t\t}\n\n\t\tout += char;\n\t}\n\n\treturn out;\n}\n\n/**\n * A regular expression on RE2 when it is installed, otherwise a JavaScript\n * one (written to run in linear time either way).\n * @param {string} source\n * @param {string} [flags]\n * @param {Function|null} [Engine] - the RE2 class, or null for JavaScript\n * @returns {RegExp}\n */\nexport function linear(source, flags = '', Engine = RE2) {\n\treturn Engine ? new Engine(re2Source(source), flags) : new RegExp(source, flags);\n}\n\n/**\n * Each match of a global pattern in text, found with exec from the start\n * (String.prototype.matchAll makes a copy of the pattern for each call, which\n * on RE2 compiles it again). A match of empty text moves on a character, so\n * the search always ends.\n * @param {RegExp} pattern - a pattern with the g flag\n * @param {string} text\n * @returns {Generator<RegExpExecArray>}\n */\nexport function * matchesOf(pattern, text) {\n\tpattern.lastIndex = 0;\n\tlet match;\n\twhile ((match = pattern.exec(text)) !== null) {\n\t\tif (match[0] === '') {\n\t\t\tpattern.lastIndex++;\n\t\t}\n\n\t\tyield match;\n\t}\n}\n\n/**\n * The string without any of `characters` at its end, by scanning back from\n * the end (a pattern such as /[).,]+$/ is tried at every position of a long\n * run of those characters).\n * @param {string} value\n * @param {string} characters\n * @returns {string}\n */\nexport function trimEndOf(value, characters) {\n\tlet end = value.length;\n\twhile (end > 0 && characters.includes(value[end - 1])) {\n\t\tend--;\n\t}\n\n\treturn value.slice(0, end);\n}\n\n/**\n * Every tag replaced by `replacement`, as `html.replace(/<[^>]*>/g, ...)`\n * does: from each \"<\" to the next \">\". Found with indexOf, which reads each\n * character once: once no \">\" follows, no later \"<\" has one either (the\n * regular expression would read the rest again from every \"<\").\n * @param {string} html\n * @param {string} [replacement]\n * @returns {string}\n */\nexport function replaceTags(html, replacement = ' ') {\n\tlet out = '';\n\tlet from = 0;\n\tlet open = html.indexOf('<');\n\twhile (open !== -1) {\n\t\tconst close = html.indexOf('>', open + 1);\n\t\tif (close === -1) {\n\t\t\tbreak;\n\t\t}\n\n\t\tout += `${html.slice(from, open)}${replacement}`;\n\t\tfrom = close + 1;\n\t\topen = html.indexOf('<', from);\n\t}\n\n\treturn out + html.slice(from);\n}\n\n/**\n * A pattern for a word of ASCII letters in either case, ASCII only. (RE2\n * folds case as Unicode does, where \"\\u017F\" is an \"s\" and \"\\u212A\" a \"k\";\n * JavaScript does only with the u flag.)\n * @param {string} word\n * @returns {string}\n */\nexport function caseless(word) {\n\treturn [...word].map(letter => `[${letter}${letter.toUpperCase()}]`).join('');\n}\n\nconst FOLDS_TO_ASCII = /[\\u017F\\u212A]/g;\n\n/**\n * Text with the two characters outside ASCII that fold to ASCII letters,\n * \"\\u017F\" and \"\\u212A\", as \"s\" and \"k\", as host names map them (UTS #46),\n * so that case-insensitive patterns read them alike on either engine. (Each\n * is one UTF-16 unit, so positions in the text stay.)\n * @param {string} text\n * @returns {string}\n */\nexport function foldToAscii(text) {\n\treturn text.replaceAll(FOLDS_TO_ASCII, character => character === '\\u017F' ? 's' : 'k');\n}\n\n// (tag names: as browsers read them, \"<\\u017Fcript>\" is no script)\nconst OPENER = linear(String.raw`<(${caseless('script')}|${caseless('style')})\\b`, 'g');\nconst CLOSERS = {\n\tscript: linear(String.raw`<\\/${caseless('script')}\\s*>`, 'g'),\n\tstyle: linear(String.raw`<\\/${caseless('style')}\\s*>`, 'g'),\n};\n\n/**\n * HTML without its script and style elements (their content included), with\n * every tag replaced by `replacement`.\n * @param {string} html\n * @param {string} [replacement]\n * @returns {string}\n */\nexport function stripTags(html, replacement = ' ') {\n\tif (typeof html !== 'string' || html === '') {\n\t\treturn '';\n\t}\n\n\t// Once an element has no closing tag, no later one of its kind has, so\n\t// the rest is not searched again.\n\tconst unclosed = new Set();\n\tlet out = '';\n\tlet from = 0;\n\tOPENER.lastIndex = 0;\n\tlet match;\n\twhile ((match = OPENER.exec(html)) !== null) {\n\t\tconst name = match[1].toLowerCase();\n\t\tif (unclosed.has(name)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst closer = CLOSERS[name];\n\t\tcloser.lastIndex = match.index + match[0].length;\n\t\tconst close = closer.exec(html);\n\t\tif (close === null) {\n\t\t\tunclosed.add(name);\n\t\t\tcontinue;\n\t\t}\n\n\t\tout += `${html.slice(from, match.index)}${replacement}`;\n\t\tfrom = close.index + close[0].length;\n\t\tOPENER.lastIndex = from;\n\t}\n\n\tout += html.slice(from);\n\treturn replaceTags(out, replacement);\n}\n", "import {parse as parseDomain} from 'tldts';\nimport {BRANDS} from './homograph.js';\nimport {decodeEntities, visibleText} from './html.js';\nimport {scriptOf, detectLanguage} from './language.js';\nimport {\n\tfoldToAscii, linear, matchesOf, trimEndOf,\n} from './linear.js';\n\n/**\n * A regular expression matching any character in the given ranges of code\n * points, each [first] or [first, last]. Built from numbers so invisible\n * characters never appear in the source.\n * @param {number[][]} ranges\n * @param {string} [flags]\n * @returns {RegExp}\n */\nexport function characterClass(ranges, flags = 'gu') {\n\tconst hex = code => `\\\\u{${code.toString(16)}}`;\n\treturn new RegExp(`[${ranges.map(([first, last]) => (last ? `${hex(first)}-${hex(last)}` : hex(first))).join('')}]`, flags);\n}\n\n// Invisible characters spammers put inside words to break filters: zero width\n// space, word joiner, byte order mark, soft hyphen and bidirectional overrides.\n// Joiners (U+200C, U+200D) are legitimate in Persian, Indic scripts and emoji,\n// so they are removed without counting as obfuscation.\nconst INVISIBLE = [[0xAD], [0x3_4F], [0x11_5F, 0x11_60], [0x17_B4, 0x17_B5], [0x18_0E], [0x20_0B], [0x20_0E, 0x20_0F], [0x20_2A, 0x20_2E], [0x20_60, 0x20_64], [0x20_66, 0x20_69], [0x31_64], [0xFE_FF], [0xFF_A0]];\nconst OBFUSCATING = characterClass(INVISIBLE);\nconst JOINERS = characterClass([[0x20_0C, 0x20_0D], [0xFE_00, 0xFE_0F]]);\n// Those characters and the joiners, as normalizeText reads them: a joiner\n// counts only between Latin letters (\"v\\u200Ci\\u200Cagra\"), whose words never\n// need one (not in Persian, Indic scripts or emoji sequences).\nconst INVISIBLE_OR_JOINER = characterClass([...INVISIBLE, [0x20_0C, 0x20_0D]]);\nconst ZERO_WIDTH_SPACE = '\\u200B';\nconst ZERO_WIDTH_JOINERS = new Set(['\\u200C', '\\u200D']);\nconst LATIN_END = /\\p{Script=Latin}$/u;\nconst LATIN_START = /^\\p{Script=Latin}/u;\n// Scripts written without spaces between words, which may separate them with\n// zero width spaces (Thai, Lao, Khmer, Burmese and Tibetan): one between two\n// of their letters (or marks) is a word break, no obfuscation.\nconst ZWSP_SCRIPTS = String.raw`\\p{Script=Thai}\\p{Script=Lao}\\p{Script=Khmer}\\p{Script=Myanmar}\\p{Script=Tibetan}`;\nconst ZWSP_SCRIPT_END = new RegExp(`[${ZWSP_SCRIPTS}]$`, 'u');\nconst ZWSP_SCRIPT_START = new RegExp(`^[${ZWSP_SCRIPTS}]`, 'u');\n// Of these, the ones that are no obfuscation: the soft hyphen, a hint where a\n// long word may break (German writes \"Garten\\u00ADm\\u00F6bel\"), and the\n// bidirectional marks, embeddings and isolates in text with Hebrew or Arabic\n// letters, which need them around numbers and Latin words (not the overrides,\n// U+202D and U+202E, which show letters in another order than they are\n// written: \"pay\\u202Elap\" reads \"paypal\").\nconst SOFT_HYPHEN = '\\u00AD';\nconst BIDI = characterClass([[0x20_0E, 0x20_0F], [0x20_2A, 0x20_2C], [0x20_66, 0x20_69]], 'u');\nconst RIGHT_TO_LEFT = /[\\p{Script=Hebrew}\\p{Script=Arabic}]/u;\n// A letter (or the mark of one) or a digit at the end or the start of a\n// string, two code units for one outside the Basic Multilingual Plane.\nconst LETTER_END = /[\\p{L}\\p{M}\\p{N}]$/u;\nconst LETTER_START = /^[\\p{L}\\p{M}\\p{N}]/u;\nconst DIGIT_END = /\\p{N}$/u;\nconst DIGIT_START = /^\\p{N}/u;\n\n// Mathematical letters and squared Latin letters, which NFKC folds to plain\n// ones (not the negative circled and squared letters after them, \"\\u{1F150}\"\n// to \"\\u{1F189}\", which NFKC keeps: emoji such as \"\\u{1F17F}\\uFE0F\" for\n// parking).\nconst STYLED_LETTERS = characterClass([[0x1_D4_00, 0x1_D7_FF], [0x1_F1_30, 0x1_F1_49]], 'u');\n// Fullwidth Latin letters and circled numbers and letters, also folded, but\n// written as they are in Chinese, Japanese and Korean text (\"\\uFF30\\uFF24\\uFF26\",\n// \"\\u2460\"): styled only in text without those scripts.\nconst WIDE_LETTERS = characterClass([[0x24_60, 0x24_FF], [0xFF_21, 0xFF_3A], [0xFF_41, 0xFF_5A]], 'u');\nconst CJK = /[\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}]/u;\n\n// Cyrillic and Greek letters that look like Latin ones. Applied only inside a\n// word that mixes scripts (\"p\u0430ypal\" with a Cyrillic \"\u0430\"), never to a word that\n// is entirely Cyrillic or Greek.\nconst LOOKALIKES = {\n\t\u0430: 'a', \u0432: 'b', \u0435: 'e', \u0451: 'e', \u043A: 'k', \u043C: 'm', \u043D: 'h', \u043E: 'o', \u0440: 'p', \u0441: 'c', \u0442: 't', \u0443: 'y', \u0445: 'x', \u0455: 's', \u0456: 'i', \u0457: 'i', \u0458: 'j', \u0501: 'd', \u051B: 'q', \u051D: 'w', \u04BB: 'h', \u04CF: 'l', \u0261: 'g', \u044C: 'b', \u043F: 'n', \u0433: 'r',\n\t\u03B1: 'a', \u03B2: 'b', \u03B5: 'e', \u03B7: 'n', \u03B9: 'i', \u03BA: 'k', \u03BD: 'v', \u03BF: 'o', \u03C1: 'p', \u03C4: 't', \u03C5: 'u', \u03C7: 'x', \u03C9: 'w', \u03B3: 'y', \u03BC: 'u',\n};\n\n// Greek letters of units, \"\\u00B5\" and \"\\u2126\" (\"10 \\u00B5g\", \"5 k\\u2126\"), as NFKC\n// and lowercasing make them: a unit (\"\\u03BCg\", \"k\\u03C9\", \"10\\u03BCmol\",\n// \"\\u03BCm\\u00B2\", which NFKC makes \"\\u03BCm2\") with\n// no other Greek letter mixes no scripts (but \"\\u03BCpdate\" does).\nconst UNIT_LETTERS = new Set(['\\u03BC', '\\u03C9']);\nconst UNIT_WORD = /^[\\d.,]*[kmg\u03BC]?[\u03BC\u03C9][a-z]{0,3}\\d?$/u;\n\n// Digits used as letters (\"fr33dom\", \"v1agra\").\nconst LEET = {\n\t0: 'o', 1: 'i', 3: 'e', 4: 'a', 5: 's', 7: 't', 8: 'b',\n};\n// One or two of those digits between letters, in a word of Latin letters (see\n// isLeet).\nconst LEET_DIGITS = /\\p{L}[0134578]{1,2}\\p{L}/u;\n\n// Scripts written without spaces between words: single characters are words.\nconst UNSPACED = new Set(['Han', 'Hiragana', 'Katakana', 'Thai', 'Lao', 'Khmer', 'Myanmar']);\n\n// Patterns for message content (see linear.js). Host names and email\n// addresses are bounded as DNS and SMTP bound them (63 characters a label,\n// 64 a local part), so a long run of letters is not read again from each\n// position in it (on RE2, which is linear anyway, the classes of letters of\n// any script in addresses are not bounded: see BOUNDED in linear.js).\nconst URL_PATTERN = linear(String.raw`\\b(?:https?:\\/\\/|www\\.)[^\\s<>\"'\\x60]+|\\b[a-z\\d][a-z\\d-]{0,62}(?:\\.[a-z\\d-]{1,63}){0,10}\\.[a-z]{2,24}\\/[^\\s<>\"'\\x60]*`, 'giu');\nexport const EMAIL_SOURCE = String.raw`[\\p{L}\\p{N}._%+-]{1,64}@([\\p{L}\\p{N}-]{1,63}(?:\\.[\\p{L}\\p{N}-]{1,63}){0,10}\\.\\p{L}{2,24})`;\nconst EMAIL_PATTERN = linear(EMAIL_SOURCE, 'gu');\nconst PHONE_PATTERN = linear(String.raw`(?:\\+|0{2})\\d[\\d\\s().-]{7,}\\d|\\(\\d{3}\\)\\s?\\d{3}[\\s.-]\\d{4}|\\b(?:\\d{3}[.-]){2}\\d{4}\\b`, 'g');\nconst IP_PATTERN = linear(String.raw`\\b(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1?\\d?\\d)){3}\\b`, 'g');\nconst MONEY_PATTERN = linear(String.raw`[$\u20AC\u00A3\u00A5\u20B9\u20BD\u20A9\u20BA\u20A6]\\s?\\d|\\d\\s?(?:[$\u20AC\u00A3\u00A5\u20B9\u20BD\u20A9\u20BA\u20A6]|usd|eur|gbp|dollars?|euros?)\\b`, 'giu');\nconst BITCOIN_PATTERN = linear(String.raw`\\b(?:bc1[ac-hj-np-z02-9]{25,60}|[13][a-km-zA-HJ-NP-Z1-9]{25,34})\\b`, 'g');\nconst CARD_PATTERN = linear(String.raw`\\b(?:\\d[ -]?){13,19}\\b`, 'g');\n// An HTML link: the opening tag, then the href in it and the text up to\n// \"</a>\" (see anchors).\n// (\"<a\" then a space, \"/\" or \">\", as browsers end a tag name: not \"<a-b\")\nconst ANCHOR_START = linear(String.raw`<a[\\t\\n\\f\\r />]`, 'gi');\n// The most characters of HTML a link's text is read from (see anchors)\nconst MAX_LINK_TEXT = 2000;\n// The most of a link's address kept (its host is always kept whole)\nconst MAX_URL = 16_384;\nconst SHOWN_URL = linear(String.raw`^(?:https?:\\/\\/|www\\.)\\S+$|^[a-z\\d-]{1,63}(?:\\.[a-z\\d-]{1,63}){1,10}(?:\\/\\S*)?$`, 'i');\nconst IMG_PIXEL = linear(String.raw`<img\\b[^<>]*\\b(?:width|height)\\s*=\\s*[\"']?[01][\"'\\s>]`, 'i');\nconst IMG = linear(String.raw`<img\\b`, 'gi');\n// A host name written without \"http://\" (\"paypa1-secure.example\") or in an\n// email address.\nconst BARE_HOST = linear(String.raw`\\b[a-z\\d][a-z\\d-]{0,62}(?:\\.[a-z\\d-]{1,63}){0,9}\\.[a-z]{2,24}\\b`, 'gi');\nconst MAX_BARE_HOSTS = 50;\n// A character of a name, at the end of a string (two code units for one\n// outside the Basic Multilingual Plane).\nconst NAME_END = /[\\p{L}\\p{N}\\p{M}_]$/u;\n\n// Whether text[index] continues a longer name: after a letter, digit or mark,\n// or after a \".\" or \"-\" that follows one (\"nchen-ups.de\" in \"m\u00FCnchen-ups.de\",\n// not \"paypa1.com\" in \"Go...paypa1.com\"). Not after a letter or mark of a\n// script written without spaces (\"paypal-secure.com/login\" in\n// \"\\u8ACB\\u8A2A\\u554Fpaypal-secure.com/login\").\nfunction continuesName(text, index) {\n\tconst before = text.slice(Math.max(0, index - 2), index);\n\tif (NAME_END.test(before)) {\n\t\treturn !UNSPACED.has(scriptOf([...before].at(-1)));\n\t}\n\n\treturn (text[index - 1] === '.' || text[index - 1] === '-') && NAME_END.test(text.slice(Math.max(0, index - 3), index - 1));\n}\n\n// Top-level domains that are also file extensions: a name with one dot and\n// one of these (\"report.zip\", \"notes.md\", \"build.sh\") is taken for a file\n// (not international names, \"xn--...\", which are not file names)\nconst FILE_TLDS = new Set(['ai', 'app', 'md', 'mov', 'pm', 'ps', 'py', 'rs', 'sh', 'so', 'zip']);\n// Of these, those that name files far more often than sites in the text of a\n// link (\"README.md\", \"setup.sh\"; not \"linear.app\" or \"character.ai\")\nconst LINK_FILE_TLDS = new Set(['md', 'mov', 'pm', 'ps', 'py', 'rs', 'sh', 'so', 'zip']);\n\n// Hosts of email services that count each click on a link of a message and\n// send the reader on to the address it shows, documented as their click\n// tracking: SendGrid (*.ct.sendgrid.net), Mailchimp (*.list-manage.com),\n// Mandrill (mandrillapp.com), Amazon SES (*.awstrack.me), Postmark\n// (click.pstmrk.it), HubSpot (*.hubspotlinks.com), Constant Contact\n// (r20.rs6.net), Klaviyo (trk.klclick.com), Mailjet (*.mjt.lu),\n// Salesforce Marketing Cloud (click.exacttarget.com), Kit, formerly\n// ConvertKit (click.convertkit-mail.com, click.convertkit-mail2.com,\n// *.click.kit-mail3.com), Brevo, formerly Sendinblue (*.sendibt2.com,\n// *.r.bh.d.sendibt3.com), Campaign Monitor (*.createsend1.com, cmail20.com),\n// MailerLite (*.clicks.mlsend.com), ActiveCampaign (*.lt.acemlnb.com) and\n// beehiiv (link.mail.beehiiv.com, bhclick.com); and Substack's\n// substack.com/redirect/ (see isClickTracker). And the security gateways that\n// rewrite each link of the mail they pass to check it when it is clicked, so\n// a message forwarded from behind one shows the address it had: Microsoft\n// Defender's Safe Links (*.safelinks.protection.outlook.com), Proofpoint\n// (urldefense.com, urldefense.proofpoint.com), Mimecast\n// (protect-*.mimecast.com, url.*.mimecastprotect.com), Barracuda\n// (linkprotect.cudasvc.com) and Cisco (secure-web.cisco.com), whose address\n// the original is read from where its form is known (see unwrapLink). A link\n// through them that shows another address is deceptive only as\n// isDeceptiveLink says.\nconst CLICK_TRACKERS = ['sendgrid.net', 'list-manage.com', 'mandrillapp.com', 'awstrack.me', 'pstmrk.it', 'hubspotlinks.com', 'rs6.net', 'klclick.com', 'mjt.lu', 'exacttarget.com', 'convertkit-mail.com', 'convertkit-mail2.com', 'kit-mail3.com', 'sendibt2.com', 'sendibt3.com', 'createsend1.com', 'cmail20.com', 'clicks.mlsend.com', 'acemlnb.com', 'link.mail.beehiiv.com', 'bhclick.com', 'safelinks.protection.outlook.com', 'urldefense.com', 'urldefense.proofpoint.com', 'mimecast.com', 'mimecastprotect.com', 'cudasvc.com', 'secure-web.cisco.com'];\n\n// The most characters of a link's host read, past those the URL parser drops\n// (below) and its port: the parser's time grows with the square of the labels\n// it converts (non-ASCII or percent-encoded), and of a punycode (\"xn--\")\n// label's length, a second for 40,000 labels or 300 KB; a host is at most\n// 253 characters, and percent-encoding takes nine for one\nconst MAX_URL_HOST = 4096;\nconst AUTHORITY_END = /[/\\\\?#]/g;\n// (the characters the URL parser drops from a host, as IDNA ignores them, e.g.\n// a soft hyphen: a host padded with them is still read)\n// (in UTF-16 code units, which is far faster on long hosts than with the\n// \"u\" flag: U+1BCA0 to U+1BCA3 and U+E0100 to U+E01EF as surrogate pairs)\n// eslint-disable-next-line no-misleading-character-class\nconst IGNORED_IN_HOST = /[\\u00AD\\u034F\\u180B-\\u180D\\u180F\\u200B\\u2060\\u2064\\uFE00-\\uFE0F\\uFEFF]|\\uD82F[\\uDCA0-\\uDCA3]|\\uDB40[\\uDD00-\\uDDEF]/g;\n\n/**\n * A URL as the URL parser reads it, refused (a TypeError, as for one that is\n * no URL) when its host is longer than any host is, so a link in a message\n * cannot hold the parser for seconds.\n * @param {string} address\n * @returns {URL}\n */\nexport function boundedUrl(address) {\n\t// (the parser drops tabs and line breaks anywhere, so they are dropped\n\t// before the host is found)\n\tconst text = String(address).replaceAll(/[\\t\\n\\r]/g, '');\n\tconst colon = text.indexOf(':');\n\tlet start = colon === -1 ? 0 : colon + 1;\n\twhile (start < text.length && (text[start] === '/' || text[start] === '\\\\')) {\n\t\tstart++;\n\t}\n\n\t// (the whole authority, so a host after long userinfo is read, in linear\n\t// time)\n\tAUTHORITY_END.lastIndex = start;\n\tconst end = AUTHORITY_END.exec(text)?.index ?? text.length;\n\t// (a file URL has neither user information nor a port: its host is the\n\t// whole authority)\n\tconst file = /^[\\0- ]*file:/i.test(text);\n\tconst at = file ? start : Math.max(start, text.lastIndexOf('@', end - 1) + 1);\n\n\t// (without its port, e.g. one padded with zeros, and with its\n\t// percent-encoding decoded, as the parser reads it)\n\tconst bracket = text.lastIndexOf(']', end - 1);\n\tconst port = text.lastIndexOf(':', end - 1);\n\tlet host = text.slice(at, !file && port > Math.max(at - 1, bracket) ? port : end);\n\tif (host.length > MAX_URL_HOST && host.includes('%')) {\n\t\ttry {\n\t\t\thost = decodeURIComponent(host);\n\t\t} catch {}\n\t}\n\n\tif (host.length > MAX_URL_HOST && host.replaceAll(IGNORED_IN_HOST, '').length > MAX_URL_HOST) {\n\t\tthrow new TypeError('Invalid URL: host too long');\n\t}\n\n\treturn new URL(text);\n}\n\n// Whether a host is a domain or under it\nconst isUnder = (host, domain) => host === domain || host.endsWith(`.${domain}`);\n\n// Whether a link goes through click tracking or a security gateway (see\n// CLICK_TRACKERS)\nexport function isClickTracker(url) {\n\tconst host = url.hostname.toLowerCase();\n\treturn CLICK_TRACKERS.some(domain => isUnder(host, domain)) || (host === 'substack.com' && url.pathname.startsWith('/redirect/'));\n}\n\n// The address a security gateway's link goes on to, as the gateway wrote it\n// in the link (see CLICK_TRACKERS), or null: the \"url\" parameter of Safe\n// Links, the \"u\" parameter of Proofpoint's second form (\"https-3A__www.example.com_a\",\n// \"-\" and two hex digits for \"%\" and \"_\" for \"/\") and what is between \"/v3/__\"\n// and \"__;\" in its third\nfunction gatewayTarget(url) {\n\tconst host = url.hostname.toLowerCase();\n\tif (isUnder(host, 'safelinks.protection.outlook.com')) {\n\t\treturn url.searchParams.get('url');\n\t}\n\n\tif (host === 'urldefense.proofpoint.com' && url.pathname.startsWith('/v2/')) {\n\t\ttry {\n\t\t\treturn decodeURIComponent((url.searchParams.get('u') ?? '').replaceAll('_', '/').replaceAll(/-([\\da-f]{2})/gi, '%$1'));\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tif (host === 'urldefense.com' && url.pathname.startsWith('/v3/__')) {\n\t\tconst rest = url.href.slice(url.href.indexOf('/v3/__') + 6);\n\t\t// (the last: a link wrapped again has the inner one's before it)\n\t\tconst end = rest.lastIndexOf('__;');\n\t\treturn end === -1 ? null : rest.slice(0, end);\n\t}\n\n\treturn null;\n}\n\n// (gateways unwrapped at most: forwarding through several organizations\n// each with its own wraps a link again; each is read once)\nconst MAX_GATEWAYS = 16;\n\n/**\n * The address a link goes to, as a URL: through a security gateway, the\n * address it goes on to (see gatewayTarget), as many as MAX_GATEWAYS\n * wrapped in one another (past them, the gateway link it got to); or null for\n * an address that is no URL.\n * @param {string} address\n * @returns {URL|null}\n */\nexport function unwrapLink(address) {\n\tlet url;\n\ttry {\n\t\turl = boundedUrl(address);\n\t} catch {\n\t\treturn null;\n\t}\n\n\tfor (let depth = 0; depth < MAX_GATEWAYS; depth++) {\n\t\tconst target = gatewayTarget(url);\n\t\t// (most links go through no gateway: no URL to read, rather than one\n\t\t// that throws, which takes far longer)\n\t\tif (!target) {\n\t\t\tbreak;\n\t\t}\n\n\t\tlet next;\n\t\ttry {\n\t\t\tnext = boundedUrl(target);\n\t\t} catch {\n\t\t\tbreak;\n\t\t}\n\n\t\tif (next.protocol !== 'http:' && next.protocol !== 'https:') {\n\t\t\tbreak;\n\t\t}\n\n\t\turl = next;\n\t}\n\n\treturn url;\n}\n\nconst BRAND_NAMES = new Set(BRANDS);\n\nconst SHORTENERS = new Set(['bit.ly', 'tinyurl.com', 'goo.gl', 't.co', 'ow.ly', 'is.gd', 'buff.ly', 'rebrand.ly', 'cutt.ly', 'shorturl.at', 'tiny.cc', 'rb.gy', 'bit.do', 'v.gd', 's.id', 't.ly', 'lnkd.in', 'qrco.de', 'shorturl.asia', 'urlz.fr']);\n\nconst FREEMAIL = new Set(['gmail.com', 'googlemail.com', 'yahoo.com', 'ymail.com', 'hotmail.com', 'outlook.com', 'live.com', 'msn.com', 'aol.com', 'icloud.com', 'me.com', 'mail.com', 'gmx.com', 'gmx.de', 'gmx.net', 'web.de', 'yandex.ru', 'yandex.com', 'mail.ru', 'qq.com', '163.com', '126.com', 'proton.me', 'protonmail.com', 'zoho.com', 'naver.com', 'daum.net', 'rediffmail.com', 'libero.it', 'orange.fr', 'laposte.net', 'wp.pl', 'o2.pl', 'seznam.cz', 'rambler.ru', 'tutanota.com', 'tuta.com']);\n\nconst segmenters = new Map();\nfunction segmenter(key = 'und') {\n\tif (!segmenters.has(key)) {\n\t\tlet instance;\n\t\ttry {\n\t\t\tinstance = new Intl.Segmenter(key, {granularity: 'word'});\n\t\t} catch {\n\t\t\tinstance = new Intl.Segmenter('und', {granularity: 'word'});\n\t\t}\n\n\t\tsegmenters.set(key, instance);\n\t}\n\n\treturn segmenters.get(key);\n}\n\n/**\n * Split text into pieces of at most `size` characters, at whitespace or\n * punctuation where possible.\n *\n * Intl.Segmenter takes quadratic time on long strings in Node.js 18 (158,000\n * characters take ten seconds), so text is segmented in short pieces.\n *\n * @param {string} text\n * @param {number} [size]\n * @returns {string[]}\n */\nexport function chunk(text, size = 1000) {\n\tconst pieces = [];\n\tlet start = 0;\n\twhile (start < text.length) {\n\t\tlet end = Math.min(start + size, text.length);\n\t\tif (end < text.length) {\n\t\t\tconst window = text.slice(start, end);\n\t\t\tconst cut = Math.max(window.lastIndexOf(' '), window.lastIndexOf('\\n'), window.lastIndexOf('\u3002'), window.lastIndexOf('\uFF0C'), window.lastIndexOf('\u3001'));\n\t\t\tif (cut > size / 4) {\n\t\t\t\tend = start + cut + 1;\n\t\t\t} else if (text.codePointAt(end - 1) > 0xFF_FF) {\n\t\t\t\t// Never split a surrogate pair (codePointAt at a high surrogate\n\t\t\t\t// reads the whole pair).\n\t\t\t\tend--;\n\t\t\t}\n\t\t}\n\n\t\tpieces.push(text.slice(start, end));\n\t\tstart = end;\n\t}\n\n\treturn pieces;\n}\n\n/**\n * Text as a person reads it, for the language model: without the invisible\n * characters put inside words to break filters (not the joiners scripts and\n * emoji need), with compatibility forms folded (\"\uD835\uDC05\uD835\uDC11\uD835\uDC04\uD835\uDC04\" and \"\uFF26\uFF32\uFF25\uFF25\" are\n * \"FREE\"), case kept.\n * @param {string} text\n * @returns {string}\n */\nexport function readableText(text) {\n\treturn typeof text === 'string' ? text.replaceAll(OBFUSCATING, '').normalize('NFKC') : '';\n}\n\n/**\n * Normalize text the way the classifier sees it: invisible characters\n * removed, compatibility forms folded (NFKC turns \"\uFF46\uFF52\uFF45\uFF45\" and \"\uD835\uDC05\uD835\uDC11\uD835\uDC04\uD835\uDC04\" into\n * \"free\"), lowercased.\n * @param {string} text\n * @returns {{text: string, invisible: number, styled: boolean}}\n */\nexport function normalizeText(text) {\n\tif (typeof text !== 'string' || text === '') {\n\t\treturn {text: '', invisible: 0, styled: false};\n\t}\n\n\t// Invisible characters are counted only inside a word, between two\n\t// letters or a letter and a digit (\"v\\u200B1\\u200Bagra\"; not a byte order\n\t// mark at the start of the text, one at the end of a word, or one between\n\t// digits, which senders put in numbers so phones do not take them for\n\t// phone numbers): each run of them once it ends. (Read one by one: a\n\t// pattern for a run of them fails on a long one.)\n\t// Zero width spaces between two letters of a script that separates words\n\t// with them (see ZWSP_SCRIPTS) do not count, and joiners count only\n\t// between Latin letters (see INVISIBLE_OR_JOINER), but neither ends a run\n\t// (\"v\\u200D\\u200Bi\" has one between two letters).\n\tconst rightToLeft = RIGHT_TO_LEFT.test(text);\n\tlet invisible = 0;\n\tlet run = 0;\n\tlet spaces = 0;\n\tlet joiners = 0;\n\tlet runEnd = -1;\n\tlet before = '';\n\tconst settle = () => {\n\t\tconst next = text.slice(runEnd, runEnd + 2);\n\t\tif (LETTER_END.test(before) && LETTER_START.test(next) && !(DIGIT_END.test(before) && DIGIT_START.test(next))) {\n\t\t\tconst wordBreak = ZWSP_SCRIPT_END.test(before) && ZWSP_SCRIPT_START.test(next);\n\t\t\tinvisible += run - (wordBreak ? spaces : 0) + (LATIN_END.test(before) && LATIN_START.test(next) ? joiners : 0);\n\t\t}\n\n\t\trun = 0;\n\t\tspaces = 0;\n\t\tjoiners = 0;\n\t};\n\n\tconst stripped = text.replaceAll(INVISIBLE_OR_JOINER, (char, offset) => {\n\t\tif (offset !== runEnd) {\n\t\t\tsettle();\n\t\t\tbefore = text.slice(Math.max(0, offset - 2), offset);\n\t\t}\n\n\t\trunEnd = offset + 1;\n\t\tif (ZERO_WIDTH_JOINERS.has(char)) {\n\t\t\tjoiners++;\n\t\t} else if (char !== SOFT_HYPHEN && !(rightToLeft && BIDI.test(char))) {\n\t\t\trun++;\n\t\t\tspaces += char === ZERO_WIDTH_SPACE ? 1 : 0;\n\t\t}\n\n\t\treturn '';\n\t}).replaceAll(JOINERS, '');\n\tsettle();\n\tconst styled = STYLED_LETTERS.test(stripped) || (WIDE_LETTERS.test(stripped) && !CJK.test(stripped));\n\treturn {text: stripped.normalize('NFKC').toLowerCase(), invisible, styled};\n}\n\n// The letters of Latin, Cyrillic and Greek in a word, counted by script\n// (not a unit's Greek letter, see UNIT_WORD)\nfunction scriptLetters(word) {\n\tconst unit = UNIT_WORD.test(word);\n\tconst scripts = new Map();\n\tfor (const char of word) {\n\t\tif (/\\p{L}/u.test(char) && !(unit && UNIT_LETTERS.has(char))) {\n\t\t\tconst script = scriptOf(char);\n\t\t\tif (script === 'Latin' || script === 'Cyrillic' || script === 'Greek') {\n\t\t\t\tscripts.set(script, (scripts.get(script) ?? 0) + 1);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn scripts;\n}\n\n// Whether a word mixes Latin, Cyrillic or Greek letters, each part between\n// apostrophes on its own: Russian writes case endings after a name in Latin\n// letters so (\"iPhone'\\u044B\", \"MacBook\\u2019\\u0438\")\nfunction mixedScripts(word) {\n\treturn word.split(/['\\u2019]/).some(part => scriptLetters(part).size > 1);\n}\n\n// Whether a word of Latin letters writes some with digits (\"v1agra\",\n// \"fr33dom\"): three letters or more and at most three digits, none in a run\n// of three, one or two of those that look like letters between letters (not\n// \"ab12cd\", \"i18n\", \"k8s\" or a tracking number such as \"1z999aa10123456784\")\nfunction isLeet(word) {\n\tif (!/^\\p{Script=Latin}+$/u.test(word.replaceAll(/\\d/g, '')) || !LEET_DIGITS.test(word) || /\\d{3}/.test(word)) {\n\t\treturn false;\n\t}\n\n\tconst digits = word.replaceAll(/\\D/g, '').length;\n\treturn digits <= 3 && word.length - digits >= 3;\n}\n\n// The lookalikes of Latin letters in Cyrillic and in Greek (\"p\" is \"\\u0440\"),\n// the first of each in LOOKALIKES\nconst LATIN_LOOKALIKES = {Cyrillic: {}, Greek: {}};\nfor (const [char, latin] of Object.entries(LOOKALIKES)) {\n\tconst target = LATIN_LOOKALIKES[scriptOf(char)];\n\tif (target && !target[latin]) {\n\t\ttarget[latin] = char;\n\t}\n}\n\n// A word that mixes scripts with the lookalike letters of the others written\n// in the script most of its letters are in: \"pa\\u0443pal\" is \"paypal\", and\n// \"\\u043Fp\\u0438\\u0432\\u0435\\u0442\" (with a Latin \"p\") is\n// \"\\u043F\\u0440\\u0438\\u0432\\u0435\\u0442\"; Latin when as many are Latin\nfunction foldLookalikes(word) {\n\tlet most = 'Latin';\n\tconst counts = scriptLetters(word);\n\tfor (const [script, count] of counts) {\n\t\tif (count > (counts.get(most) ?? 0)) {\n\t\t\tmost = script;\n\t\t}\n\t}\n\n\tlet out = '';\n\tfor (const char of word) {\n\t\tconst script = scriptOf(char);\n\t\tout += script === most ? char : ((most === 'Latin' ? LOOKALIKES[char] : script === 'Latin' && LATIN_LOOKALIKES[most][char]) || char);\n\t}\n\n\treturn out;\n}\n\n/**\n * Split text into words in any writing system.\n *\n * Uses the Unicode word boundary rules (Intl.Segmenter), with dictionaries for\n * Chinese, Japanese, Thai, Lao, Khmer and Burmese. Words are normalized with\n * normalizeText first. Returns the words and the obfuscation seen.\n *\n * @param {string} text\n * @param {object} [options]\n * @param {string} [options.locale] - hint for the segmenter, e.g. \"ja\"\n * @param {number} [options.maxLength] - characters of text to read\n * @returns {{words: string[], invisible: number, styled: boolean, mixed: number, leet: number}}\n */\nexport function segmentWords(text, options = {}) {\n\tconst {locale, maxLength = 100_000} = options;\n\tconst normalized = normalizeText(typeof text === 'string' ? text.slice(0, maxLength) : '');\n\tconst words = [];\n\tlet mixed = 0;\n\tlet leet = 0;\n\tconst seg = segmenter(locale);\n\t// (cut again: NFKC makes one character up to 18, \"\\uFDFA\")\n\tfor (const piece of chunk(normalized.text.slice(0, maxLength))) {\n\t\tfor (const {segment, isWordLike} of seg.segment(piece)) {\n\t\t\tif (!isWordLike || !/[\\p{L}\\p{N}]/u.test(segment)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet word = segment;\n\t\t\tif (mixedScripts(word)) {\n\t\t\t\tmixed++;\n\t\t\t\tword = foldLookalikes(word);\n\t\t\t}\n\n\t\t\tif (isLeet(word)) {\n\t\t\t\tleet++;\n\t\t\t\tword = word.replaceAll(/\\d/g, digit => LEET[digit] || digit);\n\t\t\t}\n\n\t\t\t// (a single Latin letter is no word, but one ideograph is, and so is\n\t\t\t// one Korean syllable: \"\\uB3C8\" is \"money\")\n\t\t\tconst first = scriptOf([...word][0]);\n\t\t\tif ([...word].length === 1 && !UNSPACED.has(first) && first !== 'Hangul' && !/\\p{N}/u.test(word)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\twords.push(word);\n\t\t}\n\t}\n\n\treturn {\n\t\twords, invisible: normalized.invisible, styled: normalized.styled, mixed, leet,\n\t};\n}\n\n// Letters of the scripts a mixed-script word mixes (see mixedScripts).\nconst LATIN = /\\p{Script=Latin}/u;\nconst CYRILLIC_OR_GREEK = /[\\p{Script=Cyrillic}\\p{Script=Greek}]/u;\n\n/**\n * The obfuscation in text, as segmentWords counts it ({invisible, mixed,\n * styled}), segmenting into words only the parts of the text that could have\n * a word mixing scripts: those with Latin letters and Cyrillic or Greek ones.\n * @param {string} text\n * @param {object} [options]\n * @param {number} [options.maxLength] - characters of text to read\n * @returns {{invisible: number, mixed: number, styled: boolean}}\n */\nexport function obfuscationOf(text, options = {}) {\n\tconst {maxLength = 100_000} = options;\n\tconst normalized = normalizeText(typeof text === 'string' ? text.slice(0, maxLength) : '');\n\t// (by the pieces segmentWords reads, segmenting only those with both)\n\tlet mixed = 0;\n\tfor (const piece of chunk(normalized.text.slice(0, maxLength))) {\n\t\tif (!LATIN.test(piece) || !CYRILLIC_OR_GREEK.test(piece)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const {segment, isWordLike} of segmenter().segment(piece)) {\n\t\t\tif (isWordLike && mixedScripts(segment)) {\n\t\t\t\tmixed++;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {invisible: normalized.invisible, mixed, styled: normalized.styled};\n}\n\nfunction bucket(n, steps) {\n\tlet i = 0;\n\twhile (i < steps.length && n > steps[i]) {\n\t\ti++;\n\t}\n\n\treturn i;\n}\n\n/**\n * The registrable domain of a host name (\"mail.example.co.uk\" gives\n * \"example.co.uk\"), or the host itself for IP addresses and unknown suffixes.\n * @param {string} host\n * @returns {string}\n */\nexport function registrableDomain(host) {\n\tconst parsed = parseDomain(host, {allowPrivateDomains: true});\n\treturn parsed.domain || parsed.hostname || host;\n}\n\n// The string without C0 control characters and spaces at either end, as the\n// URL parser reads an address (by index: a pattern such as /[\\0- ]+$/ is\n// tried at every position of a long run of them).\nfunction trimControls(value) {\n\tlet start = 0;\n\tlet end = value.length;\n\twhile (start < end && value.codePointAt(start) <= 0x20) {\n\t\tstart++;\n\t}\n\n\twhile (end > start && value.codePointAt(end - 1) <= 0x20) {\n\t\tend--;\n\t}\n\n\treturn value.slice(start, end);\n}\n\n// Whether a name is a file name (\"report.zip\", \"README.md\"): one dot and a\n// top-level domain that is also a file extension (FILE_TLDS, or those given)\nfunction isFileName(name, extensions = FILE_TLDS) {\n\tconst parsed = parseDomain(name.toLowerCase());\n\treturn !parsed.subdomain && extensions.has(parsed.publicSuffix) && !name.toLowerCase().startsWith('xn--');\n}\n\n// Whether a link's text, written as a web address (see SHOWN_URL), is one:\n// with \"http://\", \"https://\" or \"www.\", or a name under a real public suffix\n// that is no file name (\"paypal.com/signin\", not \"v2.0\", \"report.pdf\" or\n// \"report.zip\")\nfunction isAddress(text) {\n\tif (/^(?:https?:\\/\\/|www\\.)/i.test(text)) {\n\t\treturn true;\n\t}\n\n\tconst name = text.split('/')[0];\n\treturn isHost(name) && !isFileName(name, LINK_FILE_TLDS);\n}\n\n// The address a link's text shows: all of it when it is one (as before), or\n// else its first word written as one with \"http://\" or \"https://\" (\"Sign in\n// at https://www.paypal.com\"), without punctuation around it; or null. A\n// name in a sentence is none (\"Read why Booking.com switched\", \"Weitere Infos\n// unter www.firma.de\": the name of a site, or of a file, not the address of\n// the link).\nfunction shownAddress(inner) {\n\tif (SHOWN_URL.test(inner)) {\n\t\treturn isAddress(inner) ? inner : null;\n\t}\n\n\tfor (const word of inner.split(' ')) {\n\t\tconst address = trimEndOf(word.replace(/^[(\"'<[]+/, ''), ').,;:!?\\'\">]');\n\t\tif (/^https?:\\/\\/\\S/i.test(address)) {\n\t\t\treturn address;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Tags inside a line of text, which show no break or space where they are\n// (and comments, \"<!...>\"): a name split by them reads as one.\nconst INLINE_TAG = /^<\\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nobr|q|s|samp|small|span|strike|strong|sub|sup|time|tt|u|var|wbr)(?=[\\t\\n\\f\\r />])|^<!/i;\n// A character of a host name\nconst HOST_CHAR = /[\\p{L}\\p{N}.-]/u;\n\n// Whether a name is a host under a real public suffix (\"www.paypal.com\",\n// not \"www\" or \"www.paypal\")\nfunction isHost(name) {\n\tconst parsed = parseDomain(name.toLowerCase());\n\treturn Boolean(parsed.isIcann && parsed.domain);\n}\n\n// Whether a link read from HTML ends inside its host name at tags after\n// which the name goes on (\"https://www.\" of\n// \"<b>https://www.</b>paypal<b>.com/</b>\"): no link of its own (its text\n// shown, read with the text, has the whole address). Only across tags inside\n// a line (\"<b>\", \"</span>\", not \"</p>\", \"<br>\" or \"</td>\", after which the\n// text is another line or cell), and only when its host is no host on its\n// own (\"https://www.\") or the name it goes on to is another one\n// (\"https://www.pay<i></i>pal.com\"), not a word after it\n// (\"<b>https://evil.example</b>Thanks\"). Looks at most at four tags of 256\n// characters and 253 characters of name after them.\nfunction endsInsideTag(html, end, link) {\n\tconst host = trimEndOf(link.replace(/^https?:\\/\\//i, ''), '.-');\n\tif (html[end] !== '<' || /[/?#:@]/.test(host)) {\n\t\treturn false;\n\t}\n\n\tlet i = end;\n\tfor (let tags = 0; tags < 4 && html[i] === '<'; tags++) {\n\t\tconst tag = html.slice(i, i + 256);\n\t\tconst close = tag.indexOf('>');\n\t\tif (close === -1 || !INLINE_TAG.test(tag)) {\n\t\t\treturn false;\n\t\t}\n\n\t\ti += close + 1;\n\t}\n\n\tlet after = '';\n\twhile (after.length < 253 && HOST_CHAR.test(html[i + after.length] ?? '')) {\n\t\tafter += html[i + after.length];\n\t}\n\n\tconst joined = trimEndOf(link.replace(/^https?:\\/\\//i, '') + after, '.-');\n\treturn joined !== host && (!isHost(host) || (isHost(joined) && registrableDomain(joined) !== registrableDomain(host)));\n}\n\n/**\n * Find the links in plain text and HTML (in its markup, hidden text too, and\n * in the text it shows, see visibleText, where an address split by inline\n * tags reads as one). For each link, the host, whether\n * its address has a user name or password (\"https://paypal.com@evil.example/\"\n * shows one name and goes elsewhere), and for HTML links the address the\n * visible text shows, if any (see shownAddress). A link\n * written without \"http://\" or \"www.\" (\"example.com/page\") counts only with\n * a real top-level domain (not \"file.js/\"), and not from the middle of a\n * word (\"stra\u00DFe.de/\" is no link to \"e.de\").\n * @param {string} text\n * @param {string} html\n * @param {object} [options]\n * @param {number} [options.maxLength] - characters of each read for links\n * written in the text (500,000 by default)\n * @param {boolean} [options.bareHosts] - also the hosts of links without\n * \"http://\" written after punctuation inside another link\n * (\"a.example/x,evil.example/y\"), as the links looked up read them, each\n * as a link \"http://host/\" with bare: true, unless a link has its host\n * @returns {Array<{url: string, host: string, text: string|null, userinfo: boolean, bare?: true}>}\n */\nconst MAX_SHOWN_HTML = 200_000;\n\nexport function extractLinks(text, html, {maxLength = 500_000, bareHosts = false} = {}) {\n\t// (\"\\u017F\" and \"\\u212A\" as browsers read them in host names, on\n\t// either engine)\n\ttext = typeof text === 'string' ? foldToAscii(text) : text;\n\thtml = typeof html === 'string' ? foldToAscii(html) : html;\n\tconst links = [];\n\tconst seen = new Set();\n\t// (the hosts of the links found)\n\tconst linkHosts = new Set();\n\t// (with bareHosts, those of links without \"http://\" in another link)\n\tconst bareFound = [];\n\t// (whether each name of a link without \"http://\" is a host, read once)\n\tconst names = new Map();\n\tconst add = (raw, shown = null, bare = false) => {\n\t\t// (as the URL parser reads it: without tabs and line breaks anywhere,\n\t\t// \"ht\\ttps://evil.example/\" is a link to evil.example)\n\t\tlet href = trimEndOf(trimControls(raw.replaceAll(/[\\t\\n\\r]/g, '')).trim(), ').,;:!?\\'\"]');\n\t\tif (/^www\\./i.test(href)) {\n\t\t\thref = `http://${href}`;\n\t\t}\n\n\t\tif (!/^[a-z][a-z\\d+.-]*:/i.test(href)) {\n\t\t\thref = `http://${href}`;\n\t\t}\n\n\t\tlet url;\n\t\ttry {\n\t\t\turl = boundedUrl(href);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tif (url.protocol !== 'http:' && url.protocol !== 'https:') {\n\t\t\treturn;\n\t\t}\n\n\t\tconst key = `${url.href}\\n${shown || ''}`;\n\t\tif (seen.has(key)) {\n\t\t\treturn;\n\t\t}\n\n\t\tseen.add(key);\n\t\t// (an IPv6 address without its brackets, as other addresses are)\n\t\tconst host = url.hostname.toLowerCase().replace(/\\.$/, '').replace(/^\\[(.*)]$/, '$1');\n\t\t// (the address cut, so whether it has a user name or password is read\n\t\t// before)\n\t\tlinkHosts.add(host);\n\t\tlinks.push({\n\t\t\turl: url.href.slice(0, MAX_URL), host, text: shown, userinfo: Boolean(url.username || url.password), ...(bare && {bare: true}),\n\t\t});\n\t};\n\n\tif (typeof html === 'string' && html) {\n\t\tfor (const {href, inner} of anchors(html)) {\n\t\t\tadd(href, shownAddress(inner));\n\t\t}\n\t}\n\n\t// (the text of HTML with its character references decoded, as it is shown:\n\t// \"https&#58;//example.com/\" is a link, hidden text too; and the text the\n\t// HTML shows, where a name split by tags inside a line reads as one,\n\t// \"https://www.pay<b></b>pa1.com/\": read here, whatever text the caller\n\t// gives, as a link of its own only with a host under a real public\n\t// suffix, not \"https://www.example.comThanks\" of\n\t// \"<b>https://www.example.com</b>Thanks\")\n\tconst markup = typeof html === 'string' && html;\n\t// (the text shown read from as much HTML as the classifier reads: reading\n\t// it costs more than finding links)\n\tconst sources = [text, markup ? decodeEntities(withoutAnchorTags(html)) : '', markup ? visibleText(html.slice(0, Math.min(maxLength, MAX_SHOWN_HTML))).text : ''];\n\tfor (const [index, source] of sources.entries()) {\n\t\tif (typeof source === 'string' && source) {\n\t\t\tconst read = source.slice(0, maxLength);\n\t\t\tfor (const match of matchesOf(URL_PATTERN, read)) {\n\t\t\t\tlet [link] = match;\n\t\t\t\tif (index === 1 && endsInsideTag(read, match.index + link.length, link)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (index === 2 && !isHost(hostOf(link))) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// (a link in brackets, as mailparser writes the links of HTML in\n\t\t\t\t// the text it makes of it: \"here [https://example.com/]\")\n\t\t\t\tif (source[match.index - 1] === '[' && link.includes(']')) {\n\t\t\t\t\tlink = link.slice(0, link.indexOf(']'));\n\t\t\t\t}\n\n\t\t\t\tif (isBareLink(link) && !isBareHost(source, match.index, link, names)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tadd(link);\n\t\t\t}\n\n\t\t\t// (and with bareHosts, the hosts of links without \"http://\" after\n\t\t\t// punctuation in another link, see bareLinkHosts: for the links\n\t\t\t// looked up, not those the classifier learned from)\n\t\t\tif (bareHosts && index < 2) {\n\t\t\t\tbareLinkHosts(read, host => {\n\t\t\t\t\tbareFound.push(host);\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t// (after the links of every part: a host one of them has is no host\n\t// found without a link)\n\tfor (const host of bareFound) {\n\t\tif (!linkHosts.has(host)) {\n\t\t\tadd(`http://${host}/`, null, true);\n\t\t}\n\t}\n\n\treturn links;\n}\n\n// A character that is part of a word (a letter, digit or mark of any\n// script, \"_\" or \"%\"): a name does not start after it\nconst IN_WORD = /[\\p{L}\\p{N}\\p{M}_%]/u;\n// (no more is read back of a name than a host name can be)\nconst MAX_BARE_NAME = 760;\n\n// Whether a character is one of a host name written in ASCII\nfunction isHostCode(code) {\n\treturn (code >= 97 && code <= 122) || (code >= 65 && code <= 90) || (code >= 48 && code <= 57) || code === 45 || code === 46;\n}\n\n// Whether a character ends a link, as URL_PATTERN ends one: a space of any\n// kind, \"<\", \">\", a quote or a backtick\nfunction endsLink(char) {\n\treturn /[\\s<>\"'\\u0060]/u.test(char);\n}\n\n/**\n * The hosts of links written without \"http://\" (\"example.com/page\") in\n * text, also where URL_PATTERN reads them as part of another link's path\n * after punctuation (\"a.example/x,evil.example/y\"): the name before each \"/\",\n * read back to a character that is no part of a host name (so the text is\n * read once), without a port or dots after it, from its top-level domain\n * back over at most eleven labels; not a name after \"/\" or in the middle of\n * a word (\"github.com/x/example.net/\", \"stra\u00DFe.de/\"), nor in a link with\n * \"://\" (up to a character that ends a link, see endsLink), whose host is\n * read from all of it (\"https://x!y.evil.example/\"), nor a name under no\n * real public suffix (\"file.js/\"), as for links without \"http://\" (see\n * isBareHost). Each found once. In linear time.\n * @param {string} text\n * @param {(host: string) => void} add\n */\nfunction bareLinkHosts(text, add) {\n\tconst found = new Set();\n\tlet previous = -1;\n\t// (where the last space and \"://\" before a position are, read once)\n\tlet read = 0;\n\tlet space = -1;\n\tlet scheme = -1;\n\tfor (let slash = text.indexOf('/'); slash !== -1; slash = text.indexOf('/', slash + 1)) {\n\t\tconst floor = Math.max(previous + 1, slash - MAX_BARE_NAME);\n\t\tprevious = slash;\n\t\t// (before a port: \"evil.example:8080/\")\n\t\tlet end = slash;\n\t\twhile (end > floor && text.codePointAt(end - 1) >= 48 && text.codePointAt(end - 1) <= 57) {\n\t\t\tend--;\n\t\t}\n\n\t\tend = end > floor && text[end - 1] === ':' ? end - 1 : slash;\n\t\tlet start = end;\n\t\twhile (start > floor && isHostCode(text.codePointAt(start - 1))) {\n\t\t\tstart--;\n\t\t}\n\n\t\tif (start === end || (start > 0 && (text[start - 1] === '/' || IN_WORD.test(text[start - 1])))) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (; read < start; read++) {\n\t\t\tif (endsLink(text[read])) {\n\t\t\t\tspace = read;\n\t\t\t} else if (text[read] === '/' && text[read - 1] === '/' && text[read - 2] === ':') {\n\t\t\t\tscheme = read;\n\t\t\t}\n\t\t}\n\n\t\tif (scheme > space) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// (without the dots a name ends with: \"evil.example./\")\n\t\twhile (end > start && text[end - 1] === '.') {\n\t\t\tend--;\n\t\t}\n\n\t\tconst labels = text.slice(start, end).split('.');\n\t\tconst tld = labels.pop();\n\t\tif (!/^(?:[a-z]{2,24}|xn--[a-z\\d-]{1,59})$/i.test(tld)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst parts = [];\n\t\twhile (labels.length > 0 && parts.length < 11) {\n\t\t\tconst label = labels.pop();\n\t\t\tif (!/^[a-z\\d-]{1,63}$/i.test(label)) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tparts.unshift(label);\n\t\t}\n\n\t\tif (parts.length === 0) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tparts[0] = parts[0].replace(/^-+/, '');\n\t\tif (!parts[0]) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst host = `${parts.join('.')}.${tld}`.toLowerCase();\n\t\tif (found.has(host)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfound.add(host);\n\t\tif (isHost(host)) {\n\t\t\tadd(host);\n\t\t}\n\t}\n}\n\n// The host name of a link as URL_PATTERN finds it, without a user name, port\n// or punctuation after it (\"www.example.com\" of \"https://user@www.example.com:8080/a\")\nfunction hostOf(link) {\n\tconst authority = link.replace(/^https?:\\/\\//i, '').split(/[/?#\\\\]/)[0];\n\treturn trimEndOf(authority.slice(authority.lastIndexOf('@') + 1).replace(/:\\d*$/, ''), ').,;:!?\\'\"]');\n}\n\n// A link written without \"http://\" or \"www.\"\nfunction isBareLink(link) {\n\treturn !/^(?:https?:\\/\\/|www\\.)/i.test(link);\n}\n\n// Whether a link written without \"http://\" (\"example.com/page\", at\n// `index` in `text`) names a host: not from the middle of a word (see\n// continuesName), and a name under a real public suffix (\"studio.ai/login\"\n// and \"update.zip/download\" are links; \"file.js/\" is not)\nfunction isBareHost(text, index, link, names = new Map()) {\n\tif (continuesName(text, index)) {\n\t\treturn false;\n\t}\n\n\tconst host = link.slice(0, link.indexOf('/')).toLowerCase();\n\tif (!names.has(host)) {\n\t\tconst parsed = parseDomain(host);\n\t\tnames.set(host, Boolean(parsed.isIcann && parsed.domain));\n\t}\n\n\treturn names.get(host);\n}\n\n/**\n * The value of the first href attribute of a tag (\"<a ...>\"), or null:\n * read attribute by attribute, as browsers read them, so an \"href=\" inside\n * the value of another attribute is not taken for one. In one pass.\n * @param {string} tag\n * @returns {string|null}\n */\nexport function hrefOf(tag) {\n\tlet i = tag.search(/[\\t\\n\\f\\r />]/);\n\tif (i === -1) {\n\t\treturn null;\n\t}\n\n\tconst space = char => char === ' ' || char === '\\t' || char === '\\n' || char === '\\f' || char === '\\r';\n\twhile (i < tag.length && tag[i] !== '>') {\n\t\tif (space(tag[i]) || tag[i] === '/') {\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tlet end = i + 1;\n\t\twhile (end < tag.length && !space(tag[end]) && !'=/>'.includes(tag[end])) {\n\t\t\tend++;\n\t\t}\n\n\t\tconst name = tag.slice(i, end).toLowerCase();\n\t\twhile (end < tag.length && space(tag[end])) {\n\t\t\tend++;\n\t\t}\n\n\t\tif (tag[end] !== '=') {\n\t\t\ti = end;\n\t\t\tcontinue;\n\t\t}\n\n\t\tend++;\n\t\twhile (end < tag.length && space(tag[end])) {\n\t\t\tend++;\n\t\t}\n\n\t\tlet value;\n\t\tif (tag[end] === '\"' || tag[end] === '\\'') {\n\t\t\tconst close = tag.indexOf(tag[end], end + 1);\n\t\t\tconst stop = close === -1 ? tag.length : close;\n\t\t\tvalue = tag.slice(end + 1, stop);\n\t\t\tend = stop + 1;\n\t\t} else {\n\t\t\tconst start = end;\n\t\t\twhile (end < tag.length && !space(tag[end]) && tag[end] !== '>') {\n\t\t\t\tend++;\n\t\t\t}\n\n\t\t\tvalue = tag.slice(start, end);\n\t\t}\n\n\t\tif (name === 'href') {\n\t\t\treturn value;\n\t\t}\n\n\t\ti = end;\n\t}\n\n\treturn null;\n}\n\n/**\n * The end of the tag that starts at `start` (just after its \">\"), or -1 when\n * no \">\" follows. A quoted attribute may hold \">\" (\"?a=>b\"); a \"<\" outside\n * quotes is part of the tag, as browsers read it. A quote opens a value only\n * after \"=\" (in `href=https://a.example/\"x` it is part of the value). A quote\n * not closed within 6000 characters is not one: then the tag ends at the next\n * \">\".\n * @param {string} html\n * @param {number} start\n * @returns {number}\n */\nexport function tagEnd(html, start) {\n\tconst limit = Math.min(html.length, start + 6000);\n\tlet quote = null;\n\t// (the last character outside quotes that is not a space)\n\tlet last = '<';\n\tfor (let i = start + 1; i < limit; i++) {\n\t\tconst char = html[i];\n\t\tif (quote) {\n\t\t\tif (char === quote) {\n\t\t\t\tquote = null;\n\t\t\t\tlast = char;\n\t\t\t}\n\t\t} else if (char === '>') {\n\t\t\treturn i + 1;\n\t\t} else if ((char === '\"' || char === '\\'') && last === '=') {\n\t\t\tquote = char;\n\t\t} else if (char !== ' ' && char !== '\\t' && char !== '\\n' && char !== '\\f' && char !== '\\r') {\n\t\t\tlast = char;\n\t\t}\n\t}\n\n\tconst close = html.indexOf('>', start + 1);\n\treturn close === -1 ? -1 : close + 1;\n}\n\n// HTML with its <a> tags replaced by a space (their hrefs are links already),\n// as `html.replace(/<a\\b[^>]*>/gi, ' ')` does: from each \"<a\" to the next\n// \">\" (once no \">\" follows, no later \"<a\" has one either)\nfunction withoutAnchorTags(html) {\n\tlet out = '';\n\tlet from = 0;\n\tANCHOR_START.lastIndex = 0;\n\tlet match;\n\twhile ((match = ANCHOR_START.exec(html)) !== null) {\n\t\tconst close = html.indexOf('>', match.index + 2);\n\t\tif (close === -1) {\n\t\t\tbreak;\n\t\t}\n\n\t\tout += `${html.slice(from, match.index)} `;\n\t\tfrom = close + 1;\n\t\tANCHOR_START.lastIndex = from;\n\t}\n\n\treturn out + html.slice(from);\n}\n\nexport {decodeEntities, visibleText} from './html.js';\n\n// Whether the tag at `index` is \"<a\" (\"<a\" and a space, \"/\" or \">\") or, with\n// `slash`, \"</a\"\nfunction isAnchorTag(html, index, slash = false) {\n\tconst at = slash ? index + 2 : index + 1;\n\treturn (!slash || html[index + 1] === '/') && (html[at] === 'a' || html[at] === 'A') && '\\t\\n\\f\\r />'.includes(html[at + 1] ?? 'x');\n}\n\n// The text of a link whose tag ends at `end`, read tag by tag (so a \"</a>\" in\n// the value of an attribute is none): {inner} up to its \"</a>\" or the next <a>\n// tag, which ends it too, when either is within 2000 characters, else no\n// inner; and `next`, where to look for the next <a> tag (any \"<a\" before it\n// is in a tag read here).\nfunction linkText(html, end) {\n\tlet i = end;\n\tfor (;;) {\n\t\tconst open = html.indexOf('<', i);\n\t\tif (open === -1 || open - end > MAX_LINK_TEXT) {\n\t\t\treturn {inner: null, next: open === -1 ? html.length : open};\n\t\t}\n\n\t\tif (isAnchorTag(html, open)) {\n\t\t\treturn {inner: html.slice(end, open), next: open};\n\t\t}\n\n\t\tconst after = tagEnd(html, open);\n\t\tif (after === -1) {\n\t\t\t// (no \">\" after here: no tag at all, and no \"</a>\")\n\t\t\treturn {inner: null, next: html.length};\n\t\t}\n\n\t\tif (isAnchorTag(html, open, true)) {\n\t\t\treturn {inner: html.slice(end, open), next: after};\n\t\t}\n\n\t\ti = after;\n\t}\n}\n\n/**\n * The links of HTML: each <a> tag with an href, and the text it shows (see\n * visibleText: without hidden text, and here without invisible characters)\n * when \"</a>\" or another <a> tag follows within 2000 characters (otherwise\n * no text: a mail client shows a link never closed, or around a long image\n * or table, all the same).\n *\n * Read in about one pass: a tag is read up to its end, the tags of a link's\n * text up to its \"</a>\", and the next <a> tag looked for after them, so the\n * HTML is not read again for each tag. (A tag whose quote is not closed may\n * read up to 6000 characters, but another such tag within them needs the\n * other quote mark, so at most two do.)\n * @param {string} html\n * @returns {Array<{href: string, inner: string}>}\n */\nexport function anchors(html) {\n\tconst found = [];\n\tANCHOR_START.lastIndex = 0;\n\tlet match;\n\twhile ((match = ANCHOR_START.exec(html)) !== null) {\n\t\tconst end = tagEnd(html, match.index);\n\t\tif (end === -1) {\n\t\t\t// (no \">\" after here: no later tag ends either)\n\t\t\tbreak;\n\t\t}\n\n\t\tconst value = hrefOf(html.slice(match.index, end));\n\t\tif (!value) {\n\t\t\tANCHOR_START.lastIndex = end;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// (folded again: a character reference may stand for \"\\u017F\" or \"\\u212A\")\n\t\tconst href = foldToAscii(decodeEntities(value, {attribute: true}));\n\t\tconst {inner, next} = linkText(html, end);\n\t\tANCHOR_START.lastIndex = next;\n\t\tif (inner === null) {\n\t\t\tfound.push({href, inner: ''});\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst shown = foldToAscii(visibleText(inner).text).replaceAll(OBFUSCATING, '').replaceAll(JOINERS, '').replaceAll(/\\s+/g, ' ').trim();\n\t\tfound.push({href, inner: shown});\n\t}\n\n\treturn found;\n}\n\n/**\n * Host names in text that are not links: written without \"http://\"\n * (\"paypa1-secure.example\") or the domain of an email address. Only names\n * under a real public suffix count (\"node.js\" and \"file.txt\" do not), written\n * in lowercase as addresses are or all in capitals (\"PAYPAL-VERIFY.COM\", read\n * in lowercase; so \"end.Click\" in text missing a space is not taken for one),\n * not the local part of an address, and at most 50.\n * These are for lookalike checks, not classifier features.\n * @param {string} text\n * @param {Set<string>} [known] - hosts of the links already found\n * @returns {string[]}\n */\nexport function extractHosts(text, known = new Set()) {\n\tconst hosts = new Set();\n\tif (typeof text !== 'string' || text === '') {\n\t\treturn [];\n\t}\n\n\ttext = foldToAscii(text);\n\tBARE_HOST.lastIndex = 0;\n\tlet match;\n\twhile ((match = BARE_HOST.exec(text)) !== null && hosts.size < MAX_BARE_HOSTS) {\n\t\tconst [name] = match;\n\t\tconst host = name.toLowerCase();\n\t\tconst next = text[match.index + name.length];\n\t\t// (not part of a longer name with letters the pattern does not read,\n\t\t// such as \"nchen-ups.de\" in \"m\u00FCnchen-ups.de\"; in lowercase, or all in\n\t\t// capitals, \"PAYPAL-VERIFY.COM\", but not \"end.Click\")\n\t\tif (continuesName(text, match.index) || (name !== host && name !== name.toUpperCase()) || next === '@' || next === '+' || known.has(host) || hosts.has(host)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (isHost(host) && !isFileName(host)) {\n\t\t\thosts.add(host);\n\t\t}\n\t}\n\n\treturn [...hosts];\n}\n\n// Brands whose sites newsletters link to as a matter of course: social\n// networks, media and marketplaces (\"Follow us on https://www.facebook.com/\").\n// Through click tracking, their addresses are no sign of a hijacked account.\nconst LINKED_BRANDS = new Set(['airbnb', 'aliexpress', 'alibaba', 'bestbuy', 'booking', 'costco', 'discord', 'ebay', 'facebook', 'github', 'gitlab', 'instagram', 'linkedin', 'roblox', 'shopify', 'skype', 'spotify', 'steam', 'telegram', 'tiktok', 'twitter', 'uber', 'walmart', 'whatsapp', 'zoom']);\n// A part of the path of an address that asks to sign in or pay (\"signin\",\n// \"ServiceLogin\", \"YourAccount\", \"oauth2\", \"accountverification\",\n// \"updatepayment\", \"LoginHelp\"), between \"/\", \"?\", \"&\", \"=\", \".\", \"_\" or\n// \"-\", in any case: not a product, a page or a map (\"updates\", \"products\")\nconst SIGN_IN_PART = /^(?:your|my|service|secure)?(?:login|logon|signin|signon|accounts?|verify|verification|security|secure|auth|authenticate|authorize|oauth2?|billing|password|recover|recovery|unlock|confirm|update|wallet|webscr)(?:verification|payment|info|help|page|\\d{1,2})?$/;\n// (\"sign-in\" and \"log-on\" are read as one part)\nconst signInPath = path => path.toLowerCase().replaceAll(/\\b(log|sign)-(in|on)\\b/g, '$1$2').split(/[/?&=._-]/).some(part => SIGN_IN_PART.test(part));\n// The domains brands send from that hold their name and another word\n// (\"facebookmail.com\"): only these, as the others are anyone's\n// (\"paypalmail.com\", \"chasemails.com\", \"facebookmail.top\")\nconst SENDING_DOMAINS = new Map([\n\t['dropbox', ['dropboxmail.com']],\n\t['facebook', ['facebookmail.com']],\n\t['mcafee', ['mcafeemail.com']],\n\t['ups', ['upsemail.com']],\n\t['yahoo', ['yahoonews.com']],\n]);\n\n/**\n * Whether the visible text of a link names a different site than the link\n * goes to (\"https://paypal.com\" linking to another domain). A link through an\n * email service's click tracking (see CLICK_TRACKERS) goes elsewhere by\n * design, so it counts only when the sender is known (`from`, the domain of\n * the From address, or \"\" for none), the text shows a whole address with\n * \"http://\" or \"https://\" of a brand's own domain (see BRANDS; not one that\n * newsletters link to, see LINKED_BRANDS) with no path or one that asks to\n * sign in or pay, and the sender is not that brand (nor one of its sending\n * domains, \"facebookmail.com\", see SENDING_DOMAINS): a hijacked account of a mailing service\n * sending \"https://www.paypal.com\" to a phishing page, not a newsletter's\n * links. A link through a security gateway is read as the address the\n * gateway goes on to, where the gateway's link holds it (see unwrapLink).\n * @param {{host: string, text: string|null, url?: string}} link\n * @param {object} [options]\n * @param {string} [options.from] - the sender's domain\n * @returns {boolean}\n */\nexport function isDeceptiveLink(link, {from} = {}) {\n\tif (!link.text) {\n\t\treturn false;\n\t}\n\n\tlet shownUrl;\n\ttry {\n\t\tshownUrl = boundedUrl(/^https?:\\/\\//i.test(link.text) ? link.text : `http://${link.text}`);\n\t} catch {\n\t\treturn false;\n\t}\n\n\t// (through a security gateway, the address it goes on to)\n\tconst target = typeof link.url === 'string' ? unwrapLink(link.url) : null;\n\tconst host = target ? target.hostname.toLowerCase().replace(/\\.$/, '').replace(/^\\[(.*)]$/, '$1') : (typeof link.host === 'string' ? link.host : '');\n\tconst shownHost = shownUrl.hostname.toLowerCase();\n\tconst shown = registrableDomain(shownHost);\n\tif (!shownHost.includes('.') || shown === registrableDomain(host)) {\n\t\treturn false;\n\t}\n\n\tif (!isClickTracker(target ?? {hostname: host, pathname: ''})) {\n\t\treturn true;\n\t}\n\n\tconst brand = parseDomain(shown).domainWithoutSuffix;\n\tconst sender = parseDomain(from ?? '').domainWithoutSuffix ?? '';\n\tconst path = `${shownUrl.pathname}${shownUrl.search}`;\n\treturn typeof from === 'string'\n\t\t&& /^https?:\\/\\//i.test(link.text)\n\t\t&& BRAND_NAMES.has(brand) && !LINKED_BRANDS.has(brand)\n\t\t&& (path === '/' || signInPath(path))\n\t\t&& registrableDomain(from) !== shown && sender !== brand\n\t\t&& !SENDING_DOMAINS.get(brand)?.includes(registrableDomain(from));\n}\n\nfunction addressDomain(address) {\n\tconst at = address.lastIndexOf('@');\n\treturn at === -1 ? '' : trimEndOf(address.slice(at + 1).toLowerCase(), '> \\t\\r\\n\\f\\v');\n}\n\nfunction firstAddress(field) {\n\treturn field?.value?.find(entry => entry.address) || null;\n}\n\n// An email address and its domain, in a display name or any text.\nexport const ADDRESS_SOURCE = String.raw`[\\p{L}\\p{N}._%+-]{1,64}@([\\p{L}\\p{N}-]{1,63}(?:\\.[\\p{L}\\p{N}-]{1,63}){1,10})`;\nexport const NAMED_ADDRESS = linear(ADDRESS_SOURCE, 'u');\n\nfunction headerValue(mail, name) {\n\tconst value = mail.headers?.get?.(name);\n\tif (value === undefined || value === null) {\n\t\treturn '';\n\t}\n\n\tif (typeof value === 'string') {\n\t\treturn value;\n\t}\n\n\tif (Array.isArray(value)) {\n\t\treturn value.map(String).join(' ');\n\t}\n\n\treturn value.value ?? value.text ?? String(value);\n}\n\nfunction addPatternFeatures(text, add) {\n\t// Links, addresses and numbers become features of their own and are taken\n\t// out of the text, so their fragments do not become words.\n\tlet body = foldToAscii(text);\n\tconst patterns = [\n\t\t['url', URL_PATTERN],\n\t\t['email', EMAIL_PATTERN],\n\t\t['ip', IP_PATTERN],\n\t\t['btc', BITCOIN_PATTERN],\n\t\t['card', CARD_PATTERN],\n\t\t['phone', PHONE_PATTERN],\n\t];\n\tfor (const [name, pattern] of patterns) {\n\t\t// (each match replaced by a space, found with matchesOf: re2 calls a\n\t\t// replacement function in time growing with the square of the length\n\t\t// of text outside ASCII)\n\t\tlet count = 0;\n\t\tlet out = '';\n\t\tlet from = 0;\n\t\tfor (const match of matchesOf(pattern, body)) {\n\t\t\tout += `${body.slice(from, match.index)} `;\n\t\t\tfrom = match.index + match[0].length;\n\t\t\tcount++;\n\t\t}\n\n\t\tbody = out + body.slice(from);\n\t\tif (count > 0) {\n\t\t\tadd(`pat:${name}`);\n\t\t\tadd(`pat:${name}:${bucket(count, [1, 3, 10])}`);\n\t\t}\n\t}\n\n\tMONEY_PATTERN.lastIndex = 0;\n\tif (MONEY_PATTERN.test(text)) {\n\t\tadd('pat:money');\n\t}\n\n\treturn body;\n}\n\nfunction addWordFeatures(words, maxBigrams, add) {\n\tfor (const word of words) {\n\t\tconst {length} = [...word];\n\t\tif (/^\\p{N}+$/u.test(word)) {\n\t\t\tadd(`num:${Math.min(length, 12)}`);\n\t\t} else if (length > 30) {\n\t\t\tadd(`long:${scriptOf([...word][0])}:${Math.min(Math.floor(length / 10), 9)}`);\n\t\t} else {\n\t\t\tadd(word);\n\t\t}\n\t}\n\n\tconst pairs = Math.min(words.length - 1, maxBigrams);\n\tfor (let i = 0; i < pairs; i++) {\n\t\tadd(`${words[i]} ${words[i + 1]}`);\n\t}\n\n\tadd(words.length === 0 ? 'body:empty' : `body:words:${bucket(words.length, [10, 50, 200, 1000])}`);\n}\n\nfunction addSubjectFeatures(subject, words, add) {\n\tfor (const word of words) {\n\t\tadd(`s:${word}`);\n\t}\n\n\tif (subject === '') {\n\t\tadd('s:empty');\n\t\treturn;\n\t}\n\n\tconst letters = subject.match(/\\p{L}/gu) || [];\n\tconst upper = subject.match(/\\p{Lu}/gu) || [];\n\tif (letters.length >= 8 && upper.length / letters.length > 0.6) {\n\t\tadd('s:caps');\n\t}\n\n\tif (/[!?]{2,}/.test(subject)) {\n\t\tadd('s:punct');\n\t}\n\n\tif (/\\p{Extended_Pictographic}/u.test(subject)) {\n\t\tadd('s:emoji');\n\t}\n\n\tif (/^\\s*(?:re|fwd?|aw|wg|sv|tr|rv|ref)\\s*:/i.test(subject)) {\n\t\tadd('s:reply');\n\t}\n}\n\nfunction addObfuscationFeatures(parts, add) {\n\tconst invisible = parts.reduce((sum, part) => sum + part.invisible, 0);\n\tif (invisible > 0) {\n\t\tadd('obf:invisible');\n\t\tadd(`obf:invisible:${bucket(invisible, [2, 10])}`);\n\t}\n\n\tif (parts.some(part => part.styled)) {\n\t\tadd('obf:styled');\n\t}\n\n\tif (parts.some(part => part.mixed > 0)) {\n\t\tadd('obf:mixed');\n\t}\n\n\tif (parts.some(part => part.leet > 0)) {\n\t\tadd('obf:leet');\n\t}\n}\n\nfunction addLinkFeatures(links, from, add) {\n\tconst hosts = new Set();\n\tfor (const link of links) {\n\t\tconst domain = registrableDomain(link.host);\n\t\thosts.add(domain);\n\t\tadd(`url:${domain}`);\n\t\tconst tld = link.host.split('.').pop();\n\t\tif (/^\\d+$/.test(tld) || link.host.includes(':')) {\n\t\t\tadd('url:ip');\n\t\t} else {\n\t\t\tadd(`url:tld:${tld}`);\n\t\t}\n\n\t\tif (SHORTENERS.has(domain)) {\n\t\t\tadd('url:shortener');\n\t\t}\n\n\t\tif (link.host.split('.').some(label => label.startsWith('xn--'))) {\n\t\t\tadd('url:punycode');\n\t\t}\n\n\t\t// \"https://paypal.com@evil.example/\" shows one name and goes elsewhere\n\t\t// (read before the address was cut: see extractLinks).\n\t\tif (link.userinfo) {\n\t\t\tadd('url:userinfo');\n\t\t}\n\n\t\tif (isDeceptiveLink(link, {from})) {\n\t\t\tadd('url:deceptive');\n\t\t}\n\t}\n\n\tif (links.length > 0) {\n\t\tadd(`url:count:${bucket(links.length, [1, 3, 10, 30])}`);\n\t\tadd(`url:domains:${bucket(hosts.size, [1, 3, 10])}`);\n\t}\n}\n\nfunction addSenderFeatures(mail, add) {\n\tconst from = firstAddress(mail.from);\n\tif (!from) {\n\t\tadd('from:none');\n\t\treturn;\n\t}\n\n\tconst domain = addressDomain(from.address);\n\tif (domain) {\n\t\tadd(`from:${registrableDomain(domain)}`);\n\t\tadd(`from:tld:${domain.split('.').pop()}`);\n\t\tif (FREEMAIL.has(domain)) {\n\t\t\tadd('from:freemail');\n\t\t}\n\t}\n\n\tconst name = typeof from.name === 'string' ? from.name : '';\n\tconst named = name.match(NAMED_ADDRESS);\n\tif (named && registrableDomain(named[1].toLowerCase()) !== registrableDomain(domain)) {\n\t\tadd('from:name_has_other_address');\n\t}\n\n\tfor (const word of segmentWords(name).words) {\n\t\tadd(`fn:${word}`);\n\t}\n\n\tconst replyTo = firstAddress(mail.replyTo);\n\tif (replyTo && registrableDomain(addressDomain(replyTo.address)) !== registrableDomain(domain)) {\n\t\tadd('replyto:other_domain');\n\t\tif (FREEMAIL.has(addressDomain(replyTo.address))) {\n\t\t\tadd('replyto:freemail');\n\t\t}\n\t}\n}\n\n// Whether a message has HTML and no plain text of its own (mailparser makes\n// text from HTML when a message has no text part, so the Content-Type tells\n// whether the sender wrote any plain text)\nfunction isHtmlOnly(mail) {\n\tconst contentType = mail.headers?.get?.('content-type');\n\treturn !(typeof mail.text === 'string' && mail.text) || (contentType?.value ?? contentType) === 'text/html';\n}\n\n// HTML showing fewer words than this is read with its plain text part\nconst MIN_SHOWN_WORDS = 20;\n\n// Words of text for comparing two parts: letters and digits, folded, the\n// first 2000 at most\nconst PART_WORD = /[\\p{L}\\p{N}]{2,}/gu;\nfunction partWords(text) {\n\tconst words = new Set();\n\tfor (const [word] of matchesOf(PART_WORD, normalizeText(text.slice(0, 50_000)).text)) {\n\t\twords.add(word);\n\t\tif (words.size >= 2000) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn words;\n}\n\n//\n// Features of what the HTML hides, and of a plain text part that says\n// something else than the HTML beside it (as a harmless text part beside\n// spam does): `html:hidden_text`, and `html:text_mismatch` when the two\n// parts, each of 20 different words or more, share fewer than a fifth of the\n// smaller one's words\n//\nfunction addPartFeatures({visible, text, plain, alternative}, add) {\n\tif (visible.hidden > 0) {\n\t\tadd('html:hidden_text');\n\t\tadd(`html:hidden_text:${bucket(visible.hidden, [1, 50, 500])}`);\n\t}\n\n\tif (!alternative) {\n\t\treturn;\n\t}\n\n\tconst shown = partWords(text);\n\tconst other = partWords(plain);\n\tif (shown.size < MIN_SHOWN_WORDS || other.size < MIN_SHOWN_WORDS) {\n\t\treturn;\n\t}\n\n\tlet shared = 0;\n\tfor (const word of other) {\n\t\tif (shown.has(word)) {\n\t\t\tshared++;\n\t\t}\n\t}\n\n\tif (shared < Math.min(shown.size, other.size) / 5) {\n\t\tadd('html:text_mismatch');\n\t}\n}\n\nfunction addHtmlFeatures(mail, html, words, add) {\n\tif (!html) {\n\t\treturn;\n\t}\n\n\tadd(isHtmlOnly(mail) ? 'html:only' : 'html:alternative');\n\tif (/<form\\b/i.test(html)) {\n\t\tadd('html:form');\n\t}\n\n\tif (/<script\\b/i.test(html)) {\n\t\tadd('html:script');\n\t}\n\n\tif (/<(?:iframe|object|embed)\\b/i.test(html)) {\n\t\tadd('html:embed');\n\t}\n\n\tif (/display\\s*:\\s*none|visibility\\s*:\\s*hidden|font-size\\s*:\\s*[01](?:\\.\\d+)?(?:px|pt)?\\s*[;\"']/i.test(html)) {\n\t\tadd('html:hidden');\n\t}\n\n\tconst images = (html.match(IMG) || []).length;\n\tif (images > 0) {\n\t\tadd(`html:img:${bucket(images, [1, 5, 20])}`);\n\t\tif (words.length < 20) {\n\t\t\tadd('html:img_little_text');\n\t\t}\n\t}\n\n\tif (IMG_PIXEL.test(html)) {\n\t\tadd('html:pixel');\n\t}\n}\n\nfunction addAttachmentFeatures(mail, add) {\n\tconst attachments = Array.isArray(mail.attachments) ? mail.attachments : [];\n\tfor (const attachment of attachments) {\n\t\tconst name = typeof attachment.filename === 'string' ? attachment.filename.toLowerCase() : '';\n\t\tconst extension = name.includes('.') ? name.split('.').pop().slice(0, 10) : '';\n\t\tif (extension) {\n\t\t\tadd(`att:ext:${extension}`);\n\t\t}\n\n\t\tif (typeof attachment.contentType === 'string') {\n\t\t\tadd(`att:type:${attachment.contentType.toLowerCase().split(';')[0]}`);\n\t\t}\n\t}\n\n\tadd(`att:count:${bucket(attachments.length, [0, 1, 3])}`);\n}\n\nfunction addHeaderFeatures(mail, add) {\n\tif (!mail.headers?.get) {\n\t\treturn;\n\t}\n\n\tconst mailer = headerValue(mail, 'x-mailer') || headerValue(mail, 'user-agent');\n\tif (mailer) {\n\t\tadd(`hdr:mailer:${mailer.toLowerCase().split(/[\\s/(;]/)[0].slice(0, 20)}`);\n\t}\n\n\tif (!mail.messageId) {\n\t\tadd('hdr:no_message_id');\n\t}\n\n\tif (!mail.date) {\n\t\tadd('hdr:no_date');\n\t}\n\n\t// Mailparser groups List-* headers under \"list\".\n\tconst list = mail.headers.get('list') || {};\n\tif (list.unsubscribe || headerValue(mail, 'list-unsubscribe')) {\n\t\tadd('hdr:list_unsubscribe');\n\t}\n\n\tif (list.id || headerValue(mail, 'list-id')) {\n\t\tadd('hdr:list_id');\n\t}\n\n\tconst precedence = headerValue(mail, 'precedence').toLowerCase().trim();\n\tif (precedence) {\n\t\tadd(`hdr:precedence:${precedence.slice(0, 10)}`);\n\t}\n\n\t// Mailparser folds X-Priority, Importance and similar headers into \"priority\".\n\tif (headerValue(mail, 'priority') === 'high' || /^\\s*[12]\\b/.test(headerValue(mail, 'x-priority')) || /high|urgent/i.test(headerValue(mail, 'importance'))) {\n\t\tadd('hdr:priority_high');\n\t}\n\n\tconst received = mail.headers.get('received');\n\tconst hops = Array.isArray(received) ? received.length : (received ? 1 : 0);\n\tadd(`hdr:received:${bucket(hops, [0, 1, 3, 6])}`);\n}\n\n/**\n * Classifier features of a message parsed by mailparser.\n *\n * Every word of the text a mail client shows (see `visibleText`) is a\n * feature, every pair of neighbouring words is another, and the subject's\n * words are features of their own (\"s:free\").\n * The rest describe structure: link domains and tricks, the sender's domain,\n * HTML forms and hidden text, attachment types, headers, language and script,\n * and obfuscation. The same message always gives the same features.\n *\n * @param {object} mail - parsed message (text, html, subject, from, headers, attachments)\n * @param {object} [options]\n * @param {number} [options.maxLength] - characters of body text to read\n * @param {number} [options.maxBigrams] - word pairs to emit\n * @returns {{features: string[], words: string[], language: string|null, script: string|null, links: Array, hosts: string[], text: string, hidden: number}}\n */\nexport function getFeatures(mail = {}, options = {}) {\n\tconst {maxLength = 100_000, maxBigrams = 3000} = options;\n\tconst features = new Set();\n\tconst add = feature => features.add(feature);\n\n\tconst html = typeof mail.html === 'string' ? mail.html.slice(0, maxLength * 2) : '';\n\tconst plain = typeof mail.text === 'string' ? mail.text.slice(0, maxLength) : '';\n\t// The words are those a mail client shows: of the HTML when there is any\n\t// (a plain text part beside it is shown only where HTML cannot be, so a\n\t// harmless one cannot speak for spam in the HTML, nor can text the HTML\n\t// hides), else of the plain text. Links are read from both parts and\n\t// from hidden text too; host names written without a link from the\n\t// subject, the text read and a plain text part beside the HTML.\n\tconst visible = html ? visibleText(html) : {text: plain, hidden: 0};\n\tconst alternative = Boolean(html && plain && !isHtmlOnly(mail));\n\tlet text = visible.text.slice(0, maxLength);\n\t// (but HTML that shows little text, such as an image of the message, is\n\t// read with its plain text part, which says what the image does)\n\t// (counted as words are, also in scripts written without spaces)\n\tif (alternative && segmentWords(text.slice(0, 4000)).words.length < MIN_SHOWN_WORDS) {\n\t\ttext = `${text}\\n${plain}`.slice(0, maxLength);\n\t}\n\n\tconst subject = typeof mail.subject === 'string' ? mail.subject : '';\n\tconst links = extractLinks(html ? `${plain}\\n${text}` : text, html);\n\tconst hosts = extractHosts(`${subject}\\n${text}${alternative ? `\\n${plain}` : ''}`, new Set(links.map(link => link.host)));\n\tconst body = addPatternFeatures(text, add);\n\n\tconst detected = detectLanguage(`${subject}\\n${body}`);\n\tif (detected.language) {\n\t\tadd(`lang:${detected.language}`);\n\t}\n\n\tif (detected.script) {\n\t\tadd(`script:${detected.script}`);\n\t}\n\n\tif (detected.scripts.length > 1) {\n\t\tadd('script:mixed');\n\t}\n\n\tconst locale = detected.language || undefined;\n\tconst segmented = segmentWords(body, {locale, maxLength});\n\tconst subjectWords = segmentWords(subject, {locale});\n\taddWordFeatures(segmented.words, maxBigrams, add);\n\taddSubjectFeatures(subject, subjectWords.words, add);\n\taddObfuscationFeatures([segmented, subjectWords], add);\n\taddLinkFeatures(links, addressDomain(firstAddress(mail.from)?.address ?? ''), add);\n\taddSenderFeatures(mail, add);\n\taddHtmlFeatures(mail, html, segmented.words, add);\n\taddPartFeatures({\n\t\tvisible, text, plain, alternative,\n\t}, add);\n\taddAttachmentFeatures(mail, add);\n\taddHeaderFeatures(mail, add);\n\n\treturn {\n\t\tfeatures: [...features],\n\t\twords: segmented.words,\n\t\tlanguage: detected.language,\n\t\tscript: detected.script,\n\t\tlinks,\n\t\thosts,\n\t\t// (the text shown, as read for the words, and how many characters of\n\t\t// text the HTML hides)\n\t\ttext,\n\t\thidden: visible.hidden,\n\t};\n}\n", "import {domainToUnicode} from 'node:url';\nimport {parse as parseDomain} from 'tldts';\nimport {scriptOf} from './language.js';\n\n// Banks, shops, payment and delivery services imitated in other languages\n// (only names that are not words or close to words, so ordinary domains\n// are not taken for typos of them).\nconst INTERNATIONAL_BRANDS = [\n\t'alipay', 'auspost', 'bancolombia', 'bnpparibas', 'bradesco', 'caixabank', 'canadapost', 'commerzbank', 'coupang', 'creditagricole', 'deutschebank', 'flipkart', 'gosuslugi', 'japanpost', 'kakaobank', 'klarna', 'kucoin', 'lazada', 'mercadolibre', 'mercadolivre', 'mercadopago', 'mercari', 'natwest', 'nubank', 'phonepe', 'postbank', 'rakuten', 'sberbank', 'swisscom', 'trustwallet', 'unicredit', 'vodafone', 'wechat', 'yandex',\n];\n\n/**\n * Brands most often imitated in phishing: banks, payment services, mail and\n * cloud providers, shops, couriers and tax offices. A domain whose name is a\n * brand's (\"paypal\" in paypal.com, paypal.de, ...) is the brand's own and is\n * never flagged.\n */\nexport const BRANDS = [\n\t'adobe', 'airbnb', 'alibaba', 'aliexpress', 'amazon', 'americanexpress', 'amex', 'apple', 'appleid', 'att', 'binance', 'bankofamerica', 'barclays', 'bestbuy', 'blockchain', 'booking', 'bybit', 'cashapp', 'chase', 'citibank', 'cloudflare', 'coinbase', 'comcast', 'costco', 'dhl', 'discord', 'docusign', 'dropbox', 'ebay', 'facebook', 'fedex', 'forwardemail', 'github', 'gitlab', 'gmail', 'godaddy', 'google', 'hmrc', 'hotmail', 'hsbc', 'icloud', 'instagram', 'intuit', 'irs', 'kraken', 'ledger', 'linkedin', 'mastercard', 'mcafee', 'metamask', 'microsoft', 'microsoftonline', 'namecheap', 'netflix', 'norton', 'office', 'office365', 'okta', 'onedrive', 'outlook', 'paypal', 'proton', 'protonmail', 'quickbooks', 'revolut', 'roblox', 'royalmail', 'salesforce', 'samsung', 'santander', 'sharepoint', 'shopify', 'skype', 'slack', 'sparkasse', 'spotify', 'steam', 'steampowered', 'stripe', 'telegram', 'tiktok', 'tmobile', 'trezor', 'tutanota', 'twitter', 'uber', 'ups', 'usps', 'venmo', 'verizon', 'visa', 'walmart', 'wellsfargo', 'whatsapp', 'wise', 'xfinity', 'yahoo', 'zelle', 'zoom', ...INTERNATIONAL_BRANDS,\n];\n\n// Words phishing domains add to a brand (\"paypallogin\", \"secure-chase\"):\n// asking to sign in, verify, unlock or pay, not words a shop or service puts\n// in an ordinary name.\nexport const PHISHING_WORDS = ['account', 'accounts', 'alert', 'alerts', 'auth', 'authentication', 'billing', 'confirm', 'login', 'logon', 'password', 'recovery', 'refund', 'secure', 'security', 'signin', 'signon', 'suspended', 'unlock', 'update', 'validate', 'verification', 'verify'];\n\n// Brands that are also words: joined to a phishing word (\"securebooking\",\n// \"officesecurity\") they name something else as often as the brand.\nconst WORD_BRANDS = new Set(['apple', 'blockchain', 'booking', 'chase', 'discord', 'kraken', 'ledger', 'office', 'proton', 'slack', 'steam', 'stripe']);\n\n// Words phishing domains put beside a brand that is a word, offering help,\n// a gift or the brand's own wallet, bank or files (\"apple-support\",\n// \"discord-nitro\", \"chase-bank\"): with one of these, the word is the brand\n// (\"apple-tree\" and \"chase-hotel\" are not).\nconst BRAND_SERVICE_WORDS = new Set(['bank', 'banking', 'customer', 'files', 'gift', 'giftcard', 'gifts', 'help', 'helpdesk', 'id', 'nitro', 'online', 'pay', 'payment', 'payments', 'service', 'services', 'support', 'trade', 'trading', 'wallet', 'wallets']);\n// Brands that are the most common of these words, in the names of ordinary\n// businesses beside words of services too (\"office-help\", \"booking-online\",\n// \"ledger-services\", \"steam-service\", \"kraken-services\"): only the names of\n// the brand's own products and help count beside them (\"ledger-live\",\n// \"ledger-wallet\", \"steam-community\", \"steam-gift\", \"stripe-payments\",\n// \"ledger-support\"), and in another label only a word asking to sign in\n// makes them the brand (\"login.steam.example.com\", not\n// \"auth.steam.example.edu\"). For \"office\" and \"booking\", nothing does.\nconst HELP_WORDS = ['help', 'helpdesk', 'support'];\nconst COMMON_WORD_BRANDS = new Map([\n\t['blockchain', ['wallet', 'wallets', ...HELP_WORDS]],\n\t['booking', []],\n\t['kraken', ['trade', 'trading', 'wallet', 'wallets', ...HELP_WORDS]],\n\t['ledger', ['live', 'wallet', 'wallets', ...HELP_WORDS]],\n\t['office', []],\n\t['proton', ['pay', 'wallet', ...HELP_WORDS]],\n\t['steam', ['community', 'gift', 'giftcard', 'gifts', 'trade', 'trading', 'wallet', ...HELP_WORDS]],\n\t['stripe', ['payment', 'payments', ...HELP_WORDS]],\n]);\nconst SIGN_IN_LABELS = new Set(['account', 'accounts', 'login', 'logon', 'signin', 'signon']);\n\n// Brands one letter away from common words (\"finance\", \"mobile\", \"email\",\n// \"cloud\", \"adore\", \"morton\", \"belle\", \"cedex\", \"tresor\", \"twister\"), or\n// from names of places (\"venlo\", \"samsun\") or their own other domains\n// (\"tiktokv\"):\n// only two swapped neighbouring letters count as a typo of these (\"gmial\"),\n// and, but for the brands that are words, a letter left out inside the name\n// (\"twiter\", \"samsng\": not the first or the last, as \"cloud\", \"mobile\" and\n// \"samsun\" are) or one doubled (\"twittter\", \"samssung\").\nconst TYPO_WORDS = new Set([...WORD_BRANDS, 'adobe', 'binance', 'fedex', 'gmail', 'icloud', 'norton', 'samsung', 'tiktok', 'tmobile', 'trezor', 'twitter', 'venmo', 'zelle']);\n// (and not words that are such a typo: \"titter\")\nconst TYPO_EXCEPTIONS = new Set(['titter']);\n\n/**\n * Whether a name is a brand with one letter left out inside it, or with one\n * of its letters doubled.\n * @param {string} name\n * @param {string} brand\n * @returns {boolean}\n */\nexport function isDroppedOrDoubled(name, brand) {\n\tif (name.length === brand.length - 1) {\n\t\tlet i = 0;\n\t\twhile (name[i] === brand[i]) {\n\t\t\ti++;\n\t\t}\n\n\t\treturn i > 0 && i < brand.length - 1 && name.slice(i) === brand.slice(i + 1);\n\t}\n\n\tif (name.length === brand.length + 1) {\n\t\tlet i = 0;\n\t\twhile (i < brand.length && name[i] === brand[i]) {\n\t\t\ti++;\n\t\t}\n\n\t\treturn i > 0 && name[i] === name[i - 1] && name.slice(i + 1) === brand.slice(i);\n\t}\n\n\treturn false;\n}\n\n// Whether a name one edit from a brand is taken for a typo of it (see\n// TYPO_WORDS)\nconst isTypo = (name, brand) => !TYPO_WORDS.has(brand) || isSwap(name, brand) || (!WORD_BRANDS.has(brand) && !TYPO_EXCEPTIONS.has(name) && isDroppedOrDoubled(name, brand));\n\n// Words one letter or two swapped letters away from a brand (\"applied\",\n// \"appleid\"; \"raketen\", \"rakuten\"), in the dictionaries of English, German,\n// French, Spanish, Italian, Dutch, Portuguese and the Scandinavian languages.\nconst SWAPPED_WORDS = new Set([\n\t'akuten', 'alzada', 'appel', 'applied', 'auslost', 'azada', 'coupant', 'gechat', 'klara', 'lanada', 'lanzada', 'lapada', 'latada', 'lavada', 'mazada', 'mercar', 'mercati', 'pazada', 'phoneme', 'postbak', 'raketen', 'rauten', 'razada', 'stirpe', 'vazada', 'venom', 'webchat',\n]);\n\n// Official domains of the brands above whose names also hold a brand\n// (\"deutsche-bank.de\"), never flagged.\nconst OFFICIAL_DOMAINS = [\n\t'adobelogin.com', 'amazon-adsystem.com', 'apple-cloudkit.com', 'apple-dns.net', 'bnpparibas-am.com', 'canadapost-postescanada.ca', 'cdn-apple.com', 'cloudflare-dns.com', 'cloudflare-ipfs.com', 'credit-agricole.com', 'credit-agricole.fr', 'deutsche-bank.de', 'ebay-kleinanzeigen.de', 'google-analytics.com', 'icloud-content.com', 'images-amazon.com', 'instagram-engineering.com', 'media-amazon.com', 'okta-emea.com', 'paypal-communication.com', 'paypal-community.com', 'paypal-objects.com', 'paypal-prepaid.com', 'paypal-status.com', 'rakuten-bank.co.jp', 'rakuten-card.co.jp', 'rakuten-sec.co.jp', 'sberbank-ast.ru', 'slack-edge.com', 'slack-msgs.com', 'ssl-images-amazon.com', 't-mobile.com', 'tiktokcdn.com', 'tiktokv.com', 'yahoo-corp.jp', 'yandex-team.ru',\n];\n// (and each Sparkasse's own, \"sparkasse-koelnbonn.de\", unless a part of it is\n// a phishing word)\nconst SPARKASSE = /^sparkasse-[a-z\\d-]+\\.(?:de|at)$/;\n\n// Suffixes put after a brand inside another domain (\"paypal.com-verify.example\"):\n// the generic ones, which no ordinary name starts with, and country codes and\n// others, which many do (\"office.it-haus.de\", \"microsoft.co-op.org\"), so\n// these count fully only with a phishing word after them.\nconst SUFFIX_DASH = /^(?:com|net|org|gov|edu)-/;\nconst OTHER_SUFFIX_DASH = /^(?:co|info|biz|io|app|us|uk|de|fr|es|it|ca|au|jp|br|in)-/;\n// (and fully, a second-level suffix spelled out: \"paypal.co-uk.example\")\nconst SECOND_SUFFIX_DASH = /^(?:co|com)-(?:uk|jp|au|nz|za|in|kr|br)\\b/;\n// (the label after a brand in a subdomain that makes it the brand's address:\n// \"apple.com.verify.example\", not \"www.steam.ncsu.edu\")\nconst SUFFIX_LABEL = /^(?:com|net|org|gov|edu|co|info|biz|io|app|us|uk|de|fr|es|it|ca|au|jp|br|in)$/;\n\n// Letters from other scripts, and accented Latin letters, folded to the plain\n// Latin letter they look like.\nconst CONFUSABLE = {\n\t\u0430: 'a', \u04D3: 'a', \u0432: 'b', \u0441: 'c', \u0501: 'd', \u0435: 'e', \u0451: 'e', \u04BB: 'h', \u0456: 'i', \u0457: 'i', \u0458: 'j', \u043A: 'k', \u04CF: 'l', \u043C: 'm', \u043E: 'o', \u04E9: 'o', \u0440: 'p', \u051B: 'q', \u0433: 'r', \u0455: 's', \u0442: 't', \u0443: 'y', \u04AF: 'y', \u0445: 'x', \u051D: 'w', \u044C: 'b', \u043F: 'n', \u043D: 'h',\n\t\u03B1: 'a', \u03B2: 'b', \u03F2: 'c', \u03B5: 'e', \u03B7: 'n', \u03B9: 'i', \u03AF: 'i', \u03F3: 'j', \u03BA: 'k', \u03BC: 'u', \u03BD: 'v', \u03BF: 'o', \u03CC: 'o', \u03C1: 'p', \u03C4: 't', \u03C5: 'u', \u03C7: 'x', \u03C9: 'w', \u03B3: 'y',\n\t\u0251: 'a', \u0261: 'g', \u0269: 'i', \u0131: 'i', \u0237: 'j', \u029F: 'l', \u0274: 'n', \u1D0F: 'o', \u0280: 'r', \u0475: 'v', \u028F: 'y', \u1D22: 'z',\n\t\u2170: 'i', \u217C: 'l', \u217D: 'c', \u217E: 'd', \u217F: 'm', \u2113: 'l', '\u0660': 'o', '\u06F0': 'o',\n\t\u00E0: 'a', \u00E1: 'a', \u00E2: 'a', \u00E3: 'a', \u00E4: 'a', \u00E5: 'a', \u0101: 'a', \u0103: 'a', \u0105: 'a', \u00E7: 'c', \u0107: 'c', \u010D: 'c', \u010F: 'd', \u00E8: 'e', \u00E9: 'e', \u00EA: 'e', \u00EB: 'e', \u0113: 'e', \u0119: 'e', \u011B: 'e', \u00EC: 'i', \u00ED: 'i', \u00EE: 'i', \u00EF: 'i', \u012B: 'i', \u0142: 'l', \u00F1: 'n', \u0144: 'n', \u00F2: 'o', \u00F3: 'o', \u00F4: 'o', \u00F5: 'o', \u00F6: 'o', \u00F8: 'o', \u014D: 'o', \u0151: 'o', \u0155: 'r', \u0159: 'r', \u015B: 's', \u0161: 's', \u015F: 's', \u0165: 't', \u0163: 't', \u00F9: 'u', \u00FA: 'u', \u00FB: 'u', \u00FC: 'u', \u016B: 'u', \u016F: 'u', \u0171: 'u', \u00FD: 'y', \u00FF: 'y', \u017A: 'z', \u017C: 'z', \u017E: 'z',\n};\n\n// ASCII tricks: digits for letters and letter pairs that read as one letter.\nconst ASCII_TRICKS = [\n\t[/rn/g, 'm'],\n\t[/vv/g, 'w'],\n\t[/cl/g, 'd'],\n\t[/0/g, 'o'],\n\t[/1/g, 'l'],\n\t[/3/g, 'e'],\n\t[/4/g, 'a'],\n\t[/5/g, 's'],\n\t[/7/g, 't'],\n\t[/8/g, 'b'],\n];\n\n/**\n * Map every lookalike character of a domain label to the Latin letter it\n * imitates (Cyrillic \"\u0440\u0430\u0443p\u0430l\" becomes \"paypal\").\n * @param {string} label\n * @returns {string}\n */\nexport function skeleton(label) {\n\tlet out = '';\n\tfor (const char of String(label).normalize('NFKC').toLowerCase()) {\n\t\tout += CONFUSABLE[char] || char;\n\t}\n\n\treturn out;\n}\n\n// (each trick in a label doubles its variants: a label with more than\n// seven kinds of trick, which no brand's imitation needs, gets the variants\n// of its first seven, rather than thousands)\nconst MAX_VARIANTS = 256;\n\n// (the tricks of letters alone that no ordinary word or name has: \"cl\" for\n// \"d\", \"vv\" for \"w\", and below, \"l\" for \"i\"; not \"rn\" for \"m\", as in the\n// name \"stearn\")\nconst LETTER_TRICKS = ASCII_TRICKS.filter(([pattern]) => pattern.source === 'cl' || pattern.source === 'vv');\n\nfunction trickVariants(label, tricks = ASCII_TRICKS) {\n\tconst variants = new Set([label]);\n\tfor (const [pattern, letter] of tricks) {\n\t\tif (!label.includes(pattern.source)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (const value of variants) {\n\t\t\tif (variants.size >= MAX_VARIANTS / 2) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tvariants.add(value.replace(pattern, letter));\n\t\t}\n\t}\n\n\t// \"1\" and \"l\" also stand for \"i\" (paypa1, 1nstagram). (At most twice as\n\t// many.)\n\tfor (const value of variants) {\n\t\tvariants.add(value.replaceAll('l', 'i'));\n\t}\n\n\tvariants.delete(label);\n\treturn variants;\n}\n\n/**\n * Edit distance (Levenshtein, with two neighbouring letters swapped counting\n * as one edit: \"paypla\" is one from \"paypal\"), giving up once it exceeds\n * `max`.\n * @param {string} a\n * @param {string} b\n * @param {number} [max]\n * @returns {number}\n */\nexport function editDistance(a, b, max = 2) {\n\tif (Math.abs(a.length - b.length) > max) {\n\t\treturn max + 1;\n\t}\n\n\tlet before = null;\n\tlet previous = Array.from({length: b.length + 1}, (_, i) => i);\n\tfor (let i = 1; i <= a.length; i++) {\n\t\tconst current = [i];\n\t\tlet best = i;\n\t\tfor (let j = 1; j <= b.length; j++) {\n\t\t\tconst cost = a[i - 1] === b[j - 1] ? 0 : 1;\n\t\t\tcurrent[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);\n\t\t\tif (before && i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {\n\t\t\t\tcurrent[j] = Math.min(current[j], before[j - 2] + 1);\n\t\t\t}\n\n\t\t\tbest = Math.min(best, current[j]);\n\t\t}\n\n\t\t// (a swap reaches back two rows, but costs at least as much as the\n\t\t// row between, so the row's best still bounds what follows)\n\t\tif (best > max) {\n\t\t\treturn max + 1;\n\t\t}\n\n\t\tbefore = previous;\n\t\tprevious = current;\n\t}\n\n\treturn previous[b.length];\n}\n\n/**\n * The brand a name joins to a phishing word (\"paypallogin\", \"verifyamazon\"),\n * or null. Brands of five letters or more only, and not brands that are words\n * themselves.\n * @param {string} name\n * @param {Set<string>} brands\n * @returns {string|null}\n */\nexport function brandWithPhishingWord(name, brands) {\n\tfor (const word of PHISHING_WORDS) {\n\t\tif (name.length <= word.length + 4) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst before = name.endsWith(word) ? name.slice(0, -word.length) : null;\n\t\tif (before && before.length >= 5 && brands.has(before) && !WORD_BRANDS.has(before)) {\n\t\t\treturn before;\n\t\t}\n\n\t\tconst after = name.startsWith(word) ? name.slice(word.length) : null;\n\t\tif (after && after.length >= 5 && brands.has(after) && !WORD_BRANDS.has(after)) {\n\t\t\treturn after;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n/**\n * Whether two names differ only by two neighbouring letters swapped (not\n * when they are the same).\n * @param {string} a\n * @param {string} b\n * @returns {boolean}\n */\nexport function isSwap(a, b) {\n\tif (a.length !== b.length || a === b) {\n\t\treturn false;\n\t}\n\n\tlet first = 0;\n\twhile (a[first] === b[first]) {\n\t\tfirst++;\n\t}\n\n\treturn a[first] === b[first + 1] && a[first + 1] === b[first] && a.slice(first + 2) === b.slice(first + 2);\n}\n\n// (letters of no one script, as the long vowel mark of Japanese \"\u30B3\u30FC\u30D2\u30FC\",\n// are left out)\nfunction labelScripts(label) {\n\tconst scripts = new Set();\n\tfor (const char of label) {\n\t\tif (/\\p{L}/u.test(char) && !/[\\p{Script=Common}\\p{Script=Inherited}]/u.test(char)) {\n\t\t\tscripts.add(scriptOf(char));\n\t\t}\n\t}\n\n\treturn scripts;\n}\n\n// Scripts that legitimately appear together in one label.\nconst COMPATIBLE = [\n\tnew Set(['Han', 'Hiragana', 'Katakana', 'Latin']),\n\tnew Set(['Han', 'Hangul', 'Latin']),\n];\n\nfunction isSuspiciousMix(scripts) {\n\treturn scripts.size > 1 && !COMPATIBLE.some(allowed => [...scripts].every(script => allowed.has(script)));\n}\n\n/**\n * Detects domains made to look like well-known brands: lookalike letters from\n * other scripts (Cyrillic \"\u0430\" for Latin \"a\"), mixed scripts in one label,\n * digit and letter swaps (\"paypa1\", \"rnicrosoft\"), one-letter typos\n * (\"amazom\"), and a brand name inside someone else's domain\n * (\"paypal-secure-login.example\", \"paypal.com.account-check.example\").\n *\n * Legitimate international domains (m\u00FCnchen.de, \u65E5\u672C.jp, \u0441\u0430\u0439\u0442.\u0440\u0444) are not\n * flagged: only a domain that imitates a brand or mixes scripts is.\n */\nexport default class HomographDetector {\n\t/**\n\t * @param {object} [options]\n\t * @param {string[]} [options.brands] - names to protect, replacing BRANDS\n\t * @param {string[]} [options.extraBrands] - names to protect as well as BRANDS\n\t * @param {string[]} [options.allowlist] - registrable domains never flagged\n\t * @param {boolean} [options.strictMode] - flag any brand name in a subdomain\n\t */\n\tconstructor(options) {\n\t\toptions ??= {};\n\t\tthis.options = options;\n\t\tthis.brands = new Set([...(options.brands || BRANDS), ...(options.extraBrands || [])].map(brand => brand.toLowerCase()));\n\t\tthis.allowlist = new Set([...OFFICIAL_DOMAINS, ...(options.allowlist || [])].map(domain => domain.toLowerCase()));\n\t\tthis.cache = new Map();\n\t}\n\n\t/**\n\t * Analyse a host name, in Unicode or punycode.\n\t * @param {string} domain\n\t * @returns {{domain: string, unicode: string, isIDN: boolean, riskScore: number, riskFactors: string[], brand: string|null, recommendations: string[], confidence: number}}\n\t */\n\tdetectHomographAttack(domain) {\n\t\tconst written = typeof domain === 'string' ? domain.trim().replace(/\\.$/, '') : '';\n\t\tconst input = written.toLowerCase();\n\t\t// (as written when it has a capital \"I\", which may stand for \"l\")\n\t\tconst key = written.includes('I') ? written : input;\n\t\tif (this.cache.has(key)) {\n\t\t\treturn this.cache.get(key);\n\t\t}\n\n\t\tconst result = this.analyse(input, written);\n\t\tthis.cache.set(key, result);\n\t\tif (this.cache.size > 5000) {\n\t\t\tthis.cache.delete(this.cache.keys().next().value);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tanalyse(input, written = input) {\n\t\tconst unicode = (input && domainToUnicode(input)) || input;\n\t\tconst result = {\n\t\t\tdomain: input,\n\t\t\tunicode,\n\t\t\tisIDN: unicode !== input || /[^ -~]/.test(unicode),\n\t\t\triskScore: 0,\n\t\t\triskFactors: [],\n\t\t\tbrand: null,\n\t\t\trecommendations: [],\n\t\t\tconfidence: 0,\n\t\t};\n\t\tconst parsed = parseDomain(unicode);\n\t\tconst name = parsed.domainWithoutSuffix;\n\t\tif (!name || parsed.isIp || this.allowlist.has(parsed.domain)) {\n\t\t\treturn result;\n\t\t}\n\n\t\tconst flag = (score, factor, brand) => {\n\t\t\tresult.riskFactors.push(factor);\n\t\t\tif (score > result.riskScore) {\n\t\t\t\tresult.riskScore = score;\n\t\t\t\tresult.brand = brand;\n\t\t\t}\n\t\t};\n\n\t\tif (this.brands.has(name)) {\n\t\t\t// The brand's own domain, under any suffix.\n\t\t\treturn result;\n\t\t}\n\n\t\t// A capital \"I\" for \"l\" in a name of small letters (\"googIe\",\n\t\t// \"paypaI-secure\"), as the name or a part of it: not in one all in\n\t\t// capitals (\"AIRBNB\"), or in the suffix (\"google.IE\")\n\t\tconst start = input.length - parsed.domain.length;\n\t\tconst writtenName = written.slice(start, start + name.length);\n\t\tif (writtenName.includes('I') && /[a-z]/.test(writtenName) && /^[\\w.-]+$/.test(written)) {\n\t\t\tconst read = writtenName.replaceAll('I', 'l').toLowerCase();\n\t\t\tconst lower = [name, ...name.split('-')];\n\t\t\tconst capital = [read, ...read.split('-')].find((part, index) => part.length >= 4 && part !== lower[index] && this.brands.has(part));\n\t\t\tif (capital) {\n\t\t\t\tflag(0.85, `\"${written}\" imitates ${capital} with a capital I for l`, capital);\n\t\t\t}\n\t\t}\n\n\t\t// (a short brand, or one that is a word, reached by letters alone, as\n\t\t// real names are: \"lrs\" for irs, \"arnex\" for amex, \"stearn\" for\n\t\t// steam; a digit for a letter is plainly a trick, and so are \"cl\"\n\t\t// for \"d\", \"vv\" for \"w\" and \"l\" for \"i\" in a brand of five letters\n\t\t// or more, which no word has: \"discorcl\", \"offlce\", \"strlpe\")\n\t\tconst trickScore = (brand, label) => (/\\d/.test(label) || (brand.length >= 5 && (!WORD_BRANDS.has(brand) || trickVariants(label, LETTER_TRICKS).has(brand))) ? 0.85 : 0.6);\n\t\tconst scripts = labelScripts(name);\n\t\tconst folded = skeleton(name);\n\t\tif (isSuspiciousMix(scripts)) {\n\t\t\tflag(0.6, `\"${unicode}\" mixes ${[...scripts].join(' and ')} letters`, null);\n\t\t}\n\n\t\tconst pieces = /^[a-z\\d-]+$/.test(folded) && folded.includes('-') ? folded.split('-') : [];\n\t\tconst phishy = pieces.some(piece => PHISHING_WORDS.includes(piece));\n\t\tconst viaConfusable = folded === name ? null : [folded, folded.replaceAll('-', '')].find(value => this.brands.has(value));\n\t\tif (viaConfusable) {\n\t\t\tflag(0.95, `\"${unicode}\" imitates ${viaConfusable} with lookalike letters`, viaConfusable);\n\t\t} else if (/^[a-z\\d-]+$/.test(folded)) {\n\t\t\tconst variants = trickVariants(folded);\n\t\t\tconst trick = [...variants].find(variant => this.brands.has(variant));\n\t\t\tif (trick) {\n\t\t\t\tflag(trickScore(trick, folded), `\"${unicode}\" imitates ${trick} by swapping characters`, trick);\n\t\t\t} else if (folded.length >= 5 && !SWAPPED_WORDS.has(folded)) {\n\t\t\t\tconst near = [...this.brands].find(brand => brand.length >= 5 && editDistance(folded, brand, 1) === 1 && isTypo(folded, brand));\n\t\t\t\tif (near) {\n\t\t\t\t\tflag(0.5, `\"${unicode}\" is one letter away from ${near}`, near);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst written = name.split('-');\n\t\t\t// (a brand that is a word or short, \"home-office\", \"zoom-lens\",\n\t\t\t// \"e-visa\", counts beside a phishing word only, or a brand that is\n\t\t\t// a word of five letters or more beside only words of its\n\t\t\t// services, \"apple-support\", \"support-apple-id\")\n\t\t\tconst official = SPARKASSE.test(parsed.domain) && !phishy;\n\t\t\t// (beside nothing else: not \"apple-valley-bank\", \"steam-trains-online\")\n\t\t\tconst service = piece => pieces.every(other => other === piece || (COMMON_WORD_BRANDS.get(piece)?.includes(other) ?? BRAND_SERVICE_WORDS.has(other)));\n\t\t\tconst counts = piece => phishy || (piece.length >= 5 && (!WORD_BRANDS.has(piece) || service(piece)));\n\t\t\tfor (const [index, piece] of pieces.entries()) {\n\t\t\t\tif (piece.length < 4 || official) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// (with lookalike letters, as any brand of five letters or more:\n\t\t\t\t// \"appl\u00E9-store\", not \"e-v\u00EDsa\")\n\t\t\t\tif (this.brands.has(piece) && piece !== written[index] && (piece.length >= 5 || phishy)) {\n\t\t\t\t\tflag(0.95, `\"${unicode}\" imitates ${piece} with lookalike letters`, piece);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif (this.brands.has(piece) && counts(piece)) {\n\t\t\t\t\tflag(0.6, `\"${unicode}\" puts ${piece} inside another domain name`, piece);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst disguised = [...trickVariants(piece)].find(variant => this.brands.has(variant));\n\t\t\t\tif (disguised) {\n\t\t\t\t\tflag(trickScore(disguised, piece), `\"${unicode}\" imitates ${disguised} by swapping characters`, disguised);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// A brand joined to a phishing word, in the name or one of its\n\t\t\t// parts, also with swapped characters (\"paypa1login\").\n\t\t\tfor (const value of [folded, ...pieces, ...variants]) {\n\t\t\t\tconst joined = brandWithPhishingWord(value, this.brands);\n\t\t\t\tif (joined) {\n\t\t\t\t\tflag(0.6, `\"${unicode}\" joins ${joined} to a phishing word`, joined);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// \"paypal.com.account-check.example\": a brand as a subdomain label.\n\t\tconst subdomain = parsed.subdomain ? parsed.subdomain.split('.') : [];\n\t\t// (a brand that is a word or short, \"www.steam.ncsu.edu\", only as the\n\t\t// start of an address, \"apple.com.verify.example\")\n\t\tconst labels = subdomain.map(label => skeleton(label));\n\t\t// (or a brand that is a word of five letters or more, but for the\n\t\t// common nouns, beside a phishing word in the rest of the name:\n\t\t// \"login.apple.example.com\", \"secure.apple.verify-account.com\")\n\t\tconst phishyHost = phishy || labels.some(label => PHISHING_WORDS.includes(label));\n\t\tconst signInHost = labels.some(label => SIGN_IN_LABELS.has(label));\n\t\tconst asBrand = label => (COMMON_WORD_BRANDS.has(label) ? signInHost && COMMON_WORD_BRANDS.get(label).length > 0 : phishyHost);\n\t\tconst brandLabel = labels.find((label, index) => label.length >= 4 && this.brands.has(label) && (this.options.strictMode || (label.length >= 5 && (!WORD_BRANDS.has(label) || asBrand(label))) || SUFFIX_LABEL.test(labels[index + 1] ?? '')));\n\t\tif (brandLabel && (this.options.strictMode || subdomain.length >= 2)) {\n\t\t\tflag(0.6, `\"${unicode}\" puts ${brandLabel} in front of another domain`, brandLabel);\n\t\t}\n\n\t\t// \"paypal.com-verify.example\": the brand's address, then a dash.\n\t\tconst last = subdomain.length > 0 ? skeleton(subdomain.at(-1)) : '';\n\t\tif (last.length >= 3 && this.brands.has(last) && (SUFFIX_DASH.test(folded) || OTHER_SUFFIX_DASH.test(folded))) {\n\t\t\tconst strong = SUFFIX_DASH.test(folded) || SECOND_SUFFIX_DASH.test(folded) || PHISHING_WORDS.some(word => folded.includes(word));\n\t\t\tflag(strong ? 0.85 : 0.6, `\"${unicode}\" starts with ${last}'s address and continues into another domain`, last);\n\t\t}\n\n\t\tresult.confidence = result.riskScore;\n\t\tif (result.riskScore >= 0.5) {\n\t\t\tresult.recommendations.push(result.brand ? `Do not sign in from this link; go to ${result.brand}'s site directly` : 'Treat links to this domain as untrusted');\n\t\t}\n\n\t\treturn result;\n\t}\n}\n\nexport {HomographDetector};\n", "import {franc} from 'franc';\n\n// Writing systems Spam Scanner recognises, checked in this order. A letter\n// belongs to the first script whose pattern matches it.\nconst SCRIPTS = [\n\t['Latin', /\\p{Script=Latin}/u],\n\t['Cyrillic', /\\p{Script=Cyrillic}/u],\n\t['Greek', /\\p{Script=Greek}/u],\n\t['Han', /\\p{Script=Han}/u],\n\t['Hiragana', /\\p{Script=Hiragana}/u],\n\t['Katakana', /\\p{Script=Katakana}/u],\n\t['Hangul', /\\p{Script=Hangul}/u],\n\t['Arabic', /\\p{Script=Arabic}/u],\n\t['Hebrew', /\\p{Script=Hebrew}/u],\n\t['Thai', /\\p{Script=Thai}/u],\n\t['Devanagari', /\\p{Script=Devanagari}/u],\n\t['Bengali', /\\p{Script=Bengali}/u],\n\t['Tamil', /\\p{Script=Tamil}/u],\n\t['Telugu', /\\p{Script=Telugu}/u],\n\t['Gujarati', /\\p{Script=Gujarati}/u],\n\t['Gurmukhi', /\\p{Script=Gurmukhi}/u],\n\t['Kannada', /\\p{Script=Kannada}/u],\n\t['Malayalam', /\\p{Script=Malayalam}/u],\n\t['Armenian', /\\p{Script=Armenian}/u],\n\t['Georgian', /\\p{Script=Georgian}/u],\n\t['Ethiopic', /\\p{Script=Ethiopic}/u],\n\t['Khmer', /\\p{Script=Khmer}/u],\n\t['Lao', /\\p{Script=Lao}/u],\n\t['Myanmar', /\\p{Script=Myanmar}/u],\n\t['Sinhala', /\\p{Script=Sinhala}/u],\n];\n\n// Scripts used by a single major language: no statistical detection needed.\nconst SCRIPT_LANGUAGE = {\n\tHangul: 'ko',\n\tHebrew: 'he',\n\tThai: 'th',\n\tGreek: 'el',\n\tBengali: 'bn',\n\tTamil: 'ta',\n\tTelugu: 'te',\n\tGujarati: 'gu',\n\tGurmukhi: 'pa',\n\tKannada: 'kn',\n\tMalayalam: 'ml',\n\tArmenian: 'hy',\n\tGeorgian: 'ka',\n\tKhmer: 'km',\n\tLao: 'lo',\n\tMyanmar: 'my',\n\tSinhala: 'si',\n\tHiragana: 'ja',\n\tKatakana: 'ja',\n};\n\n// ISO 639-3 (what franc returns) to ISO 639-1, for the languages franc detects\n// that have a two-letter code.\nconst ISO6393_TO_1 = {\n\tafr: 'af', aka: 'ak', amh: 'am', arb: 'ar', ara: 'ar', azj: 'az', azb: 'az', aze: 'az', bel: 'be', ben: 'bn', bos: 'bs', bul: 'bg', cat: 'ca', ceb: 'ceb', ces: 'cs', ckb: 'ku', khk: 'mn', mlg: 'mg', plt: 'mg', cmn: 'zh', zho: 'zh', cym: 'cy', dan: 'da', deu: 'de', ell: 'el', eng: 'en', epo: 'eo', est: 'et', ekk: 'et', eus: 'eu', fin: 'fi', fra: 'fr', fuv: 'ff', gax: 'om', gle: 'ga', glg: 'gl', guj: 'gu', hat: 'ht', hau: 'ha', heb: 'he', hin: 'hi', hrv: 'hr', hun: 'hu', hye: 'hy', ibo: 'ig', ilo: 'ilo', ind: 'id', isl: 'is', ita: 'it', jav: 'jv', jpn: 'ja', kan: 'kn', kat: 'ka', kaz: 'kk', khm: 'km', kin: 'rw', kir: 'ky', kor: 'ko', lao: 'lo', lat: 'la', lit: 'lt', lvs: 'lv', lav: 'lv', mal: 'ml', mar: 'mr', mkd: 'mk', mlt: 'mt', mya: 'my', nep: 'ne', npi: 'ne', nld: 'nl', nno: 'nn', nob: 'nb', nor: 'no', nya: 'ny', ori: 'or', ory: 'or', pan: 'pa', pes: 'fa', fas: 'fa', pbu: 'ps', pol: 'pl', por: 'pt', ron: 'ro', run: 'rn', rus: 'ru', sin: 'si', slk: 'sk', slv: 'sl', sna: 'sn', som: 'so', spa: 'es', sqi: 'sq', als: 'sq', srp: 'sr', sun: 'su', swe: 'sv', swh: 'sw', swa: 'sw', tam: 'ta', tel: 'te', tgk: 'tg', tgl: 'tl', tha: 'th', tir: 'ti', tuk: 'tk', tur: 'tr', uig: 'ug', ukr: 'uk', urd: 'ur', uzn: 'uz', uzb: 'uz', vie: 'vi', xho: 'xh', yor: 'yo', zlm: 'ms', zsm: 'ms', msa: 'ms', zul: 'zu',\n};\n\n// Languages franc may answer with: those with tens of millions of speakers or\n// an official national role. Without a list, franc picks rare relatives\n// (Scots for English, for example) on short texts.\nconst FRANC_LANGUAGES = [\n\t'eng', 'spa', 'fra', 'deu', 'ita', 'por', 'nld', 'pol', 'ces', 'slk', 'slv', 'hrv', 'srp', 'bos', 'bul', 'mkd', 'rus', 'ukr', 'bel', 'ron', 'hun', 'fin', 'ekk', 'lvs', 'lit', 'swe', 'nob', 'dan', 'isl', 'tur', 'azj', 'kaz', 'uzn', 'kir', 'tgk', 'ind', 'zlm', 'vie', 'tgl', 'ceb', 'swh', 'hau', 'yor', 'ibo', 'amh', 'som', 'afr', 'zul', 'xho', 'cat', 'eus', 'gle', 'cym', 'als', 'arb', 'pes', 'urd', 'pbu', 'ckb', 'hin', 'mar', 'nep', 'jav', 'sun', 'mlt', 'run', 'kin', 'nya', 'sna', 'tir', 'gax', 'plt', 'hat', 'khk',\n];\n\nconst LETTER = /\\p{L}/u;\n\n/**\n * Count letters per script in the first `limit` characters of a string.\n * @param {string} text\n * @param {number} [limit]\n * @returns {Map<string, number>} script name to letter count, most frequent first\n */\nexport function countScripts(text, limit = 4000) {\n\tconst counts = new Map();\n\tlet seen = 0;\n\tfor (const char of text) {\n\t\tif (seen++ >= limit) {\n\t\t\tbreak;\n\t\t}\n\n\t\tif (!LETTER.test(char)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst entry = SCRIPTS.find(([, pattern]) => pattern.test(char));\n\t\tconst name = entry ? entry[0] : 'Other';\n\t\tcounts.set(name, (counts.get(name) || 0) + 1);\n\t}\n\n\treturn new Map([...counts].sort((a, b) => b[1] - a[1]));\n}\n\n/**\n * The script a single letter belongs to, or \"Other\".\n * @param {string} char\n * @returns {string}\n */\nexport function scriptOf(char) {\n\tconst entry = SCRIPTS.find(([, pattern]) => pattern.test(char));\n\treturn entry ? entry[0] : 'Other';\n}\n\n// Codes for the same language as one the detector answers with: Norwegian\n// (\"no\", \"nor\") is written as Bokm\u00E5l (\"nb\") almost always, Filipino as\n// Tagalog, and \"iw\", \"in\" and \"ji\" are the codes Hebrew, Indonesian and\n// Yiddish had before 1989, which Java and old systems still use.\nconst SAME_LANGUAGE = {\n\tno: 'nb', nor: 'nb', fil: 'tl', iw: 'he', in: 'id', ji: 'yi', yid: 'yi',\n};\n\n/**\n * Normalize a language code to ISO 639-1 where one exists: \"eng\" and \"en-US\"\n * both become \"en\", and a code of the same language as one detected becomes\n * that one (\"no\" is \"nb\", \"fil\" \"tl\", \"iw\" \"he\", \"in\" \"id\", \"ji\" \"yi\").\n * Unknown three-letter codes are returned lowercased.\n * @param {string} code\n * @returns {string|null}\n */\nexport function normalizeLanguageCode(code) {\n\tif (typeof code !== 'string' || code.trim() === '') {\n\t\treturn null;\n\t}\n\n\tconst lower = code.trim().toLowerCase().replace('_', '-');\n\tconst base = lower.split('-')[0];\n\tif (Object.hasOwn(SAME_LANGUAGE, base)) {\n\t\treturn SAME_LANGUAGE[base];\n\t}\n\n\tif (base.length === 3 && ISO6393_TO_1[base]) {\n\t\treturn ISO6393_TO_1[base];\n\t}\n\n\treturn base;\n}\n\n// Letters of Cyrillic alphabets of Turkic, Iranian and Mongolic languages\n// (Kazakh, Uzbek, Kyrgyz, Tajik, Tatar, Bashkir, Mongolian), not Slavic ones\nconst NON_SLAVIC_CYRILLIC = /[\u04D9\u04E9\u04AF\u04B1\u049B\u0493\u04A3\u04BB\u04B3\u04B7\u04E3\u04EF\u0497\u04A1\u0499\u04AB]/i;\n\n// Letters that belong to one Cyrillic alphabet. Trigram statistics confuse\n// Russian with Bulgarian on short texts; these letters do not. Not in text\n// with letters of Kazakh, Uzbek, Kyrgyz, Mongolian and others (see\n// NON_SLAVIC_CYRILLIC), whose alphabets have \"\u0456\" (Kazakh), \"\u045E\"\n// (Uzbek) or \"\u044B\" and \"\u044D\" (Kyrgyz, Mongolian) too: franc tells those.\nfunction cyrillicLanguage(text) {\n\tif (NON_SLAVIC_CYRILLIC.test(text)) {\n\t\treturn null;\n\t}\n\n\tif (/\u045E/i.test(text)) {\n\t\treturn 'be';\n\t}\n\n\tif (/[\u0456\u0457\u0454\u0491]/i.test(text)) {\n\t\treturn 'uk';\n\t}\n\n\tif (/[\u0452\u045B\u045F]/i.test(text)) {\n\t\treturn 'sr';\n\t}\n\n\tif (/[\u0453\u045C\u0455]/i.test(text)) {\n\t\treturn 'mk';\n\t}\n\n\tif (/[\u044B\u044D\u0451]/i.test(text)) {\n\t\treturn 'ru';\n\t}\n\n\treturn null;\n}\n\n// Slavic languages franc may name for Cyrillic text (Bosnian for Serbian)\nconst SLAVIC = new Set(['ru', 'uk', 'be', 'bg', 'mk', 'sr', 'bs']);\n\n// Words with letters of a Turkic, Iranian or Mongolic alphabet, or with \"\u0456\",\n// which Kazakh has as well as Ukrainian and Belarusian\nconst NON_SLAVIC_WORD = /[\u04D9\u04E9\u04AF\u04B1\u049B\u0493\u04A3\u04BB\u04B3\u04B7\u04E3\u04EF\u0497\u04A1\u0499\u04AB\u0456]/i;\n\n// The words of a text but for names, words in title case other than at the\n// start of a sentence or a line: the letters of a name (\"\u0420\u044B\u0441\u043A\u0443\u043B\u043E\u0432\",\n// \"\u049A\u0430\u0440\u0430\u0493\u0430\u043D\u0434\u044B\") belong to the language of the person or place, not to that of\n// the text; a word in capitals is no name (\"\u0412\u0410\u0428\u0410 \u041F\u041E\u0421\u042B\u041B\u041A\u0410 \u0417\u0410\u0414\u0415\u0420\u0416\u0410\u041D\u0410\"). (Split\n// at non-letters, not matched word by word, to stay linear on one long\n// word.)\nfunction words(text) {\n\tconst kept = [];\n\tlet sentence = true;\n\tfor (const [index, part] of text.split(/([^\\p{L}\\p{M}]+)/u).entries()) {\n\t\tif (index % 2 === 1) {\n\t\t\tsentence = /[.!?\u2026\\n]/u.test(part);\n\t\t} else if (sentence || !/^\\p{Lu}[\\p{Ll}\\p{M}]/u.test(part)) {\n\t\t\tkept.push(part);\n\t\t}\n\t}\n\n\treturn kept;\n}\n\n// The language of Cyrillic text with letters of Kazakh, Uzbek and others,\n// which franc reads as Slavic: a Russian, Bulgarian or Ukrainian text that\n// names a Kazakh person or place, or has a Kazakh word or two. Its other\n// words, but for names, decide as in a Slavic text (\"\u044B\" is Russian, never\n// Bulgarian), else franc's answer stands.\nfunction slavicLanguage(text, code) {\n\treturn cyrillicLanguage(words(text).filter(word => !NON_SLAVIC_WORD.test(word)).join(' ')) || code;\n}\n\n/**\n * Detect the language of a text.\n *\n * Scripts used by one language (Korean, Thai, Greek, Hebrew, Japanese kana and\n * others) decide directly. Otherwise franc's trigram model decides when there\n * is enough text; short texts fall back to the script alone.\n *\n * @param {string} text\n * @returns {{language: string|null, script: string|null, scripts: string[], confidence: number}}\n */\nexport function detectLanguage(text) {\n\tconst result = {\n\t\tlanguage: null, script: null, scripts: [], confidence: 0,\n\t};\n\tif (typeof text !== 'string' || text.trim() === '') {\n\t\treturn result;\n\t}\n\n\tconst sample = text.slice(0, 20_000);\n\tconst counts = countScripts(sample);\n\tconst total = [...counts.values()].reduce((sum, n) => sum + n, 0);\n\tif (total === 0) {\n\t\treturn result;\n\t}\n\n\tconst [[script, count]] = counts;\n\tresult.script = script;\n\tresult.scripts = [...counts.keys()].filter(name => counts.get(name) / total >= 0.1);\n\tresult.confidence = count / total;\n\n\t// Japanese mixes Han with kana: any meaningful kana means Japanese.\n\tconst kana = (counts.get('Hiragana') || 0) + (counts.get('Katakana') || 0);\n\tif (kana / total >= 0.05) {\n\t\tresult.language = 'ja';\n\t\treturn result;\n\t}\n\n\tif (SCRIPT_LANGUAGE[script]) {\n\t\tresult.language = SCRIPT_LANGUAGE[script];\n\t\treturn result;\n\t}\n\n\tif (script === 'Han') {\n\t\tresult.language = 'zh';\n\t\treturn result;\n\t}\n\n\t// (by the letters of words other than names; by those of names too only\n\t// in a text too short for franc, which otherwise tells: a Bulgarian text\n\t// that names \"\u0433\u043E\u0441\u043F\u043E\u0434\u0438\u043D \u0420\u044B\u0441\u043A\u0443\u043B\u043E\u0432\")\n\tif (script === 'Cyrillic') {\n\t\tconst cyrillic = cyrillicLanguage(words(sample).join(' ')) ?? (total < 40 ? cyrillicLanguage(sample) : null);\n\t\tif (cyrillic) {\n\t\t\tresult.language = cyrillic;\n\t\t\treturn result;\n\t\t}\n\t}\n\n\t// Latin and Cyrillic alphabets are shared by many languages and need more\n\t// text to tell apart than scripts such as Devanagari or Arabic.\n\tif (total >= (script === 'Latin' || script === 'Cyrillic' ? 40 : 15)) {\n\t\tconst code = franc(sample, {minLength: 20, only: FRANC_LANGUAGES});\n\t\tif (code !== 'und') {\n\t\t\tconst language = normalizeLanguageCode(code);\n\t\t\t// (Cyrillic text with letters of Kazakh and others, which\n\t\t\t// cyrillicLanguage leaves to franc, and franc reads as Slavic)\n\t\t\tresult.language = script === 'Cyrillic' && SLAVIC.has(language) && NON_SLAVIC_CYRILLIC.test(sample)\n\t\t\t\t? slavicLanguage(sample, language)\n\t\t\t\t: language;\n\t\t\treturn result;\n\t\t}\n\t}\n\n\t// Too short to tell languages of the same script apart.\n\tresult.confidence = 0;\n\treturn result;\n}\n", "// The character references browsers decode by name, with their \";\" (the\n// WHATWG HTML standard's named character references,\n// https://html.spec.whatwg.org/entities.json): \"name=code points\", each in\n// hexadecimal, \".\" between them\nconst NAMED = [\n\t'AElig=c6,AMP=26,Aacute=c1,Abreve=102,Acirc=c2,Acy=410,Afr=1d504,Agrave=c0,Alpha=391,Amacr=100,And=2a',\n\t'53,Aogon=104,Aopf=1d538,ApplyFunction=2061,Aring=c5,Ascr=1d49c,Assign=2254,Atilde=c3,Auml=c4,Backsla',\n\t'sh=2216,Barv=2ae7,Barwed=2306,Bcy=411,Because=2235,Bernoullis=212c,Beta=392,Bfr=1d505,Bopf=1d539,Bre',\n\t've=2d8,Bscr=212c,Bumpeq=224e,CHcy=427,COPY=a9,Cacute=106,Cap=22d2,CapitalDifferentialD=2145,Cayleys=',\n\t'212d,Ccaron=10c,Ccedil=c7,Ccirc=108,Cconint=2230,Cdot=10a,Cedilla=b8,CenterDot=b7,Cfr=212d,Chi=3a7,C',\n\t'ircleDot=2299,CircleMinus=2296,CirclePlus=2295,CircleTimes=2297,ClockwiseContourIntegral=2232,CloseC',\n\t'urlyDoubleQuote=201d,CloseCurlyQuote=2019,Colon=2237,Colone=2a74,Congruent=2261,Conint=222f,ContourI',\n\t'ntegral=222e,Copf=2102,Coproduct=2210,CounterClockwiseContourIntegral=2233,Cross=2a2f,Cscr=1d49e,Cup',\n\t'=22d3,CupCap=224d,DD=2145,DDotrahd=2911,DJcy=402,DScy=405,DZcy=40f,Dagger=2021,Darr=21a1,Dashv=2ae4,',\n\t'Dcaron=10e,Dcy=414,Del=2207,Delta=394,Dfr=1d507,DiacriticalAcute=b4,DiacriticalDot=2d9,DiacriticalDo',\n\t'ubleAcute=2dd,DiacriticalGrave=60,DiacriticalTilde=2dc,Diamond=22c4,DifferentialD=2146,Dopf=1d53b,Do',\n\t't=a8,DotDot=20dc,DotEqual=2250,DoubleContourIntegral=222f,DoubleDot=a8,DoubleDownArrow=21d3,DoubleLe',\n\t'ftArrow=21d0,DoubleLeftRightArrow=21d4,DoubleLeftTee=2ae4,DoubleLongLeftArrow=27f8,DoubleLongLeftRig',\n\t'htArrow=27fa,DoubleLongRightArrow=27f9,DoubleRightArrow=21d2,DoubleRightTee=22a8,DoubleUpArrow=21d1,',\n\t'DoubleUpDownArrow=21d5,DoubleVerticalBar=2225,DownArrow=2193,DownArrowBar=2913,DownArrowUpArrow=21f5',\n\t',DownBreve=311,DownLeftRightVector=2950,DownLeftTeeVector=295e,DownLeftVector=21bd,DownLeftVectorBar',\n\t'=2956,DownRightTeeVector=295f,DownRightVector=21c1,DownRightVectorBar=2957,DownTee=22a4,DownTeeArrow',\n\t'=21a7,Downarrow=21d3,Dscr=1d49f,Dstrok=110,ENG=14a,ETH=d0,Eacute=c9,Ecaron=11a,Ecirc=ca,Ecy=42d,Edot',\n\t'=116,Efr=1d508,Egrave=c8,Element=2208,Emacr=112,EmptySmallSquare=25fb,EmptyVerySmallSquare=25ab,Eogo',\n\t'n=118,Eopf=1d53c,Epsilon=395,Equal=2a75,EqualTilde=2242,Equilibrium=21cc,Escr=2130,Esim=2a73,Eta=397',\n\t',Euml=cb,Exists=2203,ExponentialE=2147,Fcy=424,Ffr=1d509,FilledSmallSquare=25fc,FilledVerySmallSquar',\n\t'e=25aa,Fopf=1d53d,ForAll=2200,Fouriertrf=2131,Fscr=2131,GJcy=403,GT=3e,Gamma=393,Gammad=3dc,Gbreve=1',\n\t'1e,Gcedil=122,Gcirc=11c,Gcy=413,Gdot=120,Gfr=1d50a,Gg=22d9,Gopf=1d53e,GreaterEqual=2265,GreaterEqual',\n\t'Less=22db,GreaterFullEqual=2267,GreaterGreater=2aa2,GreaterLess=2277,GreaterSlantEqual=2a7e,GreaterT',\n\t'ilde=2273,Gscr=1d4a2,Gt=226b,HARDcy=42a,Hacek=2c7,Hat=5e,Hcirc=124,Hfr=210c,HilbertSpace=210b,Hopf=2',\n\t'10d,HorizontalLine=2500,Hscr=210b,Hstrok=126,HumpDownHump=224e,HumpEqual=224f,IEcy=415,IJlig=132,IOc',\n\t'y=401,Iacute=cd,Icirc=ce,Icy=418,Idot=130,Ifr=2111,Igrave=cc,Im=2111,Imacr=12a,ImaginaryI=2148,Impli',\n\t'es=21d2,Int=222c,Integral=222b,Intersection=22c2,InvisibleComma=2063,InvisibleTimes=2062,Iogon=12e,I',\n\t'opf=1d540,Iota=399,Iscr=2110,Itilde=128,Iukcy=406,Iuml=cf,Jcirc=134,Jcy=419,Jfr=1d50d,Jopf=1d541,Jsc',\n\t'r=1d4a5,Jsercy=408,Jukcy=404,KHcy=425,KJcy=40c,Kappa=39a,Kcedil=136,Kcy=41a,Kfr=1d50e,Kopf=1d542,Ksc',\n\t'r=1d4a6,LJcy=409,LT=3c,Lacute=139,Lambda=39b,Lang=27ea,Laplacetrf=2112,Larr=219e,Lcaron=13d,Lcedil=1',\n\t'3b,Lcy=41b,LeftAngleBracket=27e8,LeftArrow=2190,LeftArrowBar=21e4,LeftArrowRightArrow=21c6,LeftCeili',\n\t'ng=2308,LeftDoubleBracket=27e6,LeftDownTeeVector=2961,LeftDownVector=21c3,LeftDownVectorBar=2959,Lef',\n\t'tFloor=230a,LeftRightArrow=2194,LeftRightVector=294e,LeftTee=22a3,LeftTeeArrow=21a4,LeftTeeVector=29',\n\t'5a,LeftTriangle=22b2,LeftTriangleBar=29cf,LeftTriangleEqual=22b4,LeftUpDownVector=2951,LeftUpTeeVect',\n\t'or=2960,LeftUpVector=21bf,LeftUpVectorBar=2958,LeftVector=21bc,LeftVectorBar=2952,Leftarrow=21d0,Lef',\n\t'trightarrow=21d4,LessEqualGreater=22da,LessFullEqual=2266,LessGreater=2276,LessLess=2aa1,LessSlantEq',\n\t'ual=2a7d,LessTilde=2272,Lfr=1d50f,Ll=22d8,Lleftarrow=21da,Lmidot=13f,LongLeftArrow=27f5,LongLeftRigh',\n\t'tArrow=27f7,LongRightArrow=27f6,Longleftarrow=27f8,Longleftrightarrow=27fa,Longrightarrow=27f9,Lopf=',\n\t'1d543,LowerLeftArrow=2199,LowerRightArrow=2198,Lscr=2112,Lsh=21b0,Lstrok=141,Lt=226a,Map=2905,Mcy=41',\n\t'c,MediumSpace=205f,Mellintrf=2133,Mfr=1d510,MinusPlus=2213,Mopf=1d544,Mscr=2133,Mu=39c,NJcy=40a,Nacu',\n\t'te=143,Ncaron=147,Ncedil=145,Ncy=41d,NegativeMediumSpace=200b,NegativeThickSpace=200b,NegativeThinSp',\n\t'ace=200b,NegativeVeryThinSpace=200b,NestedGreaterGreater=226b,NestedLessLess=226a,NewLine=a,Nfr=1d51',\n\t'1,NoBreak=2060,NonBreakingSpace=a0,Nopf=2115,Not=2aec,NotCongruent=2262,NotCupCap=226d,NotDoubleVert',\n\t'icalBar=2226,NotElement=2209,NotEqual=2260,NotEqualTilde=2242.338,NotExists=2204,NotGreater=226f,Not',\n\t'GreaterEqual=2271,NotGreaterFullEqual=2267.338,NotGreaterGreater=226b.338,NotGreaterLess=2279,NotGre',\n\t'aterSlantEqual=2a7e.338,NotGreaterTilde=2275,NotHumpDownHump=224e.338,NotHumpEqual=224f.338,NotLeftT',\n\t'riangle=22ea,NotLeftTriangleBar=29cf.338,NotLeftTriangleEqual=22ec,NotLess=226e,NotLessEqual=2270,No',\n\t'tLessGreater=2278,NotLessLess=226a.338,NotLessSlantEqual=2a7d.338,NotLessTilde=2274,NotNestedGreater',\n\t'Greater=2aa2.338,NotNestedLessLess=2aa1.338,NotPrecedes=2280,NotPrecedesEqual=2aaf.338,NotPrecedesSl',\n\t'antEqual=22e0,NotReverseElement=220c,NotRightTriangle=22eb,NotRightTriangleBar=29d0.338,NotRightTria',\n\t'ngleEqual=22ed,NotSquareSubset=228f.338,NotSquareSubsetEqual=22e2,NotSquareSuperset=2290.338,NotSqua',\n\t'reSupersetEqual=22e3,NotSubset=2282.20d2,NotSubsetEqual=2288,NotSucceeds=2281,NotSucceedsEqual=2ab0.',\n\t'338,NotSucceedsSlantEqual=22e1,NotSucceedsTilde=227f.338,NotSuperset=2283.20d2,NotSupersetEqual=2289',\n\t',NotTilde=2241,NotTildeEqual=2244,NotTildeFullEqual=2247,NotTildeTilde=2249,NotVerticalBar=2224,Nscr',\n\t'=1d4a9,Ntilde=d1,Nu=39d,OElig=152,Oacute=d3,Ocirc=d4,Ocy=41e,Odblac=150,Ofr=1d512,Ograve=d2,Omacr=14',\n\t'c,Omega=3a9,Omicron=39f,Oopf=1d546,OpenCurlyDoubleQuote=201c,OpenCurlyQuote=2018,Or=2a54,Oscr=1d4aa,',\n\t'Oslash=d8,Otilde=d5,Otimes=2a37,Ouml=d6,OverBar=203e,OverBrace=23de,OverBracket=23b4,OverParenthesis',\n\t'=23dc,PartialD=2202,Pcy=41f,Pfr=1d513,Phi=3a6,Pi=3a0,PlusMinus=b1,Poincareplane=210c,Popf=2119,Pr=2a',\n\t'bb,Precedes=227a,PrecedesEqual=2aaf,PrecedesSlantEqual=227c,PrecedesTilde=227e,Prime=2033,Product=22',\n\t'0f,Proportion=2237,Proportional=221d,Pscr=1d4ab,Psi=3a8,QUOT=22,Qfr=1d514,Qopf=211a,Qscr=1d4ac,RBarr',\n\t'=2910,REG=ae,Racute=154,Rang=27eb,Rarr=21a0,Rarrtl=2916,Rcaron=158,Rcedil=156,Rcy=420,Re=211c,Revers',\n\t'eElement=220b,ReverseEquilibrium=21cb,ReverseUpEquilibrium=296f,Rfr=211c,Rho=3a1,RightAngleBracket=2',\n\t'7e9,RightArrow=2192,RightArrowBar=21e5,RightArrowLeftArrow=21c4,RightCeiling=2309,RightDoubleBracket',\n\t'=27e7,RightDownTeeVector=295d,RightDownVector=21c2,RightDownVectorBar=2955,RightFloor=230b,RightTee=',\n\t'22a2,RightTeeArrow=21a6,RightTeeVector=295b,RightTriangle=22b3,RightTriangleBar=29d0,RightTriangleEq',\n\t'ual=22b5,RightUpDownVector=294f,RightUpTeeVector=295c,RightUpVector=21be,RightUpVectorBar=2954,Right',\n\t'Vector=21c0,RightVectorBar=2953,Rightarrow=21d2,Ropf=211d,RoundImplies=2970,Rrightarrow=21db,Rscr=21',\n\t'1b,Rsh=21b1,RuleDelayed=29f4,SHCHcy=429,SHcy=428,SOFTcy=42c,Sacute=15a,Sc=2abc,Scaron=160,Scedil=15e',\n\t',Scirc=15c,Scy=421,Sfr=1d516,ShortDownArrow=2193,ShortLeftArrow=2190,ShortRightArrow=2192,ShortUpArr',\n\t'ow=2191,Sigma=3a3,SmallCircle=2218,Sopf=1d54a,Sqrt=221a,Square=25a1,SquareIntersection=2293,SquareSu',\n\t'bset=228f,SquareSubsetEqual=2291,SquareSuperset=2290,SquareSupersetEqual=2292,SquareUnion=2294,Sscr=',\n\t'1d4ae,Star=22c6,Sub=22d0,Subset=22d0,SubsetEqual=2286,Succeeds=227b,SucceedsEqual=2ab0,SucceedsSlant',\n\t'Equal=227d,SucceedsTilde=227f,SuchThat=220b,Sum=2211,Sup=22d1,Superset=2283,SupersetEqual=2287,Supse',\n\t't=22d1,THORN=de,TRADE=2122,TSHcy=40b,TScy=426,Tab=9,Tau=3a4,Tcaron=164,Tcedil=162,Tcy=422,Tfr=1d517,',\n\t'Therefore=2234,Theta=398,ThickSpace=205f.200a,ThinSpace=2009,Tilde=223c,TildeEqual=2243,TildeFullEqu',\n\t'al=2245,TildeTilde=2248,Topf=1d54b,TripleDot=20db,Tscr=1d4af,Tstrok=166,Uacute=da,Uarr=219f,Uarrocir',\n\t'=2949,Ubrcy=40e,Ubreve=16c,Ucirc=db,Ucy=423,Udblac=170,Ufr=1d518,Ugrave=d9,Umacr=16a,UnderBar=5f,Und',\n\t'erBrace=23df,UnderBracket=23b5,UnderParenthesis=23dd,Union=22c3,UnionPlus=228e,Uogon=172,Uopf=1d54c,',\n\t'UpArrow=2191,UpArrowBar=2912,UpArrowDownArrow=21c5,UpDownArrow=2195,UpEquilibrium=296e,UpTee=22a5,Up',\n\t'TeeArrow=21a5,Uparrow=21d1,Updownarrow=21d5,UpperLeftArrow=2196,UpperRightArrow=2197,Upsi=3d2,Upsilo',\n\t'n=3a5,Uring=16e,Uscr=1d4b0,Utilde=168,Uuml=dc,VDash=22ab,Vbar=2aeb,Vcy=412,Vdash=22a9,Vdashl=2ae6,Ve',\n\t'e=22c1,Verbar=2016,Vert=2016,VerticalBar=2223,VerticalLine=7c,VerticalSeparator=2758,VerticalTilde=2',\n\t'240,VeryThinSpace=200a,Vfr=1d519,Vopf=1d54d,Vscr=1d4b1,Vvdash=22aa,Wcirc=174,Wedge=22c0,Wfr=1d51a,Wo',\n\t'pf=1d54e,Wscr=1d4b2,Xfr=1d51b,Xi=39e,Xopf=1d54f,Xscr=1d4b3,YAcy=42f,YIcy=407,YUcy=42e,Yacute=dd,Ycir',\n\t'c=176,Ycy=42b,Yfr=1d51c,Yopf=1d550,Yscr=1d4b4,Yuml=178,ZHcy=416,Zacute=179,Zcaron=17d,Zcy=417,Zdot=1',\n\t'7b,ZeroWidthSpace=200b,Zeta=396,Zfr=2128,Zopf=2124,Zscr=1d4b5,aacute=e1,abreve=103,ac=223e,acE=223e.',\n\t'333,acd=223f,acirc=e2,acute=b4,acy=430,aelig=e6,af=2061,afr=1d51e,agrave=e0,alefsym=2135,aleph=2135,',\n\t'alpha=3b1,amacr=101,amalg=2a3f,amp=26,and=2227,andand=2a55,andd=2a5c,andslope=2a58,andv=2a5a,ang=222',\n\t'0,ange=29a4,angle=2220,angmsd=2221,angmsdaa=29a8,angmsdab=29a9,angmsdac=29aa,angmsdad=29ab,angmsdae=',\n\t'29ac,angmsdaf=29ad,angmsdag=29ae,angmsdah=29af,angrt=221f,angrtvb=22be,angrtvbd=299d,angsph=2222,ang',\n\t'st=c5,angzarr=237c,aogon=105,aopf=1d552,ap=2248,apE=2a70,apacir=2a6f,ape=224a,apid=224b,apos=27,appr',\n\t'ox=2248,approxeq=224a,aring=e5,ascr=1d4b6,ast=2a,asymp=2248,asympeq=224d,atilde=e3,auml=e4,awconint=',\n\t'2233,awint=2a11,bNot=2aed,backcong=224c,backepsilon=3f6,backprime=2035,backsim=223d,backsimeq=22cd,b',\n\t'arvee=22bd,barwed=2305,barwedge=2305,bbrk=23b5,bbrktbrk=23b6,bcong=224c,bcy=431,bdquo=201e,becaus=22',\n\t'35,because=2235,bemptyv=29b0,bepsi=3f6,bernou=212c,beta=3b2,beth=2136,between=226c,bfr=1d51f,bigcap=',\n\t'22c2,bigcirc=25ef,bigcup=22c3,bigodot=2a00,bigoplus=2a01,bigotimes=2a02,bigsqcup=2a06,bigstar=2605,b',\n\t'igtriangledown=25bd,bigtriangleup=25b3,biguplus=2a04,bigvee=22c1,bigwedge=22c0,bkarow=290d,blackloze',\n\t'nge=29eb,blacksquare=25aa,blacktriangle=25b4,blacktriangledown=25be,blacktriangleleft=25c2,blacktria',\n\t'ngleright=25b8,blank=2423,blk12=2592,blk14=2591,blk34=2593,block=2588,bne=3d.20e5,bnequiv=2261.20e5,',\n\t'bnot=2310,bopf=1d553,bot=22a5,bottom=22a5,bowtie=22c8,boxDL=2557,boxDR=2554,boxDl=2556,boxDr=2553,bo',\n\t'xH=2550,boxHD=2566,boxHU=2569,boxHd=2564,boxHu=2567,boxUL=255d,boxUR=255a,boxUl=255c,boxUr=2559,boxV',\n\t'=2551,boxVH=256c,boxVL=2563,boxVR=2560,boxVh=256b,boxVl=2562,boxVr=255f,boxbox=29c9,boxdL=2555,boxdR',\n\t'=2552,boxdl=2510,boxdr=250c,boxh=2500,boxhD=2565,boxhU=2568,boxhd=252c,boxhu=2534,boxminus=229f,boxp',\n\t'lus=229e,boxtimes=22a0,boxuL=255b,boxuR=2558,boxul=2518,boxur=2514,boxv=2502,boxvH=256a,boxvL=2561,b',\n\t'oxvR=255e,boxvh=253c,boxvl=2524,boxvr=251c,bprime=2035,breve=2d8,brvbar=a6,bscr=1d4b7,bsemi=204f,bsi',\n\t'm=223d,bsime=22cd,bsol=5c,bsolb=29c5,bsolhsub=27c8,bull=2022,bullet=2022,bump=224e,bumpE=2aae,bumpe=',\n\t'224f,bumpeq=224f,cacute=107,cap=2229,capand=2a44,capbrcup=2a49,capcap=2a4b,capcup=2a47,capdot=2a40,c',\n\t'aps=2229.fe00,caret=2041,caron=2c7,ccaps=2a4d,ccaron=10d,ccedil=e7,ccirc=109,ccups=2a4c,ccupssm=2a50',\n\t',cdot=10b,cedil=b8,cemptyv=29b2,cent=a2,centerdot=b7,cfr=1d520,chcy=447,check=2713,checkmark=2713,ch',\n\t'i=3c7,cir=25cb,cirE=29c3,circ=2c6,circeq=2257,circlearrowleft=21ba,circlearrowright=21bb,circledR=ae',\n\t',circledS=24c8,circledast=229b,circledcirc=229a,circleddash=229d,cire=2257,cirfnint=2a10,cirmid=2aef',\n\t',cirscir=29c2,clubs=2663,clubsuit=2663,colon=3a,colone=2254,coloneq=2254,comma=2c,commat=40,comp=220',\n\t'1,compfn=2218,complement=2201,complexes=2102,cong=2245,congdot=2a6d,conint=222e,copf=1d554,coprod=22',\n\t'10,copy=a9,copysr=2117,crarr=21b5,cross=2717,cscr=1d4b8,csub=2acf,csube=2ad1,csup=2ad0,csupe=2ad2,ct',\n\t'dot=22ef,cudarrl=2938,cudarrr=2935,cuepr=22de,cuesc=22df,cularr=21b6,cularrp=293d,cup=222a,cupbrcap=',\n\t'2a48,cupcap=2a46,cupcup=2a4a,cupdot=228d,cupor=2a45,cups=222a.fe00,curarr=21b7,curarrm=293c,curlyeqp',\n\t'rec=22de,curlyeqsucc=22df,curlyvee=22ce,curlywedge=22cf,curren=a4,curvearrowleft=21b6,curvearrowrigh',\n\t't=21b7,cuvee=22ce,cuwed=22cf,cwconint=2232,cwint=2231,cylcty=232d,dArr=21d3,dHar=2965,dagger=2020,da',\n\t'leth=2138,darr=2193,dash=2010,dashv=22a3,dbkarow=290f,dblac=2dd,dcaron=10f,dcy=434,dd=2146,ddagger=2',\n\t'021,ddarr=21ca,ddotseq=2a77,deg=b0,delta=3b4,demptyv=29b1,dfisht=297f,dfr=1d521,dharl=21c3,dharr=21c',\n\t'2,diam=22c4,diamond=22c4,diamondsuit=2666,diams=2666,die=a8,digamma=3dd,disin=22f2,div=f7,divide=f7,',\n\t'divideontimes=22c7,divonx=22c7,djcy=452,dlcorn=231e,dlcrop=230d,dollar=24,dopf=1d555,dot=2d9,doteq=2',\n\t'250,doteqdot=2251,dotminus=2238,dotplus=2214,dotsquare=22a1,doublebarwedge=2306,downarrow=2193,downd',\n\t'ownarrows=21ca,downharpoonleft=21c3,downharpoonright=21c2,drbkarow=2910,drcorn=231f,drcrop=230c,dscr',\n\t'=1d4b9,dscy=455,dsol=29f6,dstrok=111,dtdot=22f1,dtri=25bf,dtrif=25be,duarr=21f5,duhar=296f,dwangle=2',\n\t'9a6,dzcy=45f,dzigrarr=27ff,eDDot=2a77,eDot=2251,eacute=e9,easter=2a6e,ecaron=11b,ecir=2256,ecirc=ea,',\n\t'ecolon=2255,ecy=44d,edot=117,ee=2147,efDot=2252,efr=1d522,eg=2a9a,egrave=e8,egs=2a96,egsdot=2a98,el=',\n\t'2a99,elinters=23e7,ell=2113,els=2a95,elsdot=2a97,emacr=113,empty=2205,emptyset=2205,emptyv=2205,emsp',\n\t'=2003,emsp13=2004,emsp14=2005,eng=14b,ensp=2002,eogon=119,eopf=1d556,epar=22d5,eparsl=29e3,eplus=2a7',\n\t'1,epsi=3b5,epsilon=3b5,epsiv=3f5,eqcirc=2256,eqcolon=2255,eqsim=2242,eqslantgtr=2a96,eqslantless=2a9',\n\t'5,equals=3d,equest=225f,equiv=2261,equivDD=2a78,eqvparsl=29e5,erDot=2253,erarr=2971,escr=212f,esdot=',\n\t'2250,esim=2242,eta=3b7,eth=f0,euml=eb,euro=20ac,excl=21,exist=2203,expectation=2130,exponentiale=214',\n\t'7,fallingdotseq=2252,fcy=444,female=2640,ffilig=fb03,fflig=fb00,ffllig=fb04,ffr=1d523,filig=fb01,fjl',\n\t'ig=66.6a,flat=266d,fllig=fb02,fltns=25b1,fnof=192,fopf=1d557,forall=2200,fork=22d4,forkv=2ad9,fparti',\n\t'nt=2a0d,frac12=bd,frac13=2153,frac14=bc,frac15=2155,frac16=2159,frac18=215b,frac23=2154,frac25=2156,',\n\t'frac34=be,frac35=2157,frac38=215c,frac45=2158,frac56=215a,frac58=215d,frac78=215e,frasl=2044,frown=2',\n\t'322,fscr=1d4bb,gE=2267,gEl=2a8c,gacute=1f5,gamma=3b3,gammad=3dd,gap=2a86,gbreve=11f,gcirc=11d,gcy=43',\n\t'3,gdot=121,ge=2265,gel=22db,geq=2265,geqq=2267,geqslant=2a7e,ges=2a7e,gescc=2aa9,gesdot=2a80,gesdoto',\n\t'=2a82,gesdotol=2a84,gesl=22db.fe00,gesles=2a94,gfr=1d524,gg=226b,ggg=22d9,gimel=2137,gjcy=453,gl=227',\n\t'7,glE=2a92,gla=2aa5,glj=2aa4,gnE=2269,gnap=2a8a,gnapprox=2a8a,gne=2a88,gneq=2a88,gneqq=2269,gnsim=22',\n\t'e7,gopf=1d558,grave=60,gscr=210a,gsim=2273,gsime=2a8e,gsiml=2a90,gt=3e,gtcc=2aa7,gtcir=2a7a,gtdot=22',\n\t'd7,gtlPar=2995,gtquest=2a7c,gtrapprox=2a86,gtrarr=2978,gtrdot=22d7,gtreqless=22db,gtreqqless=2a8c,gt',\n\t'rless=2277,gtrsim=2273,gvertneqq=2269.fe00,gvnE=2269.fe00,hArr=21d4,hairsp=200a,half=bd,hamilt=210b,',\n\t'hardcy=44a,harr=2194,harrcir=2948,harrw=21ad,hbar=210f,hcirc=125,hearts=2665,heartsuit=2665,hellip=2',\n\t'026,hercon=22b9,hfr=1d525,hksearow=2925,hkswarow=2926,hoarr=21ff,homtht=223b,hookleftarrow=21a9,hook',\n\t'rightarrow=21aa,hopf=1d559,horbar=2015,hscr=1d4bd,hslash=210f,hstrok=127,hybull=2043,hyphen=2010,iac',\n\t'ute=ed,ic=2063,icirc=ee,icy=438,iecy=435,iexcl=a1,iff=21d4,ifr=1d526,igrave=ec,ii=2148,iiiint=2a0c,i',\n\t'iint=222d,iinfin=29dc,iiota=2129,ijlig=133,imacr=12b,image=2111,imagline=2110,imagpart=2111,imath=13',\n\t'1,imof=22b7,imped=1b5,in=2208,incare=2105,infin=221e,infintie=29dd,inodot=131,int=222b,intcal=22ba,i',\n\t'ntegers=2124,intercal=22ba,intlarhk=2a17,intprod=2a3c,iocy=451,iogon=12f,iopf=1d55a,iota=3b9,iprod=2',\n\t'a3c,iquest=bf,iscr=1d4be,isin=2208,isinE=22f9,isindot=22f5,isins=22f4,isinsv=22f3,isinv=2208,it=2062',\n\t',itilde=129,iukcy=456,iuml=ef,jcirc=135,jcy=439,jfr=1d527,jmath=237,jopf=1d55b,jscr=1d4bf,jsercy=458',\n\t',jukcy=454,kappa=3ba,kappav=3f0,kcedil=137,kcy=43a,kfr=1d528,kgreen=138,khcy=445,kjcy=45c,kopf=1d55c',\n\t',kscr=1d4c0,lAarr=21da,lArr=21d0,lAtail=291b,lBarr=290e,lE=2266,lEg=2a8b,lHar=2962,lacute=13a,laempt',\n\t'yv=29b4,lagran=2112,lambda=3bb,lang=27e8,langd=2991,langle=27e8,lap=2a85,laquo=ab,larr=2190,larrb=21',\n\t'e4,larrbfs=291f,larrfs=291d,larrhk=21a9,larrlp=21ab,larrpl=2939,larrsim=2973,larrtl=21a2,lat=2aab,la',\n\t'tail=2919,late=2aad,lates=2aad.fe00,lbarr=290c,lbbrk=2772,lbrace=7b,lbrack=5b,lbrke=298b,lbrksld=298',\n\t'f,lbrkslu=298d,lcaron=13e,lcedil=13c,lceil=2308,lcub=7b,lcy=43b,ldca=2936,ldquo=201c,ldquor=201e,ldr',\n\t'dhar=2967,ldrushar=294b,ldsh=21b2,le=2264,leftarrow=2190,leftarrowtail=21a2,leftharpoondown=21bd,lef',\n\t'tharpoonup=21bc,leftleftarrows=21c7,leftrightarrow=2194,leftrightarrows=21c6,leftrightharpoons=21cb,',\n\t'leftrightsquigarrow=21ad,leftthreetimes=22cb,leg=22da,leq=2264,leqq=2266,leqslant=2a7d,les=2a7d,lesc',\n\t'c=2aa8,lesdot=2a7f,lesdoto=2a81,lesdotor=2a83,lesg=22da.fe00,lesges=2a93,lessapprox=2a85,lessdot=22d',\n\t'6,lesseqgtr=22da,lesseqqgtr=2a8b,lessgtr=2276,lesssim=2272,lfisht=297c,lfloor=230a,lfr=1d529,lg=2276',\n\t',lgE=2a91,lhard=21bd,lharu=21bc,lharul=296a,lhblk=2584,ljcy=459,ll=226a,llarr=21c7,llcorner=231e,llh',\n\t'ard=296b,lltri=25fa,lmidot=140,lmoust=23b0,lmoustache=23b0,lnE=2268,lnap=2a89,lnapprox=2a89,lne=2a87',\n\t',lneq=2a87,lneqq=2268,lnsim=22e6,loang=27ec,loarr=21fd,lobrk=27e6,longleftarrow=27f5,longleftrightar',\n\t'row=27f7,longmapsto=27fc,longrightarrow=27f6,looparrowleft=21ab,looparrowright=21ac,lopar=2985,lopf=',\n\t'1d55d,loplus=2a2d,lotimes=2a34,lowast=2217,lowbar=5f,loz=25ca,lozenge=25ca,lozf=29eb,lpar=28,lparlt=',\n\t'2993,lrarr=21c6,lrcorner=231f,lrhar=21cb,lrhard=296d,lrm=200e,lrtri=22bf,lsaquo=2039,lscr=1d4c1,lsh=',\n\t'21b0,lsim=2272,lsime=2a8d,lsimg=2a8f,lsqb=5b,lsquo=2018,lsquor=201a,lstrok=142,lt=3c,ltcc=2aa6,ltcir',\n\t'=2a79,ltdot=22d6,lthree=22cb,ltimes=22c9,ltlarr=2976,ltquest=2a7b,ltrPar=2996,ltri=25c3,ltrie=22b4,l',\n\t'trif=25c2,lurdshar=294a,luruhar=2966,lvertneqq=2268.fe00,lvnE=2268.fe00,mDDot=223a,macr=af,male=2642',\n\t',malt=2720,maltese=2720,map=21a6,mapsto=21a6,mapstodown=21a7,mapstoleft=21a4,mapstoup=21a5,marker=25',\n\t'ae,mcomma=2a29,mcy=43c,mdash=2014,measuredangle=2221,mfr=1d52a,mho=2127,micro=b5,mid=2223,midast=2a,',\n\t'midcir=2af0,middot=b7,minus=2212,minusb=229f,minusd=2238,minusdu=2a2a,mlcp=2adb,mldr=2026,mnplus=221',\n\t'3,models=22a7,mopf=1d55e,mp=2213,mscr=1d4c2,mstpos=223e,mu=3bc,multimap=22b8,mumap=22b8,nGg=22d9.338',\n\t',nGt=226b.20d2,nGtv=226b.338,nLeftarrow=21cd,nLeftrightarrow=21ce,nLl=22d8.338,nLt=226a.20d2,nLtv=22',\n\t'6a.338,nRightarrow=21cf,nVDash=22af,nVdash=22ae,nabla=2207,nacute=144,nang=2220.20d2,nap=2249,napE=2',\n\t'a70.338,napid=224b.338,napos=149,napprox=2249,natur=266e,natural=266e,naturals=2115,nbsp=a0,nbump=22',\n\t'4e.338,nbumpe=224f.338,ncap=2a43,ncaron=148,ncedil=146,ncong=2247,ncongdot=2a6d.338,ncup=2a42,ncy=43',\n\t'd,ndash=2013,ne=2260,neArr=21d7,nearhk=2924,nearr=2197,nearrow=2197,nedot=2250.338,nequiv=2262,nesea',\n\t'r=2928,nesim=2242.338,nexist=2204,nexists=2204,nfr=1d52b,ngE=2267.338,nge=2271,ngeq=2271,ngeqq=2267.',\n\t'338,ngeqslant=2a7e.338,nges=2a7e.338,ngsim=2275,ngt=226f,ngtr=226f,nhArr=21ce,nharr=21ae,nhpar=2af2,',\n\t'ni=220b,nis=22fc,nisd=22fa,niv=220b,njcy=45a,nlArr=21cd,nlE=2266.338,nlarr=219a,nldr=2025,nle=2270,n',\n\t'leftarrow=219a,nleftrightarrow=21ae,nleq=2270,nleqq=2266.338,nleqslant=2a7d.338,nles=2a7d.338,nless=',\n\t'226e,nlsim=2274,nlt=226e,nltri=22ea,nltrie=22ec,nmid=2224,nopf=1d55f,not=ac,notin=2209,notinE=22f9.3',\n\t'38,notindot=22f5.338,notinva=2209,notinvb=22f7,notinvc=22f6,notni=220c,notniva=220c,notnivb=22fe,not',\n\t'nivc=22fd,npar=2226,nparallel=2226,nparsl=2afd.20e5,npart=2202.338,npolint=2a14,npr=2280,nprcue=22e0',\n\t',npre=2aaf.338,nprec=2280,npreceq=2aaf.338,nrArr=21cf,nrarr=219b,nrarrc=2933.338,nrarrw=219d.338,nri',\n\t'ghtarrow=219b,nrtri=22eb,nrtrie=22ed,nsc=2281,nsccue=22e1,nsce=2ab0.338,nscr=1d4c3,nshortmid=2224,ns',\n\t'hortparallel=2226,nsim=2241,nsime=2244,nsimeq=2244,nsmid=2224,nspar=2226,nsqsube=22e2,nsqsupe=22e3,n',\n\t'sub=2284,nsubE=2ac5.338,nsube=2288,nsubset=2282.20d2,nsubseteq=2288,nsubseteqq=2ac5.338,nsucc=2281,n',\n\t'succeq=2ab0.338,nsup=2285,nsupE=2ac6.338,nsupe=2289,nsupset=2283.20d2,nsupseteq=2289,nsupseteqq=2ac6',\n\t'.338,ntgl=2279,ntilde=f1,ntlg=2278,ntriangleleft=22ea,ntrianglelefteq=22ec,ntriangleright=22eb,ntria',\n\t'nglerighteq=22ed,nu=3bd,num=23,numero=2116,numsp=2007,nvDash=22ad,nvHarr=2904,nvap=224d.20d2,nvdash=',\n\t'22ac,nvge=2265.20d2,nvgt=3e.20d2,nvinfin=29de,nvlArr=2902,nvle=2264.20d2,nvlt=3c.20d2,nvltrie=22b4.2',\n\t'0d2,nvrArr=2903,nvrtrie=22b5.20d2,nvsim=223c.20d2,nwArr=21d6,nwarhk=2923,nwarr=2196,nwarrow=2196,nwn',\n\t'ear=2927,oS=24c8,oacute=f3,oast=229b,ocir=229a,ocirc=f4,ocy=43e,odash=229d,odblac=151,odiv=2a38,odot',\n\t'=2299,odsold=29bc,oelig=153,ofcir=29bf,ofr=1d52c,ogon=2db,ograve=f2,ogt=29c1,ohbar=29b5,ohm=3a9,oint',\n\t'=222e,olarr=21ba,olcir=29be,olcross=29bb,oline=203e,olt=29c0,omacr=14d,omega=3c9,omicron=3bf,omid=29',\n\t'b6,ominus=2296,oopf=1d560,opar=29b7,operp=29b9,oplus=2295,or=2228,orarr=21bb,ord=2a5d,order=2134,ord',\n\t'erof=2134,ordf=aa,ordm=ba,origof=22b6,oror=2a56,orslope=2a57,orv=2a5b,oscr=2134,oslash=f8,osol=2298,',\n\t'otilde=f5,otimes=2297,otimesas=2a36,ouml=f6,ovbar=233d,par=2225,para=b6,parallel=2225,parsim=2af3,pa',\n\t'rsl=2afd,part=2202,pcy=43f,percnt=25,period=2e,permil=2030,perp=22a5,pertenk=2031,pfr=1d52d,phi=3c6,',\n\t'phiv=3d5,phmmat=2133,phone=260e,pi=3c0,pitchfork=22d4,piv=3d6,planck=210f,planckh=210e,plankv=210f,p',\n\t'lus=2b,plusacir=2a23,plusb=229e,pluscir=2a22,plusdo=2214,plusdu=2a25,pluse=2a72,plusmn=b1,plussim=2a',\n\t'26,plustwo=2a27,pm=b1,pointint=2a15,popf=1d561,pound=a3,pr=227a,prE=2ab3,prap=2ab7,prcue=227c,pre=2a',\n\t'af,prec=227a,precapprox=2ab7,preccurlyeq=227c,preceq=2aaf,precnapprox=2ab9,precneqq=2ab5,precnsim=22',\n\t'e8,precsim=227e,prime=2032,primes=2119,prnE=2ab5,prnap=2ab9,prnsim=22e8,prod=220f,profalar=232e,prof',\n\t'line=2312,profsurf=2313,prop=221d,propto=221d,prsim=227e,prurel=22b0,pscr=1d4c5,psi=3c8,puncsp=2008,',\n\t'qfr=1d52e,qint=2a0c,qopf=1d562,qprime=2057,qscr=1d4c6,quaternions=210d,quatint=2a16,quest=3f,questeq',\n\t'=225f,quot=22,rAarr=21db,rArr=21d2,rAtail=291c,rBarr=290f,rHar=2964,race=223d.331,racute=155,radic=2',\n\t'21a,raemptyv=29b3,rang=27e9,rangd=2992,range=29a5,rangle=27e9,raquo=bb,rarr=2192,rarrap=2975,rarrb=2',\n\t'1e5,rarrbfs=2920,rarrc=2933,rarrfs=291e,rarrhk=21aa,rarrlp=21ac,rarrpl=2945,rarrsim=2974,rarrtl=21a3',\n\t',rarrw=219d,ratail=291a,ratio=2236,rationals=211a,rbarr=290d,rbbrk=2773,rbrace=7d,rbrack=5d,rbrke=29',\n\t'8c,rbrksld=298e,rbrkslu=2990,rcaron=159,rcedil=157,rceil=2309,rcub=7d,rcy=440,rdca=2937,rdldhar=2969',\n\t',rdquo=201d,rdquor=201d,rdsh=21b3,real=211c,realine=211b,realpart=211c,reals=211d,rect=25ad,reg=ae,r',\n\t'fisht=297d,rfloor=230b,rfr=1d52f,rhard=21c1,rharu=21c0,rharul=296c,rho=3c1,rhov=3f1,rightarrow=2192,',\n\t'rightarrowtail=21a3,rightharpoondown=21c1,rightharpoonup=21c0,rightleftarrows=21c4,rightleftharpoons',\n\t'=21cc,rightrightarrows=21c9,rightsquigarrow=219d,rightthreetimes=22cc,ring=2da,risingdotseq=2253,rla',\n\t'rr=21c4,rlhar=21cc,rlm=200f,rmoust=23b1,rmoustache=23b1,rnmid=2aee,roang=27ed,roarr=21fe,robrk=27e7,',\n\t'ropar=2986,ropf=1d563,roplus=2a2e,rotimes=2a35,rpar=29,rpargt=2994,rppolint=2a12,rrarr=21c9,rsaquo=2',\n\t'03a,rscr=1d4c7,rsh=21b1,rsqb=5d,rsquo=2019,rsquor=2019,rthree=22cc,rtimes=22ca,rtri=25b9,rtrie=22b5,',\n\t'rtrif=25b8,rtriltri=29ce,ruluhar=2968,rx=211e,sacute=15b,sbquo=201a,sc=227b,scE=2ab4,scap=2ab8,scaro',\n\t'n=161,sccue=227d,sce=2ab0,scedil=15f,scirc=15d,scnE=2ab6,scnap=2aba,scnsim=22e9,scpolint=2a13,scsim=',\n\t'227f,scy=441,sdot=22c5,sdotb=22a1,sdote=2a66,seArr=21d8,searhk=2925,searr=2198,searrow=2198,sect=a7,',\n\t'semi=3b,seswar=2929,setminus=2216,setmn=2216,sext=2736,sfr=1d530,sfrown=2322,sharp=266f,shchcy=449,s',\n\t'hcy=448,shortmid=2223,shortparallel=2225,shy=ad,sigma=3c3,sigmaf=3c2,sigmav=3c2,sim=223c,simdot=2a6a',\n\t',sime=2243,simeq=2243,simg=2a9e,simgE=2aa0,siml=2a9d,simlE=2a9f,simne=2246,simplus=2a24,simrarr=2972',\n\t',slarr=2190,smallsetminus=2216,smashp=2a33,smeparsl=29e4,smid=2223,smile=2323,smt=2aaa,smte=2aac,smt',\n\t'es=2aac.fe00,softcy=44c,sol=2f,solb=29c4,solbar=233f,sopf=1d564,spades=2660,spadesuit=2660,spar=2225',\n\t',sqcap=2293,sqcaps=2293.fe00,sqcup=2294,sqcups=2294.fe00,sqsub=228f,sqsube=2291,sqsubset=228f,sqsubs',\n\t'eteq=2291,sqsup=2290,sqsupe=2292,sqsupset=2290,sqsupseteq=2292,squ=25a1,square=25a1,squarf=25aa,squf',\n\t'=25aa,srarr=2192,sscr=1d4c8,ssetmn=2216,ssmile=2323,sstarf=22c6,star=2606,starf=2605,straightepsilon',\n\t'=3f5,straightphi=3d5,strns=af,sub=2282,subE=2ac5,subdot=2abd,sube=2286,subedot=2ac3,submult=2ac1,sub',\n\t'nE=2acb,subne=228a,subplus=2abf,subrarr=2979,subset=2282,subseteq=2286,subseteqq=2ac5,subsetneq=228a',\n\t',subsetneqq=2acb,subsim=2ac7,subsub=2ad5,subsup=2ad3,succ=227b,succapprox=2ab8,succcurlyeq=227d,succ',\n\t'eq=2ab0,succnapprox=2aba,succneqq=2ab6,succnsim=22e9,succsim=227f,sum=2211,sung=266a,sup=2283,sup1=b',\n\t'9,sup2=b2,sup3=b3,supE=2ac6,supdot=2abe,supdsub=2ad8,supe=2287,supedot=2ac4,suphsol=27c9,suphsub=2ad',\n\t'7,suplarr=297b,supmult=2ac2,supnE=2acc,supne=228b,supplus=2ac0,supset=2283,supseteq=2287,supseteqq=2',\n\t'ac6,supsetneq=228b,supsetneqq=2acc,supsim=2ac8,supsub=2ad4,supsup=2ad6,swArr=21d9,swarhk=2926,swarr=',\n\t'2199,swarrow=2199,swnwar=292a,szlig=df,target=2316,tau=3c4,tbrk=23b4,tcaron=165,tcedil=163,tcy=442,t',\n\t'dot=20db,telrec=2315,tfr=1d531,there4=2234,therefore=2234,theta=3b8,thetasym=3d1,thetav=3d1,thickapp',\n\t'rox=2248,thicksim=223c,thinsp=2009,thkap=2248,thksim=223c,thorn=fe,tilde=2dc,times=d7,timesb=22a0,ti',\n\t'mesbar=2a31,timesd=2a30,tint=222d,toea=2928,top=22a4,topbot=2336,topcir=2af1,topf=1d565,topfork=2ada',\n\t',tosa=2929,tprime=2034,trade=2122,triangle=25b5,triangledown=25bf,triangleleft=25c3,trianglelefteq=2',\n\t'2b4,triangleq=225c,triangleright=25b9,trianglerighteq=22b5,tridot=25ec,trie=225c,triminus=2a3a,tripl',\n\t'us=2a39,trisb=29cd,tritime=2a3b,trpezium=23e2,tscr=1d4c9,tscy=446,tshcy=45b,tstrok=167,twixt=226c,tw',\n\t'oheadleftarrow=219e,twoheadrightarrow=21a0,uArr=21d1,uHar=2963,uacute=fa,uarr=2191,ubrcy=45e,ubreve=',\n\t'16d,ucirc=fb,ucy=443,udarr=21c5,udblac=171,udhar=296e,ufisht=297e,ufr=1d532,ugrave=f9,uharl=21bf,uha',\n\t'rr=21be,uhblk=2580,ulcorn=231c,ulcorner=231c,ulcrop=230f,ultri=25f8,umacr=16b,uml=a8,uogon=173,uopf=',\n\t'1d566,uparrow=2191,updownarrow=2195,upharpoonleft=21bf,upharpoonright=21be,uplus=228e,upsi=3c5,upsih',\n\t'=3d2,upsilon=3c5,upuparrows=21c8,urcorn=231d,urcorner=231d,urcrop=230e,uring=16f,urtri=25f9,uscr=1d4',\n\t'ca,utdot=22f0,utilde=169,utri=25b5,utrif=25b4,uuarr=21c8,uuml=fc,uwangle=29a7,vArr=21d5,vBar=2ae8,vB',\n\t'arv=2ae9,vDash=22a8,vangrt=299c,varepsilon=3f5,varkappa=3f0,varnothing=2205,varphi=3d5,varpi=3d6,var',\n\t'propto=221d,varr=2195,varrho=3f1,varsigma=3c2,varsubsetneq=228a.fe00,varsubsetneqq=2acb.fe00,varsups',\n\t'etneq=228b.fe00,varsupsetneqq=2acc.fe00,vartheta=3d1,vartriangleleft=22b2,vartriangleright=22b3,vcy=',\n\t'432,vdash=22a2,vee=2228,veebar=22bb,veeeq=225a,vellip=22ee,verbar=7c,vert=7c,vfr=1d533,vltri=22b2,vn',\n\t'sub=2282.20d2,vnsup=2283.20d2,vopf=1d567,vprop=221d,vrtri=22b3,vscr=1d4cb,vsubnE=2acb.fe00,vsubne=22',\n\t'8a.fe00,vsupnE=2acc.fe00,vsupne=228b.fe00,vzigzag=299a,wcirc=175,wedbar=2a5f,wedge=2227,wedgeq=2259,',\n\t'weierp=2118,wfr=1d534,wopf=1d568,wp=2118,wr=2240,wreath=2240,wscr=1d4cc,xcap=22c2,xcirc=25ef,xcup=22',\n\t'c3,xdtri=25bd,xfr=1d535,xhArr=27fa,xharr=27f7,xi=3be,xlArr=27f8,xlarr=27f5,xmap=27fc,xnis=22fb,xodot',\n\t'=2a00,xopf=1d569,xoplus=2a01,xotime=2a02,xrArr=27f9,xrarr=27f6,xscr=1d4cd,xsqcup=2a06,xuplus=2a04,xu',\n\t'tri=25b3,xvee=22c1,xwedge=22c0,yacute=fd,yacy=44f,ycirc=177,ycy=44b,yen=a5,yfr=1d536,yicy=457,yopf=1',\n\t'd56a,yscr=1d4ce,yucy=44e,yuml=ff,zacute=17a,zcaron=17e,zcy=437,zdot=17c,zeetrf=2128,zeta=3b6,zfr=1d5',\n\t'37,zhcy=436,zigrarr=21dd,zopf=1d56b,zscr=1d4cf,zwj=200d,zwnj=200c',\n].join('');\n\n/** @type {Map<string, string>} */\nexport const NAMED_REFERENCES = new Map(NAMED.split(',').map(entry => {\n\tconst [name, codes] = entry.split('=');\n\treturn [name, String.fromCodePoint(...codes.split('.').map(code => Number.parseInt(code, 16)))];\n}));\n", "import {NAMED_REFERENCES} from './entities.js';\nimport {caseless, linear} from './linear.js';\n\n// The names of the characters of Latin-1 (160 to 255, in order), which\n// browsers also read without a \";\" (names are case-sensitive: \"&Eacute;\" is\n// \"\u00C9\", \"&eacute;\" is \"\u00E9\")\nconst LATIN1 = 'nbsp iexcl cent pound curren yen brvbar sect uml copy ordf laquo not shy reg macr deg plusmn sup2 sup3 acute micro para middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest Agrave Aacute Acirc Atilde Auml Aring AElig Ccedil Egrave Eacute Ecirc Euml Igrave Iacute Icirc Iuml ETH Ntilde Ograve Oacute Ocirc Otilde Ouml times Oslash Ugrave Uacute Ucirc Uuml Yacute THORN szlig agrave aacute acirc atilde auml aring aelig ccedil egrave eacute ecirc euml igrave iacute icirc iuml eth ntilde ograve oacute ocirc otilde ouml divide oslash ugrave uacute ucirc uuml yacute thorn yuml'.split(' ');\n// The names browsers also read without a \";\" (\"&nbsp\", \"&amp\"), the longest\n// at the start of a name (\"&notit\" is \"\u00ACit\", \"Vi&shyagra;\" is \"Vi\u00ADagra\" in\n// text); a name with a \";\" is read whole by every name browsers know\n// (\"&notin;\" is \"\u2209\", \"&iopf;\" is \"\uD835\uDD5A\"), see NAMED_REFERENCES\nconst LEGACY = new Set([...LATIN1, 'amp', 'lt', 'gt', 'quot', 'AMP', 'LT', 'GT', 'QUOT', 'COPY', 'REG']);\nconst LEGACY_LONGEST = Math.max(...[...LEGACY].map(name => name.length));\n// Windows-1252 for the references to C1 controls, as browsers read them\n// (\"don&#146;t\" is \"don\u2019t\")\nconst WINDOWS_1252 = [0x20_AC, 0x81, 0x20_1A, 0x1_92, 0x20_1E, 0x20_26, 0x20_20, 0x20_21, 0x2_C6, 0x20_30, 0x1_60, 0x20_39, 0x1_52, 0x8D, 0x1_7D, 0x8F, 0x90, 0x20_18, 0x20_19, 0x20_1C, 0x20_1D, 0x20_22, 0x20_13, 0x20_14, 0x2_DC, 0x21_22, 0x1_61, 0x20_3A, 0x1_53, 0x9D, 0x1_7E, 0x1_78];\nconst REFERENCE = /&(?:#([xX])([\\da-fA-F]+)|#(\\d+)|([a-zA-Z][a-zA-Z\\d]{0,31}))(;?)/g;\n\nfunction fromCode(code) {\n\tif (code === 0 || code > 0x10_FF_FF || (code >= 0xD8_00 && code <= 0xDF_FF)) {\n\t\treturn '\uFFFD';\n\t}\n\n\tif (code >= 0x80 && code <= 0x9F) {\n\t\treturn String.fromCodePoint(WINDOWS_1252[code - 0x80]);\n\t}\n\n\treturn String.fromCodePoint(code);\n}\n\n/**\n * Text with its character references decoded, as browsers decode them: by\n * number, also without a \";\", and by name (every name browsers know; also\n * without a \";\" for the names browsers read so,\n * except in an attribute where a letter, digit or \"=\" follows, as in a link\n * \"?a=1&copy=2\"). An unknown name stays as it is.\n * @param {string} value\n * @param {object} [options]\n * @param {boolean} [options.attribute] - the value of an attribute\n * @returns {string}\n */\nexport function decodeEntities(value, {attribute = false} = {}) {\n\treturn value.replaceAll(REFERENCE, (...found) => {\n\t\tconst [match, , hex, decimal, name, semicolon, offset] = found;\n\t\tif (hex !== undefined || decimal !== undefined) {\n\t\t\treturn fromCode(hex === undefined ? Number.parseInt(decimal, 10) : Number.parseInt(hex, 16));\n\t\t}\n\n\t\tif (semicolon && NAMED_REFERENCES.has(name)) {\n\t\t\treturn NAMED_REFERENCES.get(name);\n\t\t}\n\n\t\t// (the longest name read without a \";\" that the name starts with; an\n\t\t// unknown name stays as it is)\n\t\tlet legacy;\n\t\tfor (let length = Math.min(name.length, LEGACY_LONGEST); length >= 2 && legacy === undefined; length--) {\n\t\t\tlegacy = LEGACY.has(name.slice(0, length)) ? name.slice(0, length) : undefined;\n\t\t}\n\n\t\tif (legacy === undefined) {\n\t\t\treturn match;\n\t\t}\n\n\t\tconst rest = match.slice(legacy.length + 1);\n\t\tconst next = rest[0] ?? value[offset + match.length] ?? '';\n\t\tif (attribute && /[\\da-zA-Z=]/.test(next)) {\n\t\t\treturn match;\n\t\t}\n\n\t\treturn NAMED_REFERENCES.get(legacy) + rest;\n\t});\n}\n\n// Elements whose content is not shown (raw text up to their end tag, which is\n// searched for once), and those whose content is shown as raw text\nconst UNSHOWN = new Set(['script', 'style', 'title', 'noembed', 'noframes', 'iframe']);\nconst RAW_SHOWN = new Set(['textarea', 'xmp']);\nconst RAW_END = Object.fromEntries([...UNSHOWN, ...RAW_SHOWN].map(name => [name, linear(String.raw`<\\/${caseless(name)}[\\s/>]`, 'g')]));\n// Elements whose tags break a word: blocks, and what takes a place of its own\n// (an image, a field); the others, also unknown ones (`<o:p>`, custom\n// elements), are shown in the line, so their tags join a word\n// (\"Free Sp<span>ins</span>\" reads \"Free Spins\", as a browser shows it)\nconst BREAKS = new Set(['address', 'applet', 'area', 'article', 'aside', 'audio', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'col', 'colgroup', 'dd', 'details', 'dialog', 'dir', 'div', 'dl', 'dt', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'iframe', 'image', 'img', 'input', 'keygen', 'legend', 'li', 'listing', 'main', 'marquee', 'math', 'menu', 'meter', 'nav', 'object', 'ol', 'optgroup', 'option', 'p', 'picture', 'plaintext', 'pre', 'progress', 'search', 'section', 'select', 'summary', 'svg', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'ul', 'video', 'xmp']);\n// Elements an end tag of another name does not close through, as browsers\n// read it (\"<span hidden><div>a</span>b</div>\": the </span> does nothing),\n// and those it closes through when they are open\nconst SPECIAL = new Set(['address', 'applet', 'area', 'article', 'aside', 'base', 'blockquote', 'body', 'button', 'caption', 'center', 'col', 'colgroup', 'dd', 'details', 'dir', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'html', 'iframe', 'li', 'listing', 'main', 'marquee', 'menu', 'nav', 'object', 'ol', 'p', 'plaintext', 'pre', 'search', 'section', 'select', 'summary', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'ul', 'xmp']);\n// (and the elements an end tag of a block or a formatting element does not\n// close through: \"<div hidden><table><tr><td></div>x\": \"x\" is in the cell)\nconst SCOPE = new Set(['applet', 'caption', 'html', 'marquee', 'object', 'table', 'td', 'template', 'th']);\nconst FORMATTING = new Set(['a', 'b', 'big', 'code', 'em', 'font', 'i', 'nobr', 's', 'small', 'strike', 'strong', 'tt', 'u']);\n// Elements that break a line (the others break a word)\nconst LINE = new Set(['br', 'p', 'div', 'tr', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'blockquote', 'pre']);\n// (of those, the ones that take a place of their own however a style shows\n// them; the others break only shown as blocks, see displayKind)\nconst OWN_PLACE = new Set(['applet', 'area', 'audio', 'br', 'button', 'canvas', 'embed', 'frame', 'iframe', 'image', 'img', 'input', 'keygen', 'marquee', 'math', 'meter', 'object', 'picture', 'progress', 'select', 'svg', 'textarea', 'video']);\n// Elements without content\nconst VOID = new Set(['area', 'base', 'basefont', 'bgsound', 'br', 'col', 'command', 'embed', 'frame', 'hr', 'image', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);\n// Elements a browser closes by itself when another starts: a paragraph at a\n// block, a list item at the next, a cell at the next cell or row, and so on\n// (each with the elements that end the search for it)\nconst CLOSES_PARAGRAPH = new Set(['address', 'article', 'aside', 'blockquote', 'center', 'dd', 'details', 'dialog', 'dir', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'listing', 'main', 'menu', 'nav', 'ol', 'p', 'plaintext', 'pre', 'search', 'section', 'summary', 'table', 'ul', 'xmp']);\nconst PARAGRAPH = [['p'], ['button', 'table', 'td', 'th', 'caption', 'marquee', 'object', 'applet', 'html']];\n// (a list item at the next one not past a block other than an address, a\n// <div> or a <p>)\n// (without a prototype: tag names such as \"constructor\" are looked up in it)\nconst IMPLIED = {\n\t__proto__: null,\n\tli: [['li']],\n\tdt: [['dt', 'dd']],\n\tdd: [['dt', 'dd']],\n\ttd: [['td', 'th'], ['tr', 'table']],\n\tth: [['td', 'th'], ['tr', 'table']],\n\ttr: [['tr'], ['table', 'tbody', 'thead', 'tfoot']],\n\ttbody: [['tbody', 'thead', 'tfoot'], ['table']],\n\tthead: [['tbody', 'thead', 'tfoot'], ['table']],\n\ttfoot: [['tbody', 'thead', 'tfoot'], ['table']],\n\toption: [['option'], ['select', 'datalist', 'optgroup']],\n\toptgroup: [['optgroup', 'option'], ['select']],\n};\n// Elements whose text and other content a browser puts in front of the table\n// they are written in, in the table's parent (\"foster parenting\"), and those\n// it keeps in the table\n// (and the parts of a table, nothing outside one)\nconst TABLE_PARTS = new Set(['table', 'tbody', 'thead', 'tfoot', 'tr']);\n// (the parts of a table that grow to their content in a table laid out\n// automatically, whatever their width, height and overflow)\nconst TABLE_BOXES = new Set(['table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr']);\nconst HEADINGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];\n// (elements whose end a browser implies before an end tag such as </form>)\nconst IMPLIED_END = new Set(['dd', 'dt', 'li', 'optgroup', 'option', 'p', 'rb', 'rp', 'rt', 'rtc']);\nconst PARTS_OF_TABLE = new Set(['caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr']);\nconst TABLE_STRUCTURE = [...PARTS_OF_TABLE, 'table', 'template'];\nconst IN_TABLE = new Set(['caption', 'col', 'colgroup', 'form', 'input', 'script', 'style', 'table', 'tbody', 'td', 'template', 'tfoot', 'th', 'thead', 'tr']);\n// Elements a browser opens again in the next text when something else closed\n// them (\"<p><b>a</p>b\": \"b\" is bold too), and those that begin a new list of\n// them; the elements before which it does not do so. (Browsers keep at most\n// three alike after the last marker, and no bound in all; at most this many\n// are kept here, so that opening them again takes a bounded time.)\nconst MAX_FORMATTING = 12;\nconst MARKERS = new Set(['applet', 'caption', 'marquee', 'object', 'td', 'template', 'th']);\nconst FOSTERED_MARKERS = new Set(['applet', 'marquee', 'object']);\nconst NOT_REOPENING = new Set([...CLOSES_PARAGRAPH, 'base', 'basefont', 'bgsound', 'body', 'caption', 'col', 'colgroup', 'frame', 'frameset', 'head', 'html', 'iframe', 'link', 'meta', 'noembed', 'noframes', 'script', 'style', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr']);\n// Elements that end an <svg> or <math> they are written in, in a browser, and\n// the elements of these that hold HTML; and those of an <svg> not shown\nconst LEAVES_FOREIGN = new Set(['b', 'big', 'blockquote', 'body', 'br', 'center', 'code', 'dd', 'div', 'dl', 'dt', 'em', 'embed', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'i', 'img', 'li', 'listing', 'menu', 'meta', 'nobr', 'ol', 'p', 'pre', 'ruby', 's', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table', 'tt', 'u', 'ul', 'var']);\nconst HOLDS_HTML = {svg: new Set(['foreignobject', 'desc', 'title']), math: new Set(['mi', 'mo', 'mn', 'ms', 'mtext'])};\nconst SVG_UNSHOWN = new Set(['desc', 'metadata', 'script', 'style', 'title']);\n// (open elements kept)\nconst MAX_OPEN = 1024;\nconst MAX_ATTRIBUTES = 64;\nconst MAX_STYLE = 10_000;\n// (the nearest end of a comment, searched for once)\nconst COMMENT_END = linear('--!?>', 'g');\nconst WHITESPACE = new Set([' ', '\\t', '\\n', '\\r', '\\f']);\n\n//\n// The tag that starts at `start` (a \"<\" and a letter, or \"</\" and a letter):\n// its end (just after its \">\", or -1 when the input ends inside it), its name\n// and its attributes (the first of each name), read as browsers read them: a\n// quote opens a value only after \"=\", and a quoted value may hold \">\"\n//\nfunction readTag(html, start) {\n\tconst closing = html[start + 1] === '/';\n\tlet i = start + (closing ? 2 : 1);\n\tconst nameStart = i;\n\twhile (i < html.length && !WHITESPACE.has(html[i]) && html[i] !== '/' && html[i] !== '>') {\n\t\ti++;\n\t}\n\n\tconst name = html.slice(nameStart, Math.min(i, nameStart + 64)).toLowerCase();\n\tconst attributes = new Map();\n\tconst spans = new Map();\n\t// (a \"/\" on its own just before the \">\", not one ending a value)\n\tlet slash = false;\n\twhile (i < html.length) {\n\t\tconst char = html[i];\n\t\tif (char === '>') {\n\t\t\treturn {\n\t\t\t\tend: i + 1, name, closing, attributes, spans, selfClosing: slash,\n\t\t\t};\n\t\t}\n\n\t\tif (WHITESPACE.has(char) || char === '/') {\n\t\t\tslash = char === '/' || (slash && WHITESPACE.has(char));\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tslash = false;\n\n\t\tconst attributeStart = i++;\n\t\twhile (i < html.length && !WHITESPACE.has(html[i]) && html[i] !== '/' && html[i] !== '>' && html[i] !== '=') {\n\t\t\ti++;\n\t\t}\n\n\t\tconst attribute = html.slice(attributeStart, i).toLowerCase();\n\t\twhile (WHITESPACE.has(html[i])) {\n\t\t\ti++;\n\t\t}\n\n\t\tlet value = '';\n\t\tlet valueStart = i;\n\t\tif (html[i] === '=') {\n\t\t\ti++;\n\t\t\twhile (WHITESPACE.has(html[i])) {\n\t\t\t\ti++;\n\t\t\t}\n\n\t\t\tif (html[i] === '\"' || html[i] === '\\'') {\n\t\t\t\tconst close = html.indexOf(html[i], i + 1);\n\t\t\t\tif (close === -1) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tvalueStart = i + 1;\n\t\t\t\tvalue = html.slice(valueStart, close);\n\t\t\t\ti = close + 1;\n\t\t\t} else {\n\t\t\t\tvalueStart = i;\n\t\t\t\twhile (i < html.length && !WHITESPACE.has(html[i]) && html[i] !== '>') {\n\t\t\t\t\ti++;\n\t\t\t\t}\n\n\t\t\t\tvalue = html.slice(valueStart, i);\n\t\t\t}\n\t\t}\n\n\t\tif (!attributes.has(attribute) && attributes.size < MAX_ATTRIBUTES) {\n\t\t\tattributes.set(attribute, value);\n\t\t\tspans.set(attribute, [valueStart, valueStart + value.length]);\n\t\t}\n\t}\n\n\treturn {end: -1};\n}\n\n// Attributes whose values hold no words a reader or a program is given, but\n// an address or a style: every other attribute may (the text of an image, a\n// tooltip, a field's value, name or hint, a meta tag's content, a class, an\n// id, data of any name, and attributes of no known name)\nconst NO_WORD_ATTRIBUTES = new Set(['href', 'src', 'srcset', 'style', 'action', 'formaction', 'background', 'poster', 'cite', 'codebase', 'longdesc', 'usemap', 'xmlns']);\n\n// The tags of an HTML document in order, each with where it starts: not\n// those in comments or in the raw text of scripts and styles, and none after\n// a tag the input ends inside, as browsers read them\nfunction eachTag(html, visit) {\n\tlet from = 0;\n\twhile (from < html.length) {\n\t\tconst lt = html.indexOf('<', from);\n\t\tif (lt === -1) {\n\t\t\tbreak;\n\t\t}\n\n\t\tfrom = lt + 1;\n\t\t// (comments as browsers end them, \"<!-->\" and \"<!--->\" at once)\n\t\tif (html.startsWith('<!--', lt)) {\n\t\t\tif (html.startsWith('>', lt + 4) || html.startsWith('->', lt + 4)) {\n\t\t\t\tfrom = html.indexOf('>', lt + 4) + 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tCOMMENT_END.lastIndex = lt + 4;\n\t\t\tconst end = COMMENT_END.exec(html);\n\t\t\tfrom = end === null ? html.length : end.index + end[0].length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (!/[a-zA-Z]/.test(html[lt + 1] ?? '')) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst tag = readTag(html, lt);\n\t\tif (tag.end === -1) {\n\t\t\tbreak;\n\t\t}\n\n\t\tfrom = tag.end;\n\t\tvisit(tag, lt);\n\t\t// (the raw text of a script, a style, a title or a text area)\n\t\tconst closer = Object.hasOwn(RAW_END, tag.name) ? RAW_END[tag.name] : null;\n\t\tif (closer) {\n\t\t\tcloser.lastIndex = tag.end;\n\t\t\tconst close = closer.exec(html);\n\t\t\tfrom = close === null ? html.length : close.index;\n\t\t}\n\t}\n}\n\n// What a form shows to help fill it in, a few words and no more: a field's\n// hint, its label for a screen reader, its tooltip and the value it shows\n// (not that of a hidden field, which no one is shown), a hint on any element,\n// and the text of a <label>\nconst FIELDS = new Set(['input', 'textarea', 'select', 'option', 'button']);\nconst FIELD_HELP = new Set(['placeholder', 'aria-placeholder', 'aria-label', 'aria-description', 'title', 'value']);\nconst HINTS = new Set(['placeholder', 'aria-placeholder']);\nconst MAX_HELP = 300;\nconst LABEL_END = linear(String.raw`<\\/${caseless('label')}[\\s/>]`);\nconst isHelp = (tag, name, value) => value.length <= MAX_HELP\n\t&& (HINTS.has(name) || (FIELDS.has(tag.name) && FIELD_HELP.has(name) && !(tag.name === 'input' && tag.attributes.get('type')?.trim().toLowerCase() === 'hidden')));\n\n/**\n * The values of the attributes of an HTML document that may hold words (see\n * NO_WORD_ATTRIBUTES), with their character references decoded, where the\n * tag that holds each starts and ends, and whether it is a form's help text\n * (see FIELD_HELP), in order: not\n * those in comments or in the raw text of scripts and styles, and none after\n * a tag the input ends inside, as browsers read them. In time linear in the\n * input.\n * @param {string} html\n * @returns {Array<{text: string, start: number, end: number, help: boolean}>}\n */\nexport function attributeText(html) {\n\tconst values = [];\n\teachTag(html, (tag, lt) => {\n\t\tfor (const [name, value] of tag.attributes) {\n\t\t\tif (value && !NO_WORD_ATTRIBUTES.has(name)) {\n\t\t\t\tvalues.push({\n\t\t\t\t\ttext: decodeEntities(value, {attribute: true}), start: lt, end: tag.end, help: isHelp(tag, name, value),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t});\n\n\treturn values;\n}\n\n/**\n * Where a form's help text is in an HTML document (see FIELD_HELP): the\n * values of those attributes and the text of each <label> of at most\n * MAX_HELP characters, as places in the input, in order of where they start\n * (a label's text may hold another's). In time linear in the input.\n * @param {string} html\n * @returns {Array<{from: number, to: number}>}\n */\nexport function formHelp(html) {\n\tconst places = [];\n\teachTag(html, tag => {\n\t\tif (tag.name === 'label' && !tag.closing) {\n\t\t\tconst close = LABEL_END.exec(html.slice(tag.end, tag.end + MAX_HELP + 8));\n\t\t\tif (close !== null && close.index <= MAX_HELP) {\n\t\t\t\tplaces.push({from: tag.end, to: tag.end + close.index});\n\t\t\t}\n\t\t}\n\n\t\tfor (const [name, value] of tag.attributes) {\n\t\t\tif (value && isHelp(tag, name, value)) {\n\t\t\t\tconst [from, to] = tag.spans.get(name);\n\t\t\t\tplaces.push({from, to});\n\t\t\t}\n\t\t}\n\t});\n\n\treturn places;\n}\n\n// The declarations of a style, the last of each property winning (an\n// important one over one that is not), without comments\nfunction declarations(style) {\n\tlet css = '';\n\tlet from = 0;\n\tconst text = style.slice(0, MAX_STYLE);\n\twhile (from < text.length) {\n\t\tconst open = text.indexOf('/*', from);\n\t\tif (open === -1) {\n\t\t\tcss += text.slice(from);\n\t\t\tbreak;\n\t\t}\n\n\t\tcss += text.slice(from, open);\n\t\tconst close = text.indexOf('*/', open + 2);\n\t\tif (close === -1) {\n\t\t\tbreak;\n\t\t}\n\n\t\tfrom = close + 2;\n\t}\n\n\tconst found = new Map();\n\tfor (const declaration of css.split(';')) {\n\t\tconst colon = declaration.indexOf(':');\n\t\tif (colon === -1) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst property = declaration.slice(0, colon).trim().toLowerCase();\n\t\tlet value = declaration.slice(colon + 1).trim().toLowerCase();\n\t\tconst important = /!\\s*important$/.test(value);\n\t\tif (important) {\n\t\t\tvalue = value.replace(/!\\s*important$/, '').trim();\n\t\t}\n\n\t\tconst before = found.get(property);\n\t\tif (!before || important || !before.important) {\n\t\t\tfound.set(property, {value, important});\n\t\t}\n\t}\n\n\treturn found;\n}\n\n// (a value longer than any real one is not read: the numbers are read by a\n// regular expression)\nconst MAX_VALUE = 64;\nconst LENGTH = /^([+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?)(px|pt|pc|in|cm|mm|q|em|rem|%|ex|ch|)$/;\nconst UNITS = {\n\tpx: 1, pt: 4 / 3, pc: 16, in: 96, cm: 96 / 2.54, mm: 96 / 25.4, q: 96 / 101.6, rem: 16,\n};\nconst SIZE_KEYWORDS = {\n\t'xx-small': 9, 'x-small': 10, small: 13, medium: 16, large: 18, 'x-large': 24, 'xx-large': 32, 'xxx-large': 48,\n};\n\n// A length in pixels, given the font size it is relative to, or null for one\n// that is not valid (a number without a unit counts as pixels, as in mail,\n// which is read in quirks mode)\nfunction pixels(value, parent) {\n\tconst length = value.length > MAX_VALUE ? null : LENGTH.exec(value);\n\tif (!length) {\n\t\treturn null;\n\t}\n\n\tconst number = Number(length[1]);\n\tif (!Number.isFinite(number)) {\n\t\treturn null;\n\t}\n\n\tconst unit = length[2];\n\tif (unit === 'em') {\n\t\treturn number * parent;\n\t}\n\n\tif (unit === '%') {\n\t\treturn number * parent / 100;\n\t}\n\n\tif (unit === 'ex' || unit === 'ch') {\n\t\treturn number * parent / 2;\n\t}\n\n\treturn number * (UNITS[unit] ?? 1);\n}\n\n// A font size in pixels, or null for none (or one not valid)\nfunction fontSize(value, parent) {\n\tif (Object.hasOwn(SIZE_KEYWORDS, value)) {\n\t\treturn SIZE_KEYWORDS[value];\n\t}\n\n\tif (value === 'smaller') {\n\t\treturn parent / 1.2;\n\t}\n\n\tif (value === 'larger') {\n\t\treturn parent * 1.2;\n\t}\n\n\tconst size = pixels(value, parent);\n\treturn size === null || size < 0 ? null : size;\n}\n\n// The font size in the \"font\" shorthand (\"bold 12px/1.4 Arial\", \"0/0 a\")\nfunction shorthandSize(value, parent) {\n\tfor (const token of value.slice(0, MAX_STYLE).split(/\\s+/)) {\n\t\t// (a size has a unit or is a keyword, or comes before a \"/\" and the\n\t\t// line height; a number alone is a weight: \"400 0px Arial\")\n\t\tconst [size, lineHeight] = token.split('/');\n\t\tif (lineHeight === undefined && /^[+-]?[\\d.]+$/.test(size)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst pixelSize = fontSize(size, parent);\n\t\tif (pixelSize !== null) {\n\t\t\treturn pixelSize;\n\t\t}\n\t}\n\n\treturn null;\n}\n\n// Whether a colour is transparent: \"transparent\", or an alpha of 0\nfunction isTransparent(color) {\n\tif (color === 'transparent') {\n\t\treturn true;\n\t}\n\n\tconst hex = /^#(?:[\\da-f]{3}(0)|[\\da-f]{6}(00))$/.exec(color);\n\tif (hex) {\n\t\treturn true;\n\t}\n\n\tconst functional = /^(?:rgb|hsl)a?\\((.*)\\)$/.exec(color);\n\tif (!functional) {\n\t\treturn false;\n\t}\n\n\t// (the alpha after a \"/\", or as a fourth value)\n\tconst slash = functional[1].lastIndexOf('/');\n\tconst parts = functional[1].split(',');\n\tif (slash === -1 && parts.length < 4) {\n\t\treturn false;\n\t}\n\n\tconst alpha = (slash === -1 ? parts.at(-1) : functional[1].slice(slash + 1)).trim();\n\treturn ZERO_NUMBER.test(alpha);\n}\n\n// (a number that is zero: \"0\", \"0.0\", \".0\", \"0%\")\nconst ZERO_NUMBER = /^[+-]?(?:0+(?:\\.0*)?|\\.0+)%?$/;\n\n// Whether a display shows an element as a block, which breaks the line\n// (\"block\", \"flex\", \"table-cell\", \"list-item\"), or in the line (\"inline\",\n// \"inline-block\", \"contents\"); undefined for a value browsers do not know,\n// which leaves the element as it is. (Read in one or two keywords.)\nconst DISPLAY_INLINE = /^(?:inline(?:-[a-z]+)?|contents|ruby(?:-[a-z]+)?)$/;\nconst DISPLAY_BLOCK = /^(?:block|flex|grid|flow|flow-root|list-item|run-in|table(?:-[a-z]+)?)$/;\nfunction displayKind(display) {\n\tconst keywords = display.length <= MAX_VALUE ? display.split(/\\s+/) : ['?'];\n\tif (!keywords.every(keyword => DISPLAY_INLINE.test(keyword) || DISPLAY_BLOCK.test(keyword))) {\n\t\treturn undefined;\n\t}\n\n\treturn keywords.some(keyword => DISPLAY_INLINE.test(keyword)) ? 'inline' : 'block';\n}\n\nconst isZero = value => {\n\tconst size = pixels(value, 16);\n\treturn size !== null && size <= 0;\n};\n\n//\n// What an element's style does to the text it holds, given its parent's\n// state: whether it hides all of it (display, opacity, a clipped box of no\n// size, or text moved off the page), and the visibility, transparency and\n// font size its content has (inherited: a child can set them again)\n//\n// (the declarations of the styles one document repeats, read once: opening\n// formatting elements again reads theirs before every text)\nlet declarationCache = new Map();\nfunction styleState(style, parent, name) {\n\tlet css = declarationCache.get(style);\n\tif (css === undefined) {\n\t\tcss = declarations(style);\n\t\tdeclarationCache.set(style, css);\n\t}\n\n\tconst value = property => css.get(property)?.value;\n\t// (a size, overflow and indent apply to a box, not to text in the line:\n\t// to a block, or an element shown as one, or taken out of the line)\n\tconst display = value('display');\n\tconst position = value('position');\n\tconst boxed = display === undefined ? BREAKS.has(name) : !['inline', 'contents'].includes(display);\n\tconst box = boxed || ['absolute', 'fixed'].includes(position) || ['left', 'right'].includes(value('float'));\n\t// (a part of a table shown as one, and not taken out of it, is no box\n\t// that clips: it grows to what it holds)\n\tconst tablePart = (display === undefined ? TABLE_BOXES.has(name) : /^(?:inline-)?table(?!-caption)/.test(display)) && !['absolute', 'fixed'].includes(position) && value('float') === undefined;\n\tconst opacity = value('opacity');\n\tconst overflow = [value('overflow'), value('overflow-x'), value('overflow-y')].some(item => item === 'hidden' || item === 'clip');\n\tconst noHeight = (isZero(value('height') ?? '1') || isZero(value('max-height') ?? '1')) && isZero(value('min-height') ?? '0');\n\tconst noWidth = (isZero(value('width') ?? '1') || isZero(value('max-width') ?? '1')) && isZero(value('min-width') ?? '0');\n\tconst indent = pixels(value('text-indent') ?? '0', parent.size);\n\tconst offPage = ['absolute', 'fixed'].includes(position) && ['left', 'top'].some(side => (pixels(value(side) ?? '0', parent.size) ?? 0) <= -999);\n\tconst hides = display === 'none'\n\t\t|| (opacity !== undefined && (ZERO_NUMBER.test(opacity) || (opacity.length <= MAX_VALUE && Number.parseFloat(opacity) < 0)))\n\t\t|| (box && !tablePart && overflow && (noHeight || noWidth))\n\t\t|| (box && indent !== null && indent <= -999)\n\t\t|| offPage;\n\n\tconst visibility = value('visibility');\n\tconst color = value('color');\n\tconst size = value('font-size') === undefined ? (value('font') === undefined ? null : shorthandSize(value('font'), parent.size)) : fontSize(value('font-size'), parent.size);\n\treturn {\n\t\thides,\n\t\tdisplay,\n\t\tkind: display === undefined ? undefined : displayKind(display),\n\t\t// (an element taken out of the line, where it takes no place)\n\t\toutOfFlow: ['absolute', 'fixed'].includes(position) || ['left', 'right'].includes(value('float')),\n\t\tvisible: visibility === 'hidden' || visibility === 'collapse' ? false : (visibility === 'visible' ? true : parent.visible),\n\t\ttransparent: color === undefined ? parent.transparent : isTransparent(color),\n\t\tsize: size ?? parent.size,\n\t};\n}\n\nconst ROOT = {\n\tname: '', namespace: 'html', visible: true, transparent: false, size: 16, hides: false, hiding: 0, gone: 0, collapsed: 0,\n};\n\n// Whether a document is read in quirks mode, where a paragraph holds a table\n// rather than ending at it: without a doctype, or with one of the old ones\n// mail is written with (\"-//W3C//DTD HTML 4.01 Transitional//EN\", without\n// its address); with \"<!doctype html>\", an XHTML one or another, not\nconst DOCTYPE = /^\\s*<!doctype\\s+([^\\s>]+)(?:\\s+public\\s*(?:\"([^\"]*)\"|'([^']*)')\\s*(?:\"[^\"]*\"|'[^']*')?)?/i;\nconst QUIRKS_PUBLIC = /^-\\/\\/(?:w3c\\/\\/dtd html (?:[23]|4\\.0 (?:transitional|frameset)|3\\.2)|ietf\\/\\/|netscape|microsoft|sq|o'reilly|webtechs|spyglass|softquad|sun microsystems|metrius|advasoft|as\\/\\/|w30)/i;\nconst QUIRKS_IF_NO_SYSTEM = /^-\\/\\/w3c\\/\\/dtd html 4\\.01 (?:transitional|frameset)\\/\\//i;\n\nfunction isQuirks(html) {\n\tconst doctype = DOCTYPE.exec(html.slice(0, 1000));\n\tif (!doctype) {\n\t\treturn true;\n\t}\n\n\tconst publicId = doctype[2] ?? doctype[3];\n\tconst system = /[\"'][^\"']*[\"']\\s*[\"']/.test(doctype[0]);\n\treturn doctype[1].toLowerCase() !== 'html' || (publicId !== undefined && (QUIRKS_PUBLIC.test(publicId) || (QUIRKS_IF_NO_SYSTEM.test(publicId) && !system)));\n}\n\n/**\n * The text of HTML as a mail client shows it, read in linear time: without\n * tags, comments, scripts, styles and the title, with character references\n * decoded, without a break inside a word at an inline tag\n * (\"Free Sp<span>ins</span>\" is \"Free Spins\") but with one between blocks\n * (\"<td>a</td><td>b</td>\" is \"a\" and \"b\"), and without text that is hidden:\n * by an element that hides what it holds (display, opacity, the hidden\n * attribute, a clipped box of no size, text moved off the page), or that is\n * invisible, transparent or in a font of a pixel or less where it is (which\n * an element inside can set again). Not colours on a background, or classes\n * of a style sheet: text hidden so is read as shown.\n * @param {string} html\n * @returns {{text: string, hidden: number}} the text, and how many\n * characters of text were hidden\n */\nexport function visibleText(html) {\n\tif (typeof html !== 'string' || html === '') {\n\t\treturn {text: '', hidden: 0};\n\t}\n\n\tconst quirks = isQuirks(html);\n\tdeclarationCache = new Map();\n\tconst decodedStyles = new Map();\n\tconst parts = [];\n\tlet hidden = 0;\n\t// (the open elements, each with its state: whether it and those around\n\t// it hide what they hold, counted, and how many closed <details> it is\n\t// in, less the summaries; for each name the places of those open, and\n\t// the places of the elements an end tag of another name stops at)\n\tconst stack = [];\n\tconst places = new Map();\n\tconst specials = [];\n\tconst blocks = [];\n\tconst scopes = [];\n\t// (the formatting elements a browser opens again, and markers)\n\tconst formatting = [];\n\t// (the form a browser has open, outside a template: no other starts in\n\t// it, and its end tag ends it wherever it is)\n\tlet form = null;\n\tconst state = () => stack.at(-1) ?? ROOT;\n\tconst latest = name => places.get(name)?.at(-1) ?? -1;\n\tconst nearest = names => {\n\t\tlet index = -1;\n\t\tfor (const name of names) {\n\t\t\tindex = Math.max(index, latest(name));\n\t\t}\n\n\t\treturn index;\n\t};\n\n\t// (the break an element's tag makes in the text, if any: of an element\n\t// that takes a place on the page, a block, as its name or a style shows\n\t// it, or what takes a place of its own, as an image)\n\tconst breakOf = at => {\n\t\tif (at.gone > 0 || at.box > 0) {\n\t\t\treturn '';\n\t\t}\n\n\t\tconst kind = OWN_PLACE.has(at.name) || at.namespace !== 'html' ? undefined : at.kind;\n\t\tif (kind === undefined) {\n\t\t\treturn BREAKS.has(at.name) ? (LINE.has(at.name) ? '\\n' : ' ') : '';\n\t\t}\n\n\t\treturn kind === 'block' ? (LINE.has(at.name) || !BREAKS.has(at.name) ? '\\n' : ' ') : '';\n\t};\n\n\tconst shown = at => at.hiding === 0 && at.collapsed === 0 && at.visible && !at.transparent && at.size > 1;\n\t// (where a browser puts text and elements: in front of a table they are\n\t// written in, in the table's parent; but in a template inside the table,\n\t// or with no table, in the template's content, which is not shown)\n\tconst fostering = () => state().namespace === 'html' && TABLE_PARTS.has(state().name) && latest('table') > latest('template');\n\tconst parentOf = () => (fostering() ? stack[latest('table') - 1] ?? ROOT : state());\n\tconst write = (text, at = state()) => {\n\t\tif (shown(at)) {\n\t\t\tparts.push(text);\n\t\t} else {\n\t\t\thidden += text.replaceAll(/\\s+/g, '').length;\n\t\t}\n\t};\n\n\t// (and the elements a browser took out of the middle of the open ones,\n\t// as a </form> does, once those after them are closed; when closing\n\t// back to a table, the markers of what was put in front of it are kept,\n\t// as browsers keep them; and with `breaks`, a break where each block\n\t// ends, which an end tag of a formatting element does not make: a\n\t// browser moves the block out of it, and keeps it open)\n\tconst popTo = (index, toTable = false, breaks = true) => {\n\t\twhile (stack.length > index || stack.at(-1)?.removed) {\n\t\t\tconst element = stack.pop();\n\t\t\telement.open = false;\n\t\t\tplaces.get(element.name).pop();\n\t\t\t// (a block ends here, also where a browser ends it by itself: \"<p>a<div\n\t\t\t// hidden></div>b\" shows \"a\" and \"b\" on lines of their own)\n\t\t\tif (breaks) {\n\t\t\t\tparts.push(breakOf(element));\n\t\t\t}\n\n\t\t\tif (specials.at(-1) === stack.length) {\n\t\t\t\tspecials.pop();\n\t\t\t}\n\n\t\t\tif (blocks.at(-1) === stack.length) {\n\t\t\t\tblocks.pop();\n\t\t\t}\n\n\t\t\tif (scopes.at(-1) === stack.length) {\n\t\t\t\tscopes.pop();\n\t\t\t}\n\n\t\t\tif (MARKERS.has(element.name) && !(toTable && FOSTERED_MARKERS.has(element.name))) {\n\t\t\t\tformatting.length = Math.max(0, formatting.findLastIndex(entry => entry.marker === element));\n\t\t\t}\n\t\t}\n\t};\n\n\t// (an implied end: the nearest of `names`, not past one of `limits`,\n\t// or for a list item a block)\n\tconst closeImplied = ([names, limits]) => {\n\t\tconst index = nearest(names);\n\t\tif (index !== -1 && index >= (limits ? nearest(limits) + 1 : blocks.at(-1))) {\n\t\t\tpopTo(index);\n\t\t}\n\t};\n\n\t// The state of an element opened in `parent`\n\tconst elementOf = (name, attributes, parent, namespace) => {\n\t\tconst style = attributes.get('style');\n\t\tconst element = style === undefined\n\t\t\t? {\n\t\t\t\tvisible: parent.visible, transparent: parent.transparent, size: parent.size, hides: false,\n\t\t\t}\n\t\t\t: styleState(decodedStyles.get(style) ?? decodedStyles.set(style, decodeEntities(style, {attribute: true})).get(style), parent, name);\n\t\telement.name = name;\n\t\telement.namespace = namespace;\n\t\telement.attributes = attributes;\n\t\telement.open = true;\n\t\t// (the hidden attribute, unless a style shows the element; a\n\t\t// template's content, which is not shown; what an <svg> does not\n\t\t// show; a closed <details>, but for its summary)\n\t\tconst unshown = (attributes.has('hidden') && (element.display === undefined || element.display === 'none')) || (name === 'template' && namespace === 'html') || (namespace === 'svg' && SVG_UNSHOWN.has(name));\n\t\tif (unshown) {\n\t\t\telement.hides = true;\n\t\t}\n\n\t\telement.hiding = parent.hiding + (element.hides ? 1 : 0);\n\t\t// (and whether it or one around it takes no place on the page, so\n\t\t// breaks nothing: not shown at all, or hidden out of the line; an\n\t\t// element hidden in its place, as by its opacity, still breaks the\n\t\t// line where it is a block)\n\t\telement.gone = parent.gone + (unshown || (element.hides && (element.display === 'none' || element.outOfFlow)) ? 1 : 0);\n\t\tconst collapses = namespace === 'html' && name === 'details' && !attributes.has('open');\n\t\tconst reveals = namespace === 'html' && name === 'summary' && parent.collapses === true;\n\t\telement.collapses = collapses;\n\t\t// (whether the element itself is shown, and what it holds)\n\t\telement.box = parent.collapsed - (reveals ? 1 : 0);\n\t\telement.collapsed = element.box + (collapses ? 1 : 0);\n\t\treturn element;\n\t};\n\n\tconst push = element => {\n\t\t// (past the most kept, the innermost is closed: the element goes in\n\t\t// beside it, as deep elements do in browsers)\n\t\tif (stack.length >= MAX_OPEN) {\n\t\t\tpopTo(stack.length - 1);\n\t\t}\n\n\t\tif (!places.has(element.name)) {\n\t\t\tplaces.set(element.name, []);\n\t\t}\n\n\t\tplaces.get(element.name).push(stack.length);\n\t\t// (where the <svg> or <math> an element is in starts)\n\t\telement.run = element.namespace === 'html' ? -1 : (state().namespace === 'html' ? stack.length : state().run);\n\t\tif (element.namespace === 'html' ? SPECIAL.has(element.name) : HOLDS_HTML[element.namespace]?.has(element.name)) {\n\t\t\tspecials.push(stack.length);\n\t\t\tif (!['address', 'div', 'p'].includes(element.name)) {\n\t\t\t\tblocks.push(stack.length);\n\t\t\t}\n\n\t\t\tif (element.namespace !== 'html' || SCOPE.has(element.name)) {\n\t\t\t\tscopes.push(stack.length);\n\t\t\t}\n\t\t}\n\n\t\tstack.push(element);\n\t};\n\n\t// (the formatting elements closed by something else, opened again with\n\t// their attributes, as browsers do before text and most elements)\n\tconst reopen = () => {\n\t\tlet first = formatting.length;\n\t\twhile (first > 0 && !formatting[first - 1].marker && !formatting[first - 1].element.open) {\n\t\t\tfirst--;\n\t\t}\n\n\t\tif (first === formatting.length) {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const entry of formatting.slice(first)) {\n\t\t\tentry.element = elementOf(entry.element.name, entry.element.attributes, parentOf(), 'html');\n\t\t\tpush(entry.element);\n\t\t}\n\t};\n\n\t// (closing what an end tag closes, as browsers do: an end tag of a block\n\t// or a formatting element closes it with all inside it; another one\n\t// only when no block is open inside it; in an <svg> or <math>, the\n\t// element of the name in it. A formatting element something else\n\t// closed is no longer opened again. The element closed, or the state\n\t// where nothing is.)\n\tconst close = name => {\n\t\t// (a </p> or </br> ends an <svg> or <math>, as a start tag that\n\t\t// leaves it does)\n\t\tif ((name === 'p' || name === 'br') && inForeign()) {\n\t\t\tleaveForeign();\n\t\t}\n\n\t\t// (an end tag of a heading ends any heading)\n\t\tconst index = HEADINGS.includes(name) ? nearest(HEADINGS) : latest(name);\n\t\tif (name === 'form' && latest('template') === -1 && !(state().namespace !== 'html' && index >= state().run)) {\n\t\t\treturn closeForm();\n\t\t}\n\n\t\tconst entry = FORMATTING.has(name) ? formatting.findLast(item => item.element?.name === name) : undefined;\n\t\tif (entry) {\n\t\t\tformatting.splice(formatting.indexOf(entry), 1);\n\t\t}\n\n\t\t// (a block or formatting element in scope: not past a table, a cell\n\t\t// and the like, also a <button> for a paragraph and a list for a\n\t\t// list item; a part of a table not past a table)\n\t\tconst scope = TABLE_STRUCTURE.includes(name) ? nearest(['table', 'template']) : Math.max(scopes.at(-1) ?? -1, name === 'p' ? latest('button') : -1, name === 'li' ? nearest(['ol', 'ul']) : -1);\n\t\tif (index !== -1 && ((state().namespace !== 'html' && index >= state().run) || ((SPECIAL.has(name) || FORMATTING.has(name)) && index >= scope) || index >= (specials.at(-1) ?? -1))) {\n\t\t\tconst element = stack[index];\n\t\t\tpopTo(index, false, !FORMATTING.has(name));\n\t\t\treturn element;\n\t\t}\n\n\t\treturn state();\n\t};\n\n\t// (a </form> ends the form open, also when other elements are open in it:\n\t// they stay open, and the form is taken out from under them; or does\n\t// nothing when it is not open, or not in scope)\n\tconst closeForm = () => {\n\t\tconst element = form;\n\t\tform = null;\n\t\tconst place = element?.open ? places.get('form').findLast(at => stack[at] === element) : -1;\n\t\tif (place === -1 || place < (scopes.at(-1) ?? -1)) {\n\t\t\treturn state();\n\t\t}\n\n\t\t// (the elements whose end a browser implies, closed first)\n\t\twhile (stack.length - 1 > place && IMPLIED_END.has(state().name) && state().namespace === 'html') {\n\t\t\tpopTo(stack.length - 1);\n\t\t}\n\n\t\tif (place === stack.length - 1) {\n\t\t\tpopTo(place);\n\t\t} else {\n\t\t\t// (no longer an element the others stop at: a <li> after it\n\t\t\t// closes the <li> it is in, as in a browser)\n\t\t\telement.removed = true;\n\t\t\tfor (const list of [specials, blocks, scopes]) {\n\t\t\t\tconst at = list.indexOf(place);\n\t\t\t\tif (at !== -1) {\n\t\t\t\t\tlist.splice(at, 1);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn element;\n\t};\n\n\t// What a browser closes and opens again before an HTML element starts,\n\t// or false for one it leaves out: a part of a table outside one; in one,\n\t// it closes what was put in front of the table. A link ends the link\n\t// open, and a table in a table that table.\n\tconst prepare = name => {\n\t\tif (PARTS_OF_TABLE.has(name)) {\n\t\t\tif (latest('table') === -1 && latest('template') === -1) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// (a caption ends at any part of a table, and a cell, with its\n\t\t\t// row, at a caption or a column)\n\t\t\tconst structure = nearest(TABLE_STRUCTURE);\n\t\t\tconst ends = stack[structure].name === 'caption' || (['caption', 'col', 'colgroup'].includes(name) && ['td', 'th'].includes(stack[structure].name));\n\t\t\tpopTo((ends ? nearest(['table', 'template']) : structure) + 1, true);\n\t\t}\n\n\t\tif (name === 'a' && formatting.slice(formatting.findLastIndex(item => item.marker) + 1).some(item => item.element.name === 'a')) {\n\t\t\tclose('a');\n\t\t}\n\n\t\tif (name === 'table' && latest('table') !== -1 && TABLE_PARTS.has(stack[nearest(TABLE_STRUCTURE)].name)) {\n\t\t\tpopTo(latest('table'));\n\t\t}\n\n\t\tif (CLOSES_PARAGRAPH.has(name) && !(name === 'table' && quirks)) {\n\t\t\tcloseImplied(PARAGRAPH);\n\t\t}\n\n\t\t// (a heading ends a heading it starts in, and a button a button open)\n\t\tif (HEADINGS.includes(name) && state().namespace === 'html' && HEADINGS.includes(state().name)) {\n\t\t\tpopTo(stack.length - 1);\n\t\t}\n\n\t\tif (name === 'button' && latest('button') !== -1 && latest('button') > (scopes.at(-1) ?? -1)) {\n\t\t\tpopTo(latest('button'));\n\t\t}\n\n\t\tif (IMPLIED[name]) {\n\t\t\tcloseImplied(IMPLIED[name]);\n\t\t}\n\n\t\tif (!NOT_REOPENING.has(name)) {\n\t\t\treopen();\n\t\t\t// (a <nobr> ends the one open, when no block is open in it, and\n\t\t\t// the others are opened again)\n\t\t\tif (name === 'nobr' && latest('nobr') > (specials.at(-1) ?? -1)) {\n\t\t\t\tclose('nobr');\n\t\t\t\treopen();\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t};\n\n\t// (whether elements opened now are in an <svg> or <math>, and whether in\n\t// a column group)\n\tconst inForeign = () => state().namespace !== 'html' && !HOLDS_HTML[state().namespace].has(state().name);\n\tconst inColumnGroup = () => state().namespace === 'html' && state().name === 'colgroup';\n\t// (ending an <svg> or <math> and what it holds)\n\tconst leaveForeign = () => {\n\t\twhile (inForeign()) {\n\t\t\tpopTo(stack.length - 1);\n\t\t}\n\t};\n\n\t// (and whether in a table, not in a cell or caption of it: also in what\n\t// was put in front of it)\n\tconst inTable = () => {\n\t\tconst structure = nearest(TABLE_STRUCTURE);\n\t\treturn structure !== -1 && TABLE_PARTS.has(stack[structure].name) && latest('table') > latest('template');\n\t};\n\n\t// (what a start tag ends before its element: a column group, which holds\n\t// only columns; an <svg> or <math>, at an element that leaves it, with\n\t// what it holds, unless in an element of theirs that holds HTML)\n\tconst endBefore = tag => {\n\t\tif (inColumnGroup() && tag.name !== 'col' && tag.name !== 'template') {\n\t\t\tpopTo(stack.length - 1);\n\t\t}\n\n\t\tif (inForeign() && (LEAVES_FOREIGN.has(tag.name) || (tag.name === 'font' && ['color', 'face', 'size'].some(attribute => tag.attributes.has(attribute))))) {\n\t\t\tleaveForeign();\n\t\t}\n\t};\n\n\t// (how a form starts: left out in a form open outside a template; empty\n\t// in a table, where a browser closes it at once, or leaves it out in a\n\t// template, which shows nothing either way; or opened)\n\tconst startsForm = () => {\n\t\tif (form && latest('template') === -1) {\n\t\t\treturn 'none';\n\t\t}\n\n\t\treturn inTable() ? 'empty' : 'open';\n\t};\n\n\t// (a formatting element or a marker added to those opened again: at most\n\t// three alike after the last marker, as browsers keep, and at most\n\t// MAX_FORMATTING in all)\n\tconst remember = (element, foreign) => {\n\t\tconst {name, attributes} = element;\n\t\tif (!foreign && FORMATTING.has(name)) {\n\t\t\tconst alike = formatting.filter(item => item.element?.name === name && item.element.attributes.get('style') === attributes.get('style') && item.element.attributes.has('hidden') === attributes.has('hidden'));\n\t\t\tconst marker = formatting.findLastIndex(item => item.marker);\n\t\t\tif (alike.length >= 3 && formatting.indexOf(alike.at(-3)) > marker) {\n\t\t\t\tformatting.splice(formatting.indexOf(alike.at(-3)), 1);\n\t\t\t}\n\n\t\t\tformatting.push({element});\n\t\t} else if (!foreign && MARKERS.has(name)) {\n\t\t\tformatting.push({marker: element});\n\t\t}\n\n\t\tif (formatting.length > MAX_FORMATTING) {\n\t\t\tformatting.shift();\n\t\t}\n\t};\n\n\tlet from = 0;\n\twhile (from < html.length) {\n\t\tconst lt = html.indexOf('<', from);\n\t\tconst text = lt === from ? '' : decodeEntities(html.slice(from, lt === -1 ? html.length : lt));\n\t\tif (text !== '') {\n\t\t\tif (inColumnGroup() && /\\S/.test(text)) {\n\t\t\t\tpopTo(stack.length - 1);\n\t\t\t}\n\n\t\t\tif (state().namespace === 'html') {\n\t\t\t\treopen();\n\t\t\t}\n\n\t\t\twrite(text, fostering() && /\\S/.test(text) ? parentOf() : state());\n\t\t}\n\n\t\tif (lt === -1) {\n\t\t\tbreak;\n\t\t}\n\n\t\tfrom = lt + 1;\n\t\tconst next = html[lt + 1] ?? '';\n\n\t\t// Comments, as browsers end them (\"<!-->\" and \"--!>\" too); other\n\t\t// markup declarations and processing instructions end at a \">\"\n\t\tif (html.startsWith('<!--', lt)) {\n\t\t\tif (html.startsWith('>', lt + 4) || html.startsWith('->', lt + 4)) {\n\t\t\t\tfrom = html.indexOf('>', lt + 4) + 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tCOMMENT_END.lastIndex = lt + 4;\n\t\t\tconst end = COMMENT_END.exec(html);\n\t\t\tfrom = end === null ? html.length : end.index + end[0].length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst closingTag = next === '/';\n\t\tconst letter = /[a-zA-Z]/.test(closingTag ? (html[lt + 2] ?? '') : next);\n\t\tif (!letter) {\n\t\t\tif (closingTag && lt + 2 >= html.length) {\n\t\t\t\t// (a \"</\" the input ends with is text)\n\t\t\t\twrite('</');\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif (next === '!' || next === '?' || (closingTag && html[lt + 2] !== '>')) {\n\t\t\t\tconst end = html.indexOf('>', lt + 2);\n\t\t\t\tfrom = end === -1 ? html.length : end + 1;\n\t\t\t} else if (closingTag) {\n\t\t\t\t// (\"</>\" is nothing)\n\t\t\t\tfrom = lt + 3;\n\t\t\t} else {\n\t\t\t\t// (a \"<\" before anything else is text)\n\t\t\t\twrite('<');\n\t\t\t}\n\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst tag = readTag(html, lt);\n\t\tif (tag.end === -1) {\n\t\t\tbreak;\n\t\t}\n\n\t\tfrom = tag.end;\n\t\tconst {name} = tag;\n\t\t// (whether the tag's element and what is around it are hidden: a\n\t\t// hidden element breaks no word)\n\t\tlet at = state();\n\t\tif (tag.closing) {\n\t\t\tat = close(name);\n\t\t} else {\n\t\t\tendBefore(tag);\n\t\t\t// (read as HTML, also an <svg> or <math> that starts in HTML, before\n\t\t\t// which a browser opens formatting elements again)\n\t\t\tconst readAsHtml = !inForeign();\n\t\t\tconst namespace = readAsHtml ? (name === 'svg' || name === 'math' ? name : 'html') : state().namespace;\n\t\t\tconst foreign = namespace !== 'html';\n\t\t\tconst formStart = name === 'form' && !foreign ? startsForm() : 'open';\n\t\t\tif (formStart === 'none' || (readAsHtml && formStart === 'open' && !prepare(name))) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst parent = (foreign && state().namespace !== 'html') || IN_TABLE.has(name) ? state() : parentOf();\n\t\t\tconst element = elementOf(name, tag.attributes, parent, namespace);\n\t\t\tat = element;\n\t\t\tif (name === 'form' && !foreign && latest('template') === -1) {\n\t\t\t\tform = element;\n\t\t\t\telement.open = formStart === 'open';\n\t\t\t}\n\n\t\t\t// (an element is opened unless it has no content, or closes\n\t\t\t// itself in an <svg> or <math>)\n\t\t\tif (foreign ? !tag.selfClosing : !VOID.has(name) && formStart === 'open') {\n\t\t\t\tpush(element);\n\t\t\t\tremember(element, foreign);\n\t\t\t}\n\n\t\t\tif (name === 'plaintext' && !foreign) {\n\t\t\t\t// (all after it is shown as it is)\n\t\t\t\tparts.push('\\n');\n\t\t\t\twrite(html.slice(tag.end));\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tif ((UNSHOWN.has(name) || RAW_SHOWN.has(name)) && !foreign) {\n\t\t\t\tconst closer = RAW_END[name];\n\t\t\t\tcloser.lastIndex = tag.end;\n\t\t\t\tconst close = closer.exec(html);\n\t\t\t\tconst end = close === null ? html.length : close.index;\n\t\t\t\tif (RAW_SHOWN.has(name)) {\n\t\t\t\t\tconst raw = html.slice(tag.end, end);\n\t\t\t\t\twrite(name === 'textarea' ? decodeEntities(raw) : raw);\n\t\t\t\t}\n\n\t\t\t\tfrom = end;\n\t\t\t}\n\t\t}\n\n\t\t// (a start tag breaks as its element does, and an end tag that closed\n\t\t// nothing as one of its name: a stray \"</p>\" or \"</br>\" breaks the\n\t\t// line; one that closed an element broke where it ended)\n\t\tif (!tag.closing) {\n\t\t\tparts.push(breakOf(at));\n\t\t} else if (at.open !== false && BREAKS.has(name) && at.gone === 0 && (at.box ?? at.collapsed) <= 0) {\n\t\t\tparts.push(LINE.has(name) ? '\\n' : ' ');\n\t\t}\n\t}\n\n\treturn {text: parts.join(''), hidden};\n}\n"],
5
+ "mappings": "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ACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,E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",
6
+ "names": ["import_node_buffer", "import_node_crypto", "import_node_buffer", "import_tldts", "BOUNDED", "escaped", "key", "parser", "escaped", "escaped"]
7
7
  }