Most efficient way to traverse Gmail attachments and download to Google Drive? - google-apps-script

Using Google Apps Script, is there a more efficient way to traverse my Gmail - picking out 'non-starred' emails that have a particular label assigned to them and then download the attachments to Google Drive?
My code works, but typically 'times out' after processing about 25 image attachments (using non-paid Gmail account)
The piece of code that does the work is as follows:
// Loop through the messages for each thread
for (var i = 0 ; i < messages.length; i++) {
for (var j = 0; j < messages[i].length; j++) {
var CurrentMsg = messages[i][j];
if (!CurrentMsg.isStarred()){
var att = CurrentMsg.getAttachments();
// If there were no attachments, create a 'dummy text file' to notify the user that there were no attachments for that email.
var MsgSubject = CurrentMsg.getSubject();
if (att.length == 0){
var file = folder.createFile(MsgSubject,'There were no attachments',MimeType.PLAIN_TEXT);
}
else{
for (var k = 0; k < att.length; k++){
var file = folder.createFile(att[k].copyBlob().getAs('image/jpeg').setName(MsgSubject));
}
}
CurrentMsg.star();
}
}
}
Any tips gratefully received!

If you want to look through certain emails only, like those that are not starred in your case, consider using the search() method. This will help you avoid looping over threads and messages you don't need.
If you need to bypass your maximum execution time, check out this answer and this article (also linked in the answer).

I would recommend limiting results via a query then using the foreach function to go through messages:
// limit the list of messages to iterate through
var query = 'has:attachment label:particular';
var results = Gmail.Users.Messages.list(userId, {q: query});
results.messages.forEach(function (m) {
var msg = GmailApp.getMessageById(m.id);
msg.getAttachments().forEach(function (a) {
var fileName = a.getName();
fileName = saveAttachmentToFolder(folder, a, fileName, msg.getDate(), input.tz);
});
});
The code snippet above is based on a Gmail add-on that I created, specifically for saving attachments to labeled folders in Drive: https://github.com/ellaqezi/archiveByLabel/blob/main/Code.gs#L24
In the label field, you can define nested directories to create in Drive e.g. foo/bar.
In the query field, you can copy the parameters as you would use them in Gmail's search bar.

Related

Using Google Apps Script to get responses from multiple forms and send an email once to each respondent

I have been puzzling this over for some time and have searched extensively, but found no solution.
I'm using Google Apps Script and I run events for a large organization and we have about 80 different registration Google Forms for these events. I want to get the registrant's email address and send them an email when they submit their form. This is easy to accomplish by setting up each individual form. Ideally, I would set up the onSubmit trigger for the form and then copy that form for each new event. However, it seems you cannot install a trigger programmatically without going to the form and running the script manually, and then authorize it. When a form is copied it also loses all its triggers. Am I wrong about this? Doing this for each form is not realistic given that events are added all the time and I have other responsibilities.
Is there no way to set a trigger for these files without running and authorizing each one?
My other solution is:
I am trying to get all the forms in a folder and then get the responses and send a single email to each registrant. This seems overly complicated and requires checking all the forms regularly since there are no triggers for the individual forms. I tried setting triggers in my spreadsheet for the forms and this works, but the number of triggers for a spreadsheet is limited to 20, so doesn't work here. Running my script every minute and then checking if an email has been sent to each respondent seems possible, but complex and possibly prone to errors...
Thanks for any help you can offer!
pgSystemTester's answer worked for me.
I added two bits of code.
One, to declare the time stamp value to zero if there wasn't one
there.
Two, the code needed a "-1" when you get dRange or you insert a new
row which each run.
function sendEmailsCalendarInvite() {
const dRange = sheet.getRange(2, registrationFormIdId, sheet.getLastRow() - 1, 2);
var theList = dRange.getValues();
for (i = 0; i < theList.length; i++) {
if (theList [i][1] == ''){
theList[i][1] = 0;
}
if (theList[i][0] != '') {
var aForm = FormApp.openById(theList[i][0]);
var latestReply = theList[i][1];
var allResponses = aForm.getResponses();
for (var r = 0; r < allResponses.length; r++) {
var aResponse = allResponses[r];
var rTime = aResponse.getTimestamp();
if (rTime > theList[i][1]) {
//run procedure on response using aForm and aResponse variables
console.log('If ran')
if (rTime > latestReply) {
//updates latest timestamp if needed
latestReply = rTime;
}
//next reply
}
}
theList[i][1] = latestReply;
//next form
}
}
//updates timestamps
dRange.setValues(theList);
}
This is probably a simple solution that will work. Setup a spreadsheet that holds all Form ID's you wish to check and then a corresponding latest response. Then set this below trigger to run every ten minutes or so.
const ss = SpreadsheetApp.getActiveSheet();
const dRange = ss.getRange(2,1,ss.getLastRow(),2 );
function loopFormsOnSheet() {
var theList = dRange.getValues();
for(i=0;i<theList.length;i++){
if(theList[i][0]!=''){
var aForm = FormApp.openById(theList[i][0]);
var latestReply = theList[i][1];
var allResponses = aForm.getResponses();
for(var r=0;r<allResponses.length;r++){
var aResponse = allResponses[r];
var rTime = aResponse.getTimestamp();
if(rTime > theList[i][1]){
//run procedure on response using aForm and aResponse variables
if(rTime >latestReply){
//updates latest timestamp if needed
latestReply=rTime;
}
//next reply
}
}
theList[i][1] = latestReply;
//next form
}
}
//updates timestamps
dRange.setValues(theList);
}

How do I resolve OAuth scopes to GSheets and in call out to a GClassroom function, in order to grab assignment data and post to GSheet?

I don't know JSON, so I'm trying to code this with GScript. I want to combine a call out to this function that gets Classroom info from a working script function that posts array info to a GSheet.
The first time I ran the script below, I triggered the API authentication and got the information I needed, although only in Logger.
var email = "my_email#something.org";
function countWork(email) {
var courseId = "valid_courseId";
var data = ""; // String of resulting information from loop below
var assignments = Classroom.Courses.CourseWork.list(courseId);
var length = assignments.courseWork.length;
// Loop to gather info
for (j = 0; j < length; j++) {
var assignment = assignments.courseWork[j];
var title = assignment.title;
var created = assignment.creationTime;
Logger.log('-->Assignment No. %s -->%s -->(%s)',j+1,title,created);
}
return data;
}
But for some reason, I can't OAuth scopes on this version of the script where I've substituted the array I need for posting to GSheet. I get the error message "Classroom is not defined (line 7...)." What do I need to do so Classroom.Courses.etc will be recognized?
var email = "my_email#something.org";
function extractAssignmentData(email) {
var courseId = "valid_courseId"; //
var data = []; // Array of resulting information from loop below
var assignments = Classroom.Courses.CourseWork.list(courseId); // error: Classroom is not defined (line 7)
var length = assignments.courseWork.length;
// Loop to gather data
for (j = 0; j < length; j++) {
var assignment = assignments.courseWork[j];
// types of information: description, creationTime, updateTime, dueDate, dueTime, workType
var title = assignment.title;
var created = assignment.creationTime;
var info = [j+1,title,created];
data.push(info);
}
return data;
}
Thanks so much, Tanaike, for your helpful responses!
Based on your suggestions, I was able to find this post, which explicitly described how to consult the manifest file, and how to incorporate scopes into the appsscript.json.
I'm still not sure why the first version of the script triggered the scope
"https://www.googleapis.com/auth/classroom.coursework.students"
while the second instead added this one:
"https://www.googleapis.com/auth/classroom.coursework.me.readonly"
But, since I now know how to add what I need and can access the Classroom info I need, it's a mute point. Thanks, again!
(I'm not sure how to mark your comment as the answer to my question -- you should get the points!)

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 substring from null

I put my first Google Apps Script together to write emails to a spreadsheet. It works fine, except of the substring method I would like to use to shorten the message body. The script engine returns "cannot call substring from null […]". I found this thread (google apps script TypeError: Cannot call method "substring" of undefined) but the solution didn't help – or I didn't understand it which is about as likely.
Here's my script.
function labelToSpreadsheet() {
var threads = GmailApp.getUserLabelByName('newaddress').getThreads();
var spreadsheet = SpreadsheetApp.openById("onehellofanID")
var sheet = spreadsheet.getSheets()[0];
Logger.log (spreadsheet.getName());
for (var i = 0; i < threads.length; i++) {
var messages = threads[i].getMessages();
for (var j = 0; j < messages.length; j++) {
var shortendContent = messages[j].getPlainBody().substring(0, 500);
sheet.appendRow([messages[j].getSubject(), messages[j].getFrom(), messages[j].getReplyTo(), messages[j].getDate(), shortendContent]);
}
}
};
This was driving me nuts, as there was no reason your code wouldn't work, and I was getting the same error. I narrowed it down to the 'getPlainBody()' was returning 'null', but couldn't figure out why this was the case even when I used GMails own example.
I was almost going to call it a bug when I realized that what was happening was that some messages were returning nothing in the body. Specifically, some companies send out newsletters that aren't text content at all, but in fact are images with their content inside (Which was the case with the first message in the test label I had, thus driving me bonkers).
So, the issue here is that the label you're running this under has some messages where the content is only an image, no text whatsoever (Or potentially, is just completely blank), thus 'getPlainBody()' returns 'There's nothing there'(Null) and you can't get a substring of nothing.
A simple 'if' statement actually handles this error really well, as you can then tell the script to write to the sheet 'The content of this message was an image' (Or whatever you want).
This slightly modified version of your code works for me:
function labelToSpreadsheet() {
var threads = GmailApp.getUserLabelByName(LABELNAME).getThreads();
var spreadsheet = SpreadsheetApp.openById(SHEETID);
var sheet = spreadsheet.getSheets()[0];
for (var i = 0; i < threads.length; i++) {
var messages = threads[i].getMessages();
for (var j = 0; j < messages.length; j++) {
if(messages[j].getPlainBody() == null){
var shortendContent = 'This message was an image';
}else{
var shortendContent = messages[j].getPlainBody().substring(0, 500);
};
sheet.appendRow([messages[j].getSubject(), messages[j].getFrom(), messages[j].getReplyTo(), messages[j].getDate(), shortendContent]);
}
}
};
I'm giving myself a gold star for this one, it was annoying to figure out.

How to share a private spreadsheet on a public site using Awesome Table gadget

I would like to run the google awesome table gadget for a public site by accessing a private sheet url. This is to protect the data, otherwise the sheet url is visible to the public and they can copy the whole sheet. I want users to get the information only through the site.
How can I accomplish this. Is there a way to run the gadget like app script where it run as myself.
Do I need to modify the gadget xml to access my private sheet like app script?
You can use a proxy script between your private sheet and the public site. This will not hide the sheet URL, but avoids the need to share the sheet itself.
In the documentation for Awesome Tables, see "Use row-level permissions", which describes how to set up a proxy script. Instead of controlling display of specific rows, however, this simple proxy will serve your entire table, while hiding the underlying spreadsheet from the public.
Set up your Awesome Table spreadsheet & gadget normally. There is no special configuration of your data required.
Deploy the Simple Proxy script as a web app.
Copy the script below into a new script in your account.
Run it once to authorize it.
Deploy it as a web app, "Execute as me", Access to "anyone, including anonymous".
Copy the public URL of the app.
On the "Advanced parameters" tab of the Awesome Table gadget, paste the public URL of your Simple Proxy into the "Apps Script Proxy URL" field.
Simple Proxy.gs
This script was adapted from Romain's original, removing the Domain-only features that provided user-level data filtering.
// Simple proxy for AwesomeTables
// Adapted from https://script.google.com/d/1UfKnjB6jcemv5-BRP-ckaI5UCoEQI2KuvFdjNzmLpyadelNLCwpvaFsO/edit
function doGet(e) {
var ssUrl = e.parameter.url;
var sheetName = e.parameter.sheet;
var a1Notation = e.parameter.range;
var sheet = SpreadsheetApp.openByUrl(ssUrl).getSheetByName(sheetName);
var range = sheet.getRange(a1Notation);
var data = range.getValues();
var dt = {cols:[], rows:[]};
for(var i = 0; i < data[0].length; i++) {
dt.cols.push({id:i, label:data[0][i] + ' ' + data[1][i], type: 'string', isNumber:true, isDate:true});
}
for(var i = 2; i < data.length; i++) {
var row = [];
for(var j = 0; j < data[i].length; j++) {
if(isNaN(data[i][j])) dt.cols[j].isNumber = false;
if(data[i][j] instanceof Date == false) dt.cols[j].isDate = false;
else if(data[i][j].getFullYear() == 1899) {
dt.cols[j].isDate = false;
data[i][j] = data[i][j].getHours()+':'+(data[i][j].getMinutes()<10?'0':'')+data[i][j].getMinutes();
}
else data[i][j] = "Date("+data[i][j].getTime()+")";
row.push({v:data[i][j]});
}
dt.rows.push({c:row});
}
for(var i = 0; i < data[0].length; i++) {
if(dt.cols[i].isDate) dt.cols[i].type = 'datetime';
else if(dt.cols[i].isNumber) dt.cols[i].type = 'number';
}
var output = e.parameters.callback + '(' + JSON.stringify({
dataTable: dt
}) + ')';
return ContentService.createTextOutput(output).setMimeType(ContentService.MimeType.JAVASCRIPT);
}
Caveats
A user will be able to view the URL of your spreadsheet in the HTML source for the page hosting Awesome Tables. If you have enabled sharing, the spreadsheet could be wide open to them.
A single Simple Proxy can serve ALL spreadsheets that your account has access to. This is both a feature and a risk that you should be aware of.