Skip to content

Instantly share code, notes, and snippets.

@adamwolf
Last active March 1, 2020 03:47
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adamwolf/d2fe318c9de36ea3692cb2d3ea0f5952 to your computer and use it in GitHub Desktop.
Save adamwolf/d2fe318c9de36ea3692cb2d3ea0f5952 to your computer and use it in GitHub Desktop.
Integrate Gmail and Beeminder using Google Apps Script. Beemind the age of your oldest email in your inbox, or the number of unread threads.
/* Beemind Gmail, by Adam Wolf
2019/07/26
This is a Google Apps Script that lets you beemind the oldest message in your Gmail inbox in “days old”, as well as the
unread thread count.
You can share it to multiple Gmail accounts of yours, and each one will beemind to a different goal.
This is not user-friendly right now (more on this later), but it does the job for me.
## Getting Started
1. Go to https://script.google.com/home, and setup Google Apps Script if you haven’t. Create a new project. I called
mine BeemindGmail. Copy this script into the file it creates.
2. Get your Beeminder username and auth token. Your username and auth token can be found at
https://www.beeminder.com/api/v1/auth_token.json from a browser where you have logged into Beeminder. You should see
some JSON, with two keys and two values. The two values are your username and auth token. They do not include the
quotation marks. Don't share your auth token with anyone that you wouldn't give your password.
3. Put the username and auth token in the Script Properties as BEEMINDER_USER and BEEMINDER_TOKEN. You can do this
in File > Project Properties.
4. Run the function copyScriptCredentialsToUserProperties. You can do this in the top bar. Hit Save, and then there’s
a function dropdown. Select copyScriptCredentialsToUserProperties, and then hit the play button. This pulls the
username and token from the Script properties, which get copied along when you share, to User properties, which do not.
(You can delete the Script properties now, if you want, or you can wait until after we’ve done some testing!)
5. Configure the userGoalMapping. This ties the Gmail user to the Beeminder goal, and the type of datapoint.
I use this on multiple gmail accounts, so mine looks like:
var userGoalMapping = {'adamwwolf@gmail.com':
{'oldestMessage': 'oldest_msg_in_gmail_inbox',
'readThreads': 'mail_unread_count'},
'coldbloomlabs@coldbloomlabs.com':
{'oldestMessage': 'cbl_oldest_msg_in_inbox',
'readThreads': 'cbl_mail_unread_count'}
}
If you only want oldestMessage or readThreads, only include those. Remember to create the goal first!
6. Try it out! Run the function BeemindGmail using the top bar. You can see the logs in View > Logs.
7. Set this to run automatically. I have it run hourly. Do this in Edit > Current project's Triggers. I also set
mine to email me immediately if there is a script issue.
8. Delete the Script Properties if you haven’t already. This ensures that if you share this script with a buddy, you
aren’t sharing your auth token by accident.
Feel free to file an issue, contact me with problems, or even submit a fix :)
One idea is to have this submit datapoints via email, rather than the API! It would be much more user friendly.
However, this will never be able to be super user friendly, as Google has really locked down the Gmail API and requires
a pretty strict security audit for anything that goes through all your email like this. (This is good, and I'm not
complaining.)
If lots of people want this, rather than polish this up, we should talk to Beeminder themselves and let them know that
folks are interested in these metrics!
*/
var userGoalMapping = {'adamwwolf@gmail.com': {'oldestMessage': 'oldest_msg_in_gmail_inbox', 'readThreads': 'mail_unread_count'},
'coldbloomlabs@coldbloomlabs.com': {'oldestMessage': 'cbl_oldest_msg_in_inbox', 'readThreads': 'cbl_mail_unread_count'}
}
function getAllInboxThreads()
{
var out = [];
var index = 0;
const perPage = 50;
do {
page = GmailApp.getInboxThreads(index, perPage);
out = out.concat(page);
index += page.length;
} while (page.length == perPage);
return out;
}
function getOldestMessageInInbox() {
// may return null, if the inbox is empty.
//Get the oldest thread in the inbox, and get the newest message in it.
var threads = getAllInboxThreads();
if (threads.length == 0)
{
return null;
}
var messages = GmailApp.getMessagesForThread(threads[threads.length-1]);
var message = messages[messages.length-1];
return message;
}
function getReadThreadsInInbox()
{
var threads = getAllInboxThreads();
var messages = GmailApp.getMessagesForThreads(threads);
var readThreads = []; //list of subjects
for (var thread_index = 0 ; thread_index < messages.length; thread_index++) {
for (var message_index = 0; message_index < messages[thread_index].length; message_index++) {
var message = messages[thread_index][message_index];
var subject = message.getSubject();
var inInbox = message.isInInbox();
var isUnread = message.isUnread();
if (inInbox && !isUnread)
{
readThreads.push(subject);
break; // go to next thread
}
}
}
return readThreads;
}
function beemindGmail()
{
var userEmail = Session.getEffectiveUser().getEmail();
Logger.log("Beeminding Gmail for: "+ userEmail);
var userTimeZone = CalendarApp.getDefaultCalendar().getTimeZone();
if (!(userEmail in userGoalMapping)) {
throw new Error( "User " + userEmail + " not in goal mapping." );
} else
{
if ('oldestMessage' in userGoalMapping[userEmail])
{
Logger.log("Beeminding oldest message.");
oldestMessage = getOldestMessageInInbox();
var days_old = 0;
var comment;
if (oldestMessage)
{
Logger.log("Oldest message in inbox: " + oldestMessage.getDate() + ", " + oldestMessage.getSubject());
var d = oldestMessage.getDate().getTime();
var now = Date.now();
var diff = now - d;
comment = oldestMessage.getSubject();
days_old = ((now - d)/ 1000 / 60 / 60 / 24);
} else
{
Logger.log("No messages in inbox. Nice work.");
comment = "";
}
createDatapoint(userGoalMapping[userEmail]['oldestMessage'], days_old, comment);
}
if ('readThreads' in userGoalMapping[userEmail])
{
Logger.log("Beeminding read threads.");
readThreads = getReadThreadsInInbox();
Logger.log("Read threads in inbox: " + readThreads.length + " (" + readThreads + ")");
createDatapoint(userGoalMapping[userEmail]['readThreads'], readThreads.length);
}
}
Logger.log("Done.");
}
function copyScriptCredentialsToUserProperties()
{
//Run this function manually to copy the proper
var userProperties = PropertiesService.getUserProperties();
var scriptProperties = PropertiesService.getScriptProperties();
userProperties.setProperty("BEEMINDER_USER", scriptProperties.getProperty("BEEMINDER_USER"));
userProperties.setProperty("BEEMINDER_TOKEN", scriptProperties.getProperty("BEEMINDER_TOKEN"));
}
function createDatapoint(slug, value, comment) {
//TODO don't post duplicates!
var userProperties = PropertiesService.getUserProperties();
var url = "https://www.beeminder.com/api/v1/users/" + userProperties.getProperty("BEEMINDER_USER") + "/goals/" + slug + "/datapoints.json";
var data = {"value": value}
if (comment)
{
data["comment"] = comment;
}
Logger.log("Posting datapoint with data: " + JSON.stringify(data) + "to URL: " + url);
data["auth_token"] = userProperties.getProperty('BEEMINDER_TOKEN');
var payload = JSON.stringify(data);
var headers = { "Accept":"application/json",
"Content-Type":"application/json",
};
var options = {"method":"POST",
"contentType" : "application/json",
"headers": headers,
"payload" : payload
};
Logger.log(data);
var response = UrlFetchApp.fetch(url, options);
Logger.log(response);
}
@lawrenceevalyn
Copy link

I love it! I love it so much, I'd love to be able to use it to beemind the oldest emails that have particular tags or are in particular folders! I'm not sure how to add an "issue" but this comment is the official proof of my desire :)

@adamwolf
Copy link
Author

Sorry! I am not sure Github ever alerted me of this, and I'm just seeing this now. Is this still something you're interested in?

@lawrenceevalyn
Copy link

No worries about the delay. Actually, the specific use case I had in mind -- staying on top of all of the time-sensitive scheduling tasks for one of my part-time jobs -- is no longer necessary, as I no longer have that part-time job. (Whereas it's no big deal for many of my own business to wait up to a week, those emails consistently needed 48hrs or less turnaround, and it would have been nice to have Beeminder nagging me.) So the current state of this script meets all my needs now! Thank you again for writing and sharing this!

@adamwolf
Copy link
Author

adamwolf commented Mar 1, 2020

I hope things are going well, but I guess I get to check that off the list :)

@lawrenceevalyn
Copy link

Haha, yes, I wasn't fired for slow response time or anything! :p I got a fellowship that paid the bills better, and a colleague is now doing the job better than I was, so a good outcome all around I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment