Newer
Older
import dataUriToBlob from "data-uri-to-blob";
import libmime from "libmime";
import union from "lodash/union";
import {
fixNewLines,
parseMIME,
getHTML,
getPlain,
getAttachments,
getAttachment,
getGlobalHeaderValue
} from "../helpers/mailparser";
import {
getCertificateChain,
parseSignedData
} from "./signingUtilities";
import {
byteArrayToBase64,
stringToUtf8Base64,
stringToUtf8ByteArray
import {getCrypto} from "pkijs";
export const SIGNATURE_CONTENT_TYPE = "application/pkcs7-signature";
const splitParticipants = participantsList => {
if (!participantsList) {
return [];
}
const participants = participantsList.map(participants =>
participants.split(",").map(p => p.trim())
);
return union.apply(null, participants);
};
export const parseSMIME = smimeString => {
return new Promise(resolve => {
setTimeout(async () => {
const emailString = fixNewLines(smimeString);
const parts = parseMIME(emailString);
const html = getHTML(emailString, parts);
const plain = getPlain(emailString, parts);
const rawAttachments = getAttachments(emailString, parts);
const attachments = [];
let signatureBase64;
for (const rawAttachment of rawAttachments) {
const { contentType, base64 } = getAttachment(
emailString,
rawAttachment
);
if (contentType.startsWith(SIGNATURE_CONTENT_TYPE)) {
signatureBase64 = base64;
}
const dataURI = "data:" + contentType + ";base64, " + base64;
const blob = dataUriToBlob(dataURI);
const filename = getFilenameFromHeaders(rawAttachment.headers);
attachments.push({
blob,
filename
});
}
const signedData = parseSignedData(signatureBase64);
if (signedData) {
//TODO revise and use signedData's generation of cert chain (per signer)
certificateChain = getCertificateChain(signedData);
}
const from = splitParticipants(getGlobalHeaderValue("from", parts));
const to = splitParticipants(getGlobalHeaderValue("to", parts));
const cc = splitParticipants(getGlobalHeaderValue("cc", parts));
from,
to,
cc,
subject: getGlobalHeaderValue("subject", parts).join(" "),
html: extractHtmlBodyFromString(html),
plain,
attachments,
certificateChain
};
resolve(message);
}, 50);
});
};
/**
* Function extracts file name from content type header
* @param headers
* @returns {string} ('file.txt')
*/
export const getFilenameFromHeaders = headers => {
const headersToSearch = ["content-type", "content-disposition"];
const filename =
headers &&
Object.keys(headers)
.filter(key => headersToSearch.includes(key))
.reduce((result, key) => {
const headerValue = libmime.parseHeaderValue(headers[key][0]);
return result || headerValue.params.name || headerValue.params.filename;
}, "");
return filename || DEFAULT_ATTACHMENT_NAME;
};
/**
* Function extracts all tags within <body></body> from provided string
* and removes whitespaces between tags and HTML comments.
* @param string
* @returns {*}
*/
export const extractHtmlBodyFromString = string => {
const extractBodyRegex = /<body.*?>([\s\S]+)<\/body>/gm;
const bodyMatch = extractBodyRegex.exec(string);
if (bodyMatch && bodyMatch[1]) {
body = bodyMatch[1];
return body
.replace(/>\s+</gm, "><")
.replace(/<!--[\s\S]*?-->/gm, "")
.trim();
const capitalizeHeaderName = str => {
if (!str || typeof str !== 'string') { return; }
const strChunks = splitBy(str,'-');
return strChunks.map(capitalizeFirstLetter).join('-');
};
const splitBy = (string,separator) =>{
if (typeof string !== 'string') { return; }
return string.split(separator);
};
const capitalizeFirstLetter = (s) => {
if (typeof s !== 'string') { return; }
return s.charAt(0).toUpperCase() + s.slice(1);
};
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
async function sha256(array) {
const cryptoLib = getCrypto();
const digestTmpBuf = await cryptoLib.digest({ name: "SHA-256" }, array);
const digestTmpArray = new Uint8Array(digestTmpBuf);
return digestTmpArray;
}
async function hashBody(part) {
const contentType = part.headers["Content-Type"];
const origContentType = part.headers["Original-Content-Type"];
if (!origContentType &&
!part.headers["Content-Type"].startsWith("application/hash") &&
!part.headers["Content-Type"].startsWith("text/plain") &&
!part.headers["Content-Type"].startsWith("text/html")) {
if (part.body) {
if (typeof part.body === "string") {
part.body = stringToUtf8ByteArray(part.body);
}
if (part.body instanceof ArrayBuffer) {
part.body = byteArrayToBase64(new Uint8Array(part.body));
}
if (!(part.body instanceof Uint8Array)) {
throw new Error('part body is neither string, nor Uint8Array, nor ArrayBuffer'); // should not happen
}
if (contentType) {
part.headers["Original-Content-Type"] = contentType;
}
part.headers["Content-Type"] = "application/hash; algorithm=SHA-256";
part.body = await sha256(part.body);
}
}
export async function prepareVCardParts(parts) {
const count = {
textParts: 0,
htmlParts: 0
};
if (!parts) {
return count;
}
for (const part of parts) {
if (!part.headers) {
part.headers = {
"Content-Type": "application/octet-stream"
};
} else {
const capitalizedHeaders = {};
for (const key of Object.keys(part.headers)) {
capitalizedHeaders[capitalizeHeaderName(key)] = part.headers[key];
}
part.headers = capitalizedHeaders;
if (!part.headers["Content-Type"]) {
part.headers["Content-Type"] = "application/octet-stream";
} else {
if (part.headers["Content-Type"].startsWith("text/plain")) {
count.textParts++;
} else if (part.headers["Content-Type"].startsWith("text/html")) {
count.htmlParts++;
}
}
}
part.body = stringToUtf8Base64(part.body);
} else
if (part.body instanceof Uint8Array) {
part.body = byteArrayToBase64(part.body);
} else
if (part.body instanceof ArrayBuffer) {
part.body = byteArrayToBase64(new Uint8Array(part.body));
} else {
throw new Error('part body is neither string, nor Uint8Array, nor ArrayBuffer'); // should not happen
}
}
if (part.parts) {
const subcount = await prepareVCardParts(part.parts);
count.textParts += subcount.textParts;
count.htmlParts += subcount.htmlParts;