Skip to content

Instantly share code, notes, and snippets.

@oshliaer
Last active September 30, 2020 02:33
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save oshliaer/7e96514c3cb3075dde02 to your computer and use it in GitHub Desktop.
Save oshliaer/7e96514c3cb3075dde02 to your computer and use it in GitHub Desktop.
Create a Gmail draft with attachments via Google Apps Script #gas #gmail

X-51 © 2015 MARVEL

Create a Gmail draft with attachments via Google Apps Script #gas #gmail

How to use

Intro

Copy Message.gs to your project. You have to enable advanced services for Gmail API.

Be sure your project has next files:

Additional

Clone

git clone https://gist.github.com/7e96514c3cb3075dde02.git

For biulds you have to have git & node & npm. Then you can

git clone https://gist.github.com/7e96514c3cb3075dde02.git && npm install

It's strange but it can be.

For the tests you need to connect the TestApp Library

Create a draft

var msg = new Message();
msg.to = 'test@test.ru';
msg.body = '<h1>TEST</h1>';
msg.subject = 'TEST';
var file = DriveApp.createFile('~myfile.txt', 'content' + new Date(), 'text/plain');
msg.attachments = [file];
msg.createDraft();

Define several attachments

msg.attachments = [File, File];

File is a file in Google Drive. Files can be accessed or created from DriveApp.

Create with existing file

var file = DriveApp.getFileById(id);
msg.attachments = [file];

Create with existing files from a folder

var filesi = DriveApp.getFolderById(id).getFilesByType('text/plain');
var files = [];
while(filesi.hasNext()){
    files.push(filesi.next());
}
msg.attachments = files;

Create as a thread's child

...
var thread = Gmail.Users.Threads.list('me').threads[0].id;
msg.threadId = thread;
var resp = msg.createDraft();
# npm
node_modules
npm-debug.log
# dependencies
utf8.gs
quoted-printable.gs
# node-google-apps-script
src
gapps.config.json
src0
/* CODE */
//JUST A GAG. DON'T USE FOR PRODUCTION
function Message() {
// TODO separate methods
function nonASCII(str) {
return '=?UTF-8?B?' + Utilities.base64Encode(str, Utilities.Charset.UTF_8) + '?=';
}
this.to = '';
this.subject = '';
this.from = '';
this.body = '';
this.attachments = [];
this.toRFC822 = function() {
// var mt = new MimeType();
var boundaryHL = 'b314159265359a';
var separator = '\n';
var rows = [];
rows.push('MIME-Version: 1.0');
rows.push('To: ' + this.to);
rows.push('Subject: ' + nonASCII(this.subject));
rows.push('From: ' + this.from);
rows.push('Content-Type: multipart/mixed; boundary=' + boundaryHL + separator);
rows.push('--' + boundaryHL);
rows.push('Content-Type: text/html; charset=UTF-8');
rows.push('Content-Transfer-Encoding: quoted-printable' + separator);
rows.push(quotedPrintable.encode(utf8.encode(this.body)));
for (var i = 0; i < this.attachments.length; i++) {
rows.push('--' + boundaryHL);
var fmt = this.attachments[i].getMimeType();
var fn = this.attachments[i].getName();
// var ffn = mt.getFullName(fn, fmt);
// rows.push('Content-Type: ' + fmt + '; charset=UTF-8; name="' + ffn + '"');
// rows.push('Content-Disposition: attachment; filename="' + ffn + '"');
rows.push('Content-Type: ' + fmt + '; charset=UTF-8; name="' + fn + '"');
rows.push('Content-Disposition: attachment; filename="' + fn + '"');
rows.push('Content-Transfer-Encoding: base64' + separator);
rows.push(Utilities.base64Encode(this.attachments[i].getBlob().getBytes()));
}
rows.push('--' + boundaryHL + '--');
return rows.join(separator);
}
this.createDraft = function() {
//var me = this;
var message = {};
message.raw = Utilities.base64EncodeWebSafe(this.toRFC822());
if (this.threadId) message.threadId = this.threadId;
var res = Gmail.Users.Drafts.create({
'message': message
}, 'me');
return res;
}
}
// JUST A GAG. DON'T USE
function MimeType(){this.library={"image/jpeg":{ext:"jpg"},"text/plain":{ext:"txt"}}}MimeType.prototype.getFullName=function(c,b){var d=this.library[b]||{ext:"unknown"};if(!c){return"noname."+b}var a=c.lastIndexOf(".");if(a>0){if(d.ext!="unknown"){return c}else{return c.substring(0,a+1)+d.ext}}return c+"."+d.ext};
{
"name": "7e96514c3cb3075dde02",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"utf8": "^2.1.1",
"quoted-printable": "^1.0.0"
},
"devDependencies": {
"del-cli": "^0.1.2",
"ncp": "^2.0.0",
"node-google-apps-script": "^1.1.4"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"g": "gapps",
"push": "git add . && git commit -a -m $1",
"clear": "de *.gs",
"copy": "ncp ./node_modules/utf8/utf8.js ./src/utf8.gs && ncp ./node_modules/quoted-printable/quoted-printable.js ./src/quoted-printable.gs && ncp ./src/ ./ --filter=.*",
"build": "npm run clear && npm run copy && npm run push",
"bp": "npm run build && git push"
},
"repository": {
"type": "git",
"url": "git+https://gist.github.com/7e96514c3cb3075dde02.git"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://gist.github.com/7e96514c3cb3075dde02"
},
"homepage": "https://gist.github.com/7e96514c3cb3075dde02"
}
/*
tests.gs are using TestApp
https://github.com/Spencer-Easton/Apps-Script-TestApp-Library.git
Apps-Script-TestApp-Library
TestApp is a service for unit testing in Google Apps Script.
It can be added with the key Mm17sZ23ccjNtfj2PvKV798h00DPSBbB3 or from the source in the src folder.
*/
/*
Wraps Message() and returns TRUE if a draft was established
*/
function messageWrapper1_() {
var msg = new Message();
msg.to = 'test@test.ru';
msg.body = '<h1>TEST</h1><p>UNICODE АБВГДЕЁЪЫЮЭ</p>';
msg.subject = 'TEST UNICODE ЩШЖЫФЫВАФОДЫВЩШЦУШ';
var file1 = DriveApp.createFile(['runTests_', Utilities.formatDate(new Date, 'GMT', 'yyyy-MM-dd@hh_mm_ss'), '_testFile1.txt'].join(''), 'test file 1', 'text/plain');
var file2 = DriveApp.createFile(['runTests_', Utilities.formatDate(new Date, 'GMT', 'yyyy-MM-dd@hh_mm_ss'), '_testFile2.txt'].join(''), 'test file 2', 'text/plain');
msg.attachments = [file1, file2];
var resp = msg.createDraft();
return resp.hasOwnProperty('message') && resp.message.labelIds[0] == 'DRAFT';
}
/*
Tests runer
*/
function runTests() {
var testObj = TestApp.TestBuilder();
testObj.addFunction('messageWrapper1_', null, true);
var tr = new TestApp.TestRunner(this); //global this
tr.RunTest(testObj.TestObject(), checkResults_);
}
/*
Callback. Saves Logger.getLog() to root folder
*/
function checkResults_(testLog) {
Logger.log("TESTS FAILED:");
for (var test in testLog.getResults()) {
if (testLog.getResults()[test].status == "FAILED") {
Logger.log(testLog.getResults()[test]);
}
}
Logger.log("TESTS PASSED:");
for (var test in testLog.getResults()) {
if (testLog.getResults()[test].status == "PASSED") {
Logger.log(testLog.getResults()[test]);
}
}
DriveApp.createFile(['runTests_', Utilities.formatDate(new Date, 'GMT', 'yyyy-MM-dd@hh_mm_ss'), '.txt'].join(''), Logger.getLog(), 'text/plain');
}
@MrGreggles
Copy link

