jens.hatlak.de / About me    
Atari   Mozilla / Mozilla dualboot  
Dreamweaver Manual     Building Mozilla  
Acronyms     Installing Mozilla  
    Bookmark Indicator  
Diese Seite in deutsch Diese Seite
in deutsch
    Custom Buttons  
  MailNews Status Icons  

Custom Buttons

Check All Checkboxes / Compact All Folders / Empty All Trashes / Get All Messages / Copy Mails / Switch View / Reload View / Prepare SMTT / Bookmark Feed Website

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.

Terms of Use

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.

Environment

JHBF.jsm

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 2011-12-13
 */
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 all subfolders of a folder as an array
     */
    getSubFolders: function(aFolder) {
      let subFolders = [];
      let allFolders = Components.classes["@mozilla.org/supports-array;1"]
                                 .createInstance(Ci.nsISupportsArray);
      aFolder.ListDescendents(allFolders);
      for (let i = 0; i < allFolders.Count(); ++i)
        subFolders.push(allFolders.GetElementAt(i).QueryInterface(Ci.nsIMsgFolder));
      return subFolders;
    },

	/**
	 * 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);
  }
};

loadJHBF.js

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 (i.e. you can choose the target path yourself). The path statement depends on the operating system; e.g. E:\Mozilla\buttons for Windows or /home/moz/buttons for Linux. If you click one of the "custombutton://" links below, the contents of this file are prepended to the actual script code automatically if the respective button code requires it.

/**
 * 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);

Buttons

Check All Checkboxes

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;

Compact All Folders

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) {}
  });
}

Empty All Trashes

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

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);
});

Copy Mails

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);

Switch View

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);
}

Reload View

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

Prepare SMTT

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, "&amp;");
  subject = subject.replace(/</g, "&lt;");
  subject = subject.replace(/>/g, "&gt;");
  subject = subject.replace(/"/g, "&quot;");
  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);

Bookmark Feed Website

Creates a bookmark for the website of the displayed feed item.

/**
 * Bookmark Feed Website
 * @author Jens Hatlak <jh@junetz.de>
 * @version 2011-12-13
 */
if (currentHeaderData && "content-base" in currentHeaderData) {
  let url = currentHeaderData["content-base"].headerValue;
  let title = currentHeaderData["subject"].headerValue;
  PlacesUIUtils.showMinimalAddBookmarkUI(makeURI(url), title);
}

 

Valid HTML 4.0!
Jens Hatlak
December 25, 2011