Dev Blog: Thunderbird Add-On Pt. 1

Internally, Thunderbird is built on top of Mozilla’s Gecko web browser engine. The UI of Thunderbird itself consists of a DOM (Document Object Model) written in Mozilla’s XML User Interface Language (XUL) which is backed by native Cross-Platform Component Objct Model (XPCOM) components. XPCOM allows developers to create classes in a variety of languages (typically C++) that can be constructed dynamically and managed inside of Gecko. The interfaces to these XPCOM objects are defined by the developer in Cross Platform Inderface Description Language (XPIDL) and must implement certain interfaces in order to register themselves in the environment and assist Gecko in constructing and destroying them. The XPIDL definitions allow Gecko to translate method calls through a common type system such that the objects can be used by any of the other supported XPCOM languages. The XUL DOM can be scripted using a priveleged Javascript layer (or using native code via XPCOM) and styled using CSS in much the same way that HTML can. Additionally XHTML, SVG and other DOMs can be embedded within the XUL DOM. Add-ons bind into the DOM by registering XUL-based overlays that are integrated into the DOM at runtime. For more information about how to write your own plugins, see Mozilla’s tutorial and resources.

Add-On Layout

The Thunderbird Add-On covered in this post looks like this (full source on github):

├── README.md
├── chrome                             # chrome as in window chrome (GUI)
│   ├── content                          # JS, XUL, etc. files go here.
│   │   ├── Makefile                       # Mac/Linux Makefile
│   │   ├── encryptmail.sln                # Windows VC++ project
│   │   ├── encryptmail.vcxproj            # Windows VC++ project
│   │   ├── encryptmail.vcxproj.filters    # Windows VC++ project
│   │   ├── plugin.def                     # Windows VC++ project
│   │   ├── lib
│   │   │   └── ISAgentSDK -> .../ISAgentSDK # Extracted Ionic SDK
│   │   ├── libencryptmail.js              # Library loading, js-ctypes wrapper
│   │   │                                  # definitions.
│   │   ├── libencryptmaillinux.so         # Windows SDK wrapper library
│   │   ├── libencryptmailmac.so           # Mac SDK wrapper library
│   │   ├── libencryptmailwin.dll          # Linux SDK wrapper library
│   │   ├── messenger.xul                  # Message view overlay that adds a
│   │   │                                  # decrypt button and defines the
│   │   │                                  # decrypted messages modal.
│   │   ├── messenger.js                   # Scripting for messenger.xul
│   │   ├── messengercompose.xul           # Message composition overlay that
│   │   │                                  # adds the encrypt overlay and
│   │   │                                  # defines the attribute and
│   │   │                                  # profile selection dialog.
│   │   ├── messengercompose.js            # Scripting for message composition.
│   │   └── src
│   │       ├── ISAgentKeyspaceWrapper.cpp # ISAgent wrapper that is keyspace
│   │       ├── ISAgentKeyspaceWrapper.h   # agnostic - same interface.
│   │       └── encryptmailnative.cpp      # SDK wrapping functions to provide
│   │                                      # easy / simple access from
│   │                                      # Thunderbird JS environment
│   └── skin                             # This is where CSS, images go
│       ├── ionic16.png
│       └── ionic24.png
├── chrome.manifest                    # Defines our overlays
├── install.rdf                        # Describes project (name, version,
│                                      # requirements, installation, etc.)
└── package.sh                         # Zips up directory structure to produce
# the xpi add-on file.

 

XUL Layouts

Looking at the manifest file, we see that our overlays are being mapped onto internal URIs for the built-in message composition UI and the built-in message viewing UI:

content encryptmail chrome/content/
skin encryptmail chrome/content/

overlay chrome://messenger/content/messenger.xul \
  chrome://encryptmail/content/messenger.xul
overlay chrome://messenger/content/messengercompose/messengercompose.xul \
  chrome://encryptmail/content/messengercompose.xul

 

messengercompose.xul
(Encryption)

The message composition overlay consists of a button that is added to the composition window and a popup panel that allows the user to modify, add, and remove attributes about the email that are stored with the encryption key for policy decisions and analysitcs puroposes, and to pick a device profile to use when encrypting which determines where the key being created is stored and which organization owns that key (if the user has registered with multiple entities within Ionic).

