I am working with Google apps script and would like to create a script which picks up mail from the drafts and sends them if they have label "send-tomorrow".
Finding drafts with a certain label is pretty simple:
var threads = GmailApp.search('in:draft label:send-tomorrow');
However I don't see an API to send the message!
The only option I see is to:
- open the message
- extract body/attachments/title/from/to/cc/bcc
- send a new message with the above params
- destroy the previous draft
which seems pretty annoying and I'm not sure would work well with embedded images, multiple attachments etc...
any hint?
The only option I see is to: - open the message - extract body/attachments/title/from/to/cc/bcc - send a new message with the above params - destroy the previous draft
This is the exact topic of this blog by Amit Agarawal. His script does just what you describe, but doesn't handle inline images. For those, you can adapt the code from this article.
But you're right - what's the point of even having a draft message if you can't just send the stupid thing?!
We can use the GMail API Users.drafts: send from Google Apps Script to send a draft. The following stand-alone script does that, and handles the necessary authorization.
Script
The full script is available in this gist.
/*
* Send all drafts labeled "send-tomorrow".
*/
function sendDayOldDrafts() {
var threads = GmailApp.search('in:draft label:send-tomorrow');
for (var i=0; i<threads.length; i++) {
var msgId = threads[0].getMessages()[0].getId();
sendDraftMsg( msgId );
}
}
/**
* Sends a draft message that matches the given message ID.
* Throws if unsuccessful.
* See https://developers.google.com/gmail/api/v1/reference/users/drafts/send.
*
* @param {String} messageId Immutable Gmail Message ID to send
*
* @returns {Object} Response object if successful, see
* https://developers.google.com/gmail/api/v1/reference/users/drafts/send#response
*/
function sendDraftMsg( msgId ) {
// Get draft message.
var draftMsg = getDraftMsg(msgId,"json");
if (!getDraftMsg(msgId)) throw new Error( "Unable to get draft with msgId '"+msgId+"'" );
// see https://developers.google.com/gmail/api/v1/reference/users/drafts/send
var url = 'https://www.googleapis.com/gmail/v1/users/me/drafts/send'
var headers = {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
};
var params = {
method: "post",
contentType: "application/json",
headers: headers,
muteHttpExceptions: true,
payload: JSON.stringify(draftMsg)
};
var check = UrlFetchApp.getRequest(url, params)
var response = UrlFetchApp.fetch(url, params);
var result = response.getResponseCode();
if (result == '200') { // OK
return JSON.parse(response.getContentText());
}
else {
// This is only needed when muteHttpExceptions == true
var err = JSON.parse(response.getContentText());
throw new Error( 'Error (' + result + ") " + err.error.message );
}
}
/**
* Gets the current user's draft messages.
* Throws if unsuccessful.
* See https://developers.google.com/gmail/api/v1/reference/users/drafts/list.
*
* @returns {Object[]} If successful, returns an array of
* Users.drafts resources.
*/
function getDrafts() {
var url = 'https://www.googleapis.com/gmail/v1/users/me/drafts';
var headers = {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
};
var params = {
headers: headers,
muteHttpExceptions: true
};
var check = UrlFetchApp.getRequest(url, params)
var response = UrlFetchApp.fetch(url, params);
var result = response.getResponseCode();
if (result == '200') { // OK
return JSON.parse(response.getContentText()).drafts;
}
else {
// This is only needed when muteHttpExceptions == true
var error = JSON.parse(response.getContentText());
throw new Error( 'Error (' + result + ") " + error.message );
}
}
/**
* Gets the draft message ID that corresponds to a given Gmail Message ID.
*
* @param {String} messageId Immutable Gmail Message ID to search for
*
* @returns {String} Immutable Gmail Draft ID, or null if not found
*/
function getDraftId( messageId ) {
if (messageId) {
var drafts = getDrafts();
for (var i=0; i<drafts.length; i++) {
if (drafts[i].message.id === messageId) {
return drafts[i].id;
}
}
}
// Didn't find the requested message
return null;
}
/**
* Gets the draft message content that corresponds to a given Gmail Message ID.
* Throws if unsuccessful.
* See https://developers.google.com/gmail/api/v1/reference/users/drafts/get.
*
* @param {String} messageId Immutable Gmail Message ID to search for
* @param {String} optFormat Optional format; "object" (default) or "json"
*
* @returns {Object or String} If successful, returns a Users.drafts resource.
*/
function getDraftMsg( messageId, optFormat ) {
var draftId = getDraftId( messageId );
var url = 'https://www.googleapis.com/gmail/v1/users/me/drafts'+"/"+draftId;
var headers = {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
};
var params = {
headers: headers,
muteHttpExceptions: true
};
var check = UrlFetchApp.getRequest(url, params)
var response = UrlFetchApp.fetch(url, params);
var result = response.getResponseCode();
if (result == '200') { // OK
if (optFormat && optFormat == "JSON") {
return response.getContentText();
}
else {
return JSON.parse(response.getContentText());
}
}
else {
// This is only needed when muteHttpExceptions == true
var error = JSON.parse(response.getContentText());
throw new Error( 'Error (' + result + ") " + error.message );
}
}
Authorization
To use Google's APIs, we need to have an OAuth2 token for the current user - just as we do for Advanced Services. This is done using ScriptApp.getOAuthToken()
.
After copying the code to your own script, open Resources -> Advanced Google Services, open the link for the Google Developers Console, and enable the Gmail API for your project.
As long as the script contains at least one GMailApp method that requires user authority, the authentication scope will be set properly for the OAuthToken. In this example, that's taken care of by GmailApp.search()
in sendDayOldDrafts()
; but for insurance you could include a non-reachable function call directly in the functions using the API.
I did it using the GmailMessage.forward method.
It works with upload images and attachments, but I had to set the subject to avoid the prefix "Fwd:", and the user name because it only displayed the user email to the recipients.
I didn't find a way to dispose the draft, so I just remove the label to prevent sending it again.
Script:
function getUserFullName(){
var email = Session.getActiveUser().getEmail();
var contact = ContactsApp.getContact(email);
return contact.getFullName();
}
function testSendTomorrow(){
var threads = GmailApp.search('in:draft label:send-tomorrow');
if(threads.length == 0){
return;
}
var labelSendTomorrow = GmailApp.getUserLabelByName("send-tomorrow");
for(var i = 0; i < threads.length; i++){
var messages = threads[i].getMessages();
for(var j = 0; j < messages.length; j++){
var mssg = messages[j];
if(mssg.isDraft()){
mssg.forward(mssg.getTo(), {
cc: mssg.getCc(),
bcc: mssg.getBcc(),
subject: mssg.getSubject(),
name: getUserFullName()
});
}
}
threads[i].removeLabel(labelSendTomorrow);
}
}
You can search through all drafts and then send that specific draft no problem.
function sendMessage(id){
GmailApp.getDrafts().forEach(function (draft) {
mes = draft.getMessage()
if (mes.getId() == id) {
draft.send()
}
})
}
A simpler alternative is to use the gmail api instead of gmailApp:
function sendtestDraft(draftId){
var request = Gmail.Users.Drafts.send({id : draftId},'me');
Logger.log(request);
}
above function example is used within a gs script at https://script.google.com.
It needs the draftId (not the message Id) and the draft will be sent. Images and attachments are all OK!
Info:https://developers.google.com/gmail/api/v1/reference/users/drafts/send
I'm new around here and don't have enough "reputation" to comment, so couldn't comment on Mogsdad's original answer so I'm having to create a new answer:
I've adapted Mogsdad's solution to also support replying/forwarding existing threads, not just brand new messages.
To use it on existing threads, you should first create the reply/forward, and only then label the thread. My code also supports several labels and setting up those labels.
I created a new gist for it, forking Mogsdad's, here: https://gist.github.com/hadasfester/81bfc5668cb7b666b4fd6eeb6db804c3
I still need to add some screenshot links in the doc but otherwise this is ready for use, and I've been using it myself. Hope you find it useful.
Also inlining it here:
/**
* This script allows you to mark threads/drafts with a predetermined label and have them get sent the next time your trigger
* sets off.
*
* Setup instructions:
* 1. Make a copy of this script (File -> Make a copy)
* 2. Follow the "Authorization" instructions on https://stackoverflow.com/a/27215474. (If later during setup/testing you get
* another permissions approval dialog, approve there as well).
* 2. I created two default labels, you can edit/add your own. See "TODO(user):" below. After that, to create them in gmail,
* choose "setUpLabel" function above and click the play button (TODO: screenshot). Refresh your gmail tab, you should see
* the new labels.
* 3. Click the clock icon above (TODO: screenshot) and set time triggers, e.g. like so: (TODO: screenshot)
* 4. I recommend also setting up error notifications: (TODO: screenshot).
*
* Testing setup:
* When you're first setting this up, if you want to test it, create a couple
* of drafts and label them. Then, in this
* script, select "sendWeekStartDrafts" or "sendTomorrowDrafts" in the function dropdown
* and press play. This manually triggers the script (instead of relying on the
* timer) so you can see how it works.
*
* Usage instructions:
* 1. To get a draft sent out on the next trigger, mark your draft with the label you chose.
* NOTE: If your draft is a reply to a thread, make sure you first create the draft and only then set the label on the
* thread, not the other way around.
* That's it! Upon trigger your draft will be sent and the label will get removed from the thread.
*
* Some credits and explanation of differences/improvements from other existing solutions:
* 1. This script was adapted from https://stackoverflow.com/a/27215474 to also support replying existing threads, not only
* sending brand new messages.
* 2. Other solutions I've run into are based on creating a new message, copying it field-by-field, and sending the new one,
* but those have many issues, some of which are that they also don't handle replies and forwards very elegantly.
*
* Enjoy!
**/
var TOMORROW_LABEL = '!send-tomorrow';
var WEEK_START_LABEL = '!send-week-start';
// TODO(user): add more labels here.
/**
* Set up the label for delayed send!
**/
function setUpLabels() {
GmailApp.createLabel(TOMORROW_LABEL);
GmailApp.createLabel(WEEK_START_LABEL);
// TODO(user): add more labels here.
}
function sendTomorrowDrafts() {
sendLabeledDrafts(TOMORROW_LABEL);
}
function sendWeekStartDrafts() {
sendLabeledDrafts(WEEK_START_LABEL);
}
// TODO(user): add more sendXDrafts() functions for your additional labels here.
/*
* Send all drafts labeled $MY_LABEL.
* @param {String} label The label for which to send drafts.
*/
function sendLabeledDrafts(label) {
var threads = GmailApp.search('in:draft label:' + label);
for (var i=0; i<threads.length; i++) {
var thread = threads[i];
var messages = thread.getMessages();
var success = false;
for (var j=messages.length-1; j>=0; j--) {
var msgId = messages[j].getId();
if (sendDraftMsg( msgId )) {
success = true;
}
}
if (!success) { throw Error( "Failed sending msg" ) };
if (success) {
var myLabel = GmailApp.getUserLabelByName(label);
thread.removeLabel(myLabel);
}
}
}
/**
* Sends a draft message that matches the given message ID.
* See https://developers.google.com/gmail/api/v1/reference/users/drafts/send.
*
* @param {String} messageId Immutable Gmail Message ID to send
*
* @returns {Object} Response object if successful, see
* https://developers.google.com/gmail/api/v1/reference/users/drafts/send#response
*/
function sendDraftMsg( msgId ) {
// Get draft message.
var draftMsg = getDraftMsg(msgId,"json");
if (!getDraftMsg(msgId)) return null;
// see https://developers.google.com/gmail/api/v1/reference/users/drafts/send
var url = 'https://www.googleapis.com/gmail/v1/users/me/drafts/send'
var headers = {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
};
var params = {
method: "post",
contentType: "application/json",
headers: headers,
muteHttpExceptions: true,
payload: JSON.stringify(draftMsg)
};
var check = UrlFetchApp.getRequest(url, params)
var response = UrlFetchApp.fetch(url, params);
var result = response.getResponseCode();
if (result == '200') { // OK
return JSON.parse(response.getContentText());
}
else {
// This is only needed when muteHttpExceptions == true
return null;
}
}
/**
* Gets the current user's draft messages.
* Throws if unsuccessful.
* See https://developers.google.com/gmail/api/v1/reference/users/drafts/list.
*
* @returns {Object[]} If successful, returns an array of
* Users.drafts resources.
*/
function getDrafts() {
var url = 'https://www.googleapis.com/gmail/v1/users/me/drafts';
var headers = {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
};
var params = {
headers: headers,
muteHttpExceptions: true
};
var check = UrlFetchApp.getRequest(url, params)
var response = UrlFetchApp.fetch(url, params);
var result = response.getResponseCode();
if (result == '200') { // OK
return JSON.parse(response.getContentText()).drafts;
}
else {
// This is only needed when muteHttpExceptions == true
var error = JSON.parse(response.getContentText());
throw new Error( 'Error (' + result + ") " + error.message );
}
}
/**
* Gets the draft message ID that corresponds to a given Gmail Message ID.
*
* @param {String} messageId Immutable Gmail Message ID to search for
*
* @returns {String} Immutable Gmail Draft ID, or null if not found
*/
function getDraftId( messageId ) {
if (messageId) {
var drafts = getDrafts();
for (var i=0; i<drafts.length; i++) {
if (drafts[i].message.id === messageId) {
return drafts[i].id;
}
}
}
// Didn't find the requested message
return null;
}
/**
* Gets the draft message content that corresponds to a given Gmail Message ID.
* See https://developers.google.com/gmail/api/v1/reference/users/drafts/get.
*
* @param {String} messageId Immutable Gmail Message ID to search for
* @param {String} optFormat Optional format; "object" (default) or "json"
*
* @returns {Object or String} If successful, returns a Users.drafts resource.
*/
function getDraftMsg( messageId, optFormat ) {
var draftId = getDraftId( messageId );
var url = 'https://www.googleapis.com/gmail/v1/users/me/drafts'+"/"+draftId;
var headers = {
Authorization: 'Bearer ' + ScriptApp.getOAuthToken()
};
var params = {
headers: headers,
muteHttpExceptions: true
};
var check = UrlFetchApp.getRequest(url, params)
var response = UrlFetchApp.fetch(url, params);
var result = response.getResponseCode();
if (result == '200') { // OK
if (optFormat && optFormat == "JSON") {
return response.getContentText();
}
else {
return JSON.parse(response.getContentText());
}
}
else {
// This is only needed when muteHttpExceptions == true
return null;
}
}
First, GmailDraft
now has a send()
function you can call directly. See: https://developers.google.com/apps-script/reference/gmail/gmail-draft#send()
Their code sample:
var draft = GmailApp.getDrafts()[0]; // The first draft message in the drafts folder
var msg = draft.send(); // Send it
Logger.log(msg.getDate()); // Should be approximately the current timestamp
Second, may not even need it now that google has released scheduled sending.
- Click the arrow next to
Send
- Select your preferred time to send