Working with WebExtension Experiments

Introduction to Experiment APIs

A WebExtension Experiment API is an additional API that is shipped with your WebExtension. It allows your extension to use custom features not yet available through Thunderbird’s built-in APIs.

Note

Firefox does not allow WebExtension Experiment APIs on release or beta versions. Thunderbird does.

Note

This is a very cut-down example. You may find the Firefox documentation helpful, particularly the pages on API schemas, implementing a function, and implementing an event.

There is also a detailed introduction at developer.thunderbird.net and a few Experiment examples on Github.

Extension manifest

Experiment APIs are declared in the experiment_apis property in a WebExtension’s manifest.json file. For example:

{
  "manifest_version": 2,
  "name": "Extension containing an Experiment API",
  "experiment_apis": {
    "myapi": {
      "schema": "schema.json",
      "parent": {
        "scopes": ["addon_parent"],
        "paths": [["myapi"]],
        "script": "implementation.js"
      }
    }
  }
}

Schema

The schema defines the interface between your experiment API and the rest of your extension, which would use browser.myapi in this example. In it you describe the functions, events, and types you’ll be implementing:

[
  {
    "namespace": "myapi",
    "functions": [
      {
        "name": "sayHello",
        "type": "function",
        "description": "Says hello to the user.",
        "async": true,
        "parameters": [
          {
            "name": "name",
            "type": "string",
            "description": "Who to say hello to."
          }
        ]
      }
    ]
  }
]

You can see some more-complicated schemas in the Thunderbird source code.

Implementing functions

And finally, the implementation. In this file, you’ll put all the code that directly interacts with Thunderbird UI or components. Start by creating an object with the same name as your api at the top level. (Remember to use var myapi or this.myapi, not let myapi or const myapi.)

The object has a function getAPI which returns another object containing your API. Your functions and events are members of this returned object:

var myapi = class extends ExtensionCommon.ExtensionAPI {
  getAPI(context) {
    return {
      myapi: {
        async sayHello(name) {
          Services.wm.getMostRecentWindow("mail:3pane").alert("Hello " + name + "!");
        },
      }
    }
  }
};

(Note that the sayHello function is an async function, and alert blocks until the prompt is closed. If you call browser.myapi.sayHello(), it would return a Promise that doesn’t resolve until the user closes the alert.)

Implementing events

The code for events is more complicated, but the pattern is the same every time. The interesting bit is the register function, with the argument named fire in this example. Any call to fire.async will notify listeners that the event fired with the arguments you used.

In register, add event listeners, notification observers, or whatever else is needed. register runs when the extension calls browser.myapi.onToolbarClick.addListener, and returns a function that removes the listeners and observers. This returned function runs when the extension calls browser.myapi.onToolbarClick.removeListener, or shuts down.

var myapi = class extends ExtensionCommon.ExtensionAPI {
  getAPI(context) {
    return {
      myapi: {
        onToolbarClick: new ExtensionCommon.EventManager({
          context,
          name: "myapi.onToolbarClick",
          register(fire) {
            function callback(event, id, x, y) {
              return fire.async(id, x, y);
            }

            windowListener.add(callback);
            return function() {
              windowListener.remove(callback);
            };
          },
        }).api(),
      }
    }
  }
};

Using folder and message types

The built-in schema define some common objects that you may wish to return, namely MailFolder, MessageHeader, and MessageList.

To use these types, interact with the folderManager or messageManager objects which are members of the context.extension object passed to getAPI:

// Get an nsIMsgFolder from a MailFolder:
let realFolder = context.extension.folderManager.get(accountId, path);

// Get a MailFolder from an nsIMsgFolder:
context.extension.folderManager.convert(realFolder);

// Get an nsIMsgDBHdr from a MessageHeader:
let realMessage = context.extension.messageManager.get(messageId);

// Get a MessageHeader from an nsIMsgDBHdr:
context.extension.messageManager.convert(realMessage);

// Start a MessageList from an array or enumerator of nsIMsgDBHdr:
context.extension.messageManager.startMessageList(realFolder.messages);

Using tabs and windows

To access tabs or windows using the ID values from the built-in APIs, use the tabManager or windowManager objects. These are have functions similar to, but not the same as, the APIs:

// Get a real tab from a tab ID:
let tabObject = context.extension.tabManager.get(tabId);
let realTab = tabObject.nativeTab;
let realTabWindow = tabObject.window;

// Get a tab ID from a real tab:
context.extension.tabManager.getWrapper(realTab).id;

// Query tabs: (note this returns a Generator, not an array like the API)
context.extension.tabManager.query(queryInfo);

“Tabs” are a bit weird. For a tab on the main Thunderbird window, the nativeTab property is the tabInfo object you’d get from that window’s <tabmail>. For a tab not on the main window, e.g. a “tab” representing the message composition window, both nativeTab and window properties refer to the window itself.

// Get a real window from a window ID:
let windowObject = context.extension.windowManager.get(windowId);
let realWindow = windowObject.window;

// Get a window ID from a real window:
context.extension.windowManager.getWrapper(realWindow).id;

// Get all windows: (note this returns a Generator, not an array like the API)
context.extension.windowManager.getAll();

For more things you could use on tabObject or windowObject in the examples above, see the Tab, TabMailTab, and Window classes in the source code.