Split Gmail thread and label by date in a google script - google-apps-script

Hy,
I have a server sending me several log mails by day and I want to automaticly label this mails.
I can't touch the server configuration to adapt the mail subject, so the work must be done by "receiver".
The Subject is still same so gmail merge them in a thread of 100, but I want to split them by date. So One Date, one thread. In addition, I want label them whith a nested label: "Server1" -> "Date"
I've only found a way to add label to the thread in globality and no way to split them.
Is it even possible?
After a new look on my issue, perhaps add the date at the message subject can split threads.
Like:
function AddLogSubjectADate() {
var threads = GmailApp.search('from:sender#server.com has:nouserlabels');
threads.forEach(function(messages){
messages.getMessages().forEach(function(msg){
var date = msg.getDate();
var date_of_mail = Utilities.formatDate(date, "GMT+1", "yyyy/MM/dd")
var subj = msg.getSubject()
var newsubj = subj + date_of_mail
//A way to modify subject
});
});
}
But I didn't find a way to change the subject.
Post Scriptum
I don't think it's relevant, but here is my previous work. but it add label to the thread. Like I said I haven't find a way to split threads.
function AddLogLabelbyDate() {
var today = new Date();
var tomorrow = new Date();
var yesterday = new Date();
tomorrow.setDate(today.getDate()+1);
yesterday.setDate(today.getDate()-1);
var date_today = Utilities.formatDate(today, "GMT+1", "yyyy/MM/dd")
var date_tomorrow = Utilities.formatDate(tomorrow, "GMT+1", "yyyy/MM/dd")
var date_yesterday = Utilities.formatDate(yesterday, "GMT+1", "yyyy/MM/dd")
var threads = GmailApp.search('from:sender#server.com has:nouserlabels before:'+ date_tomorrow +' after:'+ date_yesterday +'');
label.addToThreads(threads);
}

Per the API documentation, Gmail follows some rules about thread grouping:
In order to be part of a thread, a message or draft must meet the following criteria:1. The requested threadId must be specified on the Message or Draft.Message you supply with your request.2. The References and In-Reply-To headers must be set in compliance with the RFC 2822 standard.3. The Subject headers must match.
So, you can prevent the automatic grouping into a given conversation thread by modifying any of those 3 parameters.
Alternately, you can apply per-message conversation labels, though this will not really help you if you use "Conversation View" UI.
Both of these methods require the use of the Gmail REST API, for which Apps Script provides an "advanced service" client library. The native GmailApp does not provide a method for per-message thread alteration, or for manipulating messages in the manner needed.
Thread Separation
If you wanted to disable the conversation grouping, in theory you could do this:
Message#get to obtain a full message representation
Modify one of the properties Gmail uses to perform thread grouping
Message#insert or import to create the new message on the server
Message#delete to remove the original
Message#get to get the inserted message metadata, after Gmail has given it a threadId.
Get the remaining messages that should share that new threadId, modify them appropriately, and insert.
Repeat.
I haven't tested that approach, hence my "in theory" comment.
Per-message labeling
The relevant API methods include Gmail.User.Labels.list, Gmail.User.Messages.list, Gmail.User.Messages.modify, and Gmail.User.Messages.batchModify. You'll probably want to use the list and messages.batchModify methods, since you seem to have a large number of messages for which you'd like to make alterations. Note, there are non-trivial rate limits in place, so working in small batches is liable to be most resource-efficient.
This is likely to be the simplest method to implement, since you don't have to actually create or delete messages - just search for messages that should have a given label, add (or create and add) it to them, and remove any non-desired labels. To start you off, here are some minimal examples that show how to work with the Gmail REST API. I expect you will need to refer to the API documentation when you use this information to construct your actual script.
An example Labels#list:
function getLabelsWithName(labelName) {
const search = Gmail.Users.Labels.list("me");
if (!search.labels || !search.labels.length)
return [];
const matches = search.labels.filter(function (label) {
// Return true to include the label, false to omit it.
return label.name === labelName;
});
return matches;
}
An example Messages#list:
function getPartialMessagesWithLabel(labelResource) {
const options = {
labelIds: [ labelResource.id ],
fields: "nextPageToken,messages(id,threadId,labelIds,internalDate)"
};
const results = [];
// Messages#list is paginated, so we must page through them to obtain all results.
do {
var search = Gmail.Users.Messages.list("me", options);
options.pageToken = search.nextPageToken;
if (search.messages && search.messages.length)
Array.prototype.push.apply(results, search.messages);
} while (options.pageToken);
return results;
}
An example Messages#batchModify:
function batchAddLabels(messageArray, labels) {
if (!messageArray || !messageArray.length || !messageArray[0].id)
throw new Error("Missing array of messages to update");
if (!labels || !labels.length || !labels[0].id)
throw new Error("Missing array of label resources to add to the given messages");
const requestMetaData = {
"addLabelIds": labels.map(function (label) { return label.id; }),
"ids": messageArray.map(function (msg) { return msg.id; }) // max 1000 per request!
};
Gmail.Users.Messages.batchModify(requestMetaData, "me");
}
Additional Resources:
Message Searches
"fields" parameter

