Skip to content

Instantly share code, notes, and snippets.

@adamwolf
Last active March 1, 2020 03:47
Show Gist options
  • 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 = {'[email protected]':
{'oldestMessage': 'oldest_msg_in_gmail_inbox',
'readThreads': 'mail_unread_count'},
'[email protected]':
{'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 = {'[email protected]': {'oldestMessage': 'oldest_msg_in_gmail_inbox', 'readThreads': 'mail_unread_count'},
'[email protected]': {'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);
}
@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