nodemailer, how to embed an image into an email
By Flavio Copes
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.
Related posts about node: