GmailApp (Google Apps Script) Displays Inline Images as Attachments

1,158 views
Skip to first unread message

Davis Jones

unread,
May 31, 2019, 3:44:32 PM5/31/19
to google-apps-sc...@googlegroups.com

Hello folks,

I am having the most difficult time getting the GmailApp sendEmail() method to successfully use an existing email (e.g. a draft) that contains images inline as a template for new messages.

It seems like this is a problem, as I have found devs having this problem here and here and proposed solutions herehere, and here. Each of these solutions is 4+ years old, so perhaps they're out of date. I have been unable to use any of these solutions to replicate a success.

Currently, I'm running this code from my Google Scripts backend (thanks to Romain and Amit for publishing some helpful code on this topic):

function generateMessageFromTemplate () {
  var selectedTemplate = GmailApp.getMessageById('MESSAGE_ID');

  //////////////////////////////////////////////////////////////////////////////
  // Get inline images and make sure they stay as inline images (via Romain Vialard)
  //////////////////////////////////////////////////////////////////////////////
  var emailTemplate = selectedTemplate.getBody();
  var rawContent = selectedTemplate.getRawContent();
  var attachments = selectedTemplate.getAttachments();

  var regMessageId = new RegExp(selectedTemplate.getId(), "g");
  if (emailTemplate.match(regMessageId) != null) {
    var inlineImages = {};
    var nbrOfImg = emailTemplate.match(regMessageId).length;
    var imgVars = emailTemplate.match(/<img[^>]+>/g);
    var imgToReplace = [];
    if(imgVars != null){
      for (var i = 0; i < imgVars.length; i++) {
        if (imgVars[i].search(regMessageId) != -1) {
          var id = imgVars[i].match(/realattid=([^&]+)&/);
          if (id != null) {
            var temp = rawContent.split(id[1])[1];
            temp = temp.substr(temp.lastIndexOf('Content-Type'));
            var imgTitle = temp.match(/name="([^"]+)"/);
            if (imgTitle != null) imgToReplace.push([imgTitle[1], imgVars[i], id[1]]);
          }
        }
      }
    }
    for (var i = 0; i < imgToReplace.length; i++) {
      for (var j = 0; j < attachments.length; j++) {
        if(attachments[j].getName() == imgToReplace[i][0]) {
          inlineImages[imgToReplace[i][2]] = attachments[j].copyBlob();
          attachments.splice(j, 1);
          var newImg = imgToReplace[i][1].replace(/src="[^\"]+\"/, "src=\"cid:" + imgToReplace[i][2] + "\"");
          emailTemplate = emailTemplate.replace(imgToReplace[i][1], newImg);
        }
      }
    }

  }
  //////////////////////////////////////////////////////////////////////////////

    GmailApp.sendEmail('test@email.com', selectedTemplate.getSubject(), '', {
      attachments: attachments,
      htmlBody: emailTemplate,
      inlineImages: inlineImages
    });
};

The Google Scripts documentation on the sendEmail() method is here.


This is the Output of this Function as Is

When I send emails from Apps Script as is, I get emails that look like this:


Screen Shot 2019-05-31 at 2.36.44 PM.png



I've replicated the test with an old yahoo.com email account and had the exact same results as a Gmail account.


If you can help, I would be extremely grateful!

Kind regards,

Davis

Romain Vialard

unread,
Jun 4, 2019, 5:37:15 AM6/4/19
to Google Apps Script Community
Indeed the method using the realattid attribute of img tags isn't working anymore (if I remember well it's still working for some old drafts but most drafts created after November 2016 do not contain realattids).

Instead of the realattid attribute, there's now a cid attribute that you can use to retrieve the image file and add it to the list of inline images in your GmailApp.sendEmail() call.

To sum up:
  • no realattid anymore so the current regex / match isn't working
  • imgVars[i].match(/realattid=([^&]+)&/)
  • it has been replaced by a cid attribute but it's not working exactly as before, so it's not just a regex change
  • now instead of searching the rawContent of the email for a match based on the realattid, best is to retrieve the img file and add it to the list of inline images in your GmailApp.sendEmail() call.
  • How to retrieve the img file? you can use the Gmail Advanced Service and call the Users.messages.attachments endpoint with the proper ids (the cid + the email id)
Another solution could be to use the Gmail advanced service to send the email instead of GmailApp. This way you can send the raw content of your draft without having to recreate an email with a specific list of attachments and inline images (something needed when using GmailApp.sendEmail()).

Davis Jones

unread,
Jun 4, 2019, 11:31:31 AM6/4/19
to Google Apps Script Community
Thanks for your guidance, Romain. Using the Gmail API is, honestly, very complex and perhaps a bit beyond my current coding abilities. I am looking at the few examples I can find of the Gmail API being used in practice to send messages like this one, but I'm not able to successfully send a message yet. 

Romain Vialard

unread,
Jun 4, 2019, 11:53:04 AM6/4/19
to Google Apps Script Community
Here's an example where I use the Gmail API to fetch the content of a draft (which can include anything you like, including attachments and inline images), make a copy and send this copy:
function myFunction() {
 
var draftMessageId = "16b23268b028fb65";
 
var message = Gmail.newMessage();
 
var rawContent = Gmail.Users.Messages.get("me", draftMessageId, {format: 'raw'}).raw;
 
Gmail.Users.Messages.send(message, "me", Utilities.newBlob(rawContent, "message/rfc822"));
}


If you don't need to edit the content of the draft it's quite easy (else you will need to edit the "rawContent" before sending it).

Alan Wells

unread,
Jun 4, 2019, 11:54:06 AM6/4/19
to Google Apps Script Community
Do you need to use a draft email as the basis for a new email?  As another option, you can put text/html into an html file, get the contents out with HtmlService, and use that as a template for a new email.  Are the images different for every new email?  If the images are always the same, then you don't need to get them out of an existing draft email.  You can host the image somewhere.  You can convert the image to a data uri.  You can save the image as a file to Google drive, and then convert it to a blob.  If you must use a draft as the template, then obviously these other options won't interest you.  But, I thought that I'd suggest other options.

Davis Jones

unread,
Jun 4, 2019, 12:10:48 PM6/4/19
to Google Apps Script Community
THANK YOU ROMAIN. I can work from here. Also, check your inbox. I've sent you a bit of cash for helping me with this issue. It's been killing me.

Davis Jones

unread,
Jun 4, 2019, 12:12:32 PM6/4/19
to Google Apps Script Community
I appreciate these suggestions, AJ. I started to follow the line of thinking that you suggested, but I was having trouble figuring out how to grab images nested within the drafts inside someone's draft folder. Basically, I couldn't figure out how to use a cid to get the raw image file and put it into a Drive folder (that was my original plan).

Davis Jones

unread,
Jun 4, 2019, 12:49:18 PM6/4/19
to Google Apps Script Community
Romain, do you suggest using regex logic to change the recipient of the draft (for example, from a spreadsheet where we keep our students' names and email addresses) before sending the draft? Or do you suggest another method?

Davis Jones

unread,
Jun 4, 2019, 3:52:04 PM6/4/19
to Google Apps Script Community
Hi fellow GAS devs,

I finally got this to work, and, because you all were so helpful, I wanted to share my code should others find it useful. This enables us to send emails to recipients based on a draft:
function sendFromDraft (id, recipient) {
 
// get the draft by ID and its raw content
 
var message = GmailApp.getMessageById(id);
 
var rawContent = message.getRawContent();
 
 
// determines if recipient is already defined in draft, routes accordingly, updates with new recipient
 
var addRecipient = 'To: <' + recipient + '>';
 
var recipientFromTemplate = rawContent.match(/To:.[^>]+>/);
 
if (recipientFromTemplate === null) {
   
var fromField = rawContent.match(/From:.[^>]+>/);
   
var updateWithRecipient = fromField + '\n' + addRecipient;
    rawContent
= rawContent.replace(fromField, updateWithRecipient);    
 
} else {
    rawContent
= rawContent.replace(recipientFromTemplate, addRecipient);
 
}
 
 
var message = Gmail.newMessage();
 
var encodedMsg = Utilities.base64EncodeWebSafe(rawContent);

  message
.raw = encodedMsg;

 
 
Gmail.Users.Messages.send(message, "me", Utilities.newBlob(rawContent, "message/rfc822"));
};

Special thanks to Romain for helping me overcome the major obstacle to building this simple, but crucial bit of code.

Martin Hawksey

unread,
Jun 5, 2019, 3:22:03 AM6/5/19
to google-apps-sc...@googlegroups.com
... and thank you Davis for sharing your solution :)

Romain Vialard

unread,
Jun 5, 2019, 3:55:01 AM6/5/19
to Google Apps Script Community
Perfect :)

Gsuite Test1

unread,
Aug 29, 2019, 5:29:54 AM8/29/19
to Google Apps Script Community
Hi Romain,

I am having the same issue ,Instead of sending email ,Need to createDraft reply. I am trying to use the solution mentioned below "Users.messages.attachments endpoint with the proper ids (the cid + the email id)"


Gmail API is complex for my coding knowledge.

It would be really great if you help me with an eample .

Thanks in Advance !!

VG
Jaya

Romain Vialard

unread,
Sep 2, 2019, 10:35:24 AM9/2/19
to Google Apps Script Community
If you simply need to create a draft reply inside an existing thread, it should be easy enough with GmailApp.
The documentation provides examples on how to send a message with inline images:

Creating a draft instead is quite similar.
function createDraftReplyWithInlineImage() {  
 
var googleLogoUrl = "http://www.google.com/logos/doodles/2015/googles-new-logo-5078286822539264.3-hp2x.gif";
 
var googleLogoBlob = UrlFetchApp.fetch(googleLogoUrl).getBlob().setName("googleLogoBlob")
 
// Create a draft reply in the most recent inbox thread
 
GmailApp.getInboxThreads(0, 1)[0].createDraftReply("my plain text", {
    htmlBody
: "inline Google Logo<img src='cid:googleLogo'> image!",
    inlineImages
: {
      googleLogo
: googleLogoBlob
   
}
 
});
}

And if you need to create this draft reply based on another draft used as a template, you can reuse the code snippets provided before.
Reply all
Reply to author
Forward
0 new messages