Skip to content
Snippets Groups Projects
emailUtilities.js 6.85 KiB
Newer Older
  • Learn to ignore specific revisions
  • import dataUriToBlob from "data-uri-to-blob";
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    import libmime from "libmime";
    import union from "lodash/union";
    
    
    import {
      fixNewLines,
      parseMIME,
      getHTML,
      getPlain,
      getAttachments,
      getAttachment,
      getGlobalHeaderValue
    } from "../helpers/mailparser";
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    import {
      getCertificateChain,
      parseSignedData
    } from "./signingUtilities";
    
    import {
      byteArrayToBase64,
    
      stringToUtf8Base64,
      stringToUtf8ByteArray
    
    } from "./stringUtilities";
    
    import {getCrypto} from "pkijs";
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    export const SIGNATURE_CONTENT_TYPE = "application/pkcs7-signature";
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    export const DEFAULT_ATTACHMENT_NAME = "attachment";
    
    const splitParticipants = participantsList => {
    
      if (!participantsList) {
        return [];
      }
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      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
            );
    
    
    Damyan Mitev's avatar
    Damyan Mitev committed
            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
            });
          }
    
    
    Damyan Mitev's avatar
    Damyan Mitev committed
          let certificateChain;
          if (signatureBase64) {
    
            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));
    
    
            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 => {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const headersToSearch = ["content-type", "content-disposition"];
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      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]) {
    
      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);
    };
    
    
    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
      };
    
      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++;
            }
          }
        }
    
          await hashBody(part);
    
          if (typeof part.body === "string") {
    
            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
    
          const subcount = await prepareVCardParts(part.parts);
    
          count.textParts += subcount.textParts;
          count.htmlParts += subcount.htmlParts;
    
      return count;
    }