Hi Alexandr,

I tried this on a Google Doc. The image is the exact code, with the resulting Gmail. All I need is to attach a PDF, which I can send directly at the moment for a simple HTML email with:

GmailApp.sendEmail(emailTo, subject, textBody, {
    name: 'Joe from Example Ltd',
    htmlBody: htmlBody,
    attachments: [invoice.getAs(MimeType.PDF)]
});

Is it possible? Should I be pay you to get this working? It would be okay.
https://www.dropbox.com/sc/rfw1bp9pxsjjtuc/AABdMwwYwSJddHGmJIbYiNTga
https://www.dropbox.com/sc/7gvk0zdki9z6ymy/AADsFhQTiM7An_3cgZOcup8Ia

@oshliaer
Copy link
Author

Hi MrGreggles!
No money! You should not!
But Gist comments and mentions don't trigger notifications. Please, write me to G+
For the fast fix rewrite
attachments: [invoice.getAs(MimeType.PDF)]
to
attachments: [DriveApp.createFile(invoice.getAs(MimeType.PDF))]
It's not a best way, but it will work.

Give me a little time for a global fix.

@jief666
Copy link

jief666 commented Dec 25, 2015

Hi everyone,

I'd like to set the gmail threadId (because the draft is a reply).
I can do it with your example to create a draft without attachment by doing this :

var raw = 'From: ' + from + '\r\n' +
    'To: ' + to + '\r\n' +
    'Subject: ' + 'Re: ' + subject + '\r\n' + // subject of the message I'm replying to
    'In-Reply-To: ' + messageId + '\r\n' + // Message-ID of the message I'm replying to
    'References: ' + messageId + '\r\n' +
    'Content-Type: text/plain; charset=UTF-8\r\n' +
    '\r\n' + body + '\n\n\n\n';

var draftBody = Utilities.base64EncodeWebSafe(raw, Utilities.Charset.UTF_8);

var params = {
    method: "post",
    contentType: "application/json",
    headers: {
        "Authorization": "Bearer " + ScriptApp.getOAuthToken()
    },
    muteHttpExceptions: true,
    payload: JSON.stringify({
        "message": {
            "threadId": threadId, // gmail threadId
            "raw": draftBody
        }
    })
};
var resp = UrlFetchApp.fetch("https://www.googleapis.com/gmail/v1/users/me/drafts", params);

To put an attachment, the content-type of the post is message/rfc822 and not application/json anymore, so I donmt know where to put the threadId.

Any idea ?

Regards.

@oshliaer
Copy link
Author

oshliaer commented Feb 4, 2016

@jief666
You have to find threadId of the thread then push it into message as message.threadId = thread; Look at Create as a thread's child

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment