I'm trying to create a Gmail draft with the Gmail Advance Service. What I need is to have the body
contain data
in bytes format. I built the following funciton:
const createDraftWithAdvancedService = () => {
Gmail.Users.Drafts.create({
message: {
payload: {
parts: [
{
body: {
data: [
42,
123,
123,
80,
114,
],
}
}
],
headers: [
{
"value": "This is a test subject",
"name": "Subject"
},
]
}
}
}, '[email protected]');
}
However, when I run it, I get the following error:
GoogleJsonResponseException: API call to gmail.users.drafts.create failed with error: Invalid JSON payload received. Unknown name "data" at 'draft.message.payload.parts[0].body': Proto field is not repeating, cannot start list.
The error looks strange as I'm following the index properly (or so I believe after checking a hundred times).
What am I missing here?
UPDATE
The reason I'm formatting my bytes string as an array is because that's what the Gmail API return when you read a draft. If you have a different working code, I'm all ears. And I can't use raw
, I need to set the bytes string.
Here's an example of how a message object is retrieved and its format:
My draft message:
The script that retrives this draft message:
const getMessage = () => {
const id = 'r-8326849559354985208';
const msg = Gmail.Users.Drafts.get('[email protected]', id);
console.log(JSON.stringify(msg, null, 2));
}
The output of the script:
{
"message": {
"internalDate": "1633701716000",
"snippet": "Draft body",
"labelIds": [
"DRAFT"
],
"historyId": "954861",
"sizeEstimate": 534,
"payload": {
"filename": "",
"parts": [
{
"partId": "0",
"headers": [
{
"value": "text/plain; charset=\"UTF-8\"",
"name": "Content-Type"
}
],
"filename": "",
"body": {
"data": [
68,
114,
97,
102,
116,
32,
98,
111,
100,
121,
13,
10
],
"size": 12
},
"mimeType": "text/plain"
},
{
"headers": [
{
"value": "text/html; charset=\"UTF-8\"",
"name": "Content-Type"
}
],
"partId": "1",
"body": {
"size": 33,
"data": [
60,
100,
105,
118,
32,
100,
105,
114,
61,
34,
108,
116,
114,
34,
62,
68,
114,
97,
102,
116,
32,
98,
111,
100,
121,
60,
47,
100,
105,
118,
62,
13,
10
]
},
"mimeType": "text/html",
"filename": ""
}
],
"body": {
"size": 0
},
"headers": [
{
"value": "1.0",
"name": "MIME-Version"
},
{
"value": "Fri, 8 Oct 2021 16:01:56 +0200",
"name": "Date"
},
{
"value": "<CADVhnimBt3Jdod1wBgGUgB_75yrsoJMwM68mtYKmX6cN39=CNQ@mail.gmail.com>",
"name": "Message-ID"
},
{
"name": "Subject",
"value": "Draft subject"
},
{
"name": "From",
"value": "\"KOSTYUK, Dmitry\" <[email protected]>"
},
{
"value": "multipart/alternative; boundary=\"00000000000088918105cdd7d2e1\"",
"name": "Content-Type"
}
],
"mimeType": "multipart/alternative",
"partId": ""
},
"id": "17c6035e45454be8",
"threadId": "17c6035c50e83b2f"
},
"id": "r-8326849559354985208"
}
After some research and learning the RFC2822 MIMEText syntax, I have the definite answer to the problem. I will answer in three parts:
What doesn't work is using the actual Message
object like I did in my question. Don't ask me why, it's not documented anywhere, it just doesn't work. Even if you copy all or existing parts of the JSON object from another message via Gmail.Users.Drafts.get()
, GAS will still throw an error.
So what goes in is not what gets returned, even though the documentation says otherwise.
Hence the only solution is to use the raw
property of the message object, which must be a base-64-encoded string in the RFC2822 format.
Combining the solutions from here and here allowed to create a basic function that generates a draft message with emojis:
function convert(toEmail, fromEmail, subject, body) {
body = Utilities.base64Encode(body, Utilities.Charset.UTF_8);
subject = Utilities.base64Encode(subject, Utilities.Charset.UTF_8);
const boundary = "boundaryboundary";
const mailData = [
"MIME-Version: 1.0",
"To: " + toEmail,
"From: " + fromEmail,
"Subject: =?utf-8?B?" + subject + "?=",
"Content-Type: multipart/alternative; boundary=" + boundary,
"",
"--" + boundary,
"Content-Type: text/plain; charset=UTF-8",
"",
body,
"",
"--" + boundary,
"Content-Type: text/html; charset=UTF-8",
"Content-Transfer-Encoding: base64",
"",
body,
"",
"--" + boundary,
].join("\r\n");
return mailData;
}
function makeApiDraft() {
const subject = "Hello MimeText World";
const body = 'This is a plain text message';
const me = Session.getActiveUser().getEmail();
const raw = convert('[email protected]', me, subject, body);
const b64 = Utilities.base64EncodeWebSafe(raw);
console.log(raw)
Gmail.Users.Drafts.create({ message: { raw: raw } }, me);
}
Nothing wrong with this solution, it works. However, if you want to go beyond this example, like adding multiple recipients, having different plain text and html bodies, managing attachments, etc., you will have to code it all by hand and that requires understanding the RFC2822 MIMEText format.
Hence enter the new easier solution.
I stumbled upon this library that generates MIMEText emails written in Node.js. So I thought perfect. I forked the repo and adapted a few things to make it GAS-compatible, specifically:
Utilities.base64Encode()
and Utilities.base64EncodeWebSafe()
DriveApp.File
objectAnd while my pull request is pending, I transpiled the whole thing with Webpack (as the library does have a dependency) and published it as a GAS library under this ID:
1HzFRRghlhuCDl0FUnuE9uKAK39GfeuUJAE3oOsjv74Qjq1UW8YEboEit
Here's an example project that you can use to test it out, but the code is basically as follows:
const testMimeText = () => {
const { message } = MimeText;
message.setSender({
name: 'Dmitry Kostyuk',
addr: '[email protected]',
});
const file = DriveApp.getFileById('1pdMwlGL1WZTbi-Q2-Fc7nBm-9NKphkKg');
const me = Session.getActiveUser().getEmail();
message.setRecipient('[email protected]');
message.setSubject('Hello MimeText World!');
message.setMessage('This is a plain text message ' + getAllEmojis(), 'text/plain');
message.setMessage('<p>This is an html message</p><p>' + getAllEmojis() + '</p>\r\n\r\n', 'text/html');
message.setAttachments([file]);
const raw = message.asEncoded();
Gmail.Users.Drafts.create({ message: { raw: raw } }, me);
}
const getDriveAuth = () => DriveApp.getRootFolder();
I guess I went down a rabbit hole I never expected to, but I'm pretty happy with how it turned out :)