diff --git a/javascript/src/helpers/mailparser.js b/javascript/src/helpers/mailparser.js
index a6da68a5fb223932afe77acc67fc3d7149711e4e..5e3d8adfd339968f9f158e1badb76114597db66c 100644
--- a/javascript/src/helpers/mailparser.js
+++ b/javascript/src/helpers/mailparser.js
@@ -61,7 +61,10 @@ function calculateParts(body, from, to, previousBondary) {
   let boundary = findFirstBoundary(body, from, to);
 
   if (boundary == null) {
-    return [{ indices: { from: from, to: to }, boundary: previousBondary, leaf: true }];
+    return [{ indices: { from,
+      to },
+    boundary: previousBondary,
+    leaf: true }];
   }
 
   const realBoundary = boundary;
@@ -74,12 +77,16 @@ function calculateParts(body, from, to, previousBondary) {
   for (let i = 0; i < boundaryIndicesLength; i++) {
     const startBoundary = boundaryIndices[i];
     const endBoundary = body.indexOf("\r\n", startBoundary);
-    boundaryPairs.push({ start: startBoundary, end: endBoundary });
+    boundaryPairs.push({ start: startBoundary,
+      end: endBoundary });
   }
 
   let bodies = [];
   if (previousBondary !== null) {
-    bodies.push({indices: {from: from, to: to}, boundary: previousBondary, leaf: false});
+    bodies.push({indices: {from,
+      to},
+    boundary: previousBondary,
+    leaf: false});
   }
 
   for (let i = 0; i < boundaryIndicesLength - 1; i++) {
@@ -122,9 +129,34 @@ 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;
-  let headersEnd = mime.indexOf("\r\n\r\n"); //the first empty line
+  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
@@ -135,9 +167,9 @@ export function parseMIME(mime) {
     mimeStart = headersEnd + 4;
   }
 
-  let headers = libmime.decodeHeaders(mime.substring(0, headersEnd));
+  const headers = libmime.decodeHeaders(mime.substring(0, headersEnd));
 
-  let indexOfSMIME = mimeBody.indexOf(SMIMEStart);
+  const indexOfSMIME = mimeBody.indexOf(SMIMEStart);
 
   if (indexOfSMIME >= 0) {
     mimeBody = mimeBody.substring(indexOfSMIME + SMIMEStart.length);
@@ -156,7 +188,9 @@ export function parseMIME(mime) {
   }
 
   parts.push({
-    indices: { from: 0, to: mime.length, headersEnd: headersEnd },
+    indices: { from: 0,
+      to: mime.length,
+      headersEnd },
     headers,
     boundary: "mimemessage",
     leaf: false
@@ -213,7 +247,7 @@ export function decodeMimeBody(descriptor, mimeString) {
   let contentType = getHeaderValue("content-type", descriptor);
   if (contentType) {
     contentType = contentType[0];
-    let parsedContentType = libmime.parseHeaderValue(contentType);
+    const parsedContentType = libmime.parseHeaderValue(contentType);
     if (
       parsedContentType &&
       parsedContentType.params &&
@@ -230,10 +264,10 @@ export function decodeMimeBody(descriptor, mimeString) {
   }
 
   if (contentTransferEncoding.toLowerCase() === "quoted-printable") {
-    let buff = libqp.decode(mimeBody);
+    const buff = libqp.decode(mimeBody);
     return buff.toString(charset);
   } else if (contentTransferEncoding.toLowerCase() === "base64") {
-    let buff = Buffer.from(mimeBody, "base64");
+    const buff = Buffer.from(mimeBody, "base64");
     return buff.toString(charset);
   }
 
@@ -351,5 +385,6 @@ export function getAttachment(mime, part) {
     base64 = window.btoa(body);
   }
 
-  return { contentType, base64 };
+  return { contentType,
+    base64 };
 }
diff --git a/javascript/src/iframe/viamapi-iframe.js b/javascript/src/iframe/viamapi-iframe.js
index 1df7b53b1e79009ecb58efda7ad7f7f4ca658f41..370ce07850a582ae24d93a45c646a8e8a512fdb1 100644
--- a/javascript/src/iframe/viamapi-iframe.js
+++ b/javascript/src/iframe/viamapi-iframe.js
@@ -7,6 +7,7 @@ import {
   base64ToByteArray,
   byteArrayToBase64
 } from "../utilities/stringUtilities";
+import { extractMessageID } from "../helpers/mailparser";
 
 const QRCode = require("qrcode");
 const Penpal = require("penpal").default;
@@ -21,6 +22,7 @@ import {
 import { LOGIN_MODES } from "../constants/authentication";
 import {
   CertificateData,
+  ImageData,
   createOneTimePassportCertificate,
   createPassportCertificate,
   decryptMessage,
@@ -1403,6 +1405,148 @@ const connection = Penpal.connectToParent({
 
       return encodeResponse("200", "", "Document signed");
     },
+    // passportUUID - passport to sign the vCard
+    // text, html - the text and html part of the email
+    // related, attachments - array of objects, containing hashes of images/objects, related to the html in format:
+    // {
+    //   headers: {
+    //     "Content-Type": "application/hash; algorithm=SHA-256",
+    //     "Original-Content-Type": "image/jpeg", //original content type
+    //     "Content-Disposition": "inline" or "attachment",
+    //     ... //other headers
+    //   },
+    //   body: "base64 encoded hash"
+    // }
+    signVCard: async (passportUUID, text, html, parts = null) => {
+      const authenticationPublicKey = localStorage.getItem(
+        "authenticatedIdentity"
+      );
+
+      if (
+        !authenticationPublicKey ||
+        !window.loadedIdentities[authenticationPublicKey] ||
+        !extendPinCodeTtl(authenticationPublicKey)
+      ) {
+        return encodeResponse("400", "", "Identity not authenticated");
+      }
+
+      let vCardImageData;
+      let vCardImageClaimValue;
+
+      const vCardImageClaimName = "vCardImage";
+      const defaultTagName = "notag";
+
+      const vCardClaimResponse = await executeRestfulFunction(
+        "private",
+        window.viamApi,
+        window.viamApi.entityGetClaim,
+        null,
+        vCardImageClaimName,
+        defaultTagName,
+        passportUUID
+      );
+      // if (vCardClaimResponse.code !== "200") {
+      //   return encodeResponse("400", "", vCardClaimResponse.status);
+      // }
+
+      if (vCardClaimResponse.code === "200") {
+        vCardImageClaimValue = vCardClaimResponse.data;
+      }
+
+      if (vCardImageClaimValue && "state" in vCardImageClaimValue && vCardImageClaimValue.state === "disabled") {
+        vCardImageData = new ImageData({
+          contentType: "image/png",
+          contentBase64: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" //1x1px transparent pixel
+        });
+      } else {
+        const vCardImageResponse = await executeRestfulFunction(
+          "private",
+          window.viamApi,
+          window.viamApi.passportGetVCardImage,
+          null,
+          passportUUID
+        );
+        if (vCardImageResponse.code !== "200") {
+          return encodeResponse("400", "", vCardImageResponse.status);
+        }
+        vCardImageData = new ImageData(vCardImageResponse.data);
+        if (vCardImageData.contentType !== "image/png") {
+          return encodeResponse("400", "", "Content type of vCard mmust be 'image/png'");
+        }
+      }
+
+      if (!parts) {
+        parts = [];
+      }
+
+      if (html) {
+        const htmlPart = {
+          headers: {
+            "Content-Type": "text/html"
+          },
+          body: stringToUtf8Base64(html)
+        };
+        parts.unshift(htmlPart);
+      }
+
+      if (text) {
+        const textPart = {
+          headers: {
+            "Content-Type": "text/plain"
+          },
+          body: stringToUtf8Base64(text)
+        };
+        parts.unshift(textPart);
+      }
+
+      const certResponse = await getCertificateForPassport(passportUUID, true);
+
+      if (certResponse.code !== "200") {
+        return encodeResponse("400", "", certResponse.status);
+      }
+
+      const {
+        x509Certificate: passportCertificate,
+        privateKey: passportPrivateKey,
+        chain: passportChain
+      } = certResponse.data;
+
+      const keys = await createOneTimePassportCertificate(
+        makeid() + "-" + passportUUID,
+        null,
+        passportPrivateKey,
+        passportCertificate
+      );
+
+      const {
+        privateKeyPEM: privateKeyOneTime,
+        certificatePEM: certificateOneTime
+      } = keys;
+
+      passportChain.reverse();
+
+      passportChain.push(passportCertificate);
+      passportChain.push(certificateOneTime);
+
+      passportChain.reverse();
+
+      const signVCardResponse = await executeRestfulFunction(
+        "private",
+        window.viamApi,
+        window.viamApi.signSignVCardForChain,
+        null,
+        vCardImageData,
+        privateKeyOneTime,
+        passportChain,
+        parts
+      );
+      if (signVCardResponse.code !== "200") {
+        return encodeResponse("400", "", signVCardResponse.status);
+      }
+
+      const signedVCardImageData = new ImageData(signVCardResponse.data);
+      return encodeResponse("200", signedVCardImageData, "vCard signed");
+    },
     documentCreateDocument: async (passportUUID, path, contentType, title) => {
       const authenticationPublicKey = localStorage.getItem(
         "authenticatedIdentity"
@@ -1597,6 +1741,11 @@ const connection = Penpal.connectToParent({
         "Currently authenticated identity"
       );
     },
+    extractMessageID(mime) {
+      return new Penpal.Promise(result => {
+        result(extractMessageID(mime));
+      });
+    },
     stringToUtf8ByteArray(str) {
       return new Penpal.Promise(result => {
         result(stringToUtf8ByteArray(str));
diff --git a/javascript/src/utilities/signingUtilities.js b/javascript/src/utilities/signingUtilities.js
index 3fad4cbc4c74e05a39c3eb93f39a5c16ec5d406e..00d08544476fa4765696064f42de291a01811ad2 100644
--- a/javascript/src/utilities/signingUtilities.js
+++ b/javascript/src/utilities/signingUtilities.js
@@ -26,7 +26,14 @@ import {
   getFilenameFromHeaders,
   SIGNATURE_CONTENT_TYPE
 } from "./emailUtilities";
-
+import {
+  stringToUtf8ByteArray,
+  utf8ByteArrayToString,
+  stringToUtf8Base64,
+  utf8Base64ToString,
+  base64ToByteArray,
+  byteArrayToBase64
+} from "../utilities/stringUtilities";
 const libmime = require("libmime");
 const pkijs = require("pkijs");
 const asn1js = require("asn1js");
@@ -428,6 +435,11 @@ function createCertificate(certData, issuerData = null) {
       const serialNumberView = new Uint8Array(serialNumberBuffer);
       pkijs.getRandomValues(serialNumberView);
       serialNumberView[0] &= 0x7f;
+      while (serialNumberView[0] === 0 && (serialNumberView[1] & 0x80) === 0) {
+        const firstBytesView = new Uint8Array(serialNumberBuffer, 0, 2);
+        pkijs.getRandomValues(firstBytesView);
+        firstBytesView[0] &= 0x7f;
+      }
       // noinspection JSUnresolvedFunction
       certificate.serialNumber = new asn1js.Integer({
         valueHex: serialNumberView
@@ -923,7 +935,7 @@ export function createOneTimePassportCertificate(
   certicateIssuerArg
 ) {
   let certData = null;
-  if (emailArg != null && emailArg == "") {
+  if (emailArg !== null && emailArg === "") {
     emailArg = null;
   }
 
@@ -1446,5 +1458,75 @@ export const verifySMIME = (smimeString, rootCaPem) => {
   });
 };
 
+export class ImageData {
+
+  /**
+   * Constructor for ImageData class
+   * @param {Object} [parameters] Object in format
+   * {
+   *   contentType: String,
+   *   content: String -- base64 encoded
+   * }
+   */
+  constructor(parameters = {}) {
+    this.contentType = null; //string: the content type
+    this.content = null; // Uint8Array: decoded content
+    this.contentBase64 = null; // string: base64 encoded content
+
+    if (typeof parameters === "object") {
+      this.fromParameters(parameters);
+    }
+  }
+
+  fromParameters(parameters) {
+    if ("contentType" in parameters) {
+      this.contentType = parameters.contentType;
+    }
+
+    if ("content" in parameters) {
+      this.content = parameters.content;
+    }
+
+    if ("contentBase64" in parameters) {
+      this.contentBase64 = parameters.contentBase64;
+    }
+
+    this.getContent();
+    this.getContentBase64();
+  }
+
+  //fromDataURL()
+  //fromContentTypeAndContentAsByteArray()
+
+  toDataURL() {
+    return "data:" + this.contentType + ";base64," + this.getContentBase64();
+  }
+
+  getContent() {
+    if (!this.content) {
+      if (this.contentBase64) {
+        this.content = base64ToByteArray(this.contentBase64);
+      }
+    }
+    return this.content;
+  }
+
+  getContentBase64() {
+    if (!this.contentBase64) {
+      if (this.content) {
+        this.contentBase64 = byteArrayToBase64(this.content);
+      }
+    }
+    return this.contentBase64;
+  }
+
+  toJSON() {
+    return {
+      "contentType": this.contentType,
+      "contentBase64": this.getContentBase64()
+    };
+  }
+}
+
 //Initialization block
 fixPkijsRDN();