-
Alexey Lunin authoredAlexey Lunin authored
mailparser.js 9.14 KiB
import libmime from "libmime";
import libqp from "libqp";
const newline = /\r\n|\r|\n/g;
const SMIMEStart =
"This is a cryptographically signed message in MIME format.\r\n\r\n";
function findAllOccurences(text, subject, from, to) {
const result = [];
let index = text.indexOf(subject, from);
if (index < 0 || index > to) {
return result;
}
result.push(index + 2);
while (true) {
index = text.indexOf(subject, index + 1);
if (index < 0 || index > to) {
break;
} else {
result.push(index + 2);
}
}
return result;
}
function findFirstBoundary(body, from, to) {
if (from >= to) {
return null;
}
const search = "\r\n--";
let start = body.indexOf(search, from);
if (start < 0 || start > to) {
return null;
}
start += 2;
const end = body.indexOf("\r\n", start);
if (end < 0 || end > to) {
return null;
}
const boundary = body.substring(start, end);
const boundaryEnd = boundary + "--\r\n";
const startBoundaryEnd = body.indexOf(boundaryEnd, end);
if (startBoundaryEnd < 0 || startBoundaryEnd > to) {
return findFirstBoundary(body, end + 2, to);
}
return boundary;
}
function calculateParts(body, from, to, previousBondary) {
let boundary = findFirstBoundary(body, from, to);
if (boundary == null) {
return [{ indices: { from: from, to: to }, boundary: previousBondary }];
}
const realBoundary = boundary;
boundary = "\r\n" + boundary;
const boundaryPairs = [];
const boundaryIndices = findAllOccurences(body, boundary, from, to);
const boundaryIndicesLength = boundaryIndices.length;
for (let i = 0; i < boundaryIndicesLength; i++) {
const startBoundary = boundaryIndices[i];
const endBoundary = body.indexOf("\r\n", startBoundary);
boundaryPairs.push({ start: startBoundary, end: endBoundary });
}
let bodies = [];
for (let i = 0; i < boundaryIndicesLength - 1; i++) {
const firstPair = boundaryPairs[i];
const secondPair = boundaryPairs[i + 1];
const newFrom = firstPair.end + 2;
const newTo = secondPair.start - 2;
const bodyForBoundary = calculateParts(body, newFrom, newTo, realBoundary);
bodies = bodies.concat(bodyForBoundary);
}
return bodies;
}
function parsePartsHeaders(mimeBody, parts) {
const result = [];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const indices = part["indices"];
let headersEnd = mimeBody
.substring(indices.from, indices.to)
.indexOf("\r\n\r\n");
if (headersEnd < 0) {
headersEnd = indices.from;
} else {
headersEnd = headersEnd + indices.from + 4;
}
part["indices"].headersEnd = headersEnd;
part["headers"] = libmime.decodeHeaders(
mimeBody.substring(indices.from, headersEnd)
);
result.push(part);
}
return result;
}
export function fixNewLines(mime) {
return mime.replace(newline, "\r\n");
}
export function parseMIME(mime) {
let mimeStart = 0;
let headersEnd = mime.indexOf("\r\n\r\n"); //the first empty line
let mimeBody = "";
if (headersEnd < 0 && mime.startsWith("\r\n")) {
mime = mime.substring(2); //should not happen
mimeBody = mime;
mimeStart = 0;
} else {
mimeBody = mime.substring(headersEnd + 4);
mimeStart = headersEnd + 4;
}
let headers = libmime.decodeHeaders(mime.substring(0, headersEnd));
let indexOfSMIME = mimeBody.indexOf(SMIMEStart);
if (indexOfSMIME >= 0) {
mimeBody = mimeBody.substring(indexOfSMIME + SMIMEStart.length);
mimeStart += indexOfSMIME + SMIMEStart.length;
}
mimeBody = "\r\n" + mimeBody + "\r\n";
const mimeBodyLen = mimeBody.length - 1;
let parts = calculateParts(mimeBody, 0, mimeBodyLen, null);
parts = parsePartsHeaders(mimeBody, parts);
for (let i = 0; i < parts.length; i++) {
parts[i].indices.from = parts[i].indices.from + (mimeStart - 2);
parts[i].indices.headersEnd = parts[i].indices.headersEnd + (mimeStart - 2);
parts[i].indices.to = parts[i].indices.to + (mimeStart - 2);
}
parts.push({
indices: { from: 0, to: mime.length, headersEnd: headersEnd },
headers,
boundary: "mimemessage"
});
return parts;
}
function getHeaderValue(header, part) {
if (part.headers && part.headers[header] && part.headers[header].length) {
return part.headers[header];
}
return null;
}
export function getGlobalHeaderValue(header, parts) {
for (let i = 0; i < parts.length; i++) {
if (parts[i].boundary === "mimemessage") {
return getHeaderValue(header, parts[i]);
}
}
return null;
}
function getBody(mime, part) {
if (part.indices === null || part.indices === undefined) {
return null;
}
const indices = part.indices;
return mime.substring(indices.headersEnd, indices.to);
}
export function decodeMimeBody(descriptor, mimeString) {
const mimeBody = mimeString.slice(
descriptor.indices.headersEnd,
descriptor.indices.to
);
let contentTransferEncoding = getHeaderValue(
"content-transfer-encoding",
descriptor
);
if (contentTransferEncoding) {
contentTransferEncoding = contentTransferEncoding[0];
} else {
return mimeBody;
}
let charset = "utf8";
let contentType = getHeaderValue("content-type", descriptor);
if (contentType) {
contentType = contentType[0];
let parsedContentType = libmime.parseHeaderValue(contentType);
if (
parsedContentType &&
parsedContentType.params &&
parsedContentType.params.charset
) {
if (parsedContentType.params.charset.toLowerCase() === "us-ascii") {
charset = "ascii";
} else if (Buffer.isEncoding(parsedContentType.params.charset)) {
charset = parsedContentType.params.charset;
} else {
//TODO log the charset and make sure we can support it
}
}
}
if (contentTransferEncoding.toLowerCase() === "quoted-printable") {
let buff = libqp.decode(mimeBody);
return buff.toString(charset);
} else if (contentTransferEncoding.toLowerCase() === "base64") {
let buff = Buffer.from(mimeBody, "base64");
return buff.toString(charset);
}
return mimeBody;
}
export function getHTML(mime, parts) {
let html;
let htmlPart = null;
for (let i = 0; i < parts.length; i++) {
if (parts[i].boundary === "mimemessage") {
continue;
}
let contentType = getHeaderValue("content-type", parts[i]);
if (contentType === null || contentType === undefined) {
continue;
}
contentType = contentType[0];
if (contentType.indexOf("text/html") >= 0) {
htmlPart = parts[i];
break;
}
}
html = decodeMimeBody(htmlPart, mime);
for (let i = 0; i < parts.length; i++) {
let contentDisposition = getHeaderValue("content-disposition", parts[i]);
if (contentDisposition === null || contentDisposition === undefined) {
continue;
}
contentDisposition = contentDisposition[0];
if (contentDisposition.indexOf("inline") >= 0) {
let contentId = getHeaderValue("content-id", parts[i]);
if (contentId === null || contentId === undefined) {
continue;
}
contentId = contentId[0];
const contentIdLen = contentId.length;
contentId = contentId.substring(1, contentIdLen - 1);
contentId = "cid:" + contentId;
let contentType = getHeaderValue("content-type", parts[i]);
if (contentType === null || contentType === undefined) {
continue;
}
contentType = contentType[0];
const normalizedBody = getBody(mime, parts[i]).replace(newline, "");
const src = "data:" + contentType + ";base64, " + normalizedBody;
html = html.split(contentId).join(src);
}
}
return html;
}
export function getPlain(mime, parts) {
let plain;
let plainPart = null;
for (let i = 0; i < parts.length; i++) {
let contentType = getHeaderValue("content-type", parts[i]);
if (contentType === null || contentType === undefined) {
continue;
}
contentType = contentType[0];
if (contentType.indexOf("text/plain") >= 0) {
plainPart = parts[i];
break;
}
}
if (!plainPart) {
return "";
}
plain = decodeMimeBody(plainPart, mime);
return plain;
}
export function getAttachments(mime, parts) {
const attachments = [];
for (let i = 0; i < parts.length; i++) {
let contentDisposition = getHeaderValue("content-disposition", parts[i]);
if (contentDisposition === null || contentDisposition === undefined) {
continue;
}
contentDisposition = contentDisposition[0];
if (contentDisposition.indexOf("attachment") >= 0) {
attachments.push(parts[i]);
}
}
return attachments;
}
export function getAttachment(mime, part) {
let contentType = getHeaderValue("content-type", part);
if (contentType === null || contentType === undefined) {
return null;
}
contentType = contentType[0];
let contentTransferEncoding = getHeaderValue(
"content-transfer-encoding",
part
);
if (contentTransferEncoding) {
contentTransferEncoding = contentTransferEncoding[0];
} else {
return null;
}
let base64;
if (contentTransferEncoding.toLowerCase() === "base64") {
base64 = getBody(mime, part).replace(newline, "");
} else {
const body = decodeMimeBody(part, mime);
base64 = window.btoa(body);
}
return { contentType, base64 }
}