Related

how to use nextPageToken

I have a script that archives old classrooms, until the end of 2021 it was working fine.
In the lasts months I got an error (the script works ok, but terminate with error) and today I was investigating it, the script runs only once per month.
The error is due to a supposed change in .nextPageToken function.
var parametri = {"courseStates": "ARCHIVED"};
var page = Classroom.Courses.list(parametri);
var listaClassi = page.courses;
var xyz = page.nextPageToken;
if (page.nextPageToken !== '') {
parametri.pageToken = page.nextPageToken;
page = Classroom.Courses.list(parametri);
listaClassi = listaClassi.concat(page.courses);
};
var xyz has been added to better understand what was happening.
So, in this case the list does not have pagination, is only one page. var xyz returns "undefined", and the "if" statement results "true", this makes that variable listaClassi got appended the same content a second time. That generate the error and the abnormal end of the script.
I found an issue reported here https://issuetracker.google.com/issues/225941023?pli=1 that may be related with my problem.
Now I could change .nextPageToken with .getNextPageToken but I found no docs on the second function and many issues reporting that is not working, can anyone help me?
When using the nextPageToken value obtained to the response make sure to enter it as a separate parameter with a slightly different name. You will obtain nextPageToken in the response, the pageToken parameter needs to be entered in the request. It does look like you are doing it right, the way you add the parameter is a bit odd, yet it should be functional.
To discard problems with the Classroom API (that we can certainly take a look at) try with this simple code example in a new Google Apps Script project, remember you will need to add an Advanced service, information about advanced services can be found in this documentation article https://developers.google.com/apps-script/guides/services/advanced. Use listFiles as the main method in your Apps Script project.
function listFiles() {
var totalClasses = 0;
nextPageToken = "";
console.log("Found the following classes:")
do {
var response = loadPage(nextPageToken);
var classes = response.courses;
for (let x in classes){
console.log("Class ID: " + classes[x].id + " named: '" + classes[x].name + "'.");
}
totalClasses += classes.length;
} while (nextPageToken = response.nextPageToken)
console.log("There are " + totalClasses + " classes.")
}
function loadPage(token = ""){
return Classroom.Courses.list({
fields: 'nextPageToken,courses(id,name)',
pageSize: 10,
pageToken: token
});
}
When we first make the API call with Apps Script we don't specify a pageToken, since it is the first run we don't have one. All calls to the List method may return a nextPageToken value if the returned page contains an incomplete response.
while (nextPageToken = response.nextPageToken)
In my code at the line above once response.nextPageToken is empty (not in the response) inside the condition block JavaScript will return false, breaking the loop and allowing the code to finish execution.
To have your incident reviewed by a Google Workspace technician you can also submit a form to open a ticket with the Google Workspace API Support team at https://support.google.com/a/contact/wsdev.

Converting "Create Multiple Choice Form Question from spreadsheet" script to "Create Checkbox etc." script - error?

Question: What is causing the error in this script when replacing "asMultipleChoiceItem()" with "asCheckboxItem"? And is there an obvious way to correct it?
Short version: I am trying to implement a checkbox version of the solution (found here), by replacing "item.asMultipleChoiceItem()" with "item.asCheckboxItem()"; however, I'm encountering an error on debugging "Invalid conversion for item type: MULTIPLE_CHOICE." (debug image here). I'm having trouble troubleshooting to identify/understand, and therefore figure out how to correct, the error.
My code:
function DeptUpdate() {
// User settings
var OPTIONS_SPREADSHEET_ID = "1q21HxRkwXxVtiw7D5fuO-w0JCQtZRd-A35gRtmJUwKk";
var OPTIONS_SHEET_NAME = "Present";
var OPTIONS_RANGE = "A:A"; // We have the options, listed in column A
var itemNumber = 1; // which question on form does this apply to
//
var options2DArr = SpreadsheetApp.openById(OPTIONS_SPREADSHEET_ID).getSheetByName(OPTIONS_SHEET_NAME).getRange(OPTIONS_RANGE).getValues();
var options = options2DArr.reduce(function(a, e) {
if (e[0] != "") {
return a.concat(e[0]);
}
return a;
}, []);
var form = FormApp.openById("1JHZoCdJrsRIltMwKqWGZizRQuy-2Ak2-XET83s04goc");
var item = form.getItems()[itemNumber - 1];
item.asCheckboxItem()
.setTitle("SELECT NAME")
.setChoiceValues(options)
.showOtherOption(true);
}
Long version:
Goal: Google script in Google sheets, on trigger, updates targeted form's checklist options to reflect the items listed in the defined range (excluding blanks).
Purpose/Context: This is one part of a series of forms and spreadsheet that allow me to track arrivals, hall passes out/in, and departures from a study hall in which any 10-20 students from a pool of 120 possible may attend any given day. Spreadsheet is linked to forms to provide a "head's up display" of which students are present, and which are signed out to other locations (this all works fine). Restricting Hall Pass Out and Departure form answer choices (student names) to only those check in as "present" drastically cuts down on time and user errors in the logging system. Currently works with multiple choice, but students frequently arrive/leave in groups. Checkbox (multiple response) would further expedite the tracking process. Spreadsheet is otherwise set up to process multiple response entries; just need the form to appropriately update.
Process/Attempts: I've read of others who adjusted similar (different purpose) scripts to change from dropdown/multiple choice to checkbox without issue ("I just change this and it worked, great!" is the extent of what I've read), but as soon as I change to checkbox, I get the attached error for both the showOtherOption field, and (if that is removed), the setChoiceValues field. I'm thinking it could possibly be an issue with the checkbox item reading the array differently than the multiple choice item does? However, I haven't be able to find anything in the documentation or Q/A posts on a significant difference between the two functions parameters. At this point, I'm just a little flummoxed on what might be causing the issue.
Background: I have no formal (or significant informal) coding training, but have tweaked and adapted a variety of codes for about a decade. I understand the basic processes/concepts of code and programming logic, but lack a substantial/cohesive vocabulary.
I'm including a link to a dummy copy of the spreadsheet and the form, in case that's helpful.
Spreadsheet
Form
Thank you in advance for any insights!
Brandy
The problem is that you have a ListItem but try to convert it to a CheckboxItem
This is not directly possible. There is a feature request on Google's Public Issue Tracker for this feature. I recommend you to give it a "star" to increase visibility.
In the mean time, if you want to convert an item type, you need to do it manually by creating a new item, passing it the title and choice from the old one and deleting the old item:
Sample
function DeptUpdate() {
// User settings
var OPTIONS_SPREADSHEET_ID = "1wHE6b5ZuAKJTM4N7t6nlB5SdU9h24ueuxon4jnH_0sE";
var OPTIONS_SHEET_NAME = "Present";
var OPTIONS_RANGE = "A:A"; // We have the options, listed in column A
var itemNumber = 1; // which question on form does this apply to
//
var options2DArr = SpreadsheetApp.openById(OPTIONS_SPREADSHEET_ID).getSheetByName(OPTIONS_SHEET_NAME).getRange(OPTIONS_RANGE).getValues();
var options = options2DArr.reduce(function(a, e) {
if (e[0] != "") {
return a.concat(e[0]);
}
return a;
}, []);
var form = FormApp.getActiveForm();
var listItem = form.getItems()[itemNumber - 1];
listItem
.asListItem()
.setTitle("SELECT NAME")
.setChoiceValues(options)
var title = listItem.getTitle();
var choices = listItem.asListItem().getChoices();
var checkboxItem = form.addCheckboxItem();
checkboxItem.setTitle(title)
.setChoices(choices)
.showOtherOption(true);
form.deleteItem(listItem);
}

Newly created Google Task omits the supplied "TaskLink" property