<?xml version="1.0"?>
<overlay id="encryptmail-messenger" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

Here we load the native library code and the scripting for the Encrypt button and the popup panel.

  <script type="application/javascript" src="chrome://encryptmail/content/libencryptmail.js" />
  <script type="application/javascript" src="chrome://encryptmail/content/messengercompose.js" />

This XUL specifies the composition toolbar that exists in the native UI by its ID. We reference the existing toolbar in order to add our own button to it.

  <toolbar id="composeToolbar2">
    <toolbarbutton
      id="encryptmail-encrypt"
      class="toolbarbutton-1 msgHeaderView-button"
      label="Encrypt"
      oncommand="onEncryptButtonCommand(event);" />

Here we begin defining our popup panel. We are using a two column listbox to contain the current attributes (this will be populated by initAddrList() called by onEncryptButtonCommand()). This list box can be updated via the Add and Remove buttons which call onNewAttrBtnCommand() andonRemoveAttrBtnCommand() respectively. When the Encrypt button is clicked, the final list of attributes is extracted from the listbox by getAttributeList(). Each list item has a callback registered (onRowClicked()) to its click event in order to populate the editing textboxes.

    <panel id="encryptmail-attributespanel" noautohide="true"
      titlebar="normal" close="true">
      <label value="Enter any attributes that apply to the message."/>
      <label value=""/>
      <listbox id="encryptmail-attributeslist" rows="10" width="400">
        <listhead>
          <listheader label="Attribute" width="200"/>
          <listheader label="Value" width="600"/>
        </listhead>
        <listcols>
          <listcol/>
          <listcol flex="1"/>
        </listcols>
      </listbox>

Here we define a 2 by 2 grid layout and add textboxes with labels to hold the attribute currently being added or modified.

      <grid flex="1">
        <columns>
          <column flex="2"/>
          <column flex="1"/>
        </columns>
        <rows>
          <row align="center">
            <label   width="200" value="Attribute"/>
            <textbox width="600" id="encryptmail-newattribute"/>
          </row>
          <row align="center">
            <label   width="200" value="Value"/>
            <textbox width="600" id="encryptmail-newvalue"/>
          </row>
        </rows>
      </grid>

We define another grid layout (this time 3 by 2) to position our Add and Remove buttons for adding/replacing and removing attributes in the listbox.

      <grid flex="1">
        <columns>
          <column flex="3"/>
        </columns>
        <rows>
          <row>
            <label  value=""/>
            <button label="Add" oncommand="onNewAttrBtnCommand(event);"/>
            <button label="Remove" oncommand="onRemoveAttrBtnCommand(event);"/>
          </row>
          <row>
            <label  value=""/>
          </row>
        </rows>
      </grid>

This XUL defines the drop down for selecting device profiles inside of another grid layout (2 by 1). The drop down is populated by populateDeviceDropdown() called by onEncryptButtonCommand().

      <grid flex="1">
        <columns>
          <column flex="2"/>
          <column flex="1"/>
        </columns>
        <rows>
          <row align="center">
            <label   width="200" value="Device Profile"/>
            <menulist width="600" id="encryptmail-deviceprofilelist">
              <menupopup id="encryptmail-deviceprofilepopup">
              </menupopup>
            </menulist>
          </row>
        </rows>
      </grid>

Lastly we define out Encrypt and Canel buttons. The Encrypt button will trigger creating the key, iterating the DOM of the email body, and encrypting each text node and image via onEncryptAttrBtnCommand().

      <grid flex="1">
        <columns>
          <column flex="3"/>
        </columns>
        <rows>
          <row>
            <label  value=""/>
            <button label="Encrypt" oncommand="onEncryptAttrBtnCommand(event);"/>
            <button label="Cancel" oncommand="onCancelAttrBtnCommand(event);"/>
          </row>
        </rows>
      </grid>
    </panel>
  </toolbar>
</overlay>

Altogether the file looks like this messenger.xul.

Scripting

