GreaseMonkey hacking Gmail


i wanted to see if i could get category bundling working in gmail ala outlook… it’s always been a nice mental flow for me to carve out pending events from littering the “inbox zero” but still see them right there in front so i don’t forget to check up on them vs hidden a click away under a “folder”

update – don’t miss the “labs” functionality for doing exactly this kind of category bundling


the integration with gmail’s normal behavior isn’t perfect so this is still very experimental stages but it’s a pretty satisfying quick hack…


  • i followed a this tutorial for getting the gmail API cooking including OAUTH2…
  • the greasemonkey end of the code has some helper functions to get all the usual libraries loaded up like bootstrap, font-awesome and knockout… currently i’m only leveraging jQuery and Lobibox (love those sexy growls!)
  • tweak the code to call vs messages to see your label Id’s… they’re not the names we see in the gmail UI


obvious next nice to haves rush to mind:

  • bundle by multiple specified categories (aka labels)
  • clicking on message in new section actually does navigate to message since that was an easy gimme but it’s not as slick in the vertical split mode… i tried tracing the code that dynamically populates the side panel with the message body and it’s just to abstract, so it’ll have to be a matter of popping that in myself, but shouldn’t be too tough since we’re already loading the whole message body behind the scenes.
  • flip to real ECMAScript 2016 module loader or whatever’s clever right now… without a build engine to make things happen for script.js it seems like we’re still waiting for a fully native solution… i could do require.js approach but that seems to be on the outs already???

example screenshot



// ==UserScript==
// @name         GmailMonkey
// @namespace
// @version      0.1
// @description  try to take over the world!
// @author       Brent Anderson
// @match*
// @grant        none
// @run-at        document-end
// ==/UserScript==

//gmail api stuff//////////////////////////////////////////////////////////////////////////

function objArrayGetByProperty(array, propertyName, value) {
  var result = $.grep(array, function(e){ return e[propertyName] === value; });
  return result.length ? result[0].value : null;

function loadMessages() {
  var request ={
    "userId": "me", //nugget: special userId of me to indicate the currently authenticated user
    "labelIds": "Label_59",
    "maxResults": 200

  request.execute(function(response) {
    if (response.error) {
      Lobibox.notify("error", { size: "mini", delay: false, msg: "[] " + response.error.message });

    $.each(response.messages, function() {
      var messageRequest ={
        "userId": "me",

      messageRequest.execute(function (message) {
        var from = objArrayGetByProperty(message.payload.headers, "name", "From").replace(/"/g,"");
        var subj = objArrayGetByProperty(message.payload.headers, "name", "Subject");
        var date = formatDate(new Date(objArrayGetByProperty(message.payload.headers, "name", "Date")), "MMM DD");

        var onclick = "window.location.href += '/""'";

        mainTable.append(verticalSplit ? '\
          <tr class="zA yO apv" onclick="'+onclick+'">\
          <td colspan="2" rowspan="3"></td>\
          <td class="yX xY apy">'+from+'</td>\
          <td class="yf xY apt">'+date+'</td>\
          <td class="xY" rowspan="3"></td>\
          <tr class="zA yO apv" onclick="'+onclick+'">\
          <td colspan="2" class="xY apD" style="font-weight: bold">'+subj+'</td>\
          <tr class="zA yO apv apw" onclick="'+onclick+'">\
          <td colspan="2" class="xY apA apB y2">'+message.snippet+'</td>\
          ': '\
          <tr class="zA yO" onclick="window.location.href += \'/''\'">\
          <td colspan="4"></td>\
          <td class="xY">'+from+'</td>\
          <td colspan="2" class="xY" style="overflow: hidden">'+subj+'</td>\
          <td class="xW xY">'+date+'</td>\

//from here:
function handleAuthClick() {
    client_id: clientId,
    scope: scopes,
    immediate: false
  }, function (authResult) {
    if(authResult && !authResult.error) {
      gapi.client.load("gmail", "v1", loadMessages); // <<<<<<<<<<<<<<<<<<<<<<<
    } else {
      $("#authorize-button").on("click", handleAuthClick);
  return false;

function nextTechInit() {
  waitForIt(":2", function(found) {
    $("html, body").css("font-size", "inherit"); //override bootstrap's annoying default


    Lobibox.notify.DEFAULTS.soundPath = "//";
    Lobibox.notify.DEFAULTS.delay = 3000;
    //Lobibox.notify("error", { size: "mini", delay: false, msg: "lobibox test" });

    mainTable = $(".Cp > div > table");
    verticalSplit = mainTable.find("colgroup > col").length === 5;

    mainTable = $(mainTable).find("tbody");
      '<tr><td colspan="'+(verticalSplit?5:8)+'"><div style="margin-top: 1em" class="">Pending</div></td></tr>');


function formatDate(date, format) {
  var monthNames = [
    "January", "February", "March",
    "April", "May", "June", "July",
    "August", "September", "October",
    "November", "December"

  var day = date.getDate();
  var monthIndex = date.getMonth();
  var year = date.getFullYear();

  return format.replace("MMM", monthNames[monthIndex].slice(0,3)).replace("DD", day);


function waitForIt(elementId, then) {
  //console.log("waitForIt: " +elementId);
  var found = document.getElementById(":2");
  if ( !found ) {
    //console.log("not found!"); 
    setTimeout(function() {waitForIt(elementId, then);}, 500);

function loader(refsArray) {
  var element;
  for(var i = 0; i < refsArray.length; i++) {
    url = refsArray[i].toString();

    if (url.indexOf(".js") !== -1 || url.slice(0,8) === "function") {
      element = document.createElement("script");
      if (url.slice(0,8) === "function") { 
        element.innerHTML = url;
        continue; //nugget! inline script doesn't fire an onload event
      else element.src = url;
      if (i < refsArray.length-1) { //if we're not on the last element already, recurse on the remaining items
        var remaining = refsArray.slice(i+1);
        element.onload = function() {

    else if (url.indexOf(".css") !== -1) {
      element = document.createElement("link");
      element.rel = "stylesheet";
      element.type = "text/css";
      element.href = url;

    else throw("unexpected reference extension, expecting .css or .js or a function, but got: "+url);


//by simple convention, list all CSS first, then JS...
//each JS will subsequently load the next as a simple dependency mechanism so specify JS's in appropriate order
//further, including a function(s) will inline <script> it
  'var mainTable;\r\n'+
  'var verticalSplit = false;\r\n'+
  'var clientId = "";\r\n'+
  'var apiKey = "xxxxx";\r\n'+
  'var scopes = "";\r\n\r\n'+

Leave a Reply