I've been using RSS2Email to turn Gmail into Google Reader.
I'd like to create a Google Apps Script to do this instead, to get two advantages:
The scheduling would be handled by Google Apps Script. (No need to have a dedicated computer to run the script.)
The resulting emails would be labeled per-feed, nicely organizing things.
Using Google Apps Script would be the same implementation spirit as e.g. Gmail Snooze.
I understand how to fetch the feeds. Where I'm stuck is how to create the emails using a specific label. The approaches I've investigated so far:
The Gmail Service for Google Apps Scripts doesn't allow this, at least not obviously.
GmailApp.sendEmail doesn't let you specify a label. Which makes sense since this is for generic email sending. But ...
GmailApp.sendEmail returns GmailApp, not something you could use to ID the message and change its label later.
The service also doesn't appear to let you create Gmail filters programatically, ruling out another possible way to tackle this.
The Gmail Migration API would be perfect to do this -- but it doesn't work for normal, consumer Gmail accounts. Gah.
Google's IMAP extensions might allow this, but it's unclear to me.
I suppose I could use 1, putting some UID in the Subject that I could use to look up the delivered message, to add the label (and hopefully un-uglify the Subject by removing the UID). But that seems a bit kludgy.
Using 3, IMAP extensions, seems possibly less kludgy, although probably much more work to code and test.
Any recommendations among these? Is there some other API or strategy?
The Google Apps Script GmailApp API does not give a way to add a label to a message as you send it, something you can do from the UI. This appears as Issue 1859 in the issue tracker, visit and star it for updates.
A work-around would be to take advantage of the thread apis. If the message is blind-copied (bcc'd) to the sender, it will also appear as a new message in their inbox, where we can use getInboxThreads() to find it. Add the label to this thread, and optionally mark it read then archive it.
Code also available as a gist.
/**
* An alternative to GmailApp.sendEmail(), which applies a
* label to the message thread in the sender's account.
*
* Sends an email message with optional arguments. The email can
* contain plain text or an HTML body. The size of the email
* (including headers, but excluding attachments) may not
* exceed 20KB.
*
* #param {String} recipient the addresses of the recipient
* #param {String} subject the subject line
* #param {String} body the body of the email
* #param {Object} options a JavaScript object that specifies
* advanced parameters, as documented
* for GmailApp.sendEmail()
* #param {String} label the label to be applied
*/
function sendAndLabel(recipient, subject, body, options, label) {
var sender = Session.getActiveUser().getEmail();
// Add sender to bcc list
if (options.bcc) {
options.bcc = options.bcc.split(',').concat(sender).join(',');
}
else {
options.bcc = sender;
}
GmailApp.sendEmail( recipient, subject, body, options )
// Check if label already exists, create if it doesn't
var newLabel = GmailApp.getUserLabelByName(label);
if (!newLabel) newLabel = GmailApp.createLabel(label);
// Look for our new message in inbox threads
Utilities.sleep(2000); // Wait for message to be received
var inboxThreads = GmailApp.getInboxThreads();
for (var t = 0; t < inboxThreads.length; t++) {
var foundSubject = inboxThreads[t].getFirstMessageSubject();
var numLabels = inboxThreads[t].getLabels().length; // Could add more criteria
if (foundSubject === subject && numLabels === 0) {
// Found our thread - label it
inboxThreads[t].addLabel(newLabel)
.markRead()
.moveToArchive();
break;
}
}
}
Example of use:
function test_sendAndLabel() {
var recipient = Session.getActiveUser().getEmail();
var subject = "Test Labelling";
var body = "This email is testing message labelling.";
// var options = {bcc:"someone#example.com"};
var options = {};
var label = "LabelTest";
sendAndLabel(recipient, subject, body, options, label);
}
yes, IMAP is the right way to go. You can find some third party IMAP lib to easy your job.
the command to add lable is : STORE 123 +X-GM-LABELS (yourLabel)
The official docs about Gmail label :
https://developers.google.com/google-apps/gmail/imap_extensions#access_to_gmail_labels_x-gm-labels
It would be convenient for GmailApp.sendEmail() to return a GmailMessage object so we can get its id and then assign a label. It doesn't, but you can use this workaround:
Create the message as a draft using GmailApp.createDraft()
Send the draft message using GmailDraft.send(). This method does return a GmailMessage object.
Assign the label (either assign it to the whole thread with GmailMessage.getThread().addLabel() or if you want to assign it to the individual message, you'll have to use the Advanced Gmail service as described here).
Related
I am making a Gmail Add-on that suggests a reply to an email in your inbox.
I am having trouble inserting the "suggestion" into the Gmail reply window.
I am inserting the suggestion using the ComposeActionResponse API. When a user clicks the "Use Suggestion" button, the following code runs:
// Fetch the currently open Gmail message in the inbox
const message = getCurrentMessage(e)
// Create a draft reply based off the suggestion
const draft = message.createDraftReply(e.parameters.suggestion);
// Insert the draft
CardService.newComposeActionResponseBuilder().setGmailDraft(draft).build()
The draft is not being inserted into the open reply window (if it is open) when the user presses the "Use Suggestion" button. It is being inserted into the user's "Drafts". So they have to go into Drafts and click on the newly created Draft message. This is a bad user experience.
Failure case
User clicks the reply button inside of a Gmail email which opens up the reply window on the left (see screenshot below).
User reads the "suggestion" provided by the Add-on and presses the USE SUGGESTION button in the add-on on the right.
The suggestion is inserted into Drafts. It does not appear in the open reply window.
Success case
User opens an email and does not click the reply button inside of a Gmail email so the reply window is not open (see screenshot below).
User reads the "suggestion" provided by the Add-on and presses the USE SUGGESTION button in the add-on on the right.
The reply window inside of Gmail opens up and the suggestion is inserted into the reply window.
Question
How can I get the suggestion to be inserted into an already opened reply window?
From https://developers.google.com/apps-script/add-ons/gmail/compose
If you want your add-on to alter a draft the user is currently viewing, extend the compose UI instead.
From the link in the above quote, more specifically from https://developers.google.com/apps-script/add-ons/gmail/extending-compose-ui#example_2
The following code snippet shows how to build a compose UI that
inserts images into the current draft email.
/**
* Compose trigger function that fires when the compose UI is
* requested. Builds and returns a compose UI for inserting images.
*
* #param {event} e The compose trigger event object. Not used in
* this example.
* #return {Card[]}
*/
function getInsertImageComposeUI(e) {
return [buildImageComposeCard()];
}
/**
* Build a card to display images from a third-party source.
*
* #return {Card}
*/
function buildImageComposeCard() {
// Get a short list of image URLs to display in the UI.
// This function is not shown in this example.
var imageUrls = getImageUrls();
var card = CardService.newCardBuilder();
var cardSection = CardService.newCardSection().setHeader('My Images');
for (var i = 0; i < imageUrls.length; i++) {
var imageUrl = imageUrls[i];
cardSection.addWidget(
CardService.newImage()
.setImageUrl(imageUrl)
.setOnClickAction(CardService.newAction()
.setFunctionName('applyInsertImageAction')
.setParameters({'url' : imageUrl})));
}
return card.addSection(cardSection).build();
}
/**
* Adds an image to the current draft email when the image is clicked
* in the compose UI. The image is inserted at the current cursor
* location. If any content of the email draft is currently selected,
* it is deleted and replaced with the image.
*
* Note: This is not the compose action that builds a compose UI, but
* rather an action taken when the user interacts with the compose UI.
*
* #param {event} e The incoming event object.
* #return {UpdateDraftActionResponse}
*/
function applyInsertImageAction(e) {
var imageUrl = e.parameters.url;
var imageHtmlContent = '<img style=\"display: block\" src=\"'
+ imageUrl + '\"/>';
var response = CardService.newUpdateDraftActionResponseBuilder()
.setUpdateDraftBodyAction(CardService.newUpdateDraftBodyAction()
.addUpdateContent(
imageHtmlContent,
CardService.ContentType.MUTABLE_HTML)
.setUpdateType(
CardService.UpdateDraftBodyType.IN_PLACE_INSERT))
.build();
return response;
}
I have a working script for creating a new Gmail draft but when I use createDraftReplyAll in an existing thread, it puts the sender of the last email of the thread as the recipient of the draft.
So if I sent the last email, I become the recipient of the reply, and the other party is Cc'd. And if the other person wrote the last email, they are the recipient but I get CC'd. This is not normal behavior compared with manually creating a draft in Gmail (i.e. from clicking Reply To All), in which case the recipient will always be the other party.
Shown here (result of running the script below):
And normal behavior from clicking Reply All in Gmail:
This is the function I use to create the draft. The thread has already been found via the Subject and original message date:
function createDraftReplyInThread(emailThread, htmlTemplateFileName) {
Logger.log(">> IN createDraftReplyInThread");
Logger.log("htmlTemplateFileName: " + htmlTemplateFileName);
if (emailThread) {
let emailTo = emailThread.to;
Logger.log('Thread TO: ' + emailTo);
let threadMessages = emailThread.getMessages();
let messageCount = threadMessages.length;
let lastMessage = threadMessages[messageCount - 1];
let htmlTemplate = HtmlService.createTemplateFromFile(htmlTemplateFileName);
let htmlBodyContent = htmlTemplate.evaluate().getContent();
var myReplyID = lastMessage.createDraftReplyAll(
"Plain text for draft reply (not used)",
{
'htmlBody': htmlBodyContent
}).getId();
Logger.log(`Draft created in thread; myReplyID: ${myReplyID}`); // just to show it's executed okay
return;
}
return null;
}
It's probably best to ignore this, but I did try to use the limited existing update methods to copy the content, change the recipient and reinstate the the content in a new draft (pure guess work). Noting that draft.update() does work to change the recipient but it also requires a Message argument which I failed to get to work. Possible?
// based off stackoverflow: https://stackoverflow.com/questions/54745999/modify-only-the-subject-and-send-the-draft
function _sandbox_draftModify() {
var draft = GmailApp.getDrafts()[0];
var draftAsMessage = draft.getMessage();
var recipients = draftAsMessage.getTo();
var subject = draftAsMessage.getSubject();
var origRaw = draftAsMessage.getRawContent();
var newRaw = origRaw.replace("new draft", "old draft");
var newRawB64 = Utilities.base64EncodeWebSafe(newRaw, Utilities.Charset.UTF_8);
var emailTo = 'recipientsemail#gmail.com';
Logger.log(`recipients: ${recipients}`);
Logger.log(`subject: ${subject}`);
Logger.log(`newRaw: ${newRaw}`);
draft.update(emailTo, subject, { message: { raw: newRawB64 } });
var draftNew = GmailApp.getDrafts()[0];
userId = 'me';
Gmail.Users.Drafts.update({ message: { raw: newRawB64 } }, userId, draftNew.getId());
Logger.log("Done");
}
In the end all I need is to be able to create a draft email to an existing thread (which I can do already) but with the correct recipient(s), however that may be achieved.
This seems to be the expected behavior for the createDraftReplyAll method.
If you check the documentation page here:
createDraftReplyAll(body)
Creates a draft message replying to the sender of the last message in this thread, using the reply-to address and all recipients of this message. The size of the email (including headers) is quota limited.
However, you can file a feature request on Issue Tracker if you would like to have a functionality which replicates the UI behavior. The form can be found at this link and make sure to fill in all the necessary information.
Reference
Apps Script GmailThread Class - createDraftReplyAll().
Does google apps script allow for handling specific gmail addresses with a '+' tag/extension in the address? I have a script to take graphic attachments and move them to a drive folder and would like to focus it to specific tags.
You can use Regex with match
Using this sample function:
function getPlusMails() {
// get all inbox threads
let threads = GmailApp.getInboxThreads()
// initializing the output array
let messagesFromPlus = []
// for each thread in inbov
let messages = threads.forEach(t => {
// get the messages
let _messages = t.getMessages()
// for each message in thread
_messages = _messages.forEach(m => {
// check if sender has + in address, if so, add to output array
if (m.getFrom().match(/\+/)) messagesFromPlus.push(m)
})
})
return messagesFromPlus
}
You can get a list of all the messages for which the sender has an address with + in it.
How it works
It uses the native JavaScript string.match() to see if a message sender address has a + in it.
if ( message.getFrom().match(/\+/) )
This will return true if there is a + and false if not. In the function above, all the threads in the inbox are queried, although you can narrow this down if you want, via search queries. Then every message in each thread is checked. If the sender address contains a + then it is appended onto a list messagesFromPlus.
You can call this from another function and then go through each message doing what you need to do with them.
References
GmailApp
Match
I'd like to completely undo any of Gmails built in category labels. This was my attempt.
function removeBuiltInLabels() {
var updatesLabel = GmailApp.getUserLabelByName("updates");
var socialLabel = GmailApp.getUserLabelByName("social");
var forumsLabel = GmailApp.getUserLabelByName("forums");
var promotionsLabel = GmailApp.getUserLabelByName("promotions");
var inboxThreads = GmailApp.search('in:inbox');
for (var i = 0; i < inboxThreads.length; i++) {
updatesLabel.removeFromThreads(inboxThreads[i]);
socialLabel.removeFromThreads(inboxThreads[i]);
forumsLabel.removeFromThreads(inboxThreads[i]);
promotionsLabel.removeFromThreads(inboxThreads[i]);
}
}
However, this throws....
TypeError: Cannot call method "removeFromThreads" of null.
It seems you can't access the built in labels in this way even though you can successfully search for label:updates in the Gmail search box and get the correct results.
The question...
How do you access the built in Gmail Category labels in Google Apps Script and remove them from an email/thread/threads?
Thanks.
'INBOX' and other system labels like 'CATEGORY_SOCIAL' can be removed using Advanced Gmail Service. In the Script Editor, go to Resources -> Advanced Google services and enable the Gmail service.
More details about naming conventions for system labels in Gmail can be found here Gmail API - Managing Labels
Retrieve the threads labeled with 'CATEGORY_SOCIAL' by calling the list() method of the threads collection:
var threads = Gmail.Users.Threads.list("me", {labels: ["CATEGORY_SOCIAL"]});
var threads = threads.threads;
var nextPageToken = threads.nextPageToken;
Note that you are going to need to store the 'nextPageToken' to iterate over the entire collection of threads. See this answer.
When you get all thread ids, you can call the 'modify()' method of the Threads collection on them:
threads.forEach(function(thread){
var resource = {
"addLabelIds": [],
"removeLabelIds":["CATEGORY_SOCIAL"]
};
Gmail.Users.Threads.modify(resource, "me", threadId);
});
If you have lots of threads in your inbox, you may still need to call the 'modify()' method several times and save state between calls.
Anton's answer is great. I marked it as accepted because it lead directly to the version I'm using.
This function lets you define any valid gmail search to isolate messages and enables batch removal labels.
function removeLabelsFromMessages(query, labelsToRemove) {
var foundThreads = Gmail.Users.Threads.list('me', {'q': query}).threads
if (foundThreads) {
foundThreads.forEach(function (thread) {
Gmail.Users.Threads.modify({removeLabelIds: labelsToRemove}, 'me', thread.id);
});
}
}
I call it via the one minute script trigger like this.
function ProcessInbox() {
removeLabelsFromMessages(
'label:updates OR label:social OR label:forums OR label:promotions',
['CATEGORY_UPDATES', 'CATEGORY_SOCIAL', 'CATEGORY_FORUMS', 'CATEGORY_PROMOTIONS']
)
<...other_stuff_to_process...>
}
Each user on the domain initiates a simple script we run for leave entitlements but we want the welcome message to be "Hi First Name," however the script doesn't seem to be able to fetch getGivenName() from getActiveUser() for a standard user.
Is there a way?
As noted in comments, and in Documentation, the UserManager Service is only accessible by Domain Administrators.
Here's an alternative. Domain Users may have themselves in their own contacts, so how about a best-effort attempt at finding themselves there?
/**
* Get current user's name, by accessing their contacts.
*
* #returns {String} First name (GivenName) if available,
* else FullName, or login ID (userName)
* if record not found in contacts.
*/
function getOwnName(){
var email = Session.getEffectiveUser().getEmail();
var self = ContactsApp.getContact(email);
// If user has themselves in their contacts, return their name
if (self) {
// Prefer given name, if that's available
var name = self.getGivenName();
// But we will settle for the full name
if (!name) name = self.getFullName();
return name;
}
// If they don't have themselves in Contacts, return the bald userName.
else {
var userName = Session.getEffectiveUser().getUsername();
return userName;
}
}
In Apps Script, I was able to get this information using the About REST API: https://developers.google.com/drive/v2/reference/about/get
var aboutData = DriveApp.About.get();
var userEmail = aboutData["user"]["emailAddress"];
var userDisplayName = aboutData["user"]["displayName"];
You can get a user name but first you have to create a domain user using the provisioning api. You can enable the API by logging in to your admin account, and select Domain settings and the User settings tab to select the checkbox enabling the Provisioning API. Read more about it here
You can then use
user = user.getgivenName()
Since the UserManager Service is only available to a Domain Administrator, you could publish a service as the administrator, that serves user's Given Names, and invoke that from the user-run script using the UrlFetchApp.
The UserName Service
Refer to the Content Service Documentation for the background information this is based upon.
The service accepts a parameter, userName, which it uses to perform a lookup as the administrator.
Paste the following code into a script, then deploy the script as a web service. This must be done by a Domain Administrator, as the service access the UserManager Service, but the script must be made accessible by all users in the domain. (Since I'm not an admin in my domain, I cannot access the UserManager, so I've included a domain-user-invokable line for testing, calling the getOwnName() function I described in my first answer.)
Remember to invoke doGet() from the debugger to go through the authorization before accessing the published service.
/**
* When invoked as a Web Service running as Domain Administrator,
* returns the GivenName of the requested user.
*
* #param {String} userName= Should be set to Session.getEffectiveUser().getUsername().
*/
function doGet(request) {
//return ContentService.createTextOutput(getOwnName()); // for testing by non-admin user
var userName = request.parameters.userName;
var givenName = UserManager.getUser(userName).getGivenName();
return ContentService.createTextOutput(givenName);
}
Invoke service using UrlFetch
Refer to Using External APIs for an explanation of how to make use of the service written in the previous section. I'll show how to access the service from another script, but remember that you can also do this from web pages within your domain.
We will use UrlFetchApp.fetch() to get our service to return the user's first name as a String.
The service was written to accept one parameter, userName, and we append this to the url, in the form userName=<string>.
With the URL built, we fetch(), then retrieve the name from the response. While this example returns just the name, you may choose to change the service to return the complete "Hello User" string.
function testService() {
var domain = "my-google-domain.com";
var scriptId = "Script ID of service";
var url = "https://script.google.com/a/macros/"+domain+"/s/"+scriptId+"/exec?"
+ "userName="+Session.getEffectiveUser().getUsername();
var response = UrlFetchApp.fetch(url);
var myName = response.getContentText();
debugger; // pause in debugger
}
Another potential way of getting the display name on a gmail account is to find a Draft email in the GmailApp, and get the From header, which may have the full name. Some drafts might be setup with no display name in gmail, in which case the From header will only be the email address, but typically the From header is in the format:
Firstname Lastname <email#domain.com>
This code should get you the string above from the first gmail Draft: (note this will probably throw an exception if there are no drafts, so check that first.)
GmailApp.getDrafts()[0].getMessage().getHeader("From")
Ref: https://developers.google.com/apps-script/reference/gmail/gmail-message#getHeader(String)
Ref: https://www.ietf.org/rfc/rfc2822.txt