Gecko provides Thunderbird with an excellent Javascript envrionment, but ultimately, we need to get access to native code in order to be able to use the Ionic SDK functions. Depending on the environment that you are writing in, there may already be wrappers for your chosen language, but in this case, we are going to write some simple native C++ function exposing a C ABI-compatible API to wrap up the necessary SDK functions for easy consumption. We are then going to use js-ctypes – a simpel foreign function interface provided by Mozilla similar to Python’s ctypes library – to load these functions as a native library into the Javascript environment and to map the functions by their symbols into usable Javascript functions with js-ctypes performing the needed translation between Javascript and C types.

The startup() function below is bound to the window load event in order to load the proper dynamic library from the add-on’s package.

function startup() {
  Components.utils.import("resource://gre/modules/AddonManager.jsm");
  Components.utils.import('resource://gre/modules/ctypes.jsm');
 
  if('Linux' == Services.appinfo.OS) {
    AddonManager.getAddonByID("[email protected]", function(addon) {
      var uri = addon.getResourceURI('chrome/content/libencryptmaillinux.so');
      encryptmaillib = ctypes.open(uri.path);
      encryptmaillibabi = ctypes.default_abi;
      registerFunctions();
    });
  } else if('Darwin' == Services.appinfo.OS) {
    AddonManager.getAddonByID("[email protected]", function(addon) {
      var uri = addon.getResourceURI('chrome/content/libencryptmailmac.so');
      encryptmaillib = ctypes.open(uri.path);
      encryptmaillibabi = ctypes.default_abi;
      registerFunctions();
    });
  } else if('WINNT' == Services.appinfo.OS) {
    AddonManager.getAddonByID("[email protected]", function(addon) {
      var uri = addon.getResourceURI('chrome/content/libencryptmailwin.dll');
      encryptmaillib = ctypes.open(windowsPath(uri.path));
      encryptmaillibabi = ctypes.winapi_abi;
      registerFunctions();
    });
  }
}

Here is an example of binding a native function into Javascript. We pass the symbol name (function name), the calling ABI type, the type of the return value, and the types of the parameters.

  nativeCreateKey = encryptmaillib.declare('issdk_createkeycsv', encryptmaillibabi,
      ctypes.char.ptr,
      ctypes.char.ptr,
      ctypes.char.ptr);

The corresponding native function looks like this. Given an optional device profile ID string (or NULL) and an optional attribute string (or NULL) it constructs an ISAgentCreateKeysRequest for a single key and then callsISAgent::createKeys(). The returned keytag and key are formatted into a string formatted as KEYTAG,BASE64KEY using the built-in ISCryptoUtils::binToBase64() function and returned. On error, NULL is returned. The purpose of this function is to provide a way for the Javascript side of the code to create individual keys as needed and maintain a reference to them such that it can repeatedly encrypt with the same key. The base64 encoding of the key material is simply to make the binary material ASII-safe when passing back and forth across the boundary. Returning a single string containing both the keytag and key makes the memory management easier and is trivial to parse on either side. A JSON library might have been a better choice to interoperate with Javascript, but would have required taking on an additional dependency and would have added complexity to the code.

extern "C"
char *
issdk_createkeycsv(char * device, char * attributes)
{
  if(device) {
    agent->setActiveProfile(device);
  }
 
  ISAgentCreateKeysResponse keysResponse;
  ISAgentCreateKeysRequest keysRequest;
  ISAgentCreateKeysRequest::Key key("encryptmail");
 
  if(attributes) {
    if (0 != to_AttributesMap(attributes, key.getAttributes())) {
      printf("issdk_createkeycsv - failed to convert attributes\n");
      return NULL;
    }
  }
 
  keysRequest.getKeys().push_back(key);
 
  int rslt = 0;
  if(0 != (rslt = agent->createKeys(keysRequest, keysResponse))) {
    printf("issdk_createkeycsv - key create failed %d\n", rslt);
    return NULL;
  }
 
  std::string keyText;
  ISCryptoUtils::binToBase64(keysResponse.getKeys()[0].getKey(), keyText);
 
  keyText = keysResponse.getKeys()[0].getId() + "," + keyText;
  return _strdup(keyText.c_str());
}

In addition to this function, we have a similar native function that parses the key back out and uses it to encrypt cipher text. It manually constructs the equivalent of the SDK’s built-in chunk encoding. This was implemented before there was a convenient standardized method for working with the encoding, so changing this over to the newer method would be a quick and easy code improvement.

extern "C"
char *
issdk_encryptwithcsvkey(char * csvkey, char * plaintext)
{
  std::string csvKey = csvkey;
 
  int split = csvKey.find(",");
 
  if(split == std::string::npos) {
    printf("issdk_encryptwithcsvkey - given csvkey is not formatted correctly\n");
    return NULL;
  }
 
  std::string keyTag = csvKey.substr(0, split);
  std::string keyB64 = csvKey.substr(split+1, csvKey.length());
 
  ISCryptoBytes keyBytes;
  ISCryptoUtils::base64ToBin(keyB64, keyBytes);
 
  ISCryptoAesCtrCipher crypto;
  ISCryptoBytes cipherBytes;
  std::string cipherText;
  crypto.setKey(keyBytes);
  crypto.encrypt(plaintext, cipherBytes);
  ISCryptoUtils::binToBase64(cipherBytes, cipherText);
 
  cipherText = "~!" + keyTag + "~fEc!" + cipherText + "!cEf";
  return _strdup(cipherText.c_str());
}

These two functions are called from within the onEncryptAttrBtnCommand() function below which performs the main tree-walking encryption task. The original version of the encryption simply grabbed the entire body of the email as either plaintext or raw HTML and encrypted it in a single package, but the current version actually walks each node in the HTML DOM of the message and encrypts the textContent elements. This was implemented for compatibility with other internal projectsl; however, this format leaks significant side-channel information (formatting, hyperlinks, tag metadata, attachments, mutl-pars messages etc). The format is being further developed and will likely evolve into something that handles entire messages better along with full MIME conpabitibility.

function onEncryptAttrBtnCommand(event) {
  var popup = document.getElementById('encryptmail-attributespanel');
  var editor = document.getElementById('content-frame');
  var subject = document.getElementById('msgSubject');
 
  var device = null;
  var list = document.getElementById('encryptmail-deviceprofilelist');
  var item = list.selectedItem;
  if(item != null && item.value != null && item.value.length > 0) {
    device = item.value;
  }}

After grabbing the chosen device profile ID (if given), we extract the attributes from the layout, and call the nativeCreateKey function (which we saw above). Note that the returned key must be manually freed. We only create one key and use it to encrypt each text node.

  var keyNativeStr = nativeCreateKey(device, getAttributeList());
  if(keyNativeStr.isNull()) {
    popup.hidePopup();
    alert('Creating key failed. Check that your device is properly registered.');
    return;
  }

We now get a reference to the document in the editor window and construct a tree walker that will help us iterate the text nodes of the DOM.

  var plaineditor = editor.getEditor(editor.contentWindow);
 
  var walker = plaineditor.document.createTreeWalker(plaineditor.rootElement, NodeFilter.SHOW_TEXT);
  var lastNode = null;
  while(walker.nextNode()) {}

As we walk, we look for our encrypted message header in case the body contains another encrypted message that we are responding to. In that case, we will stop and assume the remainder of the body is just the previous message. We do not encrypt the already encrypted previous message a second time.

    if(walker.currentNode.textContent.indexOf("-----BEGIN IONIC MESSAGE-----") > -1) {
      lastNode = walker.currentNode;
      break;
    }

Here we call our native encrypt function with the text node. A NULL return indicates an error, so we must be sure to check for that.

    var encryptedNativeStr = nativeEncryptWithKey(keyNativeStr, walker.currentNode.textContent);
 
    if(encryptedNativeStr.isNull()) {
      popup.hidePopup();
      alert('Encryption failed. Check that your device is properly registered.');
      return;
    }

CData::readString() constructs a Javascript copy of the C string. We can use this copy and free the original native string.

    var encrypted = encryptedNativeStr.readString();
    walker.currentNode.textContent = encrypted;
    nativeStrFree(encryptedNativeStr);
  }
 
  var walker = plaineditor.document.createTreeWalker(plaineditor.rootElement, NodeFilter.SHOW_ALL);
 
  var ios = Components.classes["@mozilla.org/network/io-service;1"].
    getService(Components.interfaces.nsIIOService);
 
  var todo = [];

