| jens.hatlak.de | About me | ||||
| Atari | Mozilla | Mozilla dualboot | |||
| Dreamweaver Manual | Building Mozilla | ||||
| Acronyms | Installing Mozilla | ||||
| Bookmark Indicator | |||||
in deutsch |
Custom Buttons | ||||
Check All Checkboxes / Compact All Folders / Empty All Trashes / Get All Messages / Copy Mails / Switch View / Reload View / Prepare SMTT
Unfortunately, SeaMonkey (especially the MailNews component) is lacking some features. If like me you know the internals, you can write an extension that solves the problem at hand for almost anything. But that takes time because you will soon find yourself busy with the UI instead of working on the actual functionality.
The Custom Buttons extension allows you to run any kind of JavaScript code upon clicking a self-defined button (including a custom icon). Thus the UI part drops out and the funcionality remains, coded in JavaScript.
In the following I'll present you with some buttons for features that SeaMonkey does not provide itself (at least not exactly). Of course it is possible to adapt SeaMonkey itself but each and every SeaMonkey code needs to pass review and that can take quite some time—and the outcome is uncertain. The same applies to extensions hosted on AMO, by the way.
Each button listed below has been tested with SeaMonkey 2.0. Some of them might work with Thunderbird, too, but since I don't use it, I didn't test it. I appreciate any kind of feedback, though.
Any code from this page can be used either privately or commercially, but in any case, use at your own risk! I accept not responsibility for the correctness of the code or any consequences that may result from running the code.
If you don not redistribute the code, you can do with it whatever you want. If you do redistribute the code, you must either leave the code unchanged, including the comments, add yourself as an author if you make significant changes, or remove myself as an author if you only adopted minor parts of the code or code from other sources.
The base module JHBF provides some basic functionality. Every button code that uses JHBF needs to import that file. Since many of my buttons require that module, I made it so that it only needs to be kept on hard disk once and that it can be imported wherever it is needed. That can be achieved using the code from loadJHBF.js (see below). The purpose of the module is to wrap frequently used functionality in such a way that it can be used easily (e.g. using Array.forEach).
/**
* JH Button Functions
* @author Jens Hatlak <jh@junetz.de>
* with parts from:
* - http://developer.mozilla.org/
* - http://code.google.com/p/colorediffs/wiki/HowToGetMessageTextInThunderbird
* - http://www.fstoffel.de/tbblog/2009/04/11/how-to-get-a-message-body/
* @version 2010-07-15
*/
var EXPORTED_SYMBOLS = ["JHBF"];
var Ci = Components.interfaces;
const JHBF = {
/**
* MailNews functions
*/
mailnews: {
/**
* View functions
*/
view: {
/**
* Return view flags and type
*/
getFlagsAndType: function(gDBView) {
if (gDBView)
return [gDBView.viewFlags, gDBView.viewType];
return [0, 0];
},
/**
* Return whether the view is threaded
*/
isThreaded: function(aViewFlags) {
return (aViewFlags & Ci.nsMsgViewFlagsType.kThreadedDisplay) != 0;
},
/**
* Return whether the view shows only unread messages
*/
showsOnlyUnread: function(aViewFlags) {
return (aViewFlags & Ci.nsMsgViewFlagsType.kUnreadOnly) != 0;
}
},
/**
* Change the status bar text
*/
setStatusText: function(aDocument, aText) {
aDocument.getElementById("statusText").setAttribute("label", aText);
},
/**
* Get all servers as an array
*/
get allServers() {
let servers = [];
let allServers = Components.classes["@mozilla.org/messenger/account-manager;1"]
.getService(Ci.nsIMsgAccountManager).allServers;
for (let i = 0; i < allServers.Count(); ++i)
servers.push(allServers.GetElementAt(i).QueryInterface(Ci.nsIMsgIncomingServer));
return servers;
},
/**
* Get the folder which has aFlag set
*/
getFolderWithFlag: function(aServer, aFlag) {
let rootMsgFolder = aServer.rootMsgFolder;
if (rootMsgFolder && aFlag in Ci.nsMsgFolderFlags)
return rootMsgFolder.getFolderWithFlags(Ci.nsMsgFolderFlags[aFlag]);
return null;
},
/**
* Get the subject from a message header object
*/
getSubjectFromMsgHdr: function(aMsgHdr) {
if (aMsgHdr.mime2DecodedSubject)
return aMsgHdr.mime2DecodedSubject;
return aMsgHdr.subject;
},
/**
* Get the raw message (headers and body) from a message URI
*/
getRawMessageFromURI: function(aURI) {
let messenger = Components.classes["@mozilla.org/messenger;1"]
.createInstance(Ci.nsIMessenger);
let messageService = messenger.messageServiceFromURI(aURI);
let messageStream = Components.classes["@mozilla.org/network/sync-stream-listener;1"]
.createInstance(Ci.nsIInputStream);
let inputStream = Components.classes["@mozilla.org/scriptableinputstream;1"]
.createInstance(Ci.nsIScriptableInputStream);
inputStream.init(messageStream);
messageService.streamMessage(aURI, messageStream, {}, null, false, null);
let rawMessage = "";
inputStream.available();
while (inputStream.available())
rawMessage += inputStream.read(512);
messageStream.close();
inputStream.close();
return rawMessage;
},
/**
* Get the message body from a message URI
*/
getBodyFromURI: function(aURI) {
let rawMessage = this.getRawMessageFromURI(aURI);
let stringStream = Components.classes["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stringStream.setData(rawMessage, rawMessage.length);
let messenger = Components.classes["@mozilla.org/messenger;1"]
.createInstance(Ci.nsIMessenger);
let messageService = messenger.messageServiceFromURI(aURI);
let msgHdr = messageService.messageURIToMsgHdr(aURI);
let body = msgHdr.folder.getMsgTextFromStream(stringStream, msgHdr.Charset,
msgHdr.messageSize, msgHdr.messageSize, false, true, {});
return body;
}
},
/**
* Copy HTML (and UTF-8) to the clipboard
*/
copyHTMLToClipboard: function(aHTMLText) {
let strlen = aHTMLText.length * 2;
let str = Components.classes["@mozilla.org/supports-string;1"]
.createInstance(Ci.nsISupportsString);
if (!str)
return false;
str.data = aHTMLText;
var trans = Components.classes["@mozilla.org/widget/transferable;1"]
.createInstance(Ci.nsITransferable);
if (!trans)
return false;
trans.addDataFlavor("text/unicode");
trans.setTransferData("text/unicode", str, strlen);
trans.addDataFlavor("text/html");
trans.setTransferData("text/html", str, strlen);
var clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
.getService(Ci.nsIClipboard);
if (!clipboard)
return false;
clipboard.setData(trans, null, Ci.nsIClipboard.kGlobalClipboard);
return true;
},
/**
* Write HTML (UTF-8) to a file
*/
writeHTMLToFile: function(aHTMLText, aSuggestedName) {
let dirService = Components.classes["@mozilla.org/file/directory_service;1"]
.getService(Ci.nsIProperties);
let file = dirService.get("Desk", Ci.nsIFile);
file.append(aSuggestedName);
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
foStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0);
var converter = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Ci.nsIConverterOutputStream);
converter.init(foStream, "UTF-8", 0, 0);
converter.writeString(aHTMLText);
converter.close();
return file.path;
},
/**
* Open a file in Composer
*/
openFileInComposer: function(aWindow, aFile) {
aWindow.openDialog("chrome://editor/content/", "_blank",
"chrome,all,dialog=no", aFile);
},
/**
* Launch a file
*/
launchFile: function(aFile) {
let file = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Ci.nsILocalFile);
file.initWithPath(aFile);
file.launch();
},
/**
* Change the status bar text
*/
setStatusText: function(aDocument, aText) {
aDocument.getElementById("statusbar-display").setAttribute("label", aText);
}
};
The code from this file finds and imports JHBF.jsm. The file is searched for in the path
that the environment variable JHBFPATH points to. The path statement depends on the operating
system; e.g. E:\Mozilla\buttons for Windows or /home/moz/buttons
for Linux.
/**
* Load JHBF (prepend this to any Custom Buttons code using JHBF!)
* You need to set the JHBFPATH environment variable on your computer
* to the path where you placed JHBF.jsm to make this work.
* @author Jens Hatlak <jh@junetz.de>
* @version 2010-07-15
*/
let envSvc = Components.classes["@mozilla.org/process/environment;1"]
.getService(Components.interfaces.nsIEnvironment);
let file = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
if (!envSvc.exists("JHBFPATH"))
throw {message: "Environment variable JHBFPATH undefined"};
file.initWithPath(envSvc.get("JHBFPATH"));
file.append("JHBF.jsm");
if (!file.exists())
throw {message: "File not found: " + file.path};
let ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
let uri = ios.newFileURI(file);
Components.utils.import(uri.spec);
Checks all checkboxes on the current page.
/**
* Check All Checkboxes
* @author Jens Hatlak <jh@junetz.de>
* @version 2010-07-15
*/
let fields = window.content.document.getElementsByTagName('input');
for (let i = 0; i < fields.length; ++i)
if (fields[i].type == 'checkbox' && fields[i].disabled == false)
fields[i].checked = true;Compacts all folders of all accounts. Hold Shift to skip the confirmation.
/**
* Compact All Folders
* @author Jens Hatlak <jh@junetz.de>
* @version 2010-07-15
*/
let doIt = false;
if (event.shiftKey)
doIt = true;
else
doIt = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService)
.confirm(window, "Compact All Folders", "Really compact all folders?");
if (doIt) {
JHBF.mailnews.allServers.forEach(function(server) {
let isImap = server.type == "imap";
let isNews = server.type == "nntp";
let compactOfflineAlso = isImap || isNews;
let folder = server.rootMsgFolder;
try {
if (!isImap || (server.canCompactFoldersOnServer &&
folder.isCommandEnabled("cmd_compactFolder")))
folder.compactAll(null, msgWindow, compactOfflineAlso);
} catch (e) {}
});
}Empties all trashes of all accounts. Hold Shift to skip the confirmation.
/**
* Empty All Trashes
* @author Jens Hatlak <jh@junetz.de>
* @version 2010-07-15
*/
let doIt = false;
if (event.shiftKey)
doIt = true;
else
doIt = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService)
.confirm(window, "Empty All Trashes", "Really empty all trashes?");
if (doIt) {
JHBF.mailnews.allServers.forEach(function(server) {
let folder = JHBF.mailnews.getFolderWithFlag(server, "Trash");
if (folder)
folder.emptyTrash(msgWindow, null);
});
JHBF.mailnews.setStatusText(document, "Emptied all trashes.");
}Get all messages for all accounts, including News accounts.
/**
* Get All Messages
* @author Jens Hatlak <jh@junetz.de>
* @version 2010-07-15
*/
MailTasksGetMessagesForAllServers(false, null, null); // all but News
const nsIMsgIncomingServer = Components.interfaces.nsIMsgIncomingServer;
JHBF.mailnews.allServers.forEach(function(server) {
if (server instanceof nsIMsgIncomingServer && server.type == "nntp")
server.performExpand(msgWindow);
});Copies all selected messages to the clipboard. Hold Shift to copy the headers as well.
/**
* Copy Mails
* @author Jens Hatlak <jh@junetz.de>
* @version 2010-07-15
*/
let text = "";
let uris = gFolderDisplay.selectedMessageUris;
if (event.shiftKey) {
for (let i = 0; i < uris.length; ++i) {
let rawMail = JHBF.mailnews.getRawMessageFromURI(uris[i]);
var skip = false;
rawMail.split("\r\n").forEach(function(line) {
if (line.match(/^(From - )|(X-)/)) {
skip = true;
} else if (!skip || !line.match(/^\s+/)) {
skip = false;
text += line + "\r\n";
}
});
}
} else {
for (let i = 0; i < uris.length; ++i) {
let body = JHBF.mailnews.getBodyFromURI(uris[i]);
text += body;
}
}
JHBF.copyHTMLToClipboard(text);Cyclically switches the View (threaded mode: All/Unread/Threads with unread/Watches threads with unread, else: All/Unread).
/**
* Switch View
* @author Jens Hatlak <jh@junetz.de>
* @version 2010-07-15
*/
let [viewFlags, viewType] = JHBF.mailnews.view.getFlagsAndType(gDBView);
let unreadOnly = JHBF.mailnews.view.showsOnlyUnread(viewFlags);
let threaded = JHBF.mailnews.view.isThreaded(viewFlags);
if (threaded) {
// cf. nsIMsgDBView.idl
let modes = {
AllMessages: !unreadOnly && viewType == nsMsgViewType.eShowAllThreads,
UnreadMessages: unreadOnly,
ThreadsWithUnread: viewType == nsMsgViewType.eShowThreadsWithUnread,
WatchedThreadsWithUnread: viewType == nsMsgViewType.eShowWatchedThreadsWithUnread
};
let currentIndex = null;
let candidates = [];
let menuItemNames = ["AllMessages", "UnreadMessages", "ThreadsWithUnread", "WatchedThreadsWithUnread"];
menuItemNames.forEach(function(aItem, aIndex) {
// The checked and disabled states have not been initialized here yet, but we only need the command
let menuItem = document.getElementById("view" + aItem + "MenuItem");
if (modes[aItem])
currentIndex = aIndex;
else if (DefaultController.isCommandEnabled(menuItem.command))
candidates[aIndex] = menuItem;
});
if (candidates.length > 0) {
let nextIndex = currentIndex + 1;
while (!candidates[nextIndex]) {
if (nextIndex < candidates.length)
nextIndex++;
else
nextIndex = 0;
}
if (candidates[nextIndex]) {
JHBF.mailnews.setStatusText(document, "Threads: " + candidates[nextIndex].label);
goDoCommand(candidates[nextIndex].command);
}
}
} else {
// Toggle between All and Unread views
let viewValue = gCurrentViewValue == kViewItemUnread ? kViewItemAll : kViewItemUnread;
let viewLabel = GetLabelForValue(viewValue);
ViewChange(viewValue, viewLabel);
JHBF.mailnews.setStatusText(document, "View: " + viewLabel);
}Reloads the current view.
/** * Reload View * @author Jens Hatlak <jh@junetz.de> * @version 2010-09-05 */ gMsgFolderSelected = null; FolderPaneSelectionChange(); // reload virtual folder ViewChangeByFolder(gMsgFolderSelected); // reload view
Creates the draft of an SMTT posting from the selected messages (Pushlog feeds), copies the HTML code to the clipboard, writes it to a file and opens that in a new Composer window.
/**
* Prepare SMTT
* @author Jens Hatlak <jh@junetz.de>
* @version 2010-07-15
*/
let categories = {
"MailNews": [
"mail", "imap", "pop", "attachment", "newsgroup", "local folders", "inbox",
"message", "compose", "archiv", "account", "signature"
],
"Address Book": [ "addrbook", "address" ],
"Bookmarks": [ "bookmark" ],
"Download Manager": [ "download" ],
"History": [ "history" ],
"Help": [ "help", "document" ],
"Locales": [ "locale" ],
"Audio/Video": [ "audio", "video", "webm", "ogg", "vorbis", "theora", "vp8" ],
"Session Store": [ "session", "restore" ],
"Preferences": [ "preferences" ],
"Linux": [ "linux", "unix", "gtk" ],
"Mac": [ "mac", "os x" ],
"Compiling": [ "compiling", "build option", "client\.py" ],
"General": [],
};
function subject4smtt(aSubject) {
let subject = aSubject;
subject = subject.replace(/^[^-]* - /, "");
subject = subject.replace(/^.*RELBRANCH \| /, "");
subject = subject.replace(/(.*)(bug \d+)$/i, "$2 - $1");
subject = subject.replace(/^(bustage )?(fix )?(for )?bug (\d+)[ ,;:]*/i, "bug $4");
subject = subject.replace(/&/g, "&");
subject = subject.replace(/</g, "<");
subject = subject.replace(/>/g, ">");
subject = subject.replace(/"/g, """);
subject = subject.replace(/[ .,;]*[afmors/+]+=\S+[, ]/g, "");
subject = subject.replace(/[ .,;]*[afmors/+]+=\S+$/g, "");
subject = subject.replace(/^(Bug \d+)[- ;:]*(.*)/i, "$2 ($1)");
subject = subject.replace(/Bug (\d+)/gi, "<a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=$1\">Bug $1</a>");
return subject;
}
function catmatch(aSubject, aCat) {
for (let i = 0; i < aCat.length; ++i) {
let re = new RegExp(aCat[i], "i");
if (aSubject.search(re) != -1)
return true;
}
return false;
}
let catdata = {};
for (let cat in categories)
catdata[cat] = [];
let msgs = gFolderDisplay.selectedMessages;
for (let i = 0; i < msgs.length; ++i) {
let msgHdr = msgs[i];
let subject = JHBF.mailnews.getSubjectFromMsgHdr(msgHdr);
subject = subject4smtt(subject);
let pushed = false;
for (let cat in categories) {
if (catmatch(subject, categories[cat])) {
catdata[cat].push(subject);
pushed = true;
break;
}
}
if (!pushed)
catdata.General.push(subject);
}
let html = "";
for (let cat in catdata) {
if (catdata[cat].length) {
html += "<span style=\"font-weight: bold\">" + cat + ":</span>\n";
html += "<ul>\n";
for each (let subject in catdata[cat])
html += " <li>" + subject + "</li>\n";
html += "</ul>\n";
}
}
JHBF.copyHTMLToClipboard(html);
let file = JHBF.writeHTMLToFile(html, "smtt-prepare.html");
JHBF.openFileInComposer(window, file);
September 5, 2010 |