back to script
// ----- Gmail Client ------
// @url https://mail.google.com/mail/
// @name Gmail
// @desc Notifies about new/unread emails.
// version 0.7
//		- added drag&drop feature
// version 0.6 beta
//		- added ensure visible when clicking anywhere on the notification box, except close and
//		next and prev buttons.
// version 0.5 beta

// anonymous function that wraps whole script to prevent naming conflicts
new function() {
//SSB.console.init("debug","window");

var win = window.top;
// refreshTimeMS - refresh time in milliseconds to check for new emails
var refreshTimeMS = 10000;
var bubblesTrckr = null;
var elements = new Elements();

window.getDocumentHandlerForPrefsPane = elements.getFrameDocument;
SSB.prefs.pane.documentHandler = "window.getDocumentHandlerForPrefsPane()";
createBubblesTrckr();

if (window.self != window.top) SSB.console.info("win is not top most windows. May cause problems!");
checkTitleChange();

function checkTitleChange() {
	try {
		/* do this every refreshTimeMS seconds */
		win.setTimeout(checkTitleChange, refreshTimeMS);
		var title = win.document.title;
		SSB.console.debug("title=" + title);

		/* make sure we've got a title we can work with */
		if (title.search('Inbox') == -1) return;
		
		/* get current number of unread mails */
		var result		= title.match(/(Inbox \()(\d+)(\))/);
		var curUnread	= (result && result.length == 4) ? parseInt(result[2]) : 0;

		/* get previous number of unread mails */
		var prevUnread	= SSB.prefs.getValue('num_unread');
		SSB.console.debug("Got prevUnread = "+prevUnread);
		
		/* immediately update with new value */
		SSB.prefs.setValue('num_unread', curUnread+'');
		if (prevUnread == null)	prevUnread = 0;
		else prevUnread = parseInt(prevUnread);

		/* if the number grew (from 0 to 1, or from 3 to 899), tell the user */
		if (curUnread > prevUnread) {
			SSB.console.debug("curUnread = " + curUnread + " > " + prevUnread + " = prevUnread");
			
			if (bubblesTrckr) {
				try {
					bubblesTrckr._trackEvent("Notifications", "Gmail");
				} catch(e) {};
			}
			
			var newEmails = parseUnreadEmailsData(curUnread);
			
			var url = "http://bubbleshq.com/client/gmailNotification/";
			
			var emails = "";
			
			for (var i=0; i < newEmails.length; i++) {
				var email = getStringFromFunction(emailHTML);
				email = email.replace(/__FROM__/, newEmails[i].from);
				email = email.replace(/__SUBJECT__/, newEmails[i].subject);
				email = email.replace(/__MSG__/, newEmails[i].firstLine);
				email = email.replace(/__emailNumber__/, (i+1) );
				email = email.replace(/__leftStyle__/, (i!=0)?"left:-1000px;":"");
				emails += email;
			}
			
			var html = getStringFromFunction(notificationHTML);
//			html = html.replace(/__gmailURL__/, "http://mail.google.com/mail/");
			html = html.replace(/__numOfEmails__/g, newEmails.length);
			html = html.replace(/__EMAILS__/, emails);
			html = html.replace(/__URL__/g, url);
			SSB.notify(html, 295, 117, 10);
		}
		
		/* update icon (icons are cached so we can give direct url) */
		var iconURL = "http://www.3d3r.com/bubbles/icons/gmail_" +	(curUnread != 0 ? "unread" : "read") + ".ico";
		SSB.console.debug("The icon URL is: " + iconURL);
		SSB.setIcon(iconURL);
	} catch(e) {
		SSB.console.error("checkTitleChange: " + e.name + ": " + e.message);
	}
}

/*
 * Parse email header data (from, subject, time, etc.) from emails table.
 * Uses curUnread value to stop iteration when all unread emails are found.
 */
function parseUnreadEmailsData(curUnread) {
	try {
		SSB.console.debug("parseUnreadEmailsData(" + curUnread + ")...");
		
		// get iFrame that is used for inxob view
		var iFrameDoc = elements.getCurrIFrameDocument();
		if (iFrameDoc == null) throwError("Parsing Error","didn't get the iFrame!");
		SSB.console.debug("Current iFrame's id is: " + iFrameDoc.parentWindow.frameElement.id);
		
		// get emails Table that hold all the emails header data
		// Getting table with id is not stable, because it differs with browsers and
		// windows version.
		// var emailsTable = iFrameDoc.getElementById(elements.emailsTableId);
		var emailsTable = null;
		var tables = iFrameDoc.getElementsByTagName("table");
		SSB.console.debug("Found " + tables.length + "tables.");
		var numOfColumns = null;
		SSB.console.debug("Starting to iterate to find emails table.");
		for (var i = 0; i < tables.length; i++) {
			numOfColumns = tables[i].getElementsByTagName("col").length;
			if (numOfColumns == 7) {
				emailsTable = tables[i];
				break;
			}
		}
		if (emailsTable == null) throwError("parsingError","didn't find emailsTable!");
		SSB.console.debug("Got emails table with id: " + emailsTable.id);
		
		// get emails table rows
		var emails = emailsTable.getElementsByTagName("tr");
		if (emails == null) throwError("parsingError","didn't find emails!");
		SSB.console.debug("Got email table rows. Their length is: " + emails.length);
		
		var fields, from, subject, time, firstLine;
		var firstLineClass = elements.firstLineClass;
		var unansweredEmailsFound = 0;
		var newEmails = new Array();
		
		// run on email rows in the table.
		var unreadRegexp = new RegExp(elements.unreadClass);
		for (i = 0; i < emails.length && unansweredEmailsFound < curUnread; i++) {
			email = emails[i];
			// if the email is unread email
			// "ur" class for IE6 and "QhHSYc" class for IE7
			if (unreadRegexp.test(email.className)) {
				SSB.console.debug("Found unanswered email. His index (from 0) is: " + i);
				fields = email.getElementsByTagName("td");
				// next statements throwing errors if the text nodes not found
				// from = fields[2].getElementsByTagName(elements.fromTag)[0].firstChild.nodeValue;
				from = fields[2].innerText;
				SSB.console.debug("From: " + from);
				subject = fields[4].getElementsByTagName("b")[0].firstChild.nodeValue;
				SSB.console.debug("Subject: " + subject);
				firstLine = getElementsByClassName(firstLineClass, fields[4]);
				if (firstLine.length > 0)
					firstLine = firstLine[0].innerText;
				else
					firstLine = "";
				SSB.console.debug("FirstLine: " + firstLine);
				time = fields[6].getElementsByTagName("b")[0].firstChild.nodeValue;
				SSB.console.debug("Time: " + time);
				// with all of the data the notification looks shrinky. so I will use only the from field.
				newEmails[unansweredEmailsFound] = new Email(from,subject,firstLine,time);
				unansweredEmailsFound++;
			}
		}
//		var delta = curUnread - maxEmailHeaders;
//		if (delta > 0) {
//			SSB.console.debug("delta = curUnread - maxEmailHeaders = " + delta);
//		}
		return newEmails;
	}
	catch(e) {
		SSB.console.error("parseUnreadEmailsData(): " + e.name + ": " + e.message);
		return newEmails;
	}
}

/**
 * Email object. Holds email header data.
 */
function Email(from,subject,firstLine,time) {
	this.from = from;
	this.subject = subject;
	this.firstLine = firstLine;
	this.time = time;
}

/*
 * function for throwing custom errors.
 */
function throwError(error, msg) {
	var err = new Object();
	err.name = error;
	err.message = msg;
	throw err;				
}

/* 
 * Elements object holds all the differences between IE6 and IE7 of gmail DOM needed
 * for the script 
 * IE6:
 * There is a main 3 iFrames that is switched between each other every action, or not.
 * So on first gmail load the main inbox is in the first iFrame, but after
 * sending an email for example, the inbox opens in the third iFrame.
 * getCurrIFrameDocument() function checks which iFrame is currently viewed by
 * checking it's z-index style property.
 * IE7:
 * There is 4 iframes. The main page is in the third iframe.
 */
function Elements() {
	SSB.console.debug("elementsPath()...");
	var version = /MSIE ([\d\.]+)/.exec(win.navigator.appVersion)[1];
	SSB.console.debug("IE version: " + version);
	if (version >= 7) {
		this.emailsTableId = "1f7a";
		this.unreadClass = "QhHSYc";
		this.fromTag = "span";
		this.firstLineClass = "bEeVec";
	}
	else {
		this.fromTag = "b";
		this.emailsTableId = "tb";
		this.unreadClass = "ur";
		this.firstLineClass = "p";
	}
	this.getFrameDocument = function() {
		if (version >=7) return win.document.frames[3].document;
		return win.document.frames[0].document;
	}
	this.getCurrIFrameDocument = function() {
		try {
			if (version >=7) return win.document.frames[3].document;
			var subIFrames = win.document.frames[0].document.getElementsByTagName("iframe");
			SSB.console.debug("Iterating through subFrames. Length = " + subIFrames.length);
			for (i=0; i < subIFrames.length; i++) {
				SSB.console.debug("Checking iFrame with id: " + subIFrames[i].id);
				zIndex = subIFrames[i].style.zIndex;
				if (zIndex == 1) {
					SSB.console.debug("Found iframe with zIndex == 1. It is " + (i+1) + " iFrame");
					return win.document.frames[0].document.frames[i].document;
				}
			}
			SSB.console.error("elements.getCurrIFrameDocument(): Didn't find iFrame with zIndex == 1");
			return null;
		}
		catch (e) {
			SSB.console.error("elements.getCurrIFrameDocument():" + e.name + ": " + e.message);
			return null;
		}
	}
}

function createBubblesTrckr() {
	try {
		var script = document.createElement("script");
		script.src = "http://www.google-analytics.com/ga.js";
		script.type = "text/javascript";
		var head = document.getElementsByTagName("head")[0];
		head.appendChild(script);
		var tries = 0;
		// Give some number of tries if script did not loaded.
		function initTrckr(){
			try {
				bubblesTrckr = _gat._getTracker("UA-221829-10");
				bubblesTrckr._trackPageview("/notifications/gmail");
			} 
			catch (e) {
				tries++;
				if (tries < 10) {
					window.setTimeout(initTrckr, 200);
				}
			}
		}
		initTrckr();
	} catch (e) {}
}

/*
 * Gets function reference and returns string of first matched text between commented
 * block just like this one. 
 */
function getStringFromFunction(fnc) {
	var string = fnc.toString();
	return string.match(/\/\*([\s\S]*)\*\//)[1];
}

/**
 * HTML code for the notification.
 * All parametes that is replaced at run time is of __parameterName__ form.
 */
function notificationHTML() {/*

	
		Gmail
		
	
	
		
M
1/__numOfEmails__
previous next bubbles
close __EMAILS__
*/} /** * HTML code for every email received, all the email except the first one should have * style.left:-1000px; * All parametes that is replaced at run time is of __parameterName__ form. */ function emailHTML() {/*
__FROM__: __SUBJECT__
__MSG__
*/} /** * Gets elements by their class names * @param {String} className The class name to search. Can be complex like "title bold", then it will * find all elements that have "title" and "bold" classes. Can also be a regular expression. * @param {String} parentElement The ancestor element in which to search for elements. * @return {Array} elements Array of element nodes that matched the class name. */ function getElementsByClassName(aClassName, parentElement) { var className = aClassName.replace(/^\s+/, "").replace(/\s+$/, ""); if (className == "") return null; var classNamesArray = className.split(/\s+/); var elements = new Array(); if (!parentElement) parentElement = document; try { elements = traverseSiblings(parentElement); } catch(e) { } finally { return elements; } /** * Private recursy function for traversing node tree. */ function traverseSiblings(parentElement) { var elements = new Array(); var child = null; var hasClass = null; var regexp = null; for (var i = 0; i < parentElement.childNodes.length; i++) { child = parentElement.childNodes[i]; if ( child.nodeType == 1 ) { hasClass = true; for (var j = 0; j < classNamesArray.length && hasClass; j++) { regexp = new RegExp("\\b"+classNamesArray[j]+"\\b"); if (!regexp.test(child.className)) { hasClass = false; } } if (hasClass) { elements.push(child); } elements = elements.concat( traverseSiblings(child) ); } } return elements; } } // ------------------------------------------- // ---------- Drag and Drop Feature ---------- // ------------------------------------------- function genPost() { var boundary = '----------------{08F41DE12407479f988D0752953C36773D3R}--'; var postBody = ''; var vars = { 'redir' : '?s=d', 'nvp_bu_sd' : 'Save Draft', 'to' : '', 'cc' : '', 'bcc' : '', 'subject' : '', 'body' : '' }; for (x in vars) { postBody += '--' + boundary + '\r\n'; postBody += 'Content-Disposition: form-data; '; postBody += 'name="' + x + '"\r\n\r\n'; postBody += vars[x] + '\r\n'; } for (var i=0; i', '
Uploading...
', '
', '$FILENAMES', '

', '
', '
', '
 
', '
', '
', '
', ''].join('').replace('$FILENAMES', fileNames), 200, 92, 15, false); var xhr = SSB.getXHR(); xhr.open('POST', postUrl); xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary='+post.boundary); xhr.onreadystatechange = function() { SSB.console.debug("onreadystatechange"); SSB.console.debug("xhr.readyState = "+xhr.readyState+"\n"+ "xhr.status = "+xhr.status+"\n"+ "xhr.responseHeaders = \n"+xhr.responseHeaders+"\n"+ "xhr.responseText = \n"+xhr.responseText); if (xhr.readyState == 4) { // see if we got success if (xhr.status == 200) { SSB.console.debug("Upload Successfull"); // fetch the draft-id out of the response var draftId = xhr.responseText; draftId = draftId.split('draft=')[1].split('&')[0]; SSB.console.debug(draftId); var draftUrl = document.location.protocol + '//mail.google.com/mail/#drafts/' + draftId; SSB.console.debug(draftUrl); var userAgent = navigator.userAgent; var version = /MSIE ([\d\.]+)/.exec(userAgent)[1]; SSB.console.debug("IE version: " + version); if (version >= 7) { // this works for IE7 //document.location.replace(draftUrl); notif.document.getElementById('status').innerHTML = 'Upload Successfull'; document.write(xhr.responseText); notif.eval("SSB.notification.closeIn(2)"); } else { notif.eval("SSB.notification.closeIn(0)"); SSB.notify( "" + "
" + "File uploaded sucesfully. Due to the way GMail handles" + " Internet-Explorer 6, you will need to open the new " + "email manually. Please go to 'drafts' and open it from " + "there. For the full streamlined feature, consider " + "upgrading to Internet-Explorer 7 :-)" + "
" + "", 300, 115, 15, false); } //var url = xhr.getResponseHeader('Location'); //document.location.replace(url); } else { SSB.console.debug("Upload Failed"); notif.document.getElementById('status').innerHTML = 'Upload Failed'; notif.eval("SSB.notification.closeIn(2)"); } } } xhr.onprogress = function(progress) { SSB.console.debug("onprogress"); var p = notif.document.getElementById('progressbar'); if (!p) return; p.style.width = progress + '%'; if (progress == 100) { notif.document.getElementById('status').innerHTML = 'processing... '; } // make sure we're up for at least another 15 seconds (in case of slow upload or processing) notif.eval("SSB.notification.closeIn(50)"); } xhr.send(post.body); SSB.console.debug("XHR POSTED"); } }