Here we do a second walk in order to encrypt image content. We will look for nodes named img, attempt to read in their src data, construct a data URI of the image so that when we decrypt it is easy to re-emded the image, encrypt that data, and re-add the data as a specially formatted text node.

  while(walker.nextNode()) {
    if(!('tagName' in walker.currentNode) || walker.currentNode.tagName.toLowerCase() != "img") {
      continue;
    }
 
    var url = ios.newURI(walker.currentNode.getAttribute('src'), null, null);}

We are building XPCOM objects in order to read the image data in. Moving this construction out o the loop could potentially improve our performance.

    var pngFile = url.QueryInterface(Components.interfaces.nsIFileURL).file;
    var istream = Components.classes["@mozilla.org/network/file-input-stream;1"].
      createInstance(Components.interfaces.nsIFileInputStream);
    var bstream = Components.classes["@mozilla.org/binaryinputstream;1"].
      createInstance(Components.interfaces.nsIBinaryInputStream);
 
    istream.init(pngFile, -1, -1, false);
    bstream.setInputStream(istream);
 
    var bytes = bstream.readBytes(bstream.available());

We must be sure to close these object when we are done.

    bstream.close();
    istream.close();
 
    var toencrypt = 'data:image/png;base64,' + btoa(bytes);
    var encryptedNativeStr = nativeEncryptWithKey(keyNativeStr, toencrypt);
 
    if(encryptedNativeStr.isNull()) {
      popup.hidePopup();
      alert('Encryption failed. Check that your device is properly registered.');
      return;
    }
 
    var encrypted = encryptedNativeStr.readString();
    walker.currentNode.textContent = encrypted;
    nativeStrFree(encryptedNativeStr);
 
    encrypted = encrypted.replace('~!','!IMG!').replace('~fEc!','!IMGDATA!').replace('!cEf','!END!');
    var imgdiv = plaineditor.document.createElement('div');
    imgdiv.setAttribute('style', 'height:2px; width: 2px; overflow:hidden;');
    imgdiv.textContent = encrypted;

We construct new nodes and push a todo list of nodes to be replaced as it is not safe to replace nodes while we are iterating the tree.

    todo.push([walker.currentNode, imgdiv]);
  }
 
  for(var i = 0; i < todo.length; i++) {
    todo[i][0].parentNode.replaceChild(todo[i][1], todo[i][0]);
  }

Be sure to free the key string.

  nativeStrFree(keyNativeStr);
 
  var body = plaineditor.document.getElementsByTagName('body')[0];

To finish the message, we insert a header and footer with a friendly message about where to go if you can’t decrypt the email, and add a prefix to the subject.

  var prefix = plaineditor.document.createElement('p');
  prefix.innerHTML = "-----BEGIN IONIC MESSAGE-----";
  body.insertBefore(prefix, body.firstChild);
 
  var infonode = plaineditor.document.createElement('p');
  infonode.innerHTML =
    'Can\'t read this message? See <a href="https://helpmegettheemails.com">' +
    'this page (https://helpmegettheemails.com).</a>';
 
  var suffix = plaineditor.document.createElement('p');
  suffix.innerHTML = "-----END IONIC MESSAGE-----";
 
  if(lastNode != null) {
    lastNode.parentNode.insertBefore(infonode, lastNode);
    lastNode.parentNode.insertBefore(suffix, infonode);
  } else {
    body.appendChild(suffix);
    body.appendChild(infonode);
  }
 
  if(subject.value.indexOf(&quoquot;Ionic Protected :: ") < 0) {
    subject.value = "Ionic Protected :: " + subject.value;
  }
 
  popup.hidePopup();
}
Conclusion

We will end part one here. While we have brushed over many of the details of the UI functions and have not specifically covered the build process, this should hopefully be enough information to familize yourself with the codebase and the concepts behind the add-on. In Part 2, we will look at decryption and building the native library.

Close ()