How to alternate email forwarding recipients? - google-apps-script

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:

Related

Does the following google script exactly delete the messages with certain label?

My goal is to make sure that all incoming Gmail messages from test#test.com are immediately permanently deleted.
I have created a filter that gives new messages from this address the label "deleteforever". Next, I have made a Google script that completely deletes all messages with the label "deleteforever". To be certain that no other messages are deleted, I check an extra time whether the messages really are from test#test.com. In this way, when a thread contains messages from test#test.com and messages from another address, only the messages from test#test.com should be deleted.
I plan to run it every minute. I have 3 questions:
Does this algorithm always completely delete all new messages from test#test.com?
Does this algorithm never delete a message from another sender?
Will this not cost too much runtime, assuming that test#test.com does not send many emails? (I saw that there is a limit of 1 hour per day.)
function extractEmailAddress(header) {
return header.match(/[^#<\s]+#[^#\s>]+/)[0];
}
function deleteForever() {
var threads, msgs, label1, label2, sender,message,messageId;
label1="test#test.com";
label2="Test#test.com";
threads= GmailApp.getUserLabelByName("deleteforever").getThreads();
msgs = GmailApp.getMessagesForThreads(threads);
for (var i = 0 ; i < msgs.length; i++) {
for (var j = 0; j < msgs[i].length; j++) {
message=msgs[i][j];
sender = extractEmailAddress(message.getFrom());
if (sender==label1 || sender==label2){
messageId=message.getId();
Gmail.Users.Messages.remove("me", messageId);
}
}
}
}
UPDATE
Inspired by the comment of #TheMaster, the following strategy solves the potential runtime problem:
-modify the filter in Gmail so that messages from test#test.com skip the inbox.
-hide the "deleteforever" folder
Now the script can be run every 5 minutes or with even lower frequency.
Since you are open to muting the notifications as suggested by TheMaster and running the script/function on a less frequent basis, I suggest you improve it further by using Gmail API, specifically batchDelete to improve the performance.
Script:
function deleteForever() {
label_name = 'deleteforever';
sender1 = 'do-not-reply#stackoverflow.email';
sender2 = 'forms-receipts-noreply#google.com';
// get labelId of label_name
var labelId = Gmail.Users.Labels.list("me").labels.filter(label => label.name == label_name)[0].id;
// filter messages where it has labelId and from either sender1 or sender2
var messages = Gmail.Users.Messages.list("me", {
"labelIds": labelId,
"q": `{from: ${sender1} from: ${sender2}}`
}).messages;
// if messages is not empty
if(messages){
// get ids of the messages
var ids = messages.map(message => message.id);
// bulk delete the messages
Gmail.Users.Messages.batchDelete({"ids": ids}, "me");
}
}
This will delete the accumulated message IDs by bulk where it meets the conditions:
Sender is either sender1 or sender2
Has label label_name.
Note:
You will not need extractEmailAddress anymore.

What would be the behaviour of GmailApp.createLabel when called multiple times with same label name?

I tried the following code.
function addLabel() {
console.log(GmailApp.createLabel('FOO'));
console.log(GmailApp.createLabel('FOO'));
}
After running this function, I see that there is only one label FOO and the threads which are assigned earlier to FOO are retained. And no exception thrown at runtime for 'duplicate label name'.
Is this a valid behaviour? Can it be relied upon? The Official documentation doesn't mention anything like this.
Calling GmailApp.createLabel() with a label name that already exists will return the already existing label. It will not make any changes to your existing label.
According to the documentation, the only way to create or get a label is via the label's name. Importantly, the only identifying property listed in the GmailLabel class is the name. As such, my assumption would be that Apps Script is enforcing uniqueness of names and that it's preventing overwriting of existing labels.
We can try a simple test. If the overwrite protection does not exist, then creating a new label would likely remove the association between label & email. So let's see which emails appear under a certain label, create a new label with the same name, and see if the list of emails is the same.
function test() {
var label1 = GmailApp.getUserLabelByName("test_label"); // Get existing label
var threads = label1.getThreads();
var label1_messages = [];
for (var i in threads) {
var messages = threads[i].getMessages();
for (var j in messages) {
label1_messages.push(messages[j].getId()); // Store the message IDs in label1_messages
}
}
var label2 = GmailApp.createLabel("test_label"); // Create a new label with the same name
var threads = label2.getThreads();
var label2_messages = [];
for (var i in threads) {
var messages = threads[i].getMessages();
for (var j in messages) {
label2_messages.push(messages[j].getId()); // Store the message IDs in label2_messages
}
}
Logger.log(JSON.stringify(label1_messages) == JSON.stringify(label2_messages)); // Quick, non-robust check of the arrays results in TRUE
}
The result is that they are the same, so that confirms the assumption that createLabel() is smart enough to avoid overwriting existing labels.
We can go further, though. The Gmail API clearly indicates that labels have an ID. Here again, I don't see any requirement that label names be unique (although, we can assume it given that end users can only interact with label names–it would be terrible UX if multiple with the same name existed).
If you enable the Gmail API in Advanced Google services, we can test the API requirements. Try creating a new label with the same name of a label that we already know exists.
function createLabel() {
Gmail.Users.Labels.create({name: "test_label"}, "me");
}
That results in the below error, which then confirms that label names must be unique.
API call to gmail.users.labels.create failed with error: Label name
exists or conflicts
Let's go one step further. Initially, I assumed that Apps Script was protecting against overwriting existing labels. So let's check the ID of the existing label, then call GmailApp.createLabel() with the same label name, and see if a new label was created/the label ID changed.
function finalTest() {
var response = Gmail.Users.Labels.list("me"); // Get labels
for (var i in response.labels) {
var label = response.labels[i];
if (label.name == "test_label")
Logger.log(label.id); // ID: Label_48
}
var newLabel = GmailApp.createLabel("test_label"); // Create a new label with the same name
var response = Gmail.Users.Labels.list("me"); // Get labels again to see if any difference
for (var i in response.labels) {
var label = response.labels[i];
if (label.name == "test_label")
Logger.log(label.id); // ID: Label_48
}
}
As you can see, the label ID remains the same, meaning that GmailApp.createLabel() is indeed protecting against overwriting existing labels.

Split Gmail thread and label by date in a google 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

How to prevent Google Forms from converting form input to scientific notation format

I have a simple script set up that sends emails based on Google Form entries using a script-based VLookup to get the contact emails. In some cases, Google Forms converts longer numbers entered into the form field to scientific notation. A workaround I have been using is to enter an apostrophe before the number - for some reason this keeps the cell formatted to plaintext. I would like to find a solution that does not require this extra step.
The sheet has a form with a single field, eGCs. The eGCs field can contain ANY combination of letters and numbers and may be a multi-line string. The script sends an email to the user onFormSubmit with the eGCs field entry in the body of the email. The problem arises when I try to submit a very long string that is only numbers and the form entry variable is converted to scientific notation.
I need whatever the user enters in the eGCs field to appear EXACTLY as they entered it on both the Responses 1 sheet and in the body of the email that is sent. Here is the code:
function onFormSubmit(e) {
var eGCs = e.values[1];
var email = Session.getActiveUser().getEmail();
//Replace the Google Sheets formatted line breaks with HTML line breaks so they display properly in the email:
eGCs = eGCs.replace(/\n/g, '<br>');
//Send the email:
var subject = "This is only a test";
var body = eGCs;
MailApp.sendEmail(email, subject, body, {htmlBody: body})
return
}
If I submit
6110523527643880
...into the form, the number is changed to scientific notation format and appears as 6.11052E+15 both on the sheet and in the email that is sent. If I submit a multi-line string such as:
6110523527643880
6110523527643880
6110523527643880
...then the script works fine and the form field entry is not converted (probably because Google does not consider it a number any more). I need it to appear exactly as entered whether or not the form entry is a single line or multiple lines.
Here is my example sheet / script / form. It should be public, so please feel free to test it.
Form responses in Forms (as opposed to Spreadsheets) store responses as Strings. Your trigger function could grab the response from the form to get the string as entered by the respondent.
function onFormSubmit(e) {
// Get response sheet. First version works only in contained script,
// second works even in stand-alone scripts.
// var sheet = SpreadsheetApp.getActiveSheet();
var sheet = e.range.getSheet();
// Get URL of associated form & open it
var formUrl = sheet.getParent().getFormUrl();
var form = FormApp.openByUrl(formUrl);
// Get response matching the timestamp in this event
var timestamp = new Date(e.namedValues.Timestamp);
// NOTE: There is a race condition between the updates in Forms and Sheets.
// Sometimes (often!) the Spreadsheet Form Submission trigger function is invoked
// before the Forms database has completed persisting the new Responses. As
// a result, we might get no results when asking for the most recent response.
// To work around that, we will wait and try again.
var timeToGiveUp = 0;
do {
if (timeToGiveUp > 0) Utilities.sleep(1000); // sleep 1s on subsequent tries
timeToGiveUp++;
var responses = form.getResponses(timestamp);
} while (responses.length == 0 && (timeToGiveUp < 3));
Logger.log("time to give up "+timeToGiveUp);
var response = responses[0]; // assume just one response matches timestamp
var itemResponses = response.getItemResponses();
var eGCsItemNumber = 1; // Indicates where the question appears in the form
var eGCs = itemResponses[eGCsItemNumber-1].getResponse().toString();
// You now have exactly what the respondent typed, as a string.
// It can be used as-is in an email, for example.
var body = "The user entered: "+eGCs;
MailApp.sendEmail(
Session.getActiveUser().getEmail(),
"This is only a test",
body
);
// To preserve the value in the spreadsheet, we must
// force it to remain a string by prepending a tick (')
var eGCsCol = 2;
e.range.offset(0,eGCsCol-1,1,1).setValue("'"+eGCs);
}
Note wrt Race condition comment: The actual work that was needed in this area of the code was a single line:
var responses = form.getResponses(timestamp);
While playing with this, I found that I was frequently receiving an exception, the same as noted in comments below this answer...
Cannot find method getResponses(object)
It turned out that this only happened when the function was triggered by a form submission event, not when running from the editor/debugger with simulated events. That implies that, for a short period of time, the response we're trying to handle is not returned by the call to getResponses().
Because of the way that shared documents are implemented, there is a propagation delay for any change... that's the time it takes for a change in one view of an asset to propagate to all other views.
In this situation, our trigger function has launched with a spreadsheet event, and then opens a view of the Form and tries to read the newest responses before that view contains them.
A simple work-around would be to sleep() for a period of time that would allow propagation to complete.
Utilities.sleep(5000); // 5s pause
var responses = form.getResponses(timestamp);
Simple, yes - but inefficient, because we'd be waiting even when we didn't need to. A second problem would be determining how long was long enough... and what if that changed tomorrow?
The chosen work-around will retry getting responses only if it is not successful the first time. It will only wait when doing a retry. And it won't wait forever - there's a limiting condition applied, via timeToGiveUp. (We could have added an additional check for success after the loop, but since the next statement will through an exception if we've blown through our time limit, we can let it do the dirty work.)
var timeToGiveUp = 0;
do {
if (timeToGiveUp > 0) Utilities.sleep(1000); // sleep 1s on subsequent tries
timeToGiveUp++;
var responses = form.getResponses(timestamp);
} while (responses.length == 0 && (timeToGiveUp < 3));
Lots more than one line of code, but more robust.
I am assuming eGCs is the response with the number.
e.values[2] will always come back as a string (in this case "6.15312E+16"), therefore you cannot convert that to the original number as you loose everything after that last 2. Even if you convert it, the best you can get is "61531200000000000"
Instead, you can pull the value from the spreadsheet.
In the beginning of your onFormSubmit() function add this code:
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Form Responses 1");
var eGCs = sheet.getRange(sheet.getLastRow(), 2, 1, 1).getValue();
try {
eGCs = eGCs.toFixed();
} catch (e) {
Logger.log(eGCs);
}
Your eGCs will now return as the full number if it's a number, otherwise it will return as text.
If you want the spreadsheet to have the right format when new responses are submitted add this to your code:
sheet.getRange(sheet.getLastRow(), 2, 1, 1).setNumberFormat("000");
This will convert the new row added by the form in to the correct format. This does not affect the actual value in the spreadsheet, only the way it is formatted.

Getting thread id of a mail sent through google scripts

Is it possible to get the thread id of a mail sent through MailApp.sendEmail().I want to tag the sent mail with a label just after it is sent.
MailApp.sendEmail("samplemail#gmail.com","Sellers Required for pilot",msg_to_bd);
//get thread id for this mail, say thread 1
thread1.addLabel(labll);
First, since you want to add labels to the thread you just sent, you must be using GmailApp. MailApp only allows you to send mail, not interact with the user's inbox.
As you've seen, GmailApp.sendEmail() does not return a message or thread ID. In this case, you can search for the thread you just sent, but you must account for when you've sent several messages to this person.
As long as you are not sending duplicate mails very quickly, you can rely on the fact that a call to GmailApp.search() will return threads in the same order as the web UI. So a search for 'from:me to:customer123#domain.net' might return many threads, but the first result will be the thread for the most recently sent message.
A toy example where we send a mail to a bunch of addresses listed in a tab called Recipients:
var recipients = SpreadsheetApp.getActiveSpreadsheet()
.getSheetByName('Recipients')
.getDataRange()
.getValues();
recipients.shift(); // Only if the Recipients sheet contained a header you need to remove
var d = new Date();
var dateText = d.toLocaleString(); // Pretty-printed timestamp
var newLabel = GmailApp.createLabel(dateText); // Label corresponding to when we ran this
for (var i = 0; i < recipients.length; i++) {
GmailApp.sendEmail(recipients[i], 'This is a test', 'Of the emergency broadcast system');
var sentThreads = GmailApp.search('from:me to:' + recipients[i]);
var mostRecentThread = sentThreads[0];
mostRecentThread.addLabel(newLabel);
}
Apps Script won't return the thread ID but what you can do is search for the subject in your mailbox after sending the email and apply the label to the first thread in the result.
var to="email#example.com", subject="email subject";
GmailApp.sendEmail(to,subject,msg_to_bd);
var threads = GmailApp.search("to:" + to + " in:sent subject:" + subject, 0, 1);
threads[0].addLabel(label);
Since all of the GmailApp.send* methods (at the time of writing) do not return a message or thread identifier, and since the GmailMessage object has no send* method, the safest thing to do seems like embedding a unique identifier into the message when it is sent. Then search for an email containing the unique identifier.
This code worked for me as an experiment. Note that I had to sleep() for a couple seconds in order for the search to succeed.
function tryit() {
var searchTerm = Utilities.getUuid();
GmailApp.sendEmail('address#somewhere.com', 'oh please',
'custom id: ' + searchTerm);
Utilities.sleep(2000);
var threadIds = GmailApp.search(searchTerm);
Logger.log(threadIds);
if (threadIds.length != 1) {
Browser.msgBox('Found too many threads with unique id '
+ searchTerm);
return;
}
return threadIds[0];
}
I suspect the reason we have to jump through hoops is that the API authors don't want to make sending email synchronous (maybe it can take too long), and hence they have no way to return an error or message id upon failure or success.
If you want to go completely crazy, you could send a message to yourself with the uuid, then spin in a while-sleep-search loop until you found the uuid and hence get a thread id, then reply to the thread with the full list of recipients. This guarantees that only your inbox suffers if things go wrong.
Im using this, it adds uuid hidden code into message, and I can find/label particular email with 100% precision:
var uid = Utilities.getUuid();
var uidText = '<span style="color:transparent; display:none !important; height:0; opacity:0; visibility:hidden; width:0">' + uid + '</span>';
GmailApp.sendEmail(parameters.email, subject, "", {"htmlBody":htmlEmail + uidText});
Utilities.sleep(1000);
GmailApp.search(uid)[0].addLabel(label)