Greasemonkey Hacks/Web Mail

From WikiContent

< Greasemonkey Hacks(Difference between revisions)
Jump to: navigation, search
(Initial conversion from Docbook)
Current revision (23:10, 11 March 2008) (edit) (undo)
(Initial conversion from Docbook)
 

Current revision

Greasemonkey Hacks


Contents

Hacks 60–66: Introduction

One of the most popular uses for the Internet in the early 1990s was email. Personal email, email-based newsletters, and email-based discussion groups all drove people onto the Internet. It was the killer app: the one feature people couldn't live without.

Then the Web exploded onto the scene, and pundits and self-proclaimed experts declared that the killer app of the Web was interactive TV. And then it was search. And then it was shopping. And then it was interactive TV again. (Somewhere in there was that whole 3D VRML craze that lasted about five minutes. Boy, that was fun…not.)

And here we are, in the year 2005. What's the killer app of the Web? What's the most impressive, most fantastic, most mind-bogglingly useful thing to hit the Web in the past 10 years? Gmail, a web-based email service. God, I love the Internet.

Force Gmail to Use a Secure Connection

Protect your inbox by automatically redirecting Gmail to an https:// address.

You can use Google's web mail service through an unsecured connection (an http:// address) or a secure connection (an https:// address). When I'm out and about and browsing the Web on an untrusted network (such as an Internet cafe), I try to remember to use the https:// address. But why bother remembering, when Greasemonkey can remember it for me?

The Code

This user script is literally one line of code. The reason it can be so small is that we configure it to run only on http://mail.google.com, the insecure address of Gmail.

Save the following user script as securewebmail.user.js:

	// ==UserScript==
	// @name          Secure web mailWebmail
	// @namespace	  http://diveintomark.org/projects/greasemonkey/
	// @description	  force webmail to use secure connection
	// @include		  http://mail.google.com/*
	// ==/UserScript==

	window.location.href = window.location.href.replace(/^http:/, 'https:');

Running the Hack

After installing the user script (Tools → Install This User Script), go to http://mail.google.com/mail. Your browser will automatically redirect to https://mail.google.com/mail. Firefox will change the background color of the location bar to pale yellow (as shown in Figure 7-1) to indicate that you are now browsing a secure site.

Figure 7-1. A secure connection to Gmail

A secure connection to Gmail

Hacking the Hack

Many online applications offer the same service on an http:// or an https:// address. This script will work unmodified on any such site. There is nothing Gmail-specific about the code itself; all it does is redirect from an http:// address to the corresponding https:// address.

If you use Yahoo! Mail instead of (or in addition to) Gmail, all you need to do is change the script's configuration to tell Greasemonkey to run the script when you visit Yahoo! Mail. Under the Tools menu, select Manage User Scripts. In the list of scripts, select Gmail Secure. You will see the current configuration of where the script should run. Under "Included pages," click Add…and type http://mail.yahoo.com/*, as shown in Figure 7-2.

Now, visit Yahoo! Mail at http://mail.yahoo.com. You will immediately be redirected to https://mail.yahoo.com, and you can sign in to Yahoo! Mail securely.

Figure 7-2. Secure Yahoo! Mail configuration

Secure Yahoo! Mail configuration

Warn Before Replying to Multiple Recipients in Gmail

Don't embarrass yourself by sending private replies to everyone.

Using any email program, it's all too easy to accidentally hit the Reply All button and end up saying something to a large group that was meant for just one person. But the problem isn't limited to the Reply All button. If there are multiple people in the To: list, it's even easier to accidentally reply to them all, because the Reply button replies to everyone by default.

The Code

This user script runs in the Compose frame of Gmail, which can be identified by its query string parameter view=cv.

Tip

Use the View Frame Info menu command in the context menu to see the URL of a <frame> or <iframe>.

The script uses the following algorithm to detect a possible reply-all snafu:

  1. Listen for all click events on the page by using document.addEventListener.
  2. If the click event originated from a Send button, check the number of recipients. Recipients are usually separated by a comma followed by a space, but since you can type any amount of space around the comma, this script uses a regular expression.
  3. If there is more than one recipient, warn the user.
  4. If the user decides not to proceed, cancel the form submission. The script accomplishes this by calling the event object's stopPropagation method, which prevents the event from bubbling up to the <form> element where it would submit the form and send the message.

Tip

You can use addEventListener at any level in the document tree. Sometimes, listening at a high level and filtering by the target property is easier than finding a specific element and attaching an event listener to it.

Save the following user script as dontreplyall.user.js:

	// ==UserScript==
	// @name		 Don't Reply-All
	// @namespace    http://youngpup.net/
	// @description  Gmailwarn before multiple repliesWarn before replying to multiple recipients in web mailGmail
	// @include      http*://mail.google.com/mail/?*&view=cv*
	// ==/UserScript==

	// based on code by Aaron Boodman
	// and included here with his gracious permission

	var recipient_separator = /\s*\,\s*/g;

	document.addEventListener("click", function(e) {
		if (e.target.id == "send") {
			var form = document.getElementById("compose_form");
			var to = removeEmptyItems(
				form.elements.namedItem('to').value.split(recipient_separator)); 
			var cc = removeEmptyItems( 
				form.elements.namedItem('cc').value.split(recipient_separator)); 
			var bcc = removeEmptyItems( 
				form.elements.namedItem('bcc').value.split(recipient_separator));

			if ((to.length + cc.length + bcc.length) > 1) {
			  if (!confirm("WARNING!\n" + 
				   "Do you really want to reply to all these people?\n\n" + 
				   "To: " + to.join(", ") + "\n" + 
				   "CC: " + cc.join(", ") + "\n" + 
				   "BCC: " + bcc.join(", "))) {
				 e.stopPropagation();
			  }
		   }
		}
	}, true);

	function removeEmptyItems(arr) {
		var result = [];
		for (var i = 0, item; item = arr[i]; i++) {
			if (/\S/.test(item)) {
			   result.push(item);
			}
		}
		
		return result;
	}

Running the Hack

After installing the user script (Tools → Install This User Script), log into Gmail at http://mail.google.com and open any message. Replace the To: field with multiple test addresses and press Send. The script will display a dialog to confirm that you want to send your message to multiple recipients, as shown in Figure 7-3.

Figure 7-3. Confirmation message before replying to multiple recipients

Confirmation message before replying to multiple recipients

If you hit OK, Gmail will send the message as usual. If you hit Cancel, you will stay in the message composition window and can edit the To: or Cc: list to trim the number of recipients.

