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, to }, boundary: previousBondary, leaf: true }]; } 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 = []; if (previousBondary !== null) { bodies.push({indices: {from, to}, boundary: previousBondary, leaf: false}); } 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 extractMessageID(mime) { if (mime.startsWith("\r\n")) { mime = mime.substring(2); //should not happen } const headersEndIndex = mime.indexOf("\r\n\r\n"); //the first empty line if (headersEndIndex < 0) { return null; } const mimeHeaders = mime.substring(0, headersEndIndex); const headers = libmime.decodeHeaders(mimeHeaders); let messageId = headers["message-id"]; if (Array.isArray(messageId) && messageId.length > 0) { messageId = messageId[0]; } if (messageId && typeof messageId === "string") { messageId = messageId.replace(/^</, '').replace(/>$/, ''); return messageId; } return null; } export function parseMIME(mime) { let mimeStart = 0; const 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; } const headers = libmime.decodeHeaders(mime.substring(0, headersEnd)); const 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 }, headers, boundary: "mimemessage", leaf: false }); return parts; } export 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]; const 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") { const buff = libqp.decode(mimeBody); return buff.toString(charset); } else if (contentTransferEncoding.toLowerCase() === "base64") { const 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("attachment") >= 0 || contentDisposition.indexOf("inline") >= 0) { let contentId = getHeaderValue("content-id", parts[i]); console.log("CID", contentId); 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; } console.log("CID2", contentId); contentType = contentType[0]; console.log("ContentTYpe", contentType); const normalizedBody = getBody(mime, parts[i]).replace(newline, ""); const src = "data:" + contentType + ";base64, " + normalizedBody; console.log("SRC", src); 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 }; }