Image order

Everything you ever wanted to know about Gmail draft inline images and Google Apps Script but were afraid to ask

A common workflow solution in Google Apps Script is to use a Gmail draft message as a template for sending emails. For the user the benefit is they can draft a message in a familiar environment adding formatting without having to worry about writing HTML. This is the model I chose to use in the ‘Create a mail merge using Gmail and Google Sheets’ example in the Google Workspace Solutions Gallery.

Problem

So here’s the problem – using the draft as a template generally works well but when you call GmailApp.sendEmail() or MailApp.sendEmail() you are not sending the actual draft message, instead you are constructing a new email setting parameters such as subject, htmlBody and attachments.

Anatomy of Gmail images

The tricky bit is when you want to include images as the user can add these to the message in different ways: inserting from Google Photos; an upload; web address (url); or copy/paste:

Gmail image insert options
Gmail image insert options

When an image is inserted by URL or copy/pasted from a publicly accessible source, such as a Google Doc that has been published to the web, Gmail will keep the reference to the image source but proxy it via one of its public content servers (googleusercontent.com). For example, if I insert an image by web address for a ‘chubby cat’ on Wikimedia Commons with the url:

https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Chubby_Cat.jpg/320px-Chubby_Cat.jpg

In the sent email it is referenced as:

<img src="https://ci5.googleusercontent.com/proxy/LoR..._HUA=s0-d-e1-ft#https://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Chubby_Cat.jpg/320px-Chubby_Cat.jpg" width="320" height="213" class="CToWUd a6T" tabindex="0">

If instead images are uploaded or copy/pasted from a private source to the Gmail draft when .sendEmail() is used an inlineImages object is required to handle these inline images. Below is the excerpt from the GmailApp.sendMail() documentation:

NameTypeDescription

htmlBody

String

if set, devices capable of rendering HTML will use it instead of the required body argument; you can add an optional inlineImages field in HTML body if you have inlined images for your email

inlineImages

Object

a JavaScript object containing a mapping from image key (String) to image data (BlobSource); this assumes that the htmlBody parameter is used and contains references to these images in the format img src="cid:imageKey"

The image Content-ID (cid)

In the case of the ‘chubby cat’ image inserted from Google Photos the Gmail draft htmlBody already includes a cid references:

<img data-surl="cid:ii_kl736chs6" src="cid:ii_kl736chs6" alt="320px-Chubby_Cat.jpg" width="320" height="213">

So if we are copying the htmlBody of the draft to use in .sendMail() our inlineImages object would look like this, where BlobSource is the appropriate image data:

GmailApp.sendEmail({
    to: '[email protected]',
    subject: 'Chubby Cat',
    htmlBody: 'inline chubby cat <img data-surl="cid:ii_kl736chs6" src="cid:ii_kl736chs6" alt="320px-Chubby_Cat.jpg" width="320" height="213"> images!',
    inlineImages:
      {
        ii_kl736chs6: BlobSource
      }
  });

When we make our request for the Gmail draft we can also get the inline attachments using the .getAttachments(options) method. This returns an array of image data objects as BlobSource:

GmailMessage.getAttachments({includeInlineImages: true, includeAttachments: false})

The cid kicker

Now here’s the kicker GmailAttachment[] doesn’t have a method to return the appropriate cid AND you can’t rely on the array order as the returned GmailAttachment[] is constructed in the order the user added images to the draft, not the order that they appear in the message. I’ve updated a related Google Apps Script issue ticket, which you can star for email updates.

Solution

Option 1 – theoretical solution

Instead of the GmailApp Service you could use the Gmail API users.drafts.get method and work with the response (here is a Stack Overflow answer for doing this in Javascript that could be used in Apps Script). Once you extract the attachmentId you would then have to build your own method for calling users.messages.attachments.get and storing the inline images returned as blobs.

Option 2 – the alt/name fudge

While GmailAttacment[] doesn’t include any cid data there is a .getName() method that returns the image filename. The filename is useful because the Gmail draft uses this in the alt element:

<img data-surl="cid:ii_kl736chs6" src="cid:ii_kl736chs6" alt="320px-Chubby_Cat.jpg" width="320" height="213">

Important: It is possible for a Gmail draft to have duplicate filenames for inline images in the same message. This can happen with copy/paste or uploading more than one image with the same filename. As such you might want to throw an error when duplicate image filenames are detected.

There are various code samples for extracting the cid and alt data from Gmail draft messages. Below (or in this gist if you are reading via RSS) is my code based on this answer on Stack Overflow from Andras Sztrokay:

Summary

So there you go everything you ever wanted to know about Gmail draft inline images and Google Apps Script but were afraid to ask. If you have a better solution for handling inline images from Gmail drafts I would love to hear it. I hope you found this post useful and please feel free to incorporate it into your own Apps Script projects.

chevron_left
chevron_right
css.php