node.jsnodemailerinline-images

Move inline images to attachments with nodemailer


I have a node application successfully sending emails using nodemailer. I am sending emails with code similar to this:

const emailRequest = {
  from: { name: "Sender name", address: "sender@example.com" },
  to: [/* recipients */],
  subject: "Email subject",
  text: "Some email text",
  html: '<p>Some email text</p><p><img src="data:image/png;base64,{base64_image_content}">',
} as SendMailOptions;

const ses = new SES();
const transporter = nodemailer.createTransport({
  SES: ses,
  sendingRate: MAX_SEND_RATE,
});

transporter.sendMail(emailRequest, sendMailResponseHandler);

The problem is that sometimes the email body contains inline images formatted like this, where {base64_image_content} is the actual data containing the image:

<img src="data:image/png;base64,{base64_image_content}">

and Gmail does not support inline image data. The email sends and the image data shows up when I choose "Show original" in Gmail, but the image does not appear in the Gmail web client and it appears as a broken image in the Gmail mobile client.

I don't have control over the inline images, so I'm hoping to find a way to extract the inline image data and add it as an attachment to the email, and use a cid to refer to those attachments in the email body before I call transporter.sendMail.

I want to change the HTML from this:

<p>Some email text</p><p><img src="data:image/png;base64,{base64_image_content}">

to this:

<p>Some email text</p><p><img src="cid:myImageCid">

and add the image as an attachment:

const emailRequest = {
  // other things
  attachments: [{ cid: "myImageCid", content: base64_image_content }]
} as SendMailOptions;

The part I need help with is extracting the image data from the original email HTML and replacing it with a cid reference. Is there a way I can do that?


Solution

  • I ended up following the advise of akisoft by extracting the image data myself. Here's how I did it:

    extractImages = (html: string) => {
      // Match all image tags with data in their src
      const regex = /<img src="data:.+?">/gs;
      const images = html.match(regex) ?? [];
      const attachments = images.map((originalImageTag) => {
        // Parse out the image data and meta info
        const imageSrc = (originalImageTag.match(/<img src="data:(.+?)">/s) ?? [])[1];
        const [imageMeta, content] = imageSrc.split(",");
        const [imageType, encoding] = imageMeta.split(";");
        const fileType = imageType.split("/")[1];
        const filename = `image.${fileType}`;
        const cid = uuid();
        const newImageTag = `<img src="cid:${cid}">`;
    
        // Replace the image tags with ones that reference attached content
        html = html.replace(originalImageTag, newImageTag);
    
        // Collect attachment data to be passed to nodemailer
        return {
          filename,
          cid,
          encoding,
          content,
          contentDisposition: "inline",
        } as Attachment;
      });
    
      // Return the modified HTML and the attachments
      return { html, attachments };
    };
    

    I pass the HTML of the original email into that function and it returns modified HTML and a list of attachments I can pass directly to nodemailer.