// Gmail · Apps Script

Send an email with an inline image in Gmail.

How to embed an image directly in a Gmail message body using Apps Script's htmlBody and inlineImages options, so the image renders inline instead of appearing as an attachment.

I want to send a Gmail message from Apps Script where an image appears inside the email body, not as a file attachment the recipient has to open separately.

The script

copy · paste · trigger
sendInlineImage.gs
Apps Script
// Send a Gmail message with an image rendered inline in the body.
// The cid key in htmlBody must match a key in inlineImages exactly.
function sendEmailWithInlineImage() {
  var file = DriveApp.getFileById('YOUR_FILE_ID_HERE');
  var blob = file.getBlob().setName('chart.png');

  var html = '<h2>Weekly report</h2>'
    + '<p>Here is the chart for this week:</p>'
    + '<img src="cid:weeklyChart" width="600" />'
    + '<p>Reply with any questions.</p>';

  GmailApp.sendEmail(
    '[email protected]',
    'Weekly report — chart inside',
    '',
    {
      htmlBody: html,
      inlineImages: { weeklyChart: blob }
    }
  );
}

Need a variant? Gnaw writes a custom version from one sentence — fields, triggers, edge cases handled.

Walkthrough

The cid contract: why the key names must match

The `src="cid:weeklyChart"` attribute in your HTML and the `weeklyChart` key inside the `inlineImages` object are two halves of the same contract. Apps Script resolves the `cid:` reference at send time by looking up the matching key in `inlineImages` and substituting the blob's binary content. Get the names out of sync by even one character and the image either disappears or falls through as a broken attachment.

The key can be any string without spaces. I keep these names short and descriptive — `weeklyChart`, `headerLogo`, `qrCode` — because you will type them twice and a mismatch is the most common failure mode here. The blob's filename (set with `.setName()`) is separate; it affects the MIME part's `Content-Disposition` header but not the cid lookup.

Pulling the blob from Drive (and from a chart)

The most common source is a Drive file: `DriveApp.getFileById(id).getBlob()`. You can get the file ID from the URL — it is the long alphanumeric string between `/d/` and `/edit`.

If the image is a Sheets chart, use `chart.getBlob()` directly after retrieving it via `SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getCharts()[0]`. The blob comes out as a PNG and is ready to pass into `inlineImages` without any conversion. One thing worth knowing: Drive blobs occasionally come back without a name, and some email clients reject nameless MIME parts, so calling `.setName('something.png')` is worth the one extra line.

If you are generating the image dynamically — a QR code, a sparkline, a map tile — you can construct a blob from raw bytes using `Utilities.newBlob(bytes, 'image/png', 'name.png')` and pass that in directly.

What happens if you use attachments instead

Passing the same blob under the `attachments` key rather than `inlineImages` changes everything the recipient sees. The image goes to the paperclip section, not the body. Your `<img src="cid:..." />` tag will render as a broken image placeholder or be stripped entirely, depending on the client.

The `attachments` and `inlineImages` options are not interchangeable — they map to different MIME multipart types (`mixed` versus `related`). Apps Script handles all the MIME assembly for you, but only if you put the blob in the right option. If you want both an inline image and a downloadable file in the same send, you can use both options simultaneously: `{ htmlBody: html, inlineImages: { logo: logoBlob }, attachments: [reportBlob] }`. The third positional argument to `sendEmail` is the plain-text fallback body; passing an empty string is fine.

Want a custom version?

Describe your sheet and the rule you want. Gnaw writes the Apps Script — fields, triggers, edge cases — in one shot.

FAQ

4 questions
The image is showing as an attachment instead of appearing in the body — what's wrong?
The blob is almost certainly in the `attachments` option instead of `inlineImages`, or the key name in `inlineImages` does not match the `cid:` name in your HTML. Check both. `attachments` and `inlineImages` use different MIME structures and are not interchangeable.
Can I inline an image that is not stored in Drive?
Yes. Any Apps Script `Blob` works. Use `Utilities.newBlob(byteArray, 'image/png', 'name.png')` for raw bytes, or call `.getBlob()` on a Charts `EmbeddedChart` object to inline a Sheets chart directly.
Do I need the Gmail advanced service, or does GmailApp.sendEmail handle this?
`GmailApp.sendEmail` handles it natively via the `inlineImages` option — no advanced service required. The advanced Gmail service is only necessary for operations the basic `GmailApp` class does not expose, like batch-modifying labels or drafting in a specific thread.
Will the inline image be stripped by the recipient's email client?
Most modern clients (Gmail, Outlook, Apple Mail) render cid-referenced inline images. A small number of aggressive spam filters or corporate mail gateways strip them. There is no workaround inside Apps Script for that; it is a receiver-side policy. For critical communications, include the image content as text below it as a fallback.
// one good script a week

Get a working Apps Script snippet in your inbox, weekly.