// Copyright 2006 Mihai Parparita. All Rights Reserved. // ==UserScript== // @name Gmail Label Colors // @namespace http://persistent.info/greasemonkey // @description Optionally colors label names (when they have a #color suffix) // @include http://mail.google.com/* // @include https://mail.google.com/* // ==/UserScript== // Constants const LABELS_INDEX = 8; const RULES = [ ".ct .colored {-moz-border-radius: 8px; padding: 2px 4px 2px 4px; color: #fff;}" ]; // These are actually dynamic, and Firefox seems to crash if we initialize them // inline, so they are set in overrideEscape() var NO_ESCAPE_START; var NO_ESCAPE_END; // All data received from the server goes through the function top.js.P. By // overriding it (but passing through data we get), we can be informed when // new sets conversations arrive and update the display accordingly. try { if (unsafeWindow.P && typeof(unsafeWindow.P) == "function") { var oldP = unsafeWindow.P; overrideEscape(); unsafeWindow.P = function(iframe, data) { // Only override if it's a P(iframe, data) signature that we know about if (arguments.length == 2) { hookData(iframe, data); } oldP.apply(iframe, arguments); } } } catch (error) { // ignore; } function hookData(iframe, data) { var mode = data[0]; switch (mode) { // conversation data case "t": for (var i = 1; i < data.length; i++) { var conversationData = data[i]; var labels = conversationData[LABELS_INDEX]; for (var j = 0; j < labels.length; j++) { var info = getLabelColorInfo(labels[j]); if (!info.color) continue; labels[j] = NO_ESCAPE_START + '' + info.name + '' + NO_ESCAPE_END; } } break; } } // Label names pass through an escaping function, which means that we can't // use HTML to color them. We override this function and allow the use of // markers within it turn off escaping as necessary. function overrideEscape() { // We want the no escape start/end markers to be non-deterministic, so that // an attacker can't embed arbitrary HTML into an evil message. The action token // seems like a good secret string. NO_ESCAPE_START = ''; NO_ESCAPE_END = ''; // First find the function (most escaping functions invoke the replace method // of the string object and have the string "<" in them for (var propName in unsafeWindow) { var propValue = unsafeWindow[propName]; if (typeof(propValue) != "function") { continue; } var functionBody = propValue.toString(); if (functionBody.indexOf("replace") != -1 && functionBody.indexOf("<") != -1) { unsafeWindow[propName] = getEscapeClosure(propValue); break; } } } function getEscapeClosure(oldEscapeFunction) { return function(str) { var escaped = ""; var start, end; while ((start = str.indexOf(NO_ESCAPE_START)) != -1) { escaped += oldEscapeFunction(str.substring(0, start)); start += NO_ESCAPE_START.length; end = str.indexOf(NO_ESCAPE_END); escaped += str.substring(start, end); str = str.substring(end + NO_ESCAPE_END.length); } escaped += oldEscapeFunction(str); return escaped; } } function getLabelColorInfo(rawLabelName) { if (rawLabelName.indexOf(" #") != -1) { var split = rawLabelName.split(" #"); return {name: split[0], color: split[1]}; } else { return {name: rawLabelName}; } } if (document.getElementById("tbd")) { insertStyles(); } function insertStyles() { var styleNode = document.createElement("style"); document.body.appendChild(styleNode); var styleSheet = document.styleSheets[document.styleSheets.length - 1]; for (var i=0; i < RULES.length; i++) { styleSheet.insertRule(RULES[i], 0); } } function getCookie(name) { var re = new RegExp(name + "=([^;]+)"); var value = re.exec(document.cookie); return (value != null) ? unescape(value[1]) : null; }