Aaron Boodman

Warn Before Sending Gmail Messages with Missing Attachments

Never again forget to attach a file to your email.

It's too easy to forget to attach files when sending email. You send off the message, and a few minutes later you get a puzzled reply asking, "Was there supposed to be a file with this?"

Some desktop mail applications use heuristics to check for this condition and prompt you before sending your message. However, I have never seen this incorporated in a web mail application. Using Greasemonkey, we can guess with pretty good accuracy that a message was supposed to contain an attachment if it contains words such as attachment or files. If such a message actually contains no files, then we can show a warning before sending the message.

The Code

This user script runs in both the Reply and Compose frames in Gmail. These can be identified by their view=cw and view=page query string parameters.

The script listens for click events on the Send buttons. When the user clicks one of them, the script gets the contents of the message text box and scans each line of the message for any occurrences of specific keywords. Since messages can contain quoted text copied from a previous message, we intentionally ignore lines that start with >.

If we find any occurrences of attachment or files, we check whether there are any attachments. If there are no attachments, we prompt the user to confirm that she really wants to send the message.

Tip

You can access forms, form elements, images, and links by their elements' name attributes as well as their id attributes. If the element you need to modify is one of these types but doesn't have a unique id attribute, check to see if it has a unique name attribute instead.

Save the following user script as missingattachments.user.js:

	// ==UserScript==
	// @name		 Missing Attachment
	// @namespace	 http://youngpup.net/
	// @description  Warn before sending Gmail messages without attachments
	// @include      http*://mail.google.com/mail/?*view=cv*
	// @include      http*://mail.google.com/mail/?*view=page*
	// ==/UserScript==

	// based on code by Aaron Boodman
	// and included here with his gracious permission

	// add more keywords here if necessary
	var words = ["attach", "attachment", "attached", "file", "files"];

	// creates a regex like of the form /\b(foo|bar|baz)\b/i
	var regex = new RegExp("\\b(" + words.join("|") + ")\\b", "i");

	var form = document.getElementById("compose_form");

	document.addEventListener("click", function(e) { 
		if (e.target.id != "send") { return true; } 
		var allLines = form.elements.namedItem('msgbody').value.split("\n"); 
		for (var i = 0, line; line = allLines[i]; i++) {
			// by convention, reply lines start with ">". Some people like
			// to be clever and use other characters. If you encounter this,
			// you can test for those characters as well.
			if (line[0] == ">") { continue; }
			if (!line.match(regex)) { continue; }
			if (isFileAttached()) { continue; }
			if (!window.confirm("Gmailwarn before sending without attachmentsWARNING\n\n" +
				"This message mentions attachments, but none " +
				"are included.\n\n" +
				"Really send?\n\n" +
				"Suspicious line:\n" +
				"\"" + line + "\"")) {
				e.stopPropagation();
			}
			break;
		}
	}, true);

	function isFileAttached() {
		var iter = document.evaluate(".//input[@type='file']",
			form, null, XPathResult.ANY_TYPE, null);
		var input;
		while (input = iter.iterateNext()) {
			if (input.value != "") {
				return true;
			}
		}
		return false;
	}

The word-boundary regular expression assertion \b is the best way to tell the difference between foo and foobar. The word boundary matches when a word character (a-z, A-Z, 0-9, -and _) is preceded or followed by a nonword character.

It's fast and easy to create regular expressions in JavaScript using the literal form (i.e., /foobar/). But you might need to construct an expression dynamically, from a string that you don't know beforehand. To do this, create an instance of the RegExp object, which takes a string argument. This creates an additional problem: backslashes have special meaning inside JavaScript strings. To insert a backslash in the regular expression defined as a string, you need two backslashes. So, /hello\?/ becomes "hello\\?", and /c:\\/ becomes "c:\\\\".

Running the Hack

After installing the user script (Tools → Install This User Script), log into Gmail at http://mail.google.com and start a new message. Add some text to the message body, such as, "Hi Bob, the spreadsheet you requested is attached. Please review." Press the Send button without attaching any files. The script pops up a dialog to confirm that you really intended to send the message without any attachments, as shown in Figure 7-4.

Figure 7-4. Confirming sending without attachments

Confirming sending without attachments

If you click OK, Gmail will send the message as usual. If you click Cancel, you will stay on the message composition page, and you can click "Attach a file."

Aaron Boodman

Compose Your Mail in Gmail

Make mailto: links open the Gmail compose page.

The Web comprises many kinds of resources: web pages, newsgroups, IRC channels, FTP sites, and so on. Each kind of resource has a scheme, such as the http: in http://mozilla.org or the irc: in irc://irc.mozilla.org/firefox.

You've probably seen mailto: links on contact pages; when you click the link, it launches an external email program.

But what if you use a web mail service such as Gmail? Normally, getting mailto: links to launch a web-based email application is nontrivial. You would basically need to write an external mail program that switched back to your browser and opened the appropriate URL. What a pain! This hack solves the problem another way, by rewriting mailto: links to point to the Gmail Compose page.

The Code

This user script runs on all pages. From a high-level view, it sounds deceptively simple. Just find all the mailto: links, parse them, and replace them with links to Gmail. When you click the link, the browser just redirects to the Gmail Compose page instead of launching a separate application.

Of course, it's not really that simple. The problem is that mailto: links can be complex. RFC 2368, entitled "The mailto URL scheme," specifies the format. The overall structure is mailto:<recipient>?<querystring>, where <querystring> is a list of <name>=<value> pairs separated by ampersands (&). We want to pass these name/value pairs to Gmail, but, of course, Gmail's Compose page uses a different syntax for encoding them in the URL. So, we need to parse them out and map them individually.

The most important values that we want to transfer to Gmail are the To: and Cc: recipients, the subject line, and the email body. All of these can be encoded in that simple-looking mailto: link! The script parses them out one by one, stores them temporarily, and then uses them to construct the URL of the Gmail Compose page.

One last thing worth mentioning: this script intentionally delays its own processing of the page to take place after the page is loaded, by hooking into the window's onload event. Many sites use JavaScript to write out mailto: links (to protect them from spammers). By the time the onload event fires, the mailto: links should be set up and available to use in the DOM.

