UNPKG

postal-mime

Version:

Email parser for Node.js and browser environments

328 lines (325 loc) 11.1 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var mime_node_exports = {}; __export(mime_node_exports, { default: () => MimeNode }); module.exports = __toCommonJS(mime_node_exports); var import_decode_strings = require("./decode-strings.cjs"); var import_pass_through_decoder = __toESM(require("./pass-through-decoder.cjs"), 1); var import_base64_decoder = __toESM(require("./base64-decoder.cjs"), 1); var import_qp_decoder = __toESM(require("./qp-decoder.cjs"), 1); const defaultDecoder = (0, import_decode_strings.getDecoder)(); class MimeNode { constructor(options) { this.options = options || {}; this.postalMime = this.options.postalMime; this.root = !!this.options.parentNode; this.childNodes = []; if (this.options.parentNode) { this.parentNode = this.options.parentNode; this.depth = this.parentNode.depth + 1; if (this.depth > this.options.maxNestingDepth) { throw new Error(`Maximum MIME nesting depth of ${this.options.maxNestingDepth} levels exceeded`); } this.options.parentNode.childNodes.push(this); } else { this.depth = 0; } this.state = "header"; this.headerLines = []; this.headerSize = 0; const parentMultipartType = this.options.parentMultipartType || null; const defaultContentType = parentMultipartType === "digest" ? "message/rfc822" : "text/plain"; this.contentType = { value: defaultContentType, default: true }; this.contentTransferEncoding = { value: "8bit" }; this.contentDisposition = { value: "" }; this.headers = []; this.contentDecoder = false; } setupContentDecoder(transferEncoding) { if (/base64/i.test(transferEncoding)) { this.contentDecoder = new import_base64_decoder.default(); } else if (/quoted-printable/i.test(transferEncoding)) { this.contentDecoder = new import_qp_decoder.default({ decoder: (0, import_decode_strings.getDecoder)(this.contentType.parsed.params.charset) }); } else { this.contentDecoder = new import_pass_through_decoder.default(); } } async finalize() { if (this.state === "finished") { return; } if (this.state === "header") { this.processHeaders(); } let boundaries = this.postalMime.boundaries; for (let i = boundaries.length - 1; i >= 0; i--) { let boundary = boundaries[i]; if (boundary.node === this) { boundaries.splice(i, 1); break; } } await this.finalizeChildNodes(); this.content = this.contentDecoder ? await this.contentDecoder.finalize() : null; this.state = "finished"; } async finalizeChildNodes() { for (let childNode of this.childNodes) { await childNode.finalize(); } } // Strip RFC 822 comments (parenthesized text) from structured header values stripComments(str) { let result = ""; let depth = 0; let escaped = false; let inQuote = false; for (let i = 0; i < str.length; i++) { const chr = str.charAt(i); if (escaped) { if (depth === 0) { result += chr; } escaped = false; continue; } if (chr === "\\") { escaped = true; if (depth === 0) { result += chr; } continue; } if (chr === '"' && depth === 0) { inQuote = !inQuote; result += chr; continue; } if (!inQuote) { if (chr === "(") { depth++; continue; } if (chr === ")" && depth > 0) { depth--; continue; } } if (depth === 0) { result += chr; } } return result; } parseStructuredHeader(str) { str = this.stripComments(str); let response = { value: false, params: {} }; let key = false; let value = ""; let stage = "value"; let quote = false; let escaped = false; let chr; for (let i = 0, len = str.length; i < len; i++) { chr = str.charAt(i); switch (stage) { case "key": if (chr === "=") { key = value.trim().toLowerCase(); stage = "value"; value = ""; break; } value += chr; break; case "value": if (escaped) { value += chr; } else if (chr === "\\") { escaped = true; continue; } else if (quote && chr === quote) { quote = false; } else if (!quote && chr === '"') { quote = chr; } else if (!quote && chr === ";") { if (key === false) { response.value = value.trim(); } else { response.params[key] = value.trim(); } stage = "key"; value = ""; } else { value += chr; } escaped = false; break; } } value = value.trim(); if (stage === "value") { if (key === false) { response.value = value; } else { response.params[key] = value; } } else if (value) { response.params[value.toLowerCase()] = ""; } if (response.value) { response.value = response.value.toLowerCase(); } (0, import_decode_strings.decodeParameterValueContinuations)(response); return response; } decodeFlowedText(str, delSp) { return str.split(/\r?\n/).reduce((previousValue, currentValue) => { if (previousValue.endsWith(" ") && previousValue !== "-- " && !previousValue.endsWith("\n-- ")) { if (delSp) { return previousValue.slice(0, -1) + currentValue; } else { return previousValue + currentValue; } } else { return previousValue + "\n" + currentValue; } }).replace(/^ /gm, ""); } getTextContent() { if (!this.content) { return ""; } let str = (0, import_decode_strings.getDecoder)(this.contentType.parsed.params.charset).decode(this.content); if (/^flowed$/i.test(this.contentType.parsed.params.format)) { str = this.decodeFlowedText(str, /^yes$/i.test(this.contentType.parsed.params.delsp)); } return str; } processHeaders() { for (let i = this.headerLines.length - 1; i >= 0; i--) { let line = this.headerLines[i]; if (i && /^\s/.test(line)) { this.headerLines[i - 1] += "\n" + line; this.headerLines.splice(i, 1); } } this.rawHeaderLines = []; for (let i = this.headerLines.length - 1; i >= 0; i--) { let rawLine = this.headerLines[i]; let sep = rawLine.indexOf(":"); let rawKey = sep < 0 ? rawLine.trim() : rawLine.substr(0, sep).trim(); this.rawHeaderLines.push({ key: rawKey.toLowerCase(), line: rawLine }); let normalizedLine = rawLine.replace(/\s+/g, " "); sep = normalizedLine.indexOf(":"); let key = sep < 0 ? normalizedLine.trim() : normalizedLine.substr(0, sep).trim(); let value = sep < 0 ? "" : normalizedLine.substr(sep + 1).trim(); this.headers.push({ key: key.toLowerCase(), originalKey: key, value }); switch (key.toLowerCase()) { case "content-type": if (this.contentType.default) { this.contentType = { value, parsed: {} }; } break; case "content-transfer-encoding": this.contentTransferEncoding = { value, parsed: {} }; break; case "content-disposition": this.contentDisposition = { value, parsed: {} }; break; case "content-id": this.contentId = value; break; case "content-description": this.contentDescription = value; break; } } this.contentType.parsed = this.parseStructuredHeader(this.contentType.value); this.contentType.multipart = /^multipart\//i.test(this.contentType.parsed.value) ? this.contentType.parsed.value.substr(this.contentType.parsed.value.indexOf("/") + 1) : false; if (this.contentType.multipart && this.contentType.parsed.params.boundary) { this.postalMime.boundaries.push({ value: import_decode_strings.textEncoder.encode(this.contentType.parsed.params.boundary), node: this }); } this.contentDisposition.parsed = this.parseStructuredHeader(this.contentDisposition.value); this.contentTransferEncoding.encoding = this.contentTransferEncoding.value.toLowerCase().split(/[^\w-]/).shift(); this.setupContentDecoder(this.contentTransferEncoding.encoding); } feed(line) { switch (this.state) { case "header": if (!line.length) { this.state = "body"; return this.processHeaders(); } this.headerSize += line.length; if (this.headerSize > this.options.maxHeadersSize) { let error = new Error(`Maximum header size of ${this.options.maxHeadersSize} bytes exceeded`); throw error; } this.headerLines.push(defaultDecoder.decode(line)); break; case "body": { this.contentDecoder.update(line); } } } } // Make default export work naturally with require() if (module.exports.default) { var defaultExport = module.exports.default; var namedExports = {}; for (var key in module.exports) { if (key !== 'default' && key !== '__esModule') { namedExports[key] = module.exports[key]; } } module.exports = defaultExport; Object.assign(module.exports, namedExports); // Preserve __esModule and .default for bundler/transpiler interop Object.defineProperty(module.exports, '__esModule', { value: true }); module.exports.default = defaultExport; }