jens.hatlak.de / Über mich    
Atari   PHP    
Dreamweaver Manual   Linux    
Akronyme   Mozilla / Mozilla Dualboot  
  SciTE   Mozilla bauen  
This page in English This page
in English
  FASTA-Parser   Mozilla installieren  
Ich spreche...   Bookmark Indicator  
    Custom Buttons  
    MailNews Status Icons  

Do you prefer reading pages in English? This page is also available in English.

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

Es gibt leider einige Funktionen, die SeaMonkey (insbesondere die MailNews-Komponente) vermissen lässt. Wenn man sich wie ich mit den Interna auskennt, kann man für fast alles eine Erweiterung schreiben, die das jeweilige Problem löst. Doch das dauert, weil man sich dann schnell mehr mit der Einbindung in die Oberfläche (UI) beschäftigt, als mit der eigentlichen Funktionalität.

Die Erweiterung Custom Buttons ermöglicht es nun, beliebigen JavaScript-Code beim Anklicken einer selbstdefinierten Schaltfläche (Button, inklusive frei wählbarem Bild) auszuführen. Somit entfällt der UI-Teil und es bleibt die Funktionalität, kodiert in JavaScript.

Im Folgenden präsentiere ich einige Buttons für Funktionen, die SeaMonkey von Haus aus nicht beherrscht (jedenfalls nicht genau so). Sicherlich ist es jeweils möglich, SeaMonkey selbst anzupassen, doch jedweder SeaMonkey-Code muss den Review-Prozess durchlaufen, und das kann dauern – mit ungewissem Ausgang. Ähnliches gilt übrigens auch für Erweiterungen, die auf AMO gehostet werden.

Alle unten aufgeführten Buttons wurden mit SeaMonkey 2.0 getestet. Einige funktionieren vielleicht auch mit Thunderbird; da ich diesen jedoch nicht benutze, habe ich es nicht getestet. Für Rückmeldungen jeder Art bin ich aber dankbar.

Nutzungsbedingungen

Sämtlicher Code auf dieser Seite kann sowohl privat als auch kommerziell genutzt werden, aber in jedem Fall auf eigene Gefahr. Ich übernehme keinerlei Verantwortung für die Richtigkeit des Codes oder irgendwelche Konsequenzen, die sich aus der Ausführung desselben ergeben.

Wer den Code nicht weiter gibt, kann damit machen, was er will. Wer den Code weiter gibt (veröffentlicht), muss entweder den Code inklusive Kommentaren unverändert lassen, sich selbst als Autor mit eintragen, falls signifikante Änderungen vorgenommen werden, oder mich nicht als Autor aufführen, falls nur kleine Teile oder aus anderen Quellen stammende Teile übernommen werden.

Umgebung

JHBF.jsm

Das Basismodul JHBF stellt einige Grundfunktionen zur Verfügung. Jeder Button, dessen Code JHBF benutzt, muss diese Datei importieren. Da viele meiner Buttons dieses Modul benötigen, habe ich dafür gesorgt, dass man es nur einmal auf der Festplatte ablegen muss und überall dort importieren kann, wo es benötigt wird. Das geschieht mittels des Codes aus loadJHBF.js (s.u.). Zweck des Moduls ist es, häufig verwendete Funktionalitäten so zu kapseln, dass sie möglichst einfach verwendet werden können (etwa mittels 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

Der Code aus dieser Datei sorgt dafür, dass die Datei JHBF.jsm gefunden und importiert wird. Gesucht wird sie in dem Pfad, auf den die Umgebungsvariable JHBFPATH zeigt (d.h. der Pfad kann selbst gewählt werden). Die Pfadangabe ist abhängig vom Betriebssystem, also etwa E:\Mozilla\buttons unter Windows und /home/moz/buttons unter Linux. Die "custombutton://"-Links unten stellen den Inhalt dieser Datei automatisch dem eigentlichen Button-Code voran, falls der jeweilige Button-Code es erfordert.

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

Hakt alle Checkboxen auf der aktuellen Seite an.

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

Komprimiert alle Ordner aller Accounts. Umschalt festhalten, um die Abfrage zu überspringen.

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

Leert alle Papierkörbe aller Accounts. Umschalt festhalten, um die Abfrage zu überspringen.

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

Ruft neue Nachrichten für alle Accounts ab, inklusive 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

Kopiert alle selektierten Nachrichten in die Zwischenablage. Umschalt festhalten, um auch die Header mitzukopieren.

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

Wechelt die Ansicht zyklisch (Threads-Modus: Alle/Ungelesene/Threads mit ungelesenen/Beobachtete Thread mit ungelesenen, sonst: Alle/Ungelesen).

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

Lädt die aktuelle Ansicht neu.

/**
 * Reload View
 * @author Jens Hatlak <jh@junetz.de>
 * @version 2010-09-05
 */
gMsgFolderSelected = null;
FolderPaneSelectionChange(); // reload virtual folder
ViewChangeByFolder(gMsgFolderSelected); // reload view

Prepare SMTT

Erzeugt aus den selektierten Nachrichten (Pushlog-Feeds) die Rohform eines SMTT-Beitrags, kopiert den HTML-Code in die Zwischenablage, schreibt ihn in eine Datei und öffnet diese in einem neuen Composer-Fenster.

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

Legt ein Lesezeichen für die Website der angezeigten Feed-Nachricht an.

/**
 * 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
25. Dezember 2011