Save the following user script as mailto-compose-in-gmail.user.js:

	// ==UserScript==
	// @name         Mailto Compose In Gmail
	// @namespace    http://blog.monstuff.com/archives/cat_greasemonkey.html
	// @description  Rewrites "mailto: linksmailto:" links to Gmail compose links
    // @include		 *
	// ==/UserScript==

	// based on code by Julien Couvreur
	// and included here with his gracious permission

	function web mailprocessMailtoLinks() {
		var xpath = "//a[starts-with(@href,'mailto: linksmailto:')]";
		var res = document.evaluate(xpath, document, null,
			XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

		var linkIndex, mailtoLink;
		for (linkIndex = 0; linkIndex < res.snapshotLength; linkIndex++) {
			mailtoLink = res.snapshotItem(linkIndex);

			var href = mailtoLink.href;
			var matches = href.match(/^mailto:([^\?]*)(\?([^?]*))?/);
			var emailTo, params, emailCC, emailSubject, emailBody;

			emailTo = matches[1];
			params = matches[3];
			if (params) {
				var splitQS = params.split('&');
				var paramIndex, param;

				for (paramIndex = 0; paramIndex < splitQS.length; paramIndex++)
	 {
				param = splitQS[paramIndex];
				nameValue = param.match(/([^=]+)=(.*)/);
				if (nameValue && nameValue.length == 3) {
				switch(nameValue[1]) {
				case "to":
					emailTo += "%2C%20" + nameValue[2];
					break;
				case "cc":
					emailCC = nameValue[2];
					break;
				case "subject":
					emailSubject = nameValue[2];
					break;
				case "body":
					emailBody = nameValue[2];
					break;
				}
				}
				}
			}

			var newUrl = "https://mail.google.com/mail?view=cm&tf=0" +
				(emailTo ? ("&to=" + emailTo) : "") +
				(emailCC ? ("&cc=" + emailCC) : "") +
				(emailSubject ? ("&su=" + emailSubject) : "") +
				(emailBody ? ("&body=" + emailBody) : "");

			mailtoLink.href = newUrl;
			}
		}

		window.addEventListener("load", web mailprocessMailtoLinks, false);

Running the Hack

Before installing this script, go to http://blog.monstuff.com and hover your cursor over the link on the left titled "Contact me." You will see a mailto: address in the status bar, as shown in Figure 7-5.

Figure 7-5. A mailto: link

A mailto: link

Now, install the script (Tools→Install This User Script), and refresh the page. Again, hover your cursor over the "Contact me" link, and you will see that the URL has been changed to point to Gmail, as shown in Figure 7-6.

Figure 7-6. A "mailto:" link transformed to Gmail

A "mailto:" link transformed to Gmail

Hacking the Hack

Rewriting mailto: links to regular http: links has one disadvantage. Normally, Firefox lets you right-click on mailto: links and select "Copy email address" from the context menu, but this hack interferes with that feature by turning the mailto: link into something else.

One solution is not to rewrite the link, but attach an onclick event handler. This lets you copy the email address using Firefox's context menu, but you would still redirect to Gmail when you actually click the link.

This works, except for one thing: it won't let you open the Gmail Compose page in a new tab. But we can solve that problem with Greasemonkey, too. One of the new features in Greasemonkey 0.5 is the GM_openInTab function, which does exactly what it sounds like.

In the previous script, replace this line:

	mailtoLink.href = newUrl;

with this code snippet:

	mailtoLink.addEventListener('click', function(e) {
		GM_openInTab(newUrl);
		e.preventDefault();
	}

When you click the link, Greasemonkey will open the Gmail Compose page in a new tab. You can still right-click and select items from the regular mailto: context menu, including "Copy email address."

Julien Couvreur

Add a Delete Button to Gmail

Improve the Gmail interface with the most requested feature that Google doesn't want you to have.

Many netizens have gotten a Gmail account and been wowed with the interface, but noticed one critical missing piece: there is no easy way to delete a message. Deletion in the standard Gmail interface requires opening the drop-down actions box and selecting the last item, which is cumbersome for a frequent activity.

This hack alters the user interface of Gmail to include an extra button that lets the user delete any message with one simple click.

The Code

This script is split into five parts.

  1. The _gd_gmail_delete function performs the actual message deletion. It simply searches for a <select> element within its parent container and then finds the appropriate action within that menu. If it is found, the script triggers the onchange handler of the select box, which launches the standard Gmail code to delete the selected messages.
  2. The _gd_make_dom_button function is notable because it supports multiple languages. There is only one word that the script adds, but this function attempts to autodetect the language in the Gmail interface, and sets the Delete button caption to the proper translation. It also attaches the _gd_gmail_delete function as the onclick event handler for the button.
  3. The _gd_insert_button function does the real magic in this script. It calls the _gd_make_dom_button function to create a new element to be injected into the page, does a little checking for the most attractive place to put it, and injects the element into the page.
  4. The _gd_place_delete_buttons function is the driver function. It calls the _gd_element function to check for the four places where it might be appropriate to place the Delete button. The best thing to look for is the drop-down actions menu, which is tagged with one of four IDs, depending on the page. If we find the drop-down menu, we call the _gd_insert_button function with a reference to the container of that box.
  5. Finally, the main block of the script checks for the document.location.search element. The Gmail interface is split into multiple frames, but the frame where all the user interaction occurs will always contain a search box. If it makes sense to add a Delete button on the current page, we call the _gd_place_delete_buttons function to add it. Because of the way Gmail constantly re-creates parts of its user interface, we also need to register an event handler to re-add our Delete button after all mouse and keyboard actions.

Save the following user script as gmail-delete-button.user.js:

	// ==UserScript==
	// @name		 Gmail Delete Button
	// @namespace    http://www.arantius.com/
	// @description  Add a "Delete" button to Gmail's interface
	// @include      http*://mail.google.com/*mail*?*
	// ==/UserScript==

	// based on code by Anthony Lieuallen
	// and included here with his gracious permission
	// http://www.arantius.com/article/arantius/gmail+delete+button/
	
	function _gd_element(id) {
		try {
			var el=document.getElementById(id);
		} catch (e) {
			GM_log(
				"Gmail Delete Button:\nThere was an error!\n\n"+
				"Line: "+e.lineNumber+"\n"+
				e.Gmailadding a delete buttonname+": "+e.message+"\n"
			);
			return false;
			
		}
		if (el) return el;
		return false;
	}

	function _gd_web mailgmail_delete(delete_button) {
		//find the command box
		var command_box = delete_button.parentNode.
	getElementsByTagName('select')[0];
		var real_command_box = command_box.wrappedJSObject || command_box;
		real_command_box.onfocus();

		//find the command index for 'move to trash'
		var delete_index=-1;
		for (var i=0; i<command_box.options.length; i++) {
			if ('tr'==command_box.options[i].value &&
				!command_box.options[i].disabled ) {
				delete_index=i;
				break;
			}
		}
		//don't try to continue if we can't move to trash now
		if (-1==delete_index) {
			var box=_gd_element('nt1'); if (box) {
				try {
				//if we find the box put an error message in it
				box.firstChild.style.visibility='visible';
				box.getElementsByTagName('td')[1].innerHTML= '' +
				'Could not delete. Make sure at least one ' +
				'conversation is selected.';
				} catch (e) {}
			}
			return;
		}
		
		//set the command index and fire the change event
		command_box.selectedIndex=delete_index;
		real_command_box = command_box.wrappedJSObject || command_box;
		real_command_box.onchange();
	}
	function _gd_make_dom_button(id) {
		var delete_button= document.createElement('button');
		delete_button.setAttribute('class', 'ab');
		delete_button.setAttribute('id', '_gd_delete_button'+id);
		delete_button.addEventListener('click', function() {
			_gd_gmail_delete(delete_button);
		}, true);
		//this is Gmailadding a delete buttona little hack-y, but we can find the language code here
		var lang='';
		try {
			var urlToTest=window.top.document.getElementsByTagName('frame')[1].
	src;
			lang=urlToTest.match(/html\/([^\/]*)\/loading.html$/)[1];
		} catch (e) {}
		//now check that language, and set the button text
		var buttonText='Delete';
		switch (lang) {
		case 'it': buttonText='Elimina'; break;
		case 'es': buttonText='Borrar'; break;
		case 'fr': buttonText='Supprimer'; break;
		case 'pt-BR': buttonText='Supressão'; break;
		case 'de': buttonText='Löschen'; break;
		}

		delete_button.innerHTML='<b>'+buttonText+'</b>';
		return delete_button;
	}

	function _gd_insert_button(insert_container, id) {
		if (!insert_container) return false;
		if (_gd_element('_gd_delete_button'+id)) {
			return false;
		}
		
		//get the elements
		var spacer, delete_button;
		delete_button=_gd_make_dom_button(id);
		spacer=insert_container.firstChild.nextSibling.cloneNode(false);

		//pick the right place to put them, depending on which page we're on
		var insert_point=insert_container.firstChild;
		if (2==id || 3==id) {
			// 2 and 3 are inside the message and go at a different place
			insert_point=insert_point.nextSibling.nextSibling;
		}
		if (document.location.search.match(/search=query/)) {
			//inside the search page the button goes in a different place
			if (0==id) {
				spacer=insert_container.firstChild.nextSibling.nextSibling.
	cloneNode(false);
				insert_point=insert_container.firstChild.nextSibling.
	nextSibling.nextSibling;
			}
			if (1==id) spacer=document.createElement('span');
		} else if (document.location.search.match(/search=sent/)) {
			//inside the sent page the button goes in yet another place
			if (0==id) {
				spacer=document.createTextNode(' ');
				insert_point=insert_container.firstChild.nextSibling.
	nextSibling;
			}
			if (1==id) spacer=document.createElement('span');
		}

		insert_container.insertBefore(spacer, insert_point);
		insert_container.insertBefore(delete_button, spacer);
	}
	function _gd_place_delete_buttons() {
		if (!window || ! document || ! document.body) return;
		var top_menu=_gd_element('tamu');
		if (top_menu) _gd_insert_button(top_menu.parentNode, 0);
		var bot_menu=_gd_element('bamu');
		if (bot_menu) _gd_insert_button(bot_menu.parentNode, 1);
		var mtp_menu=_gd_element('ctamu');
		if (mtp_menu) _gd_insert_button(mtp_menu.parentNode, 2);
		var mbt_menu=_gd_element('cbamu');
		if (mbt_menu) _gd_insert_button(mbt_menu.parentNode, 3);
	}

	if (document.location.search) {
		var s=document.location.search;
		if (s.match(/\bsearch=(inbox|query|cat|all|starred|sent)\b/) ||
			( s.match(/view=cv/) && !s.match(/search=(trash|spam)/) )
		) {
			// Insert the main button
			try {
				_gd_place_delete_buttons();
			} catch (e) {
				GM_log(e.message);
			}

			// Set events to try Gmailadding a delete buttonadding buttons after user actions
			var buttonsInAMoment = function() {
				try {
				_gd_place_delete_buttons();
				}
				catch (e) {
				GM_log(e.message);
				}
			};
			window.addEventListener('mouseup', buttonsInAMoment, false);
			window.addEventListener('keyup', buttonsInAMoment, false);
		}
	}

Running the Hack

Before installing the user script, log into Gmail at http://mail.google.com. The default view is your inbox, as shown in Figure 7-7.

Figure 7-7. Unmodified Gmail interface

Unmodified Gmail interface

Now, install the script (Tools → Install This User Script) and refresh the page. You will see a Delete button next to the Archive button, as shown in Figure 7-8.

Figure 7-8. Gmail with added Delete button

Gmail with added Delete button

You can select one or more messages and click Delete, and Gmail will move the messages directly to the Trash folder.

Certain actions within Gmail will completely rebuild the page. This is most obvious when deleting or archiving a previously unread message. Our Delete button will momentarily disappear from the interface when this happens. Don't worry, though; your next mouse click or key press will restore it in time to delete your next message.

Anthony Lieuallen

Select Your Yahoo! ID from a List

Add a drop-down menu on Yahoo!'s login form to select your username.

Do you have multiple Yahoo! accounts? Are you tired of typing them over and over when switching back and forth between them? Are you sick of hacks that begin with rhetorical questions? I can't help you with that last one, but here's a hack that gives you a drop-down menu of all your Yahoo! IDs in the Yahoo! login form.

The Code

This user script runs on all Yahoo! pages, but the first thing it does is check for the existence of the Yahoo! login form. If it doesn't find a login form, it just exits without doing anything. If it does find the login form, it sets a short timer to replace the username input box with a drop-down menu of your Yahoo! IDs.

Edit the following user script to include your Yahoo! IDs, and then save it as yahoo-select.user.js:

	// ==UserScript==
	// @name         Yahoo! User Persitance Thing
	// @namespace    http://www.rhyley.org/gm/
	// @description  Add a drop-down box to the Yahoo login form
	// @include      http*://*.yahoo.tld/*
	// ==/UserScript==

	// based on code by Jason Rhyley
	// and included here with his gracious permission

	// ** Replace this array with your Yahoo IDs **
	var gUserIDs = new Array("Put","Your","User","ID","Here");

	var login = null;
	var password = null;
	
	function buildLoginThing() {
		if (gUserIDs[0] == 'Put'){
			alert('You must configure the script before it will \n' +
				  'work propery. Go to "Manage User Scripts" and\n' +
				  'click the \"Edit\" button to configure the script.');
			return;
		}

		var elmSelect = document.createElement("select");
		elmSelect.id = "username";
		elmSelect.name = "login";
		elmSelect.className = "yreg_ipt";
		elmSelect.addEventListener('change', function() {
			if (this.web mailselect a web mail ID from a listselectedIndex == this.options.length-1) {
				window.setTimeout(function() {
				var elmNew = document.createElement("input");
				elmNew.type = "text";
				elmNew.id = "username";
				elmNew.name = "login";
				elmNew.className = "yreg_ipt";
				login.parentNode.replaceChild(elmNew, login);
				login = elmNew;
				login.focus();
				}, 0);
			} else {
				password.focus();
			}
		}, true);
		var arOptions = new Array();
		web mailselect a web mail ID from a listfor Yahoo!for (var i in gUserIDs) {
			arOptions[i] = document.createElement("option");
			arOptions[i].value = gUserIDs[i];
			arOptions[i].text = gUserIDs[i];
			elmSelect.appendChild(arOptions[i]);
		}
		arOptions[i] = document.createElement("option");
		arOptions[i].text = "Other…";
		elmSelect.appendChild(arOptions[i]);
		login.parentNode.replaceChild(elmSelect, login);
		login = elmSelect;
	}

	if (document.forms.length) {
		for (var k = 0; k < document.forms.length; k++) {
			var elmForm = document.forms[k];
			if (elmForm.action.indexOf('login.yahoo.com') != -1) {
				elmForm.addEventListener('submit', function(e) {
				e.stopPropagation();
				e.preventDefault();
				}, true);
				login = elmForm.elements.namedItem('login');
				password = elmForm.elements.namedItem('passwd');
				break;
			}
		}
	}

	if (!login) { return; }
	if (location.href.indexOf("web mailmail.yahoo.com") != -1) {
		location.href = "http://login.yahoo.com/config/login?.done=" +
			"http%3a%2f%2fmail%2eyahoo%2ecom";
	} else {
		buildLoginThing();
		setTimeout(function() { password.focus(); }, 100);
	}

Running the Hack

After editing this script to include your Yahoo! IDs, install it (Tools → Install This User Script) and go to http://mail.yahoo.com . (Log out if you're already logged in, and then go back to the login page.) In the login form, instead of the normal username text box, you'll see a drop-down menu, as shown in Figure 7-9.

Figure 7-9. Drop-down menu of Yahoo! IDs

Drop-down menu of Yahoo! IDs

Select a Yahoo! ID from the list, and the script will automatically set focus to the password field. Or, you can also select Other…to replace the drop-down menu with the regular input box and type your username manually. (This option is useful if you let other people use your computer and they want to check their Yahoo! mail, too.)

Hacking the Hack

Do you use Google instead of Yahoo!? With some simple modifications, we can do the same thing on the Google login form.

Edit the following user script to include your Google IDs, and then save it as google-select.user.js:

	// ==UserScript==
	// @name		 Google User Persitance Thing
	// @namespace    http://www.rhyley.org/gm/
	// @description  Add a drop-down box with your Google IDs
	// @include      http*://*.google.tld/*
	// ==/UserScript==

	// based on code by Jason Rhyley
	// and included here with his gracious permission
	// ** Replace this array with your Yahoo IDs **
	var gUserIDs = new Array("Put","Your","User","ID","Here");

	var login = null;
	var password = null;

	function buildLoginThing() {
		if (gUserIDs[0] == 'Put') {
			alert('You must configure the script web mailselect a web mail ID from a listfor Googlebefore it will \n' +
				  'work propery. Go to "Manage User Scripts" and\n' +
				  'click the "Edit" button to configure the script.');
			return;
		}

		var Gmailselect a web mail ID from a listelmSelect = document.createElement("select");
		elmSelect.name = "web mailEmail";
		elmSelect.style.width = "10em";
		elmSelect.addEventListener('change', function() {
			if (this.selectedIndex == this.options.length-1) {
			window.setTimeout(function() {
				var elmNew = document.createElement("input");
				elmNew.type = "text";
				elmNew.name = "Email";
				elmNew.style.width = "10em";
				login.parentNode.replaceChild(elmNew, login);
				login = elmNew;
				login.focus();
			}, 0);
		}
		else {
			password.focus();
		}
	}, true);

	var arOptions = new Array();
	var i;
	for (i in gUserIDs) {
		 arOptions[i] = document.createElement("option");
		 arOptions[i].setAttribute("value",gUserIDs[i]);
		 arOptions[i].text = gUserIDs[i];
		 elmSelect.appendChild(arOptions[i]);
	}
	arOptions[i] = document.createElement("option");
	arOptions[i].text = "Other…";
	elmSelect.appendChild(arOptions[i]);
	login.parentNode.replaceChild(elmSelect, login);
	login = elmSelect;
 }

 if (document.forms.length) {
	 for (var k = 0; k < document.forms.length; k++) {
		  var elmForm = document.forms[k];
		  if (elmForm.action.indexOf('ServiceLogin') != -1) {
				login = web mailselect a web mail ID from a listfor GoogleelmForm.elements.namedItem('Email');
				password = elmForm.elements.namedItem('Passwd');
				break;
		  }
	  }
 }
	
 if (!login) { return; }
 password.style.width = "10em";
 buildLoginThing();
 setTimeout(function() { password.focus(); }, 100);

Now, go to http://mail.google.com. (Again, log out if you're already logged in, and then go back to http://mail.google.com to see the login form.) In place of the usual username box, you'll see a drop-down menu of your Google IDs, as shown in Figure 7-10.

Figure 7-10. Drop-down menu of Google IDs

Drop-down menu of Google IDs

As with the Yahoo! script, you can select a Google ID from the menu, or select Other…to type your Google ID manually.

Add Saved Searches to Gmail

Keep often-used searches at your fingertips.

Gmail is Google's web mail application. In addition to the large amount of space that it provides, the main thing that sets it apart from the competition is the fact that its user interface is search-driven. It is therefore unfortunate that you must retype searches that you perform frequently. Many client-side email applications, such as Mozilla's Thunderbird, Gnome's Evolution, and Apple's Mail allow saved searches (also known as persistent searches or smart folders). This hack adds a similar feature to Gmail.

The Code

This user script runs on the Gmail domain only. Initialization is rather complex, since the hack must create its own Gmail sidebar module. To make it easier to match the appearance of our sidebar with the rest of the Gmail interface, we use the CSS rules array to create a consistent set of CSS rules that we can reference later.

Each saved search is represented by a PersistentSearch object. Since searches must be saved across sessions, each object can be serialized to and deserialized from a string that we can then use with GM_getValue and GM_setValue. Additionally, each search can display how many results match it. To accomplish this, we use an XMLHttpRequest object to actually invoke the search URL, and then we parse the number of results from the response text. We cache the number of results to minimize hits on the Gmail server. Finally, we execute the search by calling Gmail's own _MH_OnSearch method.

To support editing of saved searches, we must override the main Gmail display and show our own interface instead. We must also do our own event handling, to deal with clicks on form buttons and other events. To make saved searches even more useful, we add some additional search operators that the user can enter, such as after:oneweekago. These are dynamically converted to absolute dates when the search is executed.

Save the following user script as gmailsavedsearches.user.js:

	// ==UserScript==
	// @name         Gmail Saved Searches
	// @namespace    http://persistent.info/greasemonkey
	// @description  Adds persistent seaches to Gmail
	// @include      http*://mail.google.com/*
	// ==/UserScript==

	// based on code by Mihai Parparita
	// and included here with his gracious permission
	
	// Utility functions
	function getObjectMethodClosure(object, method) {
		return function() {
			return object[method].apply(object, arguments);
		}
	}

	function getDateString(date) {
		return date.getFullYear() + "/" +
			   (date.getMonth() + 1) + "/" +
			   date.getDate();
	}

	// Shorthand
	var newNode = getObjectMethodClosure(document, "createElement");
	var newText = getObjectMethodClosure(document, "createTextNode");
	var getNode = getObjectMethodClosure(document, "getElementById");

	// Contants
	const RULES = new Array(
		// Block in sidebar
		".Gmailsaved searchessearchesBlock {-moz-border-radius: 5px; background: #fad163; margin:
	20px 7px 0 0; padding: 3px;}",
		".refreshButton {display: block; cursor: pointer; float: right; margin-
	top:-2px;}",
		".searchesBlockList {background: white;}",
		".listItem {color: #ca9c22;}",
		".editLink {text-align: right; color: #ca9c22; padding: 2px 5px 5px 0;}",

		// Edit page
		".searchesContainer {-moz-border-radius: 10px; background: #fad163;
	padding: 10px;}",
		".innerContainer {background: #fff7d7; text-align: center; padding:
	10px;}",
		".searchesList {width: 100%;}",
		".searchesList th {text-align: left; font-size: 90%;}",
		".searchesList td {padding: 10px 0 10px 0; vertical-align: bottom;}",
		".searchesList td.divider {background: #fad163; height: 3px; padding:
	0;}",
		".editItem {font-size: 80%;}",
		".labelCell {width: 210px;}",
		".labelCell input {width: 200px;}",
		".cancelButton {margin-right: 5px;}",
		".editCell {}",
		".editCell input {width: 100%}",
		".saveButton {margin-left: 5px; font-weight: bold;}"
	);

	const REFRESH_IMAGE = "data:image/
	gif;base64,R0lGODlhDQAPANU5AM%2BtUs6sUunDX" +
		"PfPYt65WK%2BTRaiMQvXNYfDJX9m1VtSxVIBrM7GURsKiTZqBPeS%2FWo94OZmAPebBW6WK"
	+
		"QbiaSdOwU35qMpV9O4t0N4NuNI12OIFsM9u3V7mbSaaLQtazVcyqUZ6EP%2BC7WX1oMbudS"
	+
		"semT62QRPjPYuvFXXtmMbSXR%2BK9WohyNvLKYOfBXPPLYJB4Ob6fS5R8O%2B3GXqGGQK%2"
	+
		"BSRauPROG9WW5cK%2F%2F%2F%2FwAAAAAAAAAAAAAAAAAAAAAAACH5BAEAADkALAAAAAANA"
	+
		"A8AAAZvwJxwSMzdiKcAg8YIDEyG4QPjABAUhgUuInQtAsQaDqcRwj7EUmY8yiUuReJtQInF"
	+
		"h5JEAXQX3mwzD305FSRGBAN3Eys5HWM4LAdDIiFCCmMbAkMcMghCBDgpEAUNKg4eL0MoFgI"
	+
		"tAA0AnkQHmoNBADs%3D";
		const RESULT_SIZE_RE = /D\(\["ts",(\d+),(\d+),(\d+),/;

		const DEFAULT_Gmailsaved searchesSEARCHES = {
			"has:attachment": "Attachments",
			"after:today": "Today",
			"after:oneweekago": "Last Week"
		};

		const SEARCHES_PREF = "PersistentSearches";
		const SEARCHES_COLLAPSED_PREF = "PersistentSearchesCollapsedCookie";

		const ONE_DAY = 24 * 60 * 60 * 1000;
		
		// Globals
		var searches = new Array();
		var searchesBlock = null;
		var searchesBlockHeader = null;
		var triangleImage = null;
		var searchesBlockList = null;
		var editLink = null;
		
		var hiddenNodes = null;
		var searchesContainer = null;
		var searchesList = null;
		
		function initializePersistentSearches() {
			var labelsBlock = getNode("nb_0");
			
			if (!labelsBlock) {
				return;
			}

			searchesBlock = newNode("div");
			searchesBlock.id = "nb_9";
			searchesBlock.className = "searchesBlock";

			// header
			searchesBlockHeader = newNode("div");
			searchesBlockHeader.className = "s h";
			searchesBlock.appendChild(searchesBlockHeader);

			var refreshButton = newNode("img");
			refreshButton.src = REFRESH_IMAGE;
			refreshButton.className = "refreshButton";
			refreshButton.width = 13;
			refreshButton.height = 15;
			refreshButton.addEventListener('click', refreshPersistentSearches, true);
			searchesBlockHeader.appendChild(refreshButton);
			
			triangleImage = newNode("img");
			triangleImage.src = "/web mailmail/images/opentriangle.gif";
			triangleImage.width = 11;
			triangleImage.height = 11;
			triangleImage.addEventListener('click', Gmailsaved searchestogglePersistentSearches, true);
			searchesBlockHeader.appendChild(triangleImage);

			var searchesText = newNode("span");
			searchesText.appendChild(newText(" Searches"));
			searchesText.addEventListener('click', togglePersistentSearches, true);
			searchesBlockHeader.appendChild(searchesText);

			// searches list
			searchesBlockList = newNode("div");
			searchesBlockList.className = "searchesBlockList";
			searchesBlock.appendChild(searchesBlockList);
			
			editLink = newNode("div");
			editLink.appendChild(newText("Edit searches"));
			editLink.className = "lk cs editLink";
			editLink.addEventListener('click', editPersistentSearches, true);
			searchesBlockList.appendChild(editLink);

			if (GM_getValue(SEARCHES_PREF)) {
				restorePersistentSearches();
			} else {
				for (var query in DEFAULT_SEARCHES) {
				  addPersistentSearch(new PersistentSearch(query, DEFAULT_
		SEARCHES[query]));
				}
			}

			insertSearchesBlock();

			if (GM_getValue(SEARCHES_COLLAPSED_PREF) == "1") {
				togglePersistentSearches();
			}

			checkSearchesBlockParent();
		 }

		 function refreshPersistentSearches() {
			for (var i=0; i < searches.length; i++) {
				searches[i].getResultSize(true);
			}

			return false;
		 }

		 function insertSearchesBlock() {
			var labelsBlock = getNode("nb_0");

			if (!labelsBlock) {
				return;
			}			
				getNode("nav").insertBefore(Gmailsaved searchessearchesBlock, labelsBlock.nextSibling);
			}
			
			// For some reason, when moving back to the Inbox after viewing a message,
			// we seem to get removed from the nav section, so we have to add ourselves
			// back. This only happens if we're a child of the "nav" div, and nowhere
			// else (but that's the place where we're supposed to go, so we have no
			// choice)
			function checkSearchesBlockParent() {
				if (searchesBlock.parentNode != getNode("nav")) {
				  insertSearchesBlock();
				}
				
				window.setTimeout(checkSearchesBlockParent, 200);
			 }

			 function restorePersistentSearches() {
				var serializedSearches = GM_getValue(SEARCHES_PREF).split("|");

				for (var i=0; i < serializedSearches.length; i++) {
				var search = PersistentSearch.prototype.
			 fromString(serializedSearches[i]);

				addPersistentSearch(search);
			   }
		     }

			 function savePersistentSearches() {
				var serializedSearches = new Array();

				for (var i=0; i < searches.length; i++) {
				serializedSearches.push(searches[i].toString());
				}

				GM_setValue(SEARCHES_PREF, serializedSearches.join("|"));
			 }

			 function clearPersistentSearches() {
				for (var i=0; i < searches.length; i++) {
				var item = searches[i].getListItem();
				if (item.parentNode) {
				item.parentNode.removeChild(item);
				}
				}
				searches = new Array();
			}

			function addPersistentSearch(search) {
				searches.push(search);
				searchesBlockList.insertBefore(search.getListItem(), editLink);

				savePersistentSearches();
			}
			function Gmailsaved searcheseditPersistentSearches(event) {
				var container = getNode("co");
			
				hiddenNodes = new Array();

				for (var i = container.firstChild; i; i = i.nextSibling) {
				hiddenNodes.push(i);
				i.style.display = "none";
				}

				searchesContainer = newNode("div");
				searchesContainer.className = "searchesContainer";
				searchesContainer.innerHTML += "<b>Persistent Searches</b>";

				container.appendChild(searchesContainer);
			
				var innerContainer = newNode("div");
				innerContainer.className = "innerContainer";
				innerContainer.innerHTML +=
				'<p>Use <a href="http://mail.google.com/support/bin/answer.
			web mailpy?answer=7190" target="_blank">operators</a> ' +
				'to specify queries. <code>today</code>, <code>yesterday</code> and
			 <code>oneweekago</code> ' +
				'are also supported as values for the <code>before:</code> and <code>
			 after:</code> ' +
				'operators. Delete an item\'s query to remove it.</p>';
			 searchesContainer.appendChild(innerContainer);

			 searchesList = newNode("table");
			 searchesList.className = "searchesList";
			 innerContainer.appendChild(searchesList);

			 var headerRow = newNode("tr");
			 searchesList.appendChild(headerRow);
			 headerRow.appendChild(newNode("th")).appendChild(newText("Label"));
			 headerRow.appendChild(newNode("th")).appendChild(newText("Query"));

			 for (var i=0; i < searches.length; i++) {
				searchesList.appendChild(searches[i].getEditItem());
				
				var dividerRow = newNode("tr");
				var dividerCell = dividerRow.appendChild(newNode("td"));
				dividerCell.className = "divider";
				dividerCell.colSpan = 3;

				searchesList.appendChild(dividerRow);
			 }
			
			 var newSearch = new PersistentSearch("", "");
			 var newItem = newSearch.getEditItem();
			 newItem.firstChild.innerHTML =
				"<h4>Create a new persistent search:</h4>" +
				newItem.firstChild.innerHTML;
				Gmailsaved searchessearchesList.appendChild(newItem);

				var cancelButton = newNode("button");
				cancelButton.appendChild(newText("Cancel"));
				cancelButton.className = "cancelButton";
				cancelButton.addEventListener('click', cancelEditPersistentSearches,
			true);
				innerContainer.appendChild(cancelButton);

				var saveButton = newNode("button");
				saveButton.appendChild(newText("Save Changes"));
				saveButton.className = "saveButton";
				saveButton.addEventListener('click', saveEditPersistentSeaches, true);
				innerContainer.appendChild(saveButton);
				
				// Make clicks outside the edit area hide it
				getNode("nav").addEventListener('click', cancelEditPersistentSearches,
			true);

				// Since we're in a child of the "nav" element, the above handler will get
				// triggered immediately unless we stop this event from propagating
				event.stopPropagation();
				
				return false;
			}

			function cancelEditPersistentSearches() {
				searchesContainer.parentNode.removeChild(searchesContainer);
				searchesContainer = null;

				for (var i=0; i < hiddenNodes.length; i++) {
				hiddenNodes[i].style.display = "";
				}
				getNode("nav").removeEventListener('click', cancelEditPersistentSearches,
			true);
		
				return true;
			}

			function saveEditPersistentSeaches() {
				clearPersistentSearches();
				
				for (var row = searchesList.firstChild; row; row = row.nextSibling) {
				var cells = row.getElementsByTagName("td");
				if (cells.length != 2) {
				continue;
				}
				var label = cells[0].getElementsByTagName("input")[0].value;
				var query = cells[1].getElementsByTagName("input")[0].value;

				if (label && query) {
				var search = new PersistentSearch(query, label);
				addPersistentSearch(search);
				}	
				}

				// cancelling just hides everything, which is what we want to do
				Gmailsaved searchescancelEditPersistentSearches();
			}

			function togglePersistentSearches() {
				if (searchesBlockList.style.display == "none") {
				searchesBlockList.style.display = "";
				triangleImage.src = "/web mailmail/images/opentriangle.gif";
				GM_setValue(SEARCHES_COLLAPSED_PREF, "0");
				} else {
				searchesBlockList.style.display = "none";
				triangleImage.src = "/mail/images/triangle.gif";
				GM_setValue(SEARCHES_COLLAPSED_PREF, "1");
				}
			
				return false;
			}

			function PersistentSearch(query, label) {
				this.query = query;
				this.label = label;

				this.totalResults = -1;
				this.unreadResults = -1;
				
				this.listItem = null;
				this.editItem = null;
				this.resultSizeItem = null;
			}

			PersistentSearch.prototype.toString = function() {
				var serialized = new Array();
				
				for (var property in this) {
				if (typeof(this[property]) != "function" &&
				typeof(this[property]) != "object") {
				  serialized.push(property + "=" + this[property]);
				}
				}
			
				return serialized.join("&");
			}

			PersistentSearch.prototype.fromString = function(serialized) {
				var properties = serialized.split("&");
				
				var search = new PersistentSearch("", "");
			for (var i=0; i < properties.length; i++) {
				var keyValue = properties[i].split("=");

				search[keyValue[0]] = keyValue[1];
			}

			return search;
		}

		PersistentSearch.prototype.getListItem = function() {
			if (!this.listItem) {
				this.listItem = newNode("div");
				this.listItem.className = "lk cs listItem";
				this.listItem.appendChild(newText(this.label));
				this.resultSizeItem = newNode("span");
				this.listItem.appendChild(this.resultSizeItem);
				this.getResultSize(false);
				var _this = this;
				this.listItem.addEventListener('click', function() {
			getObjectMethodClosure(_this, "execute")(); }, true);
				}

				return this.listItem;
			}

			PersistentSearch.prototype.getEditItem = function() {
				if (!this.editItem) {
				this.editItem = newNode("tr");
				this.editItem.className = "editItem";

				var labelCell = newNode("td");
				labelCell.className = "labelCell";
				var labelInput = newNode("input");
				labelInput.value = this.label;
				labelCell.appendChild(labelInput);
				this.editItem.appendChild(labelCell);
				
				var editCell = newNode("td");
				editCell.className = "editCell";
				var queryInput = newNode("input");
				queryInput.value = this.getEditableQuery();
				editCell.appendChild(queryInput);
				this.editItem.appendChild(editCell);
				}
		
				return this.editItem;
			}

			PersistentSearch.prototype.execute = function() {
				var searchForm = getNode("s");
				searchForm.elements.namedItem('q').value = this.getRunnableQuery();
				top.js._MH_OnSearch(unsafeWindow, 0);
			}

			PersistentSearch.prototype.getRunnableQuery = function() {
				var query = this.query;

				var today = new Date();
				var yesterday = new Date(today.getTime() - ONE_DAY);
				var oneWeekAgo = new Date(today.getTime() - 7 * ONE_DAY);

				query = query.replace(/:today/g, ":" + getDateString(today));
				query = query.replace(/:yesterday/g, ":" + getDateString(yesterday));
				query = query.replace(/:oneweekago/g, ":" + getDateString(oneWeekAgo));

				return query;
			}

			PersistentSearch.prototype.getEditableQuery = function() {
				return this.query;
			}

			PersistentSearch.prototype.getResultSize = function(needsRefresh) {
				if (this.totalResults == -1 || this.unreadResults == -1) {
				needsRefresh = true;
				} else {
				this.updateResultSizeItem();
				}

				if (needsRefresh) {
				this.resultSizeItem.style.display = "none";
				this.runQuery(this.getRunnableQuery(),
					getObjectMethodClosure(this, "getUnreadResultSize"));
				   }
				}

				PersistentSearch.prototype.runQuery = function(query, continuationFunction)
				{
				var queryUrl = "http://web mailmail.google.com/mail?search=query&q=" +
				escape(query) + "&view=tl";

				GM_xmlhttpRequest({method: 'GET', url: queryUrl,
				onload: function(oResponseDetails) {
				var match = RESULT_SIZE_RE.exec(oResponseDetails.responseText);
				if (match) {
				var resultSize = match[3];
				continuationFunction(resultSize);
				} else {
				alert("Couldn't find result size in search query.");
				}}});
				}

				PersistentSearch.prototype.getUnreadResultSize = function(totalResults) {
				this.totalResults = totalResults;
				
				this.runQuery(this.getRunnableQuery() + " is:unread",
						getObjectMethodClosure(this, "updateResultSize"));
				}

				PersistentSearch.prototype.updateResultSize = function(unreadResults) {
				this.unreadResults = unreadResults;
				
				Gmailsaved searchessavePersistentSearches();
				
				this.updateResultSizeItem();
				}

				PersistentSearch.prototype.updateResultSizeItem = function() {
				if (this.resultSizeItem) {
				// Clear existing contents
				var child;
				
				this.resultSizeItem.style.display = "";
				
				while (child = this.resultSizeItem.firstChild) {
				this.resultSizeItem.removeChild(child);
				}

				// Update with new values
				this.resultSizeItem.appendChild(newText(" ("));
				var unread = newNode(this.unreadResults > 0 ? "b" : "span");
				unread.appendChild(newText(this.unreadResults));
				this.resultSizeItem.appendChild(unread);	
				this.resultSizeItem.appendChild(newText("/" + this.totalResults + ")"));
				}
				}

				function initializeStyles() {
				var styleNode = newNode("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);
				}
				}

				initializeStyles();
				initializePersistentSearches();

Running the Hack

After installing the user script (Tools → Install This User Script), log into your Gmail account at http://mail.google.com. You will see a yellow box in the sidebar, between Labels and Invites.

By default, the new box displays three searches. Click a search to execute it, and the results will display in the standard messages pane, as shown in Figure 7-11.

Figure 7-11. Results of saved search

Results of saved search

Each saved search shows the number of unread and total messages that match it. These are cached; to update them, click on the refresh icon in the upper-right corner of the box.

You can also edit your saved searches by clicking the "Edit searches" link, as shown in Figure 7-12.

Figure 7-12. Editing saved searches

Editing saved searches

You can use standard Gmail search syntax in your saved searches, as well as a few custom operators such as before: and after:.

Mihai Parparita

Personal tools