15
votes

Upon using the Gmail API in Javascript to send a message with an HTML body and a ~100KB PDF attachment, the attachment correctly appears attached to the message in the sender's Gmail Sent folder, but does not appear on the message for the recipient.

The API call is a POST to:

https://www.googleapis.com/upload/gmail/v1/users/me/messages/send?uploadType=media

The request body sent to the API is:

{
  "headers": {
    "Authorization": "Bearer authToken-removedForThisPost"
  },
  "method": "POST",
  "contentType": "message/rfc822",
  "contentLength": 134044,
  "payload": "exampleBelow",
  "muteHttpExceptions": true
}

This is what the payload looks like:

MIME-Version: 1.0
To: =?utf-8?B?TWlrZSBD?=<[email protected]>
CC: =?utf-8?B?TWlrZSBD?=<[email protected]>
BCC: =?utf-8?B??=<[email protected]>
From: =?utf-8?B?TWlrZSBxWXsd2lr?=<[email protected]>
Subject: =?utf-8?B?subjectLine-removedForThisPost?=
Content-Type: multipart/alternative; boundary=__boundary__

--__boundary__
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64

base64EncodedStringHere-removedForThisPost

--__boundary__
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: base64

base64EncodedStringHere-removedForThisPost

--__boundary__
Content-Type: application/pdf; name="File Name.pdf"
Content-Disposition: attachment; filename="File Name.pdf"
Content-Transfer-Encoding: base64

base64EncodedStringHere-removedForThisPost

--__boundary__--

Note: The Gmail API Uploading Attachments documentation states that when uploading a simple attachment (under 5MB) Content-Length is required. I made it so that my code produces an integer value of the total number of bytes of the PDF attachment. However, I noticed that Content-Length is not included in the payload.

I tried changing the multipart/alternative Content-Type for the message to multipart/mixed - this made it so that the PDF attachment IS correctly attached to the recipient's message, but the HTML body of the message is rendered as plain text (the HTML tags are shown) and there is an additional attachment called noname.html which includes the HTML content rendered as HTML.

I need to make it so that the email in the recipient's message has both an HTML-rendered body AND the PDF attachment.

Update: I uploaded examples of the raw email messages here. The sent message is on the left, and the received message is on the right.

2
Have you tried quoting your boundary (boundary="__boundary__") and using the final boudnary (--__boundary__--)? Try something like this and see if it works.Tholle
Just realized that my payload DID include the final boundary of --__boundary__--, but it was cut off when I pasted it here because the console.log message was truncated due to the super long attachment base64 string. As for the double quotes - I added them to the first Content-Type: line but it didn't change the behavior at all - it works the same with or without them.Employee
Just to check, is this message been received with the same attachment when is sent from the UI? Also, is this happening with all the recipients ? or just to an specif domain/user?jds1993
Yes, when sending a message with the attachment in the Gmail UI, it is correctly received by the recipient. I've also tested with multiple different attachments to rule out the possibility of an issue with the file. And I've tested with several different recipients in different domains, and the attachment is missing for all recipients.Employee

2 Answers

5
votes

Just replace:

Content-Type: multipart/alternative; boundary=__boundary__

for:

Content-Type: multipart/mixed; boundary=__boundary__

This is my full function written in JS

function createMimeMessage_(msg) {

var nl = "\n"; var boundary = "ctrlq_dot_org";

var mimeBody = [

"MIME-Version: 1.0",
"To: "      + msg.to.email,//+ encode_(msg.to.name) + "<" + msg.to.email + ">",
"Cc: "      + msg.cc.email,
"Bcc: "      + msg.bcc.email,
"From: "    + msg.from.email,//+ encode_(msg.from.name) + "<" + msg.from.email + ">",
"Subject: " + encode_(msg.subject), // takes care of accented characters
"In-Reply-To: " + (msg.reply_to || ""),
"References: " + (msg.reply_to || ""),

"Content-Type: multipart/mixed; boundary=" + boundary + nl,
"--" + boundary,

// "Content-Type: text/plain; charset=UTF-8",
// "Content-Transfer-Encoding: 7bit",
// "Content-Disposition: inline" + nl,
// msg.body.text + nl,
// "--" + boundary,

"Content-Type: text/html; charset=UTF-8",
"Content-Transfer-Encoding: base64" + nl,
new Buffer(msg.body.text).toString('base64') + nl,

];

for (var i = 0; i < msg.files.length; i++) {

var attachment = [
  "--" + boundary,
  "Content-Type: " + msg.files[i].mimeType + '; name="' + msg.files[i].fileName + '"',
  'Content-Disposition: attachment; filename="' + msg.files[i].fileName + '"',
  "Content-Transfer-Encoding: base64" + nl,
  msg.files[i].bytes
];

mimeBody.push(attachment.join(nl));

}

mimeBody.push("--" + boundary + "--"); //console.log(mimeBody);

return mimeBody.join(nl);

}

0
votes

There are two parts to your question:

  1. How do I get the attachment to go through to the recipient?
  2. How do I include both attachment and the plain text alternative to the HTML?

This was partially answered by Tiger developer (multipart/alternative to multipart/mixed). The problem, as you noted, is that simply doing this will prevent you from having the alternative plain text. This is because you're removing the multipart/alternative, whose specific role is to provide that alternative.

What you need to do is create a second boundary and then group the plain text & HTML parts together. Take a look at this example, also sourced from CTRLQ, and note the altBoundary that I included.

function createMimeMessage_(msg) {

  var nl = "\n";
  var boundary = "__ctrlq_dot_org__";
  var altBoundary = "__alt_ctrlq_dot_org__";

  var mimeBody = [

    "MIME-Version: 1.0",
    "To: "      + encode_(msg.to.name) + "<" + msg.to.email + ">",
    "From: "    + encode_(msg.from.name) + "<" + msg.from.email + ">",
    "Subject: " + encode_(msg.subject), // takes care of accented characters

    "Content-Type: multipart/mixed; boundary=" + boundary + nl,
    "--" + boundary,

    "Content-Type: multipart/alternative; boundary=" + altBoundary + nl,
    "--" + altBoundary,

    "Content-Type: text/plain; charset=UTF-8",
    "Content-Transfer-Encoding: base64" + nl,
    Utilities.base64Encode(msg.body.text, Utilities.Charset.UTF_8) + nl,
    "--" + altBoundary,

    "Content-Type: text/html; charset=UTF-8",
    "Content-Transfer-Encoding: base64" + nl,
    Utilities.base64Encode(msg.body.html, Utilities.Charset.UTF_8) + nl,

    "--" + altBoundary + "--"

  ];

  for (var i = 0; i < msg.files.length; i++) {

    var attachment = [
      "--" + boundary,
      "Content-Type: " + msg.files[i].mimeType + '; name="' + msg.files[i].fileName + '"',
      'Content-Disposition: attachment; filename="' + msg.files[i].fileName + '"',
      "Content-Transfer-Encoding: base64" + nl,
      msg.files[i].bytes
    ];

    mimeBody.push(attachment.join(nl));

  }

  mimeBody.push("--" + boundary + "--");

  return mimeBody.join(nl);

}