Skip to content
Snippets Groups Projects
mailparser.js 10 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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 });
    
      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;
      }
    
    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 },
    
        boundary: "mimemessage",
        leaf: false
    
    export function getHeaderValue(header, part) {
    
      if (part.headers && part.headers[header] && part.headers[header].length) {
        return part.headers[header];
    
    }
    
    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];
    
    Markin Igor's avatar
    Markin Igor committed
        if (contentDisposition.indexOf("attachment") >= 0 || contentDisposition.indexOf("inline") >= 0) {
    
    Markin Igor's avatar
    Markin Igor committed
    
    
          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].split(";")[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 };