I am trying to make a small Google Script that would automatically add Google Tasks to the "My List" TaskList after searching my GMail emails.
Everything goes fine except for adding a link to the email from which the Task is generated from. Trying to follow the API documentation doesn't really help.
This is the code for the actual task generator function:
function addTask(taskListId, myTitle, myEmailLink) {
var task = Tasks.newTask(); // effectively same as "= {}".
task.title = myTitle
task.notes = 'blank';
task.links = [{}]
task.links[0].description = 'Link to corresponding email';
task.links[0].type = 'email';
task.links[0].link = 'myEmailLink';
task = Tasks.Tasks.insert(task, taskListId);
}
Any ideas why the task I receive back has no links?
As others have noted, according to the Google Tasks API Documentation the links collection is unfortunately read-only.
As a potential work around, it appears you can add links to the notes section of a task, and the links are then directly clickable from the tasks pane in GMail.
Picture: Task with clickable link
Your function can be modified to put the link in the notes section as follows:
function addTask(taskListId, myTitle, myEmailLink) {
var task = Tasks.newTask(); // effectively same as "= {}".
task.title = myTitle
task.notes = 'link: ' + myEmailLink;
task = Tasks.Tasks.insert(task, taskListId);
}
Combining this with the getPermalink() function on the GmailApp threads object allows for grabbing a deep link to the email you are looking for.
Picture: Task with permalink to email
I'm working on a set of scripts that do some of the things you're talking about in addition to a few other things: https://github.com/tedsteinmann/gmailAutoUpdate
In my solution I have a function that grabs the GMail threads with a particular label (in my case #Task) and then creates a task setting the subject to thread.getFirstMessageSubject() and the notes to thread.getPermalink()
The entire function looks like this:
function processPending_() {
var label_pending = GmailApp.getUserLabelByName(LABEL_PENDING);
var label_done = GmailApp.getUserLabelByName(LABEL_DONE);
// The threads currently assigned to the 'pending' label
var threads = label_pending.getThreads();
// Process each one in turn, assuming there's only a single
// message in each thread
for (var t in threads) {
var thread = threads[t];
// Grab the task data
var taskTitle = thread.getFirstMessageSubject();
var taskNote = 'Email: ' + thread.getPermalink();
// Insert the task
addTask_(taskTitle, taskNote, getTasklistId_(TASKLIST));
// Set to 'done' by exchanging labels
thread.removeLabel(label_pending);
thread.addLabel(label_done);
}
// Increment the processed tasks count
Logger.log('Processed %s tasks', threads.length);
}
Per the Google Tasks API Documentation:
links[] list
Collection of links. This collection is read-only.
You cannot set these links by modifying a Task resource, i.e your code
task.links = [{}]
task.links[0].description = 'Link to corresponding email';
task.links[0].type = 'email';
task.links[0].link = 'myEmailLink';
is simply ignored by the server.
TaskLinks are, to my knowledge, unusable and non-configurable outside of the Googleplex. They may as well not exist to API users.
The only way I've been able to generate a Task that has one is by using the Gmail UI and selecting "Add to Tasks". The resulting task then includes this snippet in the last line of the Task item:

Need random unique code using Google Apps Script

I'd created a Google form for collecting data from users. In this form I'm going to ask the site name and want to replace it with Unique code.
https://docs.google.com/forms/d/1JTStfAZGFBAFjVxgH0aZlMUroVsBijC0sfOvuXvqce8/viewform?usp=send_form#start=invite
I'd written the codes with google apps script for this which is given below but every time it generate new unique code and also replace previous one also.
function SendConfirmationMail(e) {
try {
var ss, bcc, sendername, subject, columns, username;
var message, value, textbody, usermail, track, code;
// This is your email address and you will be in the BCC
bcc = Session.getActiveUser().getEmail();
// This will show up as the sender's name
sendername = "Support Team";
// This is the submitter's Name
username = e.values[1];
// This is the submitter's email address
usermail = e.values[2];
// Custom subject for Google Docs emails
subject = "Support Ticket";
// Random Code for Google Docs emails
ss = SpreadsheetApp.getActiveSheet();
track = new Array();
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
var string_length = 10;
var lastRow = ss.getLastRow()-1;
var randomstring = '';
for (var i=0; i<string_length; i++) {
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum+1);
}
track.push(randomstring);
code = ss.getRange(2, 5, lastRow, 1).setValue(track);
// This is the body of the auto-reply
message = "Hi <b>" +username+ " !!!</b> <br><br> Your message has been successfully received.<br>";
GmailApp.sendEmail(usermail, subject, messag,
{bcc: bcc, name: sendername, htmlBody: message});
} catch (e) {
Logger.log(e.toString());
}
}
But I want to generate unique code for every submission. Please update me.
I've struggled with this sort of thing before, and the complexity depends on the security it needs (as far as I can tell, this is all internal, so not much).
I'm not sure I understand why you'd put your unique code as an editable text box (I'm assuming site name is just a holdover and you would eventually either make it a non-editable div or not show it at all).
Either way, one method is to keep an array of all your previous unique codes in properties, although if this is used a lot, then that could be too much (look at the quotas, which Wolfram Alpha says is about 500 words conservatively).
So, maybe you could have a few scriptDBs (or even a spreadsheet, although it's slower) that you combine to get a final array of all your previous codes. Your randomstring algorithm could be fine (as long as you check to make sure it doesn't exist in the overarching scriptDB array).
Look at Generate random string/characters in JavaScript for other random string ideas, but just make sure (again, if this gets very popular) that there are enough new possibilities.
You don't want this hanging while looking for a new code, so I would also add a counter and do something else if it takes more than 100 iterations (if this is all internal, it might not be a bad idea to have a 5-10 character code just to keep the chances of it not finding a new string quickly much lower).

How to alternate email forwarding recipients?

I'm trying to find out if it is possible to set something up to forward emails to alternating recipients.
When I receive emails, a GMail filter tags some of them, with "elephant", for example.
There are multiple people who can handle "elephant" emails, so I want to forward each new email to one of them, cycling through the list of "elephant handlers". (Joe, Amy, and Tammy, say.) Their email addresses are available in a spreadsheet.
A
1 joe#example.com
2 amy#example.com
3 tammy#example.com
Pseudo code:
Get first unread email
Forward email to Joe
Get next unread email
Forward email to Amy
Get next unread email
Forward email to Tammy
Go to start
How can I accomplish this in Google Apps Script, so that it handles all new emails as they arrive?
It's possible to do this in a number of ways. In the world of telephony, the behavior is called a Hunt Group. Here's one suggestion that borrows that concept to get you started.
Assumptions:
You're using Gmail.
The script will belong to the same account holder as the Gmail account.
Approach:
You will set up a filter in Gmail to identify incoming messages that meet your criteria - the "elephant" emails.
In this filter, you will apply a label - "elephant" - to new emails that will be used by the script to identify "work to do".
A spreadsheet will be used to contain a script that will scan for unread messages related to the "elephant" Label and forward them.
The script will be set to Trigger on a timer event at an interval that suits you.
The spreadsheet will contain a list of (properly formatted) destination email addresses; Joe, Amy, Tammy. The script will read these and use them in order.
Once an email is processed, it will be marked as "read". You could optionally un-label, relabel, archive, or trash them.
The Script
This script keeps track of which recipient will get the next forwarded message by using ScriptProperties. If you expect very large numbers of messages, you'll need to enhance it to support getting messages in batches.
Remember to change labelName appropriately.
The script does some error checking, but doesn't validate email addresses - it's possible that messages may end up failing the forwarding operation because of that. Caveat Emptor.
This is also available as a gist.
/**
* Retrieves a given user label by name and forwards unread messages
* associated with that that label to a member of the Hunt Group.
*/
function huntGroupForward() {
// get the label for given name
var labelName = "elephant"
var label = GmailApp.getUserLabelByName(labelName);
if (label == null) throw new Error("No messages for label "+labelName);
// get count of all threads in the given label
var threadCount = label.getUnreadCount();
if (threadCount == 0) return; // quick exit if nothing to do.
var threads = label.getThreads();
var messages = [];
for (var i in threads) {
if (threads[i].isUnread()) {
messages = messages.concat( threads[i].getMessages() );
}
}
for (var i = 0; i < messages.length; i++) {
if (messages[i].isUnread()) {
messages[i].forward(nextHuntGroupMember());
messages[i].markRead();
}
}
};
/*
* Global object to store working copy of the Hunt Group
*/
var huntGroup = { next : 0, members : [] };
/*
* Get the email address of the next Hunt Group Member
* to forward a message to.
*/
function nextHuntGroupMember() {
if (huntGroup.members.length == 0) {
// Load members
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
for (var i = 0; i < data.length; i++) {
huntGroup.members.push(data[i][0])
}
// Make sure we have members
if (huntGroup.members.length == 0) {
throw new Error("Found no email addresses");
}
}
// Retrieve next index. Properties are always stored as strings, so
// we need to parse the retrieved value to use it as a Number.
var next = parseInt(ScriptProperties.getProperty("nextHuntGroupMember"));
if (next != null) {
huntGroup.next = next;
}
else {
next = 0;
}
// get next member to be used
var nextMember = huntGroup.members[next];
// ... then move on to new next (increment modulo list length)
next = ++next % huntGroup.members.length;
// store the new next value
ScriptProperties.setProperty("nextHuntGroupMember", next);
return nextMember;
}
Trigger
Once you're happy with the script, set it up to run periodically. This is how you'd set it to run every hour: