How to Develop a Thunderbird 60 Add-On (2018)


I've had an idea kicking around in the back of my mind for the last few months to create a Thunderbird extension that will indicate if an email sender's domain was recently registered and alert me. With the poor state of Thunderbird add-on documentation it is a real struggle to get started with anything beyond the most basic 'hello-world' extension. This time I decided to double-down and fight my way through to develop a working (Alpha quality) plugin that accomplishes my design.

If you are thinking about developing an extension for Thunderbird 60 and would like some pointers, read on for my choppy journey through Thunderbird extension development. Hopefully one or more of the pointers will save you time

References

The Journey Begins

After fumbling around for a bit I found a high-level/basic introduction to building extensions: Building a Thunderbird extension: introduction. This took me through the basics of how to structure/layout and package an extension. It also showed me how to get a debug version of the extension loaded for testing (with an important caveat I'll get to later). This article will lead you astray with respect to the current state of Thunderbird Developer Tools and point you to generic documentation that is Firefox centric.

Key take-aways that can save time and get you in the mindset for extension development:

  • Firefox remote debugging is red herring and does not work
  • While the ''DOM Inspector'' plugin does not work in Thunderbird, it's not a big deal (see next line)
  • Thunderbird includes a robust set of developer tools in the Tools -> Developer Tools -> Developer Toolbox menu. I had to stumble upon this in frustration as I could find NOTHING online mentioning this tool
  • Pretty much any documentation you find will likely be either (a) out of date or (b) completely centered around Firefox, so be prepared for experimentation

Stubbing Out Your Extension

A Thunderbird extension is a bundle of meta-data, UI Overlay [we'll get to what this means] and Javascript files that work together to deliver functionality. For best results when developing, use a text editor that has a tree view (like Visual Studio Code, Sublime, Atom, etc...) and lets you easily move between open files. For now, create a base directory and include these files (and one sub-directory):

domain-age-monitor/
    install.rdf
    chrome.manifest
    content/
  • install.rdf is the meta-data file that describes your extensions name, version and other identifying information
  • chrome.manifest has the job of matching your UI overlay files to the base UI that needs extending. Basically, you create overlay files (suffixed with .xul) that add or modify UI elements in Thunderbird
  • content/ is a directory where you can store your overlay files and their corresponding logic (javascript)

Sample Extension Files

These sample files are pulled from my sample extension:

install.rdf

<?xml version="1.0"?>

<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     xmlns:em="http://www.mozilla.org/2004/em-rdf#">

  <Description about="urn:mozilla:install-manifest">
    <em:id>domainagechecker@rubion.com</em:id>
    <em:name>Domain Age Checker</em:name>
    <em:version>0.1</em:version>
    <em:creator>Rion Carter</em:creator>

    <em:targetApplication>
      <Description>
        <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id>
        <em:minVersion>60</em:minVersion>
        <em:maxVersion>61</em:maxVersion>
      </Description>
    </em:targetApplication>

  </Description>      
</RDF>

chrome.manifest

content     domainagechecker    content/
overlay chrome://messenger/content/msgHdrViewOverlay.xul chrome://domainagechecker/content/domainagechecker-warn.xul

As you can see, there is not a lot to these files. While the RDF is pretty self-explanatory, the manifest file is a little more confusing. There are 2 lines in this example manifest:

  • The first line tells Thunderbird that my content/ directory will store content (xul overlays and javascript actions)
  • The next line shows that I am overlaying the default msgHdrViewOverlay.xul with my own (domainagechecker-warn.xul). You'll notice that I specify a base path of chrome://domainagechecker- I get that identifier from the <em:id> xml field in the install.rdf file. The identifier is everything leading up to the @ (at) sign.

(You might be asking yourself: how on earth did you know that you needed to over lay the msgHdrViewOverlay.xul file? I'll get to that...)

Sample Content Files

The extension will not work without an overlay and its corresponding javascript file. I'll list the source from my sample plugin here before moving on to how you figure this stuff out for yourself in a later section (The JS file has had some comments removed to (try) and slimit down:

overlay-warn.js

window.addEventListener("load", function windowLoader(e) { 
    this.window.removeEventListener("load", windowLoader, false);   // Only need it to run this _one_ time
    startup(); 
}, false);

function startup() {
    let mailMessagePane = document.getElementById("messagepane");
    mailMessagePane.addEventListener("load", function messagePaneLoader(e){
        // First, get the message 'header' (Internal Thunderbird concept, not a 'mail header' like 'reply-to')
        let selectedMessage = gFolderDisplay.selectedMessageUris[0];
        let mheader = Components.classes['@mozilla.org/messenger;1'].getService(Components.interfaces.nsIMessenger).msgHdrFromURI(selectedMessage);

        // Get the mime message so we can parse headers
        MsgHdrToMimeMessage(mheader, null, function(msgHdr, mimedMessage){
            // Callback hell since I need the message in mime format to continue

            // Now, read the headers and get the relevant domains (for 'from' and 'reply-to'):
            let domains = [];
            if (mimedMessage.has("from")) {
                var email = mimedMessage.get("from");

                // Try to get just the 'domain' piece (including any relevant subdomains)
                var emailPieces = email.split("@");
                var rawDomains = emailPieces[emailPieces.length -1];

                var cleanDomain = rawDomains.match(/([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+/);
                domains.push(cleanDomain[0]);
            }
            if (mimedMessage.has("reply-to")) {
                var email = mimedMessage.get("reply-to");

                // Try to get just the 'domain' piece (including any relevant subdomains)
                var emailPieces = email.split("@");
                var rawDomains = emailPieces[emailPieces.length -1];
                var cleanDomain = rawDomains.match(/([a-z0-9|-]+\.)*[a-z0-9|-]+\.[a-z]+/);

                domains.push(cleanDomain[0]);
            }

            // For each domain, make a request to the Whois caching server to get domain details
            // https://github.com/rioncarter/whois-caching-proxy
            for(var i=0; i<domains.length; i++){
                var xhttp = new XMLHttpRequest();
                xhttp.onreadystatechange = function domainReply(){
                    if(this.readyState == 4 && this.status == 200){
                        // check the date (is it less than 7 months old?)
                        let domain = JSON.parse(this.responseText);
                        let domainDate = new Date(domain.RegisteredDate);

                        let now = Date.now();   // gets current time in milliseconds
                        let diffMs = now - domainDate.getTime();    // Get time in milliseconds
                        let diffDays = diffMs/(1000*60*60*24);      // Difference in days
                        if (diffDays < 216){
                            displayNotification(domain.Name);
                        }
                    }
                };
                xhttp.open("GET", "http://localhost:9091/checkDomain/"+domains[i], true);
                xhttp.send();
            }
        });

    }, true);
}

function displayNotification(domain){
    // Show a message in the notification bar
    let msgNotificationBar = document.getElementById("msgNotificationBar");

    msgNotificationBar.appendNotification("Suspicious domain: " + domain, "suspiciousDomainFound",
        "chrome://messenger/skin/icons/move-up.svg",
        msgNotificationBar.PRIORITY_CRITICAL_HIGH,
        []);
}

domainagechecker-warn.xul

<?xml version="1.0"?>
<overlay id="domainagechecker-warn-overlay" 
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 <script type="application/javascript" src="chrome://domainagechecker/content/overlay-warn.js"/>
 <statusbar id="status-bar">
 </statusbar>
</overlay>

The Javascript above (overlay-warn.js) is function-specific to displaying a thunderbird notification on a specific email message and took quite a bit of time for me to puzzle out. I'll describe the process I went through to puzzle this out in a later section.

The xul overlay on the other hand is not overly verbose and feels pretty tight. My extension only needs to run javascript and I am not adding any UI Elements. The sample XUL file here references the 'statusbar' only because I thought I was going to use that in the extension (but never did). So the important aspect of this xul file is that it references my content script (overlay-warn.js)

Installing Your Extension for Debugging

The installation section of the 'building a thunderbird' extension page is helpful in getting a plugin installed and working:

  • Find your extension directory (operating system specific):
    Windows: %APPDATA%\Thunderbird\Profiles\<Profile Name>\extensions\
    Linux: ~/.thunderbird/<Profile Name>/extensions/
    MAC: ~/Library/Thunderbird/Profiles/<Profile Name>/extensions/
  • Create a file (in the extensions directory) with the same name as the <em:id> from your install.rdf file (in my case this is called domainagechecker@rubion.com)
  • The file must have a single line with the absolute path to your extension development directory. The target development folder MUST have the same name as the <em:id> from install.rdf (For example, the line in my file is: /home/rcart/src/thunderbird-extensions/domainagechecker@rubion.com)

Important Note: Each time you make a change to your extension you may need to do the following to get Thunderbird to recognize the change:

  • Increment the <em:version> field in install.rdf
  • Use the linux touch command on your extension development directory to change the date/time and make it look more recent
  • Restart Thunderbird

Now the Fun Begins: How do I Know What to Do?

I've spent a lot of time this last week scouring forums, StackOverflow and mozilla documentation and tinkering around with the extension development platform. If I had to start over, this is the approach I would take:

  • Open the Thunderbird Developer Tools Tools -> Developer Tools -> Developer Toolbox
  • Use the Inspector tool to determine which UI elements I want to modify
  • Download the Thunderbird Source Code and inspect all xul and include files for the UI element that I want to modify
    • This will help you identify the name of the XUL file you want to overlay

For the Javascript side of things:

  • Find an existing Thunderbird extension that does something similar to what you want to do
  • Download the extension and examine the javascript
  • If that doesn't work, try searching the mozilla documentation or google as a hail-mary
  • Open the Thunderbird Developer Tools Tools -> Developer Tools -> Developer Toolbox
    • Use the Console tool to play around 'live' and save yourself a TON of time figuring out if your idea will work before creating an extension

Challenges

When creating this sample plugin, there were a few things that posed big challenges for me:

  • Hooking into an event that would load on a per-message basis (not the same as hooking the 'window')
  • Reading all mail headers (not just the easily packaged 'to' and 'from'
  • Displaying a notification bar similar to the 'Remote Content Warning' message that appears for messages with links to internet content

Since these challenges were hard to find solutions for, I'm posting my approach to dealing with them here:

Hook on Email Message Load

This requires you to first hook the window 'load' event then attach to the mail message pane load event. These statements should be at the top of your javascript action file:

window.addEventListener("load", function windowLoader(e) { 
    this.window.removeEventListener("load", windowLoader, false);   // Only need it to run this _one_ time
    startup(); 
}, false);

function startup() {
    let mailMessagePane = document.getElementById("messagepane");
    mailMessagePane.addEventListener("load", function messagePaneLoader(e){
        // Your per-message logic here
        // This runs every time a new message comes into focus

        // For example, get the currently selected message
        let selectedMessage = gFolderDisplay.selectedMessageUris[0];
    }, true);
}

Get a Mime Encoded Message (To Get Email Headers)

Thunderbird overloads the term 'header' to mean both an email message fragment that you can click on in the side-bar as well as an RFC compliant email header. To get the RFC headers, you need to go through a translation process and then call a function that requires you to use a call-back function to get the mime encoded message:

// First, get the message 'header' (Internal Thunderbird concept, not a 'mail header' like 'reply-to')
let selectedMessage = gFolderDisplay.selectedMessageUris[0];
let mheader = Components.classes['@mozilla.org/messenger;1']
         .getService(Components.interfaces.nsIMessenger)
         .msgHdrFromURI(selectedMessage);

        //
        // Get the mime message so we can parse headers
        MsgHdrToMimeMessage(mheader, null, function(msgHdr, mimedMessage){
            // Check if the email has a header by using the '.has()' method
            if (mimedMessage.has("reply-to")) {
                // This example shows how to pull the 'reply-to' header
                var email = mimedMessage.get("reply-to");
            }
        });

Display a Per-Message Notification

My extension is only useful if it can display domain warnings on a per-message basis. It was a struggle of terminology for me to find out how to work with notificationbox and how to use the appendNotification method reference. For reference, there are several different Priority Levels you can pick from:

PRIORITY_INFO_LOW
PRIORITY_INFO_MEDIUM
PRIORITY_INFO_HIGH
PRIORITY_WARNING_LOW
PRIORITY_WARNING_MEDIUM
PRIORITY_WARNING_HIGH
PRIORITY_CRITICAL_LOW
PRIORITY_CRITICAL_MEDIUM
PRIORITY_CRITICAL_HIGH
PRIORITY_CRITICAL_BLOCK

Here's my example for how to append a notification to the per-message notificationbox in Thunderbird 60:

function displayNotification(domain){
    // Show a message in the notification bar
    let msgNotificationBar = document.getElementById("msgNotificationBar");

    msgNotificationBar.appendNotification("Suspicious domain: " + domain, "suspiciousDomainFound",
        "chrome://messenger/skin/icons/move-up.svg",
        msgNotificationBar.PRIORITY_CRITICAL_HIGH,
        []);
}

A few notes:

  • I did not need to add any buttons, so those are left out from this example
  • You can add your own .svg file if you want a custom icon. I just decided to pick a default icon for this first attempt
  • Your notification will either overlay an existing notification or be underlayed depending on the priority level. If multiple notifications of the same priority are posted, the MOST RECENT one will bubble up to the top and cover the others
  • I could not figure out how to get MULTIPLE notifications to appear on-screen at the same time (I'd like the Remote Content Warning to display at the same time if at all possible)...