nodemailer, how to embed an image into an email

By

Learn how to embed an image in a nodemailer email by reading it with fs.readFileSync, encoding it as a base64 data URI, and dropping it into the HTML body.

~~~

To embed an image into an email sent with nodemailer, you can encode the image as a base64 data URI and put it straight into the HTML body, or attach it with a cid and reference it from an img tag. Let’s see both, starting from the data URI approach I used.

I had the need to send an image to an email I was sending with nodemailer.

I tried using an attachment but.. the image was added as attachment. It showed up in the attachments list of the email client, not inside the message where I wanted it.

So I embedded the image as base64 into the email body.

Encoding the image as base64

First I added some imports:

import fs from 'node:fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

We need to do that __filename and __dirname stuff because with ES modules (import syntax) __dirname is not available and we have to reference a file with fs.readFileSync() but that wants absolute paths, not relative ones.

Long story short, do that.

Now read the image:

const imageData = fs.readFileSync(__dirname + '/image.jpg', 'binary')

Transform that into a base64-encoded string:

const src = `data:image/jpg;base64,${Buffer.from(
  imageData,
  'binary'
).toString('base64')}`

A data URI packs the whole image into a string, so the email is self-contained. No external hosting, no separate file. The downside is size: base64 makes the data about 33% bigger, so this works best for small images like logos or charts.

By the way, you can skip the 'binary' round trip: calling fs.readFileSync() without an encoding returns a Buffer, and you can call .toString('base64') on it directly.

Finally you can add that to the email body:

const mailOptions = {
  //...
  html: `<img style="width:800px;" src="${src}">`,
}

When the image doesn’t show up

Here’s the catch with data URIs: some email clients refuse to render them. Gmail is the most notable one, it strips base64 images from the src attribute, and your recipients see a broken image.

If that hits you, use a CID attachment instead. You still attach the image, but you give it a content id, and reference that id in the HTML:

const mailOptions = {
  //...
  html: '<img style="width:800px;" src="cid:report-chart">',
  attachments: [
    {
      filename: 'image.jpg',
      path: __dirname + '/image.jpg',
      cid: 'report-chart',
    },
  ],
}

The image still travels as an attachment, but the cid: reference tells the email client to display it inline in the body instead of listing it as a downloadable file. This is what was missing from my first attachment attempt, and it’s the approach that works across all the major clients.

Tagged: Node.js · All topics
~~~

Related posts about node: