How to send a draft email using google apps script - google-apps-script

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);
}
}

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;
}
}

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()
}
})
}

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

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

Related

Google App Script oAuth2 to Questrade API Invalid Token

I'm trying to used Google Apps Script to pull Questrade account information into a Google sheets spreadsheet. I've added the oAuth2 library from GitHub(https://github.com/googleworkspace/apps-script-oauth2) then mostly copy and pasted (with minor edits) from the example code.
The weird thing is this code has worked, exactly how it is, but a day later it no longer works and returns the following:
Exception: Request failed for https://api01.iq.questrade.com returned code 401. Truncated server response: {"code":1017,"message":"Access token is invalid"} (use muteHttpExceptions option to examine full response)
My Google Apps Script is posted below. I've only removed my Questrade Client_ID and Google Script Script_ID. I have three buttons in my spreadsheet which I've linked to functions in the script:
Button 1 - QT oAuth - calls showSidebar
Button 2 - Load Account Info - calls makeRequest
Button 3 - QT Logout - calls logout
Typically, I press the QT Logout button to reset 0Auth2 services then I press the QT oAuth button. This seems to successfully go through the authorization process. I then press the Load Account Info button and about 99 times out of 100 I get the invalid access token message. I don't know if it's relevant, but when I log into Questrades API hub I can see that the script is adding an authorization after the QT oAuth button is pressed but it seems to disappear after about a minute.
The script:
function getQTService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService('QT')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://login.questrade.com/oauth2/authorize')
.setTokenUrl('https://login.questrade.com/oauth2/token')
// Set the client ID and secret.
.setClientId('Client_ID')
.setClientSecret(' ') //there is no client secret but oAuth2 requires one
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
.setCache(CacheService.getUserCache())
// Set the scopes to request (space-separated for Google services).
.setScope('read_acc')
}
function showSidebar() {
var QTService = getQTService();
if (!QTService.hasAccess()) {
var authorizationUrl = QTService.getAuthorizationUrl();
var template = HtmlService.createTemplate(
'Authorize. ' +
'Reopen the sidebar when the authorization is complete.');
template.authorizationUrl = authorizationUrl;
var page = template.evaluate();
SpreadsheetApp.getUi().showSidebar(page);
} else {
// ...
}
}
function authCallback(request) {
var QTService = getQTService();
var isAuthorized = QTService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
function makeRequest() {
var QTService = getQTService();
var token = QTService.getAccessToken();
var spreadsheet = SpreadsheetApp.openById("Script_ID");
// Get account number
var response = UrlFetchApp.fetch('https://api01.iq.questrade.com/v1/accounts',{
headers: {
Authorization: 'Bearer ' + token
}
});
var json = response.getContentText();
var accountdata = JSON.parse(json);
var j = 0;
while(j < accountdata.accounts.length) {
var Account_num = accountdata.accounts[j].number;
var Account_type = accountdata.accounts[j].type;
var sheet = spreadsheet.getSheetByName(Account_type);
// GET CASH BALANCE
var response = UrlFetchApp.fetch('https://api01.iq.questrade.com/v1/accounts/' + Account_num + '/balances',{
headers: {
Authorization: 'Bearer ' + token
}
});
json = response.getContentText();
var balancedata = JSON.parse(json);
var i = 0;
while(balancedata.perCurrencyBalances[i].currency != 'CAD') {
i=i+1;
}
//send cash value to spreadsheet
sheet.getRange("G1").setValue(balancedata.perCurrencyBalances[i].cash);
// GET POSITIONS
var response = UrlFetchApp.fetch('https://api01.iq.questrade.com/v1/accounts/' + Account_num + '/positions',{
headers: {
Authorization: 'Bearer ' + token
}
});
json = response.getContentText();
var positionsdata = JSON.parse(json);
var num_of_positions = positionsdata.positions.length;
var i = 0;
while(i < num_of_positions) { //this loop is not that smart assumes the positions are where I specify, fix later
if(positionsdata.positions[i].symbol == 'VCN.TO'){
sheet.getRange("D5").setValue(positionsdata.positions[i].openQuantity);
}
if(positionsdata.positions[i].symbol == 'VUN.TO') {
sheet.getRange("D6").setValue(positionsdata.positions[i].openQuantity);
}
if(positionsdata.positions[i].symbol == 'VIU.TO') {
sheet.getRange("D7").setValue(positionsdata.positions[i].openQuantity);
}
if(positionsdata.positions[i].symbol == 'VEE.TO') {
sheet.getRange("D8").setValue(positionsdata.positions[i].openQuantity);
}
i=i+1;
}
j=j+1;
}
//send cash value to spreadsheet
// sheet.getRange("G1").setValue(data.perCurrencyBalances[i].cash);
}
function logout() {
var service = getQTService();
service.reset();
}
Any advice on what might be going wrong here would be greatly appreciated.
I don't think you can rely on using api01. I think you have to extract the api_server from the auth call that gives you a token (or at least I did this using the example on https://www.questrade.com/api/documentation/getting-started with refresh_token). My last 3 refresh_token auths for a bearer have yielded the api06 endpoint. I took your code and with the oauth authorizing and using api06 it works fine.
The magic sauce is var api_server = QTService.getToken().api_server;

Quicken google apps script so I can return success response within Shopify 5 second limit

I have this google apps script I wrote that I'm using as a web app as an endpoint for a Shopify webhook.
The issue I'm having is that Shopify has a 5 second limit to receive a success response otherwise the webhook will fire again to ensure you don't miss it.
The problem is my script takes too long to finish triggering a duplicate webhook which runs my code multiple times which I don't want.
Is there a way to respond quicker or clean up my script to finish quicker?
PLEASE NOTE: I need my script to be easily modified since exact values might change or be different in final version as I'm still developing this app. (additionally I need a way that if a value is missing it will leave that column blank, hence not mixing up value with column headers)
function doPost(e){
var data = JSON.parse(e.postData.contents);
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
var l = data.line_items.length;
for (var i=0;i<l;i++){
var prop = data.line_items[i].properties;
if (prop.length>0){
var pdf = prop.find(function(x) {if(x.name == "_pdf") return x});
if (!pdf){pdf = "Prop not found";}else{pdf = pdf.value};
var shape = prop.find(function(x) {if(x.name.toLowerCase() == "shape") return x});
if (!shape){shape = "Prop not found";}else{shape = shape.value};
var test = prop.find(function(x) {if(x.name == "test") return x});
if (!test){test = "Prop not found";}else{test = test.value};
}else{
var pdf = "N/A"
var shape = "N/A"
var test = "N/A"
};
var count = "Item "+ (i+1) + " of " + l;
var qty = data.line_items[i].quantity;
var title = data.line_items[i].title;
var id = data.id.toString();
var email = data.email;
var totalPrice = data.total_price;
var discounts = data.total_discounts;
var acceptAds = data.buyer_accepts_marketing;
var orderStatus = data.order_status_url;
var addr = data.shipping_address.address1;
var city = data.shipping_address.city;
var state = data.shipping_address.province;
var zip = data.shipping_address.zip;
var phone = data.shipping_address.phone;
var firstName = data.shipping_address.first_name;
var lastName = data.shipping_address.last_name;
var orderNum = data.name;
var d = new Date(data.created_at).toLocaleString();
ss.appendRow([d,orderNum,email,count,title,shape,test,qty,totalPrice,discounts,pdf,firstName,lastName,addr,city,state,zip,phone,orderStatus]);
if (pdf != "N/A"){
if (pdf != "Prop not found"){
var res = UrlFetchApp.fetch(pdf);
var blob = res.getBlob();
var createFile = DriveApp.getFolderById('xxxxxxxxxxxxx').createFile(blob.getAs('application/pdf'));
var fileName = orderNum + " " + qty;
createFile.setName(fileName);
}}
};
}
It's slower than using the PropertiesService, but I like using Sheets as a queue. (I use this with services that require responses within 3 seconds.) Not only is it easier to work with, but I've actually had issues with using Properties that are addressed with the appendRow() method:
Appends a row to the spreadsheet. This operation is atomic; it prevents issues where a user asks for the last row, and then writes to that row, and an intervening mutation occurs between getting the last row and writing to it.
When you receive the POST data, simply add it to the queue and terminate. Apps Script will send a 200 success response, so Shopify shouldn't send duplicate requests.
Then have a time-driven trigger that runs a processQueue() function at the interval of your choice.
function doPost(e) {
const queue = new Queue(SpreadsheetApp.getActive().getId(), "Unprocessed", "Processed");
queue.append(e.postData.contents, skipRefresh = true);
}
function processQueue() {
const queue = new Queue(SpreadsheetApp.getActive().getId(), "Unprocessed", "Processed");
while (queue.hasNext()) {
try {
const data = JSON.parse(queue.next());
doSomethingWithShopifyData(data); // Process your data
queue.moveToProcessed();
} catch (error) {
console.error(error);
queue.skip();
}
}
}
function doSomethingWithShopifyData(data) { /* your existing code, but with appropriate modifications */ }
Here's the class I use to abstract the spreadsheet into a queue. I have it setup to preserve all of the data moving it from an unprocessed to a processed sheet. You may prefer to simply delete the data once processed.
/**
* A spreadsheet is used as a makeshift queue for processing requests asynchronously.
* #param {string} spreadsheetId - The ID of the spreadsheet to be used for the queue.
* #param {string} unprocessedSheetName - The name of the sheet to be used for storing unprocessed items.
* #param {string} processedSheetName - The name of the sheet to be used for storing processed items.
*/
class Queue {
constructor(spreadsheetId, unprocessedSheetName, processedSheetName) {
this.index = 0;
this.row = 1;
this.spreadsheet = SpreadsheetApp.openById(spreadsheetId);
this.unprocessedSheet = this.spreadsheet.getSheetByName(unprocessedSheetName);
this.processedSheet = this.spreadsheet.getSheetByName(processedSheetName);
}
/**
* Determines whether calling next() will return an item.
* #returns {boolean}
*/
hasNext() {
if (this.unprocessedValues == null) { this.refreshUnprocessedValues(); }
return this.index < this.unprocessedValues.length;
}
/**
* Get and save the unprocessed element values to the queue.
* #returns {object[]}
*/
refreshUnprocessedValues() {
try {
const range =this.unprocessedSheet.getRange(1, 1, this.unprocessedSheet.getLastRow());
this.unprocessedValues = range.getValues();
} catch (error) {
this.unprocessedValues = [];
}
return this.unprocessedValues;
}
/**
* Get the next element from the queue.
* #returns {string}
*/
next() {
return this.unprocessedValues[this.index++][0];
}
/**
* Skip the current queue element. Update row property to maintain synchronization
* with the spreadsheet range.
*/
skip() {
this.row++;
}
/**
* Add new data to the queue for processing.
* #param {string} data - The data to add to the queue.
* #param {boolean} [skipRefresh] - Default: false. If true, will skip refreshing the queue values.
*/
append(data, skipRefresh) {
this.unprocessedSheet.appendRow([data]);
if (!skipRefresh) { this.refreshUnprocessedValues(); }
}
/**
* Move a payload out of the unprocessed sheet and into the processed sheet. Uses the payload
* at the top of the unprocessed range.
*/
moveToProcessed() {
const cell = this.unprocessedSheet.getRange(this.row, 1);
// Move into processed sheet
this.processedSheet.appendRow([cell.getValue()]);
// Move out of unprocessed sheet
cell.deleteCells(SpreadsheetApp.Dimension.ROWS);
}
}

Get URL parameter passed to google form via apps script

I`m trying to get URL parameter passed to google form via apps script, but don't know exactly how does it does. Case:
https://docs.google.com/forms/d/e/XXXX/viewform?a=12
(Passed XXXX, cause this form is for clients use)
and I need to get answers and this parameter after submit.
function onSubmit(e) {
var POST_UR​L = "www.myserver.com?id=a";//this parametr
var form = FormApp.getActiveForm();
var allResponses = form.getResponses();
var latestResponse = allResponses[allResponses.length - 1];
var response = latestResponse.getItemResponses();
var payload = {};
for (var i = 0; i < response.length; i++) {
var question = response[i].getItem().getTitle();
var answer = response[i].getResponse();
payload[question] = answer;
}
var options = {
"method": "post",
"contentType": "application/json",
"payload": JSON.stringify(payload)
};
UrlFetchApp.fetch(POST_URL, options);
};
​​
This code made for sending POST request with answers of form to my server and how can I pass the ​a=12 or only 12 to this request?
You need to append the values to POST_URL instead of passing via payload. There's a nice bit of code provided by Google in their OAuth2 library that will do this for you:
/**
* Builds a complete URL from a base URL and a map of URL parameters.
* #param {string} url The base URL.
* #param {Object.<string, string>} params The URL parameters and values.
* #return {string} The complete URL.
* #private
*/
function buildUrl_(url, params) {
var paramString = Object.keys(params).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
return url + (url.indexOf('?') >= 0 ? '&' : '?') + paramString;
}
Which means you can do something like:
var POST_URL = "www.myserver.com?id=a";
var payload = {a: 12};
console.log(buildUrl_(POST_URL, payload)); // www.myserver.com?id=a&a=12
function buildUrl_(url, params) {
var paramString = Object.keys(params).map(function(key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
return url + (url.indexOf('?') >= 0 ? '&' : '?') + paramString;
}
Ultimately, the final line in your code would be:
UrlFetchApp.fetch(buildUrl_(POST_URL, payload), options);

How to generate redirect URI for Smartsheet to pass data to Apps Script

Screenshot of smartsheet and google setup screens
When attempting to get data out of Smartsheet, I'm encountering an error that says the redirect URI is missing or invalid when I follow the link that was logged by my apps script project.
I've generated a client ID and client secret on both google and smartsheet but I don't know what to do next.
Google Credentials:
I'm not sure what to put in the redirect Url section or the authorized Javascript origins at the link below.
https://console.developers.google.com/apis/credentials/oauthclient/########################2d.apps.googleusercontent.com?project=project-id-##############
Smartsheet Credentials:
I have activated my Smartsheet Developer profile and generated a client ID and client secret for my app that I've called 'Google Sheets'
Shown below is the code that I have right now which I found on gitHub.
var CLIENT_ID = '...'; // what do I put here?
var CLIENT_SECRET = '...'; // what do I put here?
/**
* Authorizes and makes a request to the Smartsheet API.
*/
function run()
{
var service = getService();
if (service.hasAccess())
{
var url = 'https://api.smartsheet.com/2.0/users/me';
var response = UrlFetchApp.fetch(url,
{
headers:
{
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
Logger.log(JSON.stringify(result, null, 2));
}
else
{
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s', authorizationUrl);
}
}
/**
* Reset the authorization state, so that it can be re-tested.
*/
function reset()
{
getService().reset();
}
/**
* Configures the service.
*/
function getService()
{
return OAuth2.createService('Smartsheet')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://app.smartsheet.com/b/authorize')
.setTokenUrl('https://api.smartsheet.com/2.0/token')
// Set the client ID and secret.
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
// Set the name of the callback function that should be invoked to
// complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
// Scopes to request
.setScope('READ_SHEETS')
// Set the handler for adding Smartsheet's required SHA hash parameter to
// the payload:
.setTokenPayloadHandler(smartsheetTokenHandler);
}
/**
* Handles the OAuth callback.
*/
function authCallback(request)
{
var service = getService();
var authorized = service.handleCallback(request);
if (authorized)
{
return HtmlService.createHtmlOutput('Success!');
}
else
{
return HtmlService.createHtmlOutput('Denied.');
}
}
/**
* Adds the Smartsheet API's required SHA256 hash parameter to the access token
* request payload.
*/
function smartsheetTokenHandler(payload)
{
var codeOrRefreshToken = payload.code ? payload.code : payload.refresh_token;
var input = CLIENT_SECRET + '|' + codeOrRefreshToken;
var hash = Utilities.computeDigest(
Utilities.DigestAlgorithm.SHA_256, input, Utilities.Charset.UTF_8);
hash = hash.map(function(val)
{
// Google appears to treat these as signed bytes, but we need them
// unsigned.
if (val < 0)
{
val += 256;
}
var str = val.toString(16);
// pad to two hex digits:
if (str.length == 1)
{
str = '0' + str;
}
return str;
});
payload.hash = hash.join('');
// The Smartsheet API doesn't need the client secret sent (secret is verified
// by the hash).
if (payload.client_secret)
{
delete payload.client_secret;
}
return payload;
}
/**
* Logs the redict URI to register.
*/
function logRedirectUri()
{
Logger.log(OAuth2.getRedirectUri());
}
function dataHandler(thing)
{
thing = getData2();
var rowTemp = thing.split(','), i, j, chunk = 7, rows = [];
for (i=0,j=rowTemp.length; i<j; i+=chunk)
{
for(var k = 0; k<2; k++)
{
rowTemp[k+2] = new Date(rowTemp[k+2])
}
rows.push(rowTemp.slice(i,i+chunk));
}
Logger.log(rows);
}
var CLIENT_ID = 'SmartSheet Client ID'; // I'm not sure if this is
// supposed to come from google
// or smartsheet
var CLIENT_SECRET = 'Smartsheet Client Secret'; // Same here
/**
* Authorizes and makes a request to the Smartsheet API.
*/
function run() {
var service = getService();
if (service.hasAccess()) {
var url = 'https://api.smartsheet.com/2.0/users/me';
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
Logger.log(JSON.stringify(result, null, 2));
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
}
}
/**
* Reset the authorization state, so that it can be re-tested.
*/
function reset() {
getService().reset();
}
/**
* Configures the service.
*/
function getService()
{
return OAuth2.createService('Smartsheet')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://app.smartsheet.com/b/authorize')
.setTokenUrl('https://api.smartsheet.com/2.0/token')
// Set the client ID and secret.
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
// Set the name of the callback function that should be invoked to
// complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
// Scopes to request
.setScope('READ_SHEETS')
// Set the handler for adding Smartsheet's required SHA hash parameter to
// the payload:
.setTokenPayloadHandler(smartsheetTokenHandler);
}
/**
* Handles the OAuth callback.
*/
function authCallback(request)
{
var service = getService();
var authorized = service.handleCallback(request);
if (authorized)
{
return HtmlService.createHtmlOutput('Success!');
}
else
{
return HtmlService.createHtmlOutput('Denied.');
}
}
/**
* Adds the Smartsheet API's required SHA256 hash parameter to the access token
* request payload.
*/
function smartsheetTokenHandler(payload)
{
var codeOrRefreshToken = payload.code ? payload.code : payload.refresh_token;
var input = CLIENT_SECRET + '|' + codeOrRefreshToken;
var hash = Utilities.computeDigest(
Utilities.DigestAlgorithm.SHA_256, input, Utilities.Charset.UTF_8);
hash = hash.map(function(val)
{
// Google appears to treat these as signed bytes, but we need them
// unsigned.
if (val < 0)
{
val += 256;
}
var str = val.toString(16);
// pad to two hex digits:
if (str.length == 1)
{
str = '0' + str;
}
return str;
});
payload.hash = hash.join('');
// The Smartsheet API doesn't need the client secret sent (secret is verified
// by the hash).
if (payload.client_secret)
{
delete payload.client_secret;
}
return payload;
}
/**
* Logs the redict URI to register.
*/
function logRedirectUri()
{
Logger.log(OAuth2.getRedirectUri());
}
I don't know much about Apps Script or the library that you are using, but you need to find the actual callback URI used by Apps Script and register that as the App Redirect URL in Smartsheet. It looks like the callback should be in the form https://script.google.com/macros/d/{SCRIPT ID}/usercallback (at least according to the library docs). That should issue the redirect which will eventually call your library authCallback with the authorization code for the token.
Here's another useful document of the process (but uses Node). https://developers.smartsheet.com/blog/creating-a-smartsheet-o-auth-flow-in-node-js
This is a complicated process that I have documented here: https://smartsheet-platform.github.io/api-docs/#third-party-app-development
If you still have questions after looking at this documentation/tutorial section, please keep asking. I'm here to help.

Fetch files based on a user's email id

I am trying to fetch the files of a suspended user, using his/her email id in google apps script, I have created a service account and implemented an app script API with service account authentication for which I used the oauth2 api even though I am trying to to "impersonate" users by making api calls, I am only able to access only the public files of the user, I am trying to all the files(including private and public) files, may I know what I am doing wrong?
/**
* Entrypoint
*/
var PRIVATE_KEY = 'PPRIVATE_KEY goes in here';
var CLIENT_EMAIL = 'CLIENT_EMAIL goes in here';
function test() {
fetchUser();
}
/**
* Fetches the suspended user details from the AdminDirectory.
*/
function fetchUser() {
// Set the constant options only once.
const options = {
domain: 'xyz.com',
orderBy: 'email',
query: 'isSuspended=true',
maxResults: 500,
fields: "nextPageToken,users"
};
// Could log the options here to ensure they are valid and in the right format.
const results = [];
do {
var search = AdminDirectory.Users.list(options);
// Update the page token in case we have more than 1 page of results.
options.pageToken = search.nextPageToken;
// Append this page of results to our collected results.
if(search.users && search.users.length)
Array.prototype.push.apply(results, search.users);
} while (options.pageToken);
//Logger.log(results);
for(var k = 0; k < results.length; k++){
var fullEmail = results[k].primaryEmail;
//Logger.log(fullEmail);
fetchAllFilesOwnedByEmail(fullEmail);
}
}
/**
* Fetch all files based on the user email id
*/
function fetchAllFilesOwnedByEmail(email) {
var service = getService(email);
if (service.hasAccess()) {
var url = 'https://www.googleapis.com/drive/v2/files?pageSize=1';
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
console.log("gs test1");
Logger.log(JSON.stringify(result, null, 2));
} else {
console.log("gs test1");
Logger.log(service.getLastError());
}
const searchParams = {
corpora: 'domain',
orderBy: 'createdDate',
q: "'" + email + "' in owners",
fields: 'nextPageToken,items(id,title,mimeType,userPermission)'
};
Logger.log('searchParams:' +searchParams);
const results = [];
do {
var search = Drive.Files.list(searchParams);
if (search.items)
Array.prototype.push.apply(results, search.items);
searchParams.pageToken = search.nextPageToken;
} while (searchParams.pageToken);
//return results;
Logger.log('Files:' +results);
}
/**
* Reset the authorization state, so that it can be re-tested.
*/
function reset() {
var service = getService();
service.reset();
}
/**
* Configures the service.
*/
function getService(userEmail) {
return OAuth2.createService('GoogleDrive:' + userEmail)
// Set the endpoint URL.
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the private key and issuer.
.setPrivateKey('PRIVATE_KEY goes in here')
.setIssuer('CLIENT_EMAIL goes in here')
// Set the name of the user to impersonate. This will only work for
// Google Apps for Work/EDU accounts whose admin has setup domain-wide
// delegation:
// https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority
.setSubject(userEmail)
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scope. This must match one of the scopes configured during the
// setup of domain-wide delegation.
.setScope('https://www.googleapis.com/auth/drive');
}