497 lines
18 KiB
JavaScript
497 lines
18 KiB
JavaScript
"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 postal_mime_exports = {};
|
|
__export(postal_mime_exports, {
|
|
addressParser: () => import_address_parser.default,
|
|
decodeWords: () => import_decode_strings.decodeWords,
|
|
default: () => PostalMime
|
|
});
|
|
module.exports = __toCommonJS(postal_mime_exports);
|
|
var import_mime_node = __toESM(require("./mime-node.cjs"), 1);
|
|
var import_text_format = require("./text-format.cjs");
|
|
var import_address_parser = __toESM(require("./address-parser.cjs"), 1);
|
|
var import_decode_strings = require("./decode-strings.cjs");
|
|
var import_base64_encoder = require("./base64-encoder.cjs");
|
|
const MAX_NESTING_DEPTH = 256;
|
|
const MAX_HEADERS_SIZE = 2 * 1024 * 1024;
|
|
function toCamelCase(key) {
|
|
return key.replace(/-(.)/g, (o, c) => c.toUpperCase());
|
|
}
|
|
class PostalMime {
|
|
static parse(buf, options) {
|
|
const parser = new PostalMime(options);
|
|
return parser.parse(buf);
|
|
}
|
|
constructor(options) {
|
|
this.options = options || {};
|
|
this.mimeOptions = {
|
|
maxNestingDepth: this.options.maxNestingDepth || MAX_NESTING_DEPTH,
|
|
maxHeadersSize: this.options.maxHeadersSize || MAX_HEADERS_SIZE
|
|
};
|
|
this.root = this.currentNode = new import_mime_node.default({
|
|
postalMime: this,
|
|
...this.mimeOptions
|
|
});
|
|
this.boundaries = [];
|
|
this.textContent = {};
|
|
this.attachments = [];
|
|
this.attachmentEncoding = (this.options.attachmentEncoding || "").toString().replace(/[-_\s]/g, "").trim().toLowerCase() || "arraybuffer";
|
|
this.started = false;
|
|
}
|
|
async finalize() {
|
|
await this.root.finalize();
|
|
}
|
|
async processLine(line, isFinal) {
|
|
let boundaries = this.boundaries;
|
|
if (boundaries.length && line.length > 2 && line[0] === 45 && line[1] === 45) {
|
|
for (let i = boundaries.length - 1; i >= 0; i--) {
|
|
let boundary = boundaries[i];
|
|
if (line.length < boundary.value.length + 2) {
|
|
continue;
|
|
}
|
|
let boundaryMatches = true;
|
|
for (let j = 0; j < boundary.value.length; j++) {
|
|
if (line[j + 2] !== boundary.value[j]) {
|
|
boundaryMatches = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!boundaryMatches) {
|
|
continue;
|
|
}
|
|
let boundaryEnd = boundary.value.length + 2;
|
|
let isTerminator = false;
|
|
if (line.length >= boundary.value.length + 4 && line[boundary.value.length + 2] === 45 && line[boundary.value.length + 3] === 45) {
|
|
isTerminator = true;
|
|
boundaryEnd = boundary.value.length + 4;
|
|
}
|
|
let hasValidTrailing = true;
|
|
for (let j = boundaryEnd; j < line.length; j++) {
|
|
if (line[j] !== 32 && line[j] !== 9) {
|
|
hasValidTrailing = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!hasValidTrailing) {
|
|
continue;
|
|
}
|
|
if (isTerminator) {
|
|
await boundary.node.finalize();
|
|
this.currentNode = boundary.node.parentNode || this.root;
|
|
} else {
|
|
await boundary.node.finalizeChildNodes();
|
|
this.currentNode = new import_mime_node.default({
|
|
postalMime: this,
|
|
parentNode: boundary.node,
|
|
parentMultipartType: boundary.node.contentType.multipart,
|
|
...this.mimeOptions
|
|
});
|
|
}
|
|
if (isFinal) {
|
|
return this.finalize();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
this.currentNode.feed(line);
|
|
if (isFinal) {
|
|
return this.finalize();
|
|
}
|
|
}
|
|
readLine() {
|
|
let startPos = this.readPos;
|
|
let endPos = this.readPos;
|
|
while (this.readPos < this.av.length) {
|
|
const c = this.av[this.readPos++];
|
|
if (c !== 13 && c !== 10) {
|
|
endPos = this.readPos;
|
|
}
|
|
if (c === 10) {
|
|
return {
|
|
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
done: this.readPos >= this.av.length
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
bytes: new Uint8Array(this.buf, startPos, endPos - startPos),
|
|
done: this.readPos >= this.av.length
|
|
};
|
|
}
|
|
async processNodeTree() {
|
|
let textContent = {};
|
|
let textTypes = /* @__PURE__ */ new Set();
|
|
let textMap = this.textMap = /* @__PURE__ */ new Map();
|
|
let forceRfc822Attachments = this.forceRfc822Attachments();
|
|
let walk = async (node, alternative, related) => {
|
|
var _a, _b, _c, _d, _e;
|
|
alternative = alternative || false;
|
|
related = related || false;
|
|
if (!node.contentType.multipart) {
|
|
if (this.isInlineMessageRfc822(node) && !forceRfc822Attachments) {
|
|
const subParser = new PostalMime();
|
|
node.subMessage = await subParser.parse(node.content);
|
|
if (!textMap.has(node)) {
|
|
textMap.set(node, {});
|
|
}
|
|
let textEntry = textMap.get(node);
|
|
if (node.subMessage.text || !node.subMessage.html) {
|
|
textEntry.plain = textEntry.plain || [];
|
|
textEntry.plain.push({ type: "subMessage", value: node.subMessage });
|
|
textTypes.add("plain");
|
|
}
|
|
if (node.subMessage.html) {
|
|
textEntry.html = textEntry.html || [];
|
|
textEntry.html.push({ type: "subMessage", value: node.subMessage });
|
|
textTypes.add("html");
|
|
}
|
|
if (subParser.textMap) {
|
|
subParser.textMap.forEach((subTextEntry, subTextNode) => {
|
|
textMap.set(subTextNode, subTextEntry);
|
|
});
|
|
}
|
|
for (let attachment of node.subMessage.attachments || []) {
|
|
this.attachments.push(attachment);
|
|
}
|
|
} else if (this.isInlineTextNode(node)) {
|
|
let textType = node.contentType.parsed.value.substr(node.contentType.parsed.value.indexOf("/") + 1);
|
|
let selectorNode = alternative || node;
|
|
if (!textMap.has(selectorNode)) {
|
|
textMap.set(selectorNode, {});
|
|
}
|
|
let textEntry = textMap.get(selectorNode);
|
|
textEntry[textType] = textEntry[textType] || [];
|
|
textEntry[textType].push({ type: "text", value: node.getTextContent() });
|
|
textTypes.add(textType);
|
|
} else if (node.content) {
|
|
const filename = ((_c = (_b = (_a = node.contentDisposition) == null ? void 0 : _a.parsed) == null ? void 0 : _b.params) == null ? void 0 : _c.filename) || node.contentType.parsed.params.name || null;
|
|
const attachment = {
|
|
filename: filename ? (0, import_decode_strings.decodeWords)(filename) : null,
|
|
mimeType: node.contentType.parsed.value,
|
|
disposition: ((_e = (_d = node.contentDisposition) == null ? void 0 : _d.parsed) == null ? void 0 : _e.value) || null
|
|
};
|
|
if (related && node.contentId) {
|
|
attachment.related = true;
|
|
}
|
|
if (node.contentDescription) {
|
|
attachment.description = node.contentDescription;
|
|
}
|
|
if (node.contentId) {
|
|
attachment.contentId = node.contentId;
|
|
}
|
|
switch (node.contentType.parsed.value) {
|
|
// Special handling for calendar events
|
|
case "text/calendar":
|
|
case "application/ics": {
|
|
if (node.contentType.parsed.params.method) {
|
|
attachment.method = node.contentType.parsed.params.method.toString().toUpperCase().trim();
|
|
}
|
|
const decodedText = node.getTextContent().replace(/\r?\n/g, "\n").replace(/\n*$/, "\n");
|
|
attachment.content = import_decode_strings.textEncoder.encode(decodedText);
|
|
break;
|
|
}
|
|
// Regular attachments
|
|
default:
|
|
attachment.content = node.content;
|
|
}
|
|
this.attachments.push(attachment);
|
|
}
|
|
} else if (node.contentType.multipart === "alternative") {
|
|
alternative = node;
|
|
} else if (node.contentType.multipart === "related") {
|
|
related = node;
|
|
}
|
|
for (let childNode of node.childNodes) {
|
|
await walk(childNode, alternative, related);
|
|
}
|
|
};
|
|
await walk(this.root, false, false);
|
|
textMap.forEach((mapEntry) => {
|
|
textTypes.forEach((textType) => {
|
|
if (!textContent[textType]) {
|
|
textContent[textType] = [];
|
|
}
|
|
if (mapEntry[textType]) {
|
|
mapEntry[textType].forEach((textEntry) => {
|
|
switch (textEntry.type) {
|
|
case "text":
|
|
textContent[textType].push(textEntry.value);
|
|
break;
|
|
case "subMessage":
|
|
{
|
|
switch (textType) {
|
|
case "html":
|
|
textContent[textType].push((0, import_text_format.formatHtmlHeader)(textEntry.value));
|
|
break;
|
|
case "plain":
|
|
textContent[textType].push((0, import_text_format.formatTextHeader)(textEntry.value));
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
} else {
|
|
let alternativeType;
|
|
switch (textType) {
|
|
case "html":
|
|
alternativeType = "plain";
|
|
break;
|
|
case "plain":
|
|
alternativeType = "html";
|
|
break;
|
|
}
|
|
(mapEntry[alternativeType] || []).forEach((textEntry) => {
|
|
switch (textEntry.type) {
|
|
case "text":
|
|
switch (textType) {
|
|
case "html":
|
|
textContent[textType].push((0, import_text_format.textToHtml)(textEntry.value));
|
|
break;
|
|
case "plain":
|
|
textContent[textType].push((0, import_text_format.htmlToText)(textEntry.value));
|
|
break;
|
|
}
|
|
break;
|
|
case "subMessage":
|
|
{
|
|
switch (textType) {
|
|
case "html":
|
|
textContent[textType].push((0, import_text_format.formatHtmlHeader)(textEntry.value));
|
|
break;
|
|
case "plain":
|
|
textContent[textType].push((0, import_text_format.formatTextHeader)(textEntry.value));
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
Object.keys(textContent).forEach((textType) => {
|
|
textContent[textType] = textContent[textType].join("\n");
|
|
});
|
|
this.textContent = textContent;
|
|
}
|
|
isInlineTextNode(node) {
|
|
var _a, _b, _c;
|
|
if (((_b = (_a = node.contentDisposition) == null ? void 0 : _a.parsed) == null ? void 0 : _b.value) === "attachment") {
|
|
return false;
|
|
}
|
|
switch ((_c = node.contentType.parsed) == null ? void 0 : _c.value) {
|
|
case "text/html":
|
|
case "text/plain":
|
|
return true;
|
|
case "text/calendar":
|
|
case "text/csv":
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
isInlineMessageRfc822(node) {
|
|
var _a, _b, _c;
|
|
if (((_a = node.contentType.parsed) == null ? void 0 : _a.value) !== "message/rfc822") {
|
|
return false;
|
|
}
|
|
let disposition = ((_c = (_b = node.contentDisposition) == null ? void 0 : _b.parsed) == null ? void 0 : _c.value) || (this.options.rfc822Attachments ? "attachment" : "inline");
|
|
return disposition === "inline";
|
|
}
|
|
// Check if this is a specially crafted report email where message/rfc822 content should not be inlined
|
|
forceRfc822Attachments() {
|
|
if (this.options.forceRfc822Attachments) {
|
|
return true;
|
|
}
|
|
let forceRfc822Attachments = false;
|
|
let walk = (node) => {
|
|
if (!node.contentType.multipart) {
|
|
if (node.contentType.parsed && ["message/delivery-status", "message/feedback-report"].includes(node.contentType.parsed.value)) {
|
|
forceRfc822Attachments = true;
|
|
}
|
|
}
|
|
for (let childNode of node.childNodes) {
|
|
walk(childNode);
|
|
}
|
|
};
|
|
walk(this.root);
|
|
return forceRfc822Attachments;
|
|
}
|
|
async resolveStream(stream) {
|
|
let chunkLen = 0;
|
|
let chunks = [];
|
|
const reader = stream.getReader();
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) {
|
|
break;
|
|
}
|
|
chunks.push(value);
|
|
chunkLen += value.length;
|
|
}
|
|
const result = new Uint8Array(chunkLen);
|
|
let chunkPointer = 0;
|
|
for (let chunk of chunks) {
|
|
result.set(chunk, chunkPointer);
|
|
chunkPointer += chunk.length;
|
|
}
|
|
return result;
|
|
}
|
|
async parse(buf) {
|
|
var _a, _b;
|
|
if (this.started) {
|
|
throw new Error("Can not reuse parser, create a new PostalMime object");
|
|
}
|
|
this.started = true;
|
|
if (buf && typeof buf.getReader === "function") {
|
|
buf = await this.resolveStream(buf);
|
|
}
|
|
buf = buf || new ArrayBuffer(0);
|
|
if (typeof buf === "string") {
|
|
buf = import_decode_strings.textEncoder.encode(buf);
|
|
}
|
|
if (buf instanceof Blob || Object.prototype.toString.call(buf) === "[object Blob]") {
|
|
buf = await (0, import_decode_strings.blobToArrayBuffer)(buf);
|
|
}
|
|
if (buf.buffer instanceof ArrayBuffer) {
|
|
buf = new Uint8Array(buf).buffer;
|
|
}
|
|
this.buf = buf;
|
|
this.av = new Uint8Array(buf);
|
|
this.readPos = 0;
|
|
while (this.readPos < this.av.length) {
|
|
const line = this.readLine();
|
|
await this.processLine(line.bytes, line.done);
|
|
}
|
|
await this.processNodeTree();
|
|
const message = {
|
|
headers: this.root.headers.map((entry) => ({ key: entry.key, originalKey: entry.originalKey, value: entry.value })).reverse()
|
|
};
|
|
for (const key of ["from", "sender"]) {
|
|
const addressHeader = this.root.headers.find((line) => line.key === key);
|
|
if (addressHeader && addressHeader.value) {
|
|
const addresses = (0, import_address_parser.default)(addressHeader.value);
|
|
if (addresses && addresses.length) {
|
|
message[key] = addresses[0];
|
|
}
|
|
}
|
|
}
|
|
for (const key of ["delivered-to", "return-path"]) {
|
|
const addressHeader = this.root.headers.find((line) => line.key === key);
|
|
if (addressHeader && addressHeader.value) {
|
|
const addresses = (0, import_address_parser.default)(addressHeader.value);
|
|
if (addresses && addresses.length && addresses[0].address) {
|
|
const camelKey = toCamelCase(key);
|
|
message[camelKey] = addresses[0].address;
|
|
}
|
|
}
|
|
}
|
|
for (const key of ["to", "cc", "bcc", "reply-to"]) {
|
|
const addressHeaders = this.root.headers.filter((line) => line.key === key);
|
|
let addresses = [];
|
|
addressHeaders.filter((entry) => entry && entry.value).map((entry) => (0, import_address_parser.default)(entry.value)).forEach((parsed) => addresses = addresses.concat(parsed || []));
|
|
if (addresses && addresses.length) {
|
|
const camelKey = toCamelCase(key);
|
|
message[camelKey] = addresses;
|
|
}
|
|
}
|
|
for (const key of ["subject", "message-id", "in-reply-to", "references"]) {
|
|
const header = this.root.headers.find((line) => line.key === key);
|
|
if (header && header.value) {
|
|
const camelKey = toCamelCase(key);
|
|
message[camelKey] = (0, import_decode_strings.decodeWords)(header.value);
|
|
}
|
|
}
|
|
let dateHeader = this.root.headers.find((line) => line.key === "date");
|
|
if (dateHeader) {
|
|
let date = new Date(dateHeader.value);
|
|
if (date.toString() === "Invalid Date") {
|
|
date = dateHeader.value;
|
|
} else {
|
|
date = date.toISOString();
|
|
}
|
|
message.date = date;
|
|
}
|
|
if ((_a = this.textContent) == null ? void 0 : _a.html) {
|
|
message.html = this.textContent.html;
|
|
}
|
|
if ((_b = this.textContent) == null ? void 0 : _b.plain) {
|
|
message.text = this.textContent.plain;
|
|
}
|
|
message.attachments = this.attachments;
|
|
message.headerLines = (this.root.rawHeaderLines || []).slice().reverse();
|
|
switch (this.attachmentEncoding) {
|
|
case "arraybuffer":
|
|
break;
|
|
case "base64":
|
|
for (let attachment of message.attachments || []) {
|
|
if (attachment == null ? void 0 : attachment.content) {
|
|
attachment.content = (0, import_base64_encoder.base64ArrayBuffer)(attachment.content);
|
|
attachment.encoding = "base64";
|
|
}
|
|
}
|
|
break;
|
|
case "utf8":
|
|
let attachmentDecoder = new TextDecoder("utf8");
|
|
for (let attachment of message.attachments || []) {
|
|
if (attachment == null ? void 0 : attachment.content) {
|
|
attachment.content = attachmentDecoder.decode(attachment.content);
|
|
attachment.encoding = "utf8";
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error("Unknown attachment encoding");
|
|
}
|
|
return message;
|
|
}
|
|
}
|
|
// Annotate the CommonJS export names for ESM import in node:
|
|
0 && (module.exports = {
|
|
addressParser,
|
|
decodeWords
|
|
});
|
|
|
|
// 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;
|
|
}
|
|
|