Ignore same-thread emails that have different labels - google-apps-script

I am writing the Date and Subject from specific new emails to a new row of a Google Sheet.
I apply a label to the new mail items with a filter.
the script processes those labeled emails
the label is removed
A new label is applied, so that these emails won't be processed next time.
Problem: When there is a myLabel email, the script processes all emails in the same thread (eg same subject and sender) regardless of their label (even Inbox and Trash).
Question: How to only process new emails i.e. ones with the label myLabel - even when the thread of those messages extends outside the myLabel folder?
My current script:
function fetchmaildata() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('mySheetName');
var label = GmailApp.getUserLabelByName('myLabel');
var threads = label.getThreads();
for (var i = 0; i < threads.length; i++)
{
var messages = threads[i].getMessages();
for (var j = 0; j < messages.length; j++)
{
var sub = messages[j].getSubject();
var dat = messages[j].getDate();
ss.appendRow([dat, sub])
}
threads[i].removeLabel(label);
threads[i].addLabel(newlabel);
}
}
I hacked a solution for my purposes by changing my for loop to this:
for (var j = messages.length-1; j > messages.length-2; j--)
This says to process only the latest email in the thread, even when there is more than one email of a thread in the myLabel folder. Oddly, the script still changes the Labels of all the myLabel emails, but only the latest one of a thread gets written to the spreadsheet, so it works for me.
I had to make another change to the code because the above code does not run as a time-triggered scheduled task. I changed the code in this way and it now runs on a time schedule !!
//var ss = SpreadsheetApp.getActiveSpreadsheet();
var ss = SpreadsheetApp.openById("myGoogleSheetID");

A label can be on a thread due to being on a single message in said thread. Your code simply goes label -> all label threads -> all thread messages, rather than accessing only the messages in a thread with a given label. That's not really your fault - it's a limitation of the Gmail Service. There are two approaches that you can use to remedy this behavior:
The (enable-before-use "advanced service") Gmail REST API
The REST API supports detailed querying of messages, including per-message label status, with Gmail.Users.Messages.list and the labelIds optional argument. For example:
// Get all messages (not threads) with this label:
function getMessageIdsWithLabel_(labelClass) {
const labelId = labelClass.getId();
const options = {
labelIds: [ labelId ],
// Only retrieve the id metadata from each message.
fields: "nextPageToken,messages/id"
};
const messages = [];
// Could be multiple pages of results.
do {
var search = Gmail.Users.Messages.list("me", options);
if (search.messages && search.messages.length)
Array.prototype.push.apply(messages, search.messages);
options.pageToken = search.nextPageToken;
} while (options.pageToken);
// Return an array of the messages' ids.
return messages.map(function (m) { return m.id; });
}
Once using the REST API, there are other methods you might utilize, such as batch message label adjustment:
function removeLabelFromMessages_(messageIds, labelClass) {
const labelId = labelClass.getId();
const resource = {
ids: messageIds,
// addLabelIds: [ ... ],
removeLabelIds: [ labelId ]
};
// https://developers.google.com/gmail/api/v1/reference/users/messages/batchModify
Gmail.Users.Messages.batchModify(resource, "me");
}
Result:
function foo() {
const myLabel = /* get the Label somehow */;
const ids = getMessageIdsWithLabel_(myLabel);
ids.forEach(function (messageId) {
var msg = GmailApp.getMessageById(messageId);
/* do stuff with the message */
});
removeLabelFromMessages_(ids, myLabel);
}
Recommended Reading:
Advanced Services
Gmail Service
Messages#list
Message#batchModify
Partial responses aka the 'fields' parameter
Tracked Processing
You could also store each message ID somewhere, and use the stored IDs to check if you've already processed a given message. The message Ids are unique.
This example uses a native JavaScript object for extremely fast lookups (vs. simply storing the ids in an array and needing to use Array#indexOf). To maintain the processed ids between script execution, it uses a sheet on either the active workbook, or a workbook of your choosing:
var MSG_HIST_NAME = "___processedMessages";
function getProcessedMessages(wb) {
// Read from a sheet on the given spreadsheet.
if (!wb) wb = SpreadsheetApp.getActive();
const sheet = wb.getSheetByName(MSG_HIST_NAME)
if (!sheet) {
try { wb.insertSheet(MSG_HIST_NAME).hideSheet(); }
catch (e) { }
// By definition, no processed messages.
return {};
}
const vals = sheet.getSheetValues(1, 1, sheet.getLastRow(), 1);
return vals.reduce(function (acc, row) {
// acc is our "accumulator", and row is an array with a single message id.
acc[ row[0] ] = true;
return acc;
}, {});
}
function setProcessedMessages(msgObject, wb) {
if (!wb) wb = SpreadsheetApp.getActive();
if (!msgObject) return;
var sheet = wb.getSheetByName(MSG_HIST_NAME);
if (!sheet) {
sheet = wb.insertSheet(MSG_HIST_NAME);
if (!sheet)
throw new Error("Unable to make sheet for storing data");
try { sheet.hideSheet(); }
catch (e) { }
}
// Convert the object into a serializable 2D array. Assumes we only care
// about the keys of the object, and not the values.
const data = Object.keys(msgObject).map(function (msgId) { return [msgId]; });
if (data.length) {
sheet.getDataRange().clearContent();
SpreadsheetApp.flush();
sheet.getRange(1, 1, data.length, data[0].length).setValues(data);
}
}
Usage would be something like:
function foo() {
const myLabel = /* get label somehow */;
const processed = getProcessedMessages();
myLabel.getThreads().forEach(function (thread) {
thread.getMessages().forEach(function (msg) {
var msgId = msg.getId();
if (processed[msgId])
return; // nothing to do for this message.
processed[msgId] = true;
// do stuff with this message
});
// do more stuff with the thread
});
setProcessedMessages(processed);
// do other stuff
}
Recommended Reading:
Is checking an object for a key more efficient than searching an array for a string?
Array#reduce
Array#map
Array#forEach

Related

Google Apps Script API iterate/ loop through ID individually

i have an endpoint that i need to go through X number of times (dependent on how many IDs), Each call will need to assign its individual LineItem ID and bring back a JSON response.
I have tried the following code, and it seems I can call the API but can't seem to figure out how to translate the response back to my sheet, so in the case below i may have upto 10 LI ids that will need to be called up individually > results brought back> copied to last row of a particular range and then the next API call with the next LI id, etc...
function ListLI360API_Agetest(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('MySheet');
var adID = 1558211;
var LIs =sheet.getRange(2, 3, sheet.getLastRow(), 1).getValues().filter(String);
var LIArrayLength = LIs.length;
for (var i = 0; i <= LIArrayLength; i++) {
if(LIs[i]!== undefined){
var url = 'https://displayvideo.googleapis.com/v1/advertisers/'+adID+'/lineItems/'+LIs[i]+'/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions'
//Logger.log(url);
var response = callApi5 (url, 'GET');
//Logger.log(response);
var content = response.getContentText();
//Logger.log(content);
var json = JSON.parse(content);
//Logger.log(json);
var ageData = json["assignedTargetingOptions"];
//Logger.log(ageData);
var rows = [],
data;
for (i = 0; i <= ageData.length; i++) {
data = ageData[i];
rows.push([data.name]);
}
//save results to spreadsheet in the next blank column and then API for next LI ID
Logger.log(rows);
}
}//endfor
}
I seem to be getting stuck on reading the results, i have tried with the following added into the script above but i get an error
"TypeError: Cannot read property "name" from undefined", im guessing there are some nulls/ blanks being returned in the JSON and hence it cant read the length
JSON looks like...
[20-06-24 21:34:57:159 BST] {
"assignedTargetingOptions": [
{
"name": "advertisers/1558211/lineItems/36917016/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions/503004",
"assignedTargetingOptionId": "503004",
"targetingType": "TARGETING_TYPE_AGE_RANGE",
"inheritance": "NOT_INHERITED",
"ageRangeDetails": {
"ageRange": "AGE_RANGE_45_54",
"targetingOptionId": "503004"
}
},
{
"name": "advertisers/1558211/lineItems/36917016/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions/503005",
"assignedTargetingOptionId": "503005",
"targetingType": "TARGETING_TYPE_AGE_RANGE",
"inheritance": "NOT_INHERITED",
"ageRangeDetails": {
"ageRange": "AGE_RANGE_55_64",
"targetingOptionId": "503005"
}
},
{
"name": "advertisers/1558211/lineItems/36917016/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions/503006",
"assignedTargetingOptionId": "503006",
"targetingType": "TARGETING_TYPE_AGE_RANGE",
"inheritance": "NOT_INHERITED",
"ageRangeDetails": {
"ageRange": "AGE_RANGE_65_PLUS",
"targetingOptionId": "503006"
}
}
]
}
[20-06-24 21:34:57:694 BST] {
"assignedTargetingOptions": [
{
"name": "advertisers/1558211/lineItems/36917017/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions/503004",
"assignedTargetingOptionId": "503004",
"targetingType": "TARGETING_TYPE_AGE_RANGE",
"inheritance": "NOT_INHERITED",
"ageRangeDetails": {
"ageRange": "AGE_RANGE_45_54",
"targetingOptionId": "503004"
}
},
{
"name": "advertisers/1558211/lineItems/36917017/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions/503005",
"assignedTargetingOptionId": "503005",
"targetingType": "TARGETING_TYPE_AGE_RANGE",
"inheritance": "NOT_INHERITED",
"ageRangeDetails": {
"ageRange": "AGE_RANGE_55_64",
"targetingOptionId": "503005"
}
},
{
"name": "advertisers/1558211/lineItems/36917017/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions/503006",
"assignedTargetingOptionId": "503006",
"targetingType": "TARGETING_TYPE_AGE_RANGE",
"inheritance": "NOT_INHERITED",
"ageRangeDetails": {
"ageRange": "AGE_RANGE_65_PLUS",
"targetingOptionId": "503006"
}
}
]
}
From this Example there are 2 LI Ids so 2 separate outputs, i need to take parts of these outputs and print them into the spreadsheet
API function looks like...
function callApi5(url, methodType, requestBody) {
var service = getService();
if (service.hasAccess()) {
var headers = {
'Content-Type': 'application/json',
'Accept' :'application/json',
'Authorization': 'Bearer ' + getService().getAccessToken()
};
var options = {
method: methodType,
headers : headers,
muteHttpExceptions: true
};
if (requestBody) {
options.payload = requestBody;
}
return UrlFetchApp.fetch(url, options);
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
}
}
function getService() {
// 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('MyService')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId("xxxxx.apps.googleusercontent.com")
.setClientSecret("xxxxxx")
// 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())
// Set the scopes to request (space-separated for Google services).
// this is blogger read only scope for write access is:
.setScope('https://www.googleapis.com/auth/display-video')
// Below are Google-specific OAuth2 parameters.
// Sets the login hint, which will prevent the account chooser screen
// from being shown to users logged in with multiple accounts.
.setParam('login_hint', 'xxxx#xxxs.com')
// Requests offline access.
.setParam('access_type', 'offline')
// Forces the approval prompt every time. This is useful for testing,
// but not desirable in a production application.
.setParam('approval_prompt', 'force');
}
I believe your goal as follows.
You want to retrieve the values from all requests, which used the URLs created by 'https://displayvideo.googleapis.com/v1/advertisers/'+adID+'/lineItems/'+LIs[i]+'/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions', and put them to the Spreadsheet.
For this, how about this answer? From your question, I thought that your script of callApi5() works and json of var json = JSON.parse(content); is the value you showed in your question. So I would like to propose to modify the function of ListLI360API_Agetest.
Modification points:
When the array is used in the for loop, please loop from 0 to array.length - 1. Because the 1st index of array is 0. So, when for (var i = 0; i <= LIArrayLength; i++) is used, an error occurs at the last loop of LIArrayLength. In this case, please modify to for (var i = 0; i < LIArrayLength; i++). Also, this can be said for for (i = 0; i <= ageData.length; i++) {.
In your script, 1 for loop is included in the for loop. And, each loop uses the variable i. In this case, the variables of i of each loop are affected. By this, the loop cannot be correctly worked.
I think that your error of TypeError: Cannot read property "name" from undefined might be due to above 2 points.
LIs of var LIs =sheet.getRange(2, 3, sheet.getLastRow(), 1).getValues().filter(String); is 2 dimensional array. So in this case, I think that LIs[i][0] is suitable instead of LIs[i].
When above points are reflected to your script, it becomes as follows.
Modified script:
Please copy and paste the following script, and set the destination sheet name to the last line of ss.getSheetByName("###").getRange(1, 10, result.length, 1).setValues(result);.
function ListLI360API_Agetest(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('MySheet'); // Modified
var adID = 1558211;
var LIs = sheet.getRange(2, 3, sheet.getLastRow(), 1).getValues().filter(String);
var LIArrayLength = LIs.length;
var result = []; // Added
for (var i = 0; i < LIArrayLength; i++) { // Modified
if (LIs[i][0] !== undefined) { // Modified
var url = 'https://displayvideo.googleapis.com/v1/advertisers/'+adID+'/lineItems/'+LIs[i][0]+'/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions' // Modified
var response = callApi5 (url, 'GET');
var content = response.getContentText();
var json = JSON.parse(content);
var ageData = json["assignedTargetingOptions"];
for (var j = 0; j < ageData.length; j++) { // Modified
var data = ageData[j];
result.push([data.name]); // Modified
}
}
}
// Please set the destination sheet name.
ss.getSheetByName("###").getRange(1, 1, result.length, 1).setValues(result); // Added
}
If data.name is not existing, you don't want to put the values, please modify result.push([data.name]); to if (data.name) result.push([data.name]);.
Note:
In this modified script, it supposes that the structure of JSON object retrieved from each URL is the same. If the structure is different for each URL created by LIs[i][0], it is required to modify the script. Please be careful this.
I couldn't understand the result situation that the values are put to the Spreadsheet from your question. So in this modified script, the values are put to the destination sheet. If this is different from your actual situation, please modify the script.
References:
Array
getValues()
I have tested the answer provided by Tanike and modified the last part to be able to print to the spreadsheet. I have added a few more fields from JSON to test this, and finally added:
dataRange = sheet.getRange(lr+1, 17, result.length,result[0].length).setValues(result);
to print onto the spreadhseet.
function ListLI360API_Agetest(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('MySheet'); // Modified
var adID = 1558211;
var LIs = sheet.getRange(2, 3, sheet.getLastRow(), 1).getValues().filter(String);
var LIArrayLength = LIs.length;
var result = []; // Added
var lr = sheet.getRange('Q1').getDataRegion().getLastRow(); //Added
for (var i = 0; i < LIArrayLength; i++) { // Modified
if (LIs[i][0] !== undefined) { // Modified
var url = 'https://displayvideo.googleapis.com/v1/advertisers/'+adID+'/lineItems/'+LIs[i][0]+'/targetingTypes/TARGETING_TYPE_AGE_RANGE/assignedTargetingOptions' // Modified
var response = callApi5 (url, 'GET');
var content = response.getContentText();
var json = JSON.parse(content);
var ageData = json["assignedTargetingOptions"];
for (var j = 0; j < ageData.length; j++) { // Modified
var data = ageData[j];
result.push([
data.name,
data.assignedTargetingOptionId,
data.ageRangeDetails.ageRange]); // Modified
}
}
}
// Each Set of results is pushed one after another
dataRange = sheet.getRange(lr+1, 17, result.length,result[0].length).setValues(result);//Modified
}

How to check Gmail Thread for Replies from Email

I am creating a basic CRM that needs to mark when a thread has been replied to.
I have created a script that can scan my inbox for threads from a list of emails in a sheet, check the last message in each thread and collect the .getFrom in order to see if I was the last to reply.
However, I can't figure out how to check if there has been a response from the person who's been contacted throughout the whole thread.
Here's the script that checks for the last message. (It's an extract of a larger script in case any references are missing):
Example Sheet
function UpdateStatus() {
// Connect to our active sheet and collect all of our email addresses in column G
var sheet = SpreadsheetApp.getActiveSheet();
var totalRows = sheet.getLastRow();
var range = sheet.getRange(2, COLUMN_WITH_EMAIL_ADDRESSES, totalRows, 1);
var emails = range.getValues();
// Attempt to iterate through 100 times (although we'll timeout before this)
for (var cntr = 0; cntr<100; cntr++ ) {
// If we've reached the end of our last, wrap to the front
if (lastRowProcessed >= totalRows) lastRowProcessed = 1;
// Increment the row we're processing
var currentRow = lastRowProcessed+1;
// Get the email address from the current row
var email = emails[currentRow-2][0];
// If the email address field is empty, skip to the next row
if (!email) {
lastRowProcessed = currentRow;
cache.put("lastRow", currentRow, 60*60*24);
continue;
}
// Look for all threads from me to this person
var threads = GmailApp.search('from:me to:'+email);
// If there are no threads, I haven't emailed them before
if (threads.length == 0) {
// Update the spreadsheet row to show we've never emailed
var range = sheet.getRange(currentRow,13, 1, 4 ).setValues([["NEVER", "", "", ""]] );
// And carry on
lastRowProcessed = currentRow;
cache.put("lastRow", currentRow, 60*60*24); // cache for 25 minutes
continue;
}
// Beyond a reasonable doubt
var latestDate = new Date(1970, 1, 1);
var starredMsg = "";
var iReplied = ""
// Iterate through each of the message threads returned from our search
for (var thread in threads) {
// Grab the last message date for this thread
var threadDate = threads[thread].getLastMessageDate();
// If this is the latest thread we've seen so far, make note!
if (threadDate > latestDate) {
latestDate = threadDate;
// Check to see if we starred the message (we may be back to overwrite this)
if (threads[thread].hasStarredMessages()) {
starredMsg = "★";
} else {
starredMsg = "";
}
// Open the thread to get messages
var messages = threads[thread].getMessages();
// See who was the last to speak
var lastMsg = messages[messages.length-1];
var lastMsgFrom = lastMsg.getFrom();
// Use regex so we can make our search case insensitive
var re = new RegExp(email,"i");
// If we can find their email address in the email address from the last message, they spoke last
// (we may be back to overwrite this)
if (lastMsgFrom.search(re) >= 0) {
iReplied = "NO";
} else {
iReplied = "YES";
}
}

google sheet script inserting data into sheet

So i have a script the gets some data from a server.
I want my script to publish the data to a sheet name "market items".
I got this working if I'm running the script directly from the sheet by using =getMarketItemsTrigger(1).
It posts all 11,669 items to my sheet.
The problem with this is that it refreshes every time the sheet is reloaded; I need it to only run once a month.
I've been trying to create a script which needs no reference in the given sheet but posts directly to a pre-named sheet but I can't figure out how I can get the data into the sheet
this is the script file i'm using
var version = '9a'
function getMarketItemsTrigger(refresh)
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Market Items");
Logger.log(sheet.getMaxColumns());
Logger.log(sheet.getMaxRows());
sheet.clear();
if(sheet.getMaxColumns()>2){
Logger.log('deleting colums');
sheet.deleteColumns(2, sheet.getMaxColumns()-2);
}
if(sheet.getMaxRows()>2){
Logger.log('deleting rows');
sheet.deleteRows(2,sheet.getMaxRows()-1);
}
var marketItemsEndpoint = 'https://crest-tq.eveonline.com/market/types/';
var marketItemsResponse = JSON.parse(fetchUrl(marketItemsEndpoint));
var totalPages = marketItemsResponse['pageCount'];
var itemList = [];
var headers = ['Item Name', 'ID'];
itemList.push(headers);
for (var currentPage = 1; currentPage <= totalPages; currentPage++)
{
Logger.log('Processing page ' + currentPage);
var marketItems = marketItemsResponse['items'];
for (var itemReference in marketItems)
{
var item = marketItems[itemReference];
itemList.push([item['type']['name'], item['id']]);
}
if (currentPage < totalPages)
{
var nextEndpoint = marketItemsResponse['next']['href'];
marketItemsResponse = JSON.parse(fetchUrl(nextEndpoint));
}
}
//sheet.insertRows(1,itemList.length+1);
// var range = sheet.getRange(1, 1,itemList.length+1,3);
// for(var i = 1;i<itemList.length;i++){
// range.getCell(i, 1).setValue([itemList]);
// range.getCell(1, i).setValue(itemList.);
// }
// Logger.log("don");
//sheet.getRange(1, 1, 1, itemList.length).setValues(itemList);
// sheet.getRange(itemList.length+1, 2).setValues(itemList);
// sheet.getDataRange().setValues([itemList]);
// sheet.appendRow(itemList);
// sheet.getRange(12+totalPages, 1, itemList.length, 1).setValues(itemList);
return itemList;
}
/**
* Private helper method that wraps the UrlFetchApp in a semaphore
* to prevent service overload.
*
* #param {url} url The URL to contact
* #param {options} options The fetch options to utilize in the request
*/
function fetchUrl(url)
{
if (gcsGetLock())
{
// Make the service call
headers = {"User-Agent": "Google Crest Script version " + version + " (/u/nuadi #Reddit.com)"}
params = {"headers": headers}
httpResponse = UrlFetchApp.fetch(url, params);
}
return httpResponse;
}
/**
* Custom implementation of a semaphore after LockService failed to support GCS properly.
* Hopefully this works a bit longer...
*
* This function searches through N semaphores, until it finds one that is not defined.
* Once it finds one, that n-th semaphore is set to TRUE and the function returns.
* If no semaphore is open, the function sleeps 0.1 seconds before trying again.
*/
function gcsGetLock()
{
var NLocks = 150;
var lock = false;
while (!lock)
{
for (var nLock = 0; nLock < NLocks; nLock++)
{
if (CacheService.getDocumentCache().get('GCSLock' + nLock) == null)
{
CacheService.getDocumentCache().put('GCSLock' + nLock, true, 1)
lock = true;
break;
}
}
}
return lock;
}
/**
* Private helper function that will check for a new version of GCS.
*/
function versionCheck()
{
var versionEndpoint = 'https://raw.githubusercontent.com/nuadi/googlecrestscript/master/version';
var newVersion = fetchUrl(versionEndpoint);
if (newVersion != null)
{
newVersion = newVersion.getContentText().trim();
Logger.log('Current version from Github: ' + newVersion);
var message = 'You are using the latest version of GCS. Fly safe. o7';
var title = 'No updates found';
if (newVersion > version)
{
message = 'A new version of GCS is available on GitHub.';
title = 'GCS version ' + newVersion + ' available!';
}
SpreadsheetApp.getActiveSpreadsheet().toast(message, title, 120);
}
}
All the code in the function getMarketItemsTrigger that's commented out is what I have tyred without luck .
The short version of this question is how can i post the values in itemList to column a and b in sheet market items
You can write the array itemList to the sheet by adding:
//your code
ss.getSheetByName('name_of_sheet_here')
.getRange(1, 1, itemList.length, itemList[0].length)
.setValues(itemList)
//more code (if needed)
} //end of code
-->> change sheet name and range to suit.
There are two ways to do this. If you did want it to run as a custom function those have access to the script property service. You could save a time stamp in the script properties and check it every time the custom function runs.
https://developers.google.com/apps-script/reference/properties/
https://developers.google.com/apps-script/guides/sheets/functions#using_apps_script_services
The second is to create a time trigger to run the code as a cron job every month.
https://developers.google.com/apps-script/guides/triggers/installable#time-driven_triggers
https://developers.google.com/apps-script/guides/triggers/installable#managing_triggers_manually

getMessageById() slows down

I am working on a script that works with e-mails and it needs to fetch the timestamp, sender, receiver and subject for an e-mail. The Google script project has several functions in separate script files so I won't be listing everything here, but essentially the main function performs a query and passes it on to a function that fetches data:
queriedMessages = Gmail.Users.Messages.list(authUsr.mail, {'q':query, 'pageToken':pageToken});
dataOutput_double(sSheet, queriedMessages.messages, queriedMessages.messages.length);
So this will send an object to the function dataOutput_double and the size of the array (if I try to get the size of the array inside the function that outputs data I get an error so that is why this is passed here). The function that outputs the data looks like this:
function dataOutput_double(sSheet, messageInfo, aLenght) {
var sheet = sSheet.getSheets()[0],
message,
dataArray = new Array(),
row = 2;
var i, dateCheck = new Date;
dateCheck.setDate(dateCheck.getDate()-1);
for (i=aLenght-1; i>=0; i--) {
message = GmailApp.getMessageById(messageInfo[i].id);
if (message.getDate().getDate() == dateCheck.getDate()) {
sheet.insertRowBefore(2);
sheet.getRange(row, 1).setValue(message.getDate());
sheet.getRange(row, 2).setValue(message.getFrom());
sheet.getRange(row, 3).setValue(message.getTo());
sheet.getRange(row, 4).setValue(message.getSubject());
}
}
return;
};
Some of this code will get removed as there are leftovers from other types of handling this.
The problem as I noticed is that some messages take a long time to get with the getMessageById() method (~ 4 seconds to be exact) and when the script is intended to work with ~1500 mails every day this makes it drag on for quite a while forcing google to stop the script as it takes too long.
Any ideas of how to go around this issue or is this just something that I have to live with?
Here is something I whipped up:
function processEmails() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var messages = Gmail.Users.Messages.list('me', {maxResults:200, q:"newer_than:1d AND label:INBOX NOT label:PROCESSED"}).messages,
headers,
headersFields = ["Date","From","To","Subject"],
outputValue=[],thisRowValue = [],
message
if(messages.length > 0){
for(var i in messages){
message = Gmail.Users.Messages.get('me', messages[i].id);
Gmail.Users.Messages.modify( {addLabelIds:["Label_4"]},'me',messages[i].id);
headers = message.payload.headers
for(var ii in headers){
if(headersFields.indexOf(headers[ii].name) != -1){
thisRowValue.push(headers[ii].value);
}
}
outputValue.push(thisRowValue)
thisRowValue = [];
}
var range = ss.getRange(ss.getLastRow()+1, ss.getLastColumn()+1, outputValue.length, outputValue[0].length);
range.setValues(outputValue);
}
}
NOTE: This is intended to run as a trigger. This will batch the trigger call in 200 messages. You will need to add the label PROCESSED to gmail. Also on the line:
Gmail.Users.Messages.modify( {addLabelIds:["Label_4"]},'me',messages[i].id);
it shows Label_4. In my gmail account "PROCESSED" is my 4th custom label.

Google Apps Script Class GmailApp Batch Operations?

I've been fooling around with GAS for a month or so now, and I've become fairly familiar with using batch operations to read/write to/from spreadsheets (e.g. getValues(), setValues()). However, I'm currently writing a script that pulls a sizable amount of data out of Gmail using class GmailApp, my code is running very slowly (and even timing out), and I can't seem to figure out how to use batch operations for what I'm trying to do. Here's my code thus far (with the email address and name changed):
function fetchEmails(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var threads = GmailApp.search('in: label:searchedLabel');
var messages = new Array();
function Email(message){
this.date = new Date(message.getDate());
this.body = message.getBody();
}
for(var i=0;i<threads.length;i++){
for(var j=0;j<threads[i].getMessageCount();j++){
if(threads[i].getMessages()[j].getFrom()=="firstName lastName <email#domain.com>"){
var message = new Email(threads[i].getMessages()[j]);
messages.push(message);
}
}
}
}
As you can see, I'm querying my email for all threads with the given label,
making an object constructor for a custom Email object (which will have the body and date of an email as properties). Then I'm looping through each thread and when a given email matches the sender I'm looking for, I create an instance of the Email object for that email and place that Email object into an array. The goal is that in the end I'll have an array of Email objects that are all from my desired sender. However as you've probably noticed the code calls Google's APIs way too often, but I can't seem to figure out batch operations for interfacing with Gmail. Any ideas? Thanks so much.
I think you are looking for GmailApp.getMessagesForThreads().
function fetchEmails(){
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var threads = GmailApp.search('label:searchedLabel');
var messages = new Array();
function Email(message){
this.date = new Date(message.getDate());
this.body = message.getBody();
}
var gmailMessages = GmailApp.getMessagesForThreads(threads);
for(var i=0;i<thread.length;i++){
var messagesForThread = gmailMessages[i];
for(var j=0;j<messagesForThread.length;j++){
if(messagesForThread[j].getFrom()=="firstName lastName <email#domain.com>"){
var message = new Email(messagesForThread[j]);
messages.push(message);
}
}
}
}
Of course you can also write this a little more concisely (sorry, I can't turn up an opportunity to educate about the wonders of JavaScript):
function fetchEmails(){
var messages = Array.prototype.concat.apply([], GmailApp.getMessagesForThreads(
GmailApp.search('label:searchedLabel')).map(function(messagesForThread) {
return messagesForThread.filter(function(message) {
return message.getFrom() == "firstName lastName <email#domain.com>";
}).map(function(message) {
return { date: new Date(message.getDate()), body: message.getBody() };
});}));
}
This makes a grand total of 2 calls to Gmail, so it's going to be fast.
In fact, if you integrate the 'from' part into the search as was suggested above, all you need is:
function fetchEmails(){
var messages = Array.prototype.concat.apply([], GmailApp.getMessagesForThreads(
GmailApp.search('label:searchedLabel from:email#domain.com')).map(
function(messagesForThread) {
return messagesForThread.map(function(message) {
return { date: new Date(message.getDate()), body: message.getBody() };
});}));
}
Finally, since you don't actually care about the thread structure, you can just concat the arrays before the map, which leads to this:
function fetchEmails(){
var messages = GmailApp.getMessagesForThreads(
GmailApp.search('label:searchedLabel from:email#domain.com'))
.reduce(function(a, b){ return a.concat(b); })
.map(function(message) {
return { date: new Date(message.getDate()), body: message.getBody() };
});
}
(I left in the earlier samples in case you do care about the thread structure and you were just giving a minimal example).