Update for Thunderbird 78
Required steps to convert deprecated Legacy WebExtensions to MailExtensions.
Support for legacy extensions was removed from Thunderbird Beta version 74, released in February 2020. Only modern MailExtensions are now compatible with Thunderbird 78. This guide is intended to help developers to port their Legacy WebExtensions to MailExtensions.
We do not suggest to convert older Legacy Bootstrapped Extensions or Legacy Overlay Extensions (as used in Thunderbird 60) directly to MailExtensions. They should first be converted to Legacy WebExtensions as described in the update guide for Thunderbird 68.
If you need any help, get in touch with the add-on developer community:
This guide covers the 'proper' migration strategy to convert a legacy add-on into a MailExtension. If you follow this strategy, you will end up with a future-proof MailExtension that will require substantially less maintenance work for future versions of Thunderbird.
This will be a complex task: almost all interactions with Thunderbird will need to be re-written to use the new APIs. If these APIs are not yet sufficient for your add-on, you may even need to implement additional APIs yourself. Don't worry though: you can find information on all aspects of the migration process below, including links to many advanced topics you may be interested in.
Knowing that following the proper migration strategy is not easy, we created two wrapper APIs which do not require all of these changes. Using these APIs, you can quickly get your add-on running in Thunderbird 78 again, but you will not get the benefits of a MailExtension. The idea behind this is to make add-ons compatible with the current ESR as quickly and easily as possible, so users can continue to work with their beloved add-ons. The actual conversion to a pure MailExtension can then take place in smaller steps. To assist you, we will release small tutorials to remove the legacy parts one after another.
The technical conversion from a Legacy WebExtension to a MailExtension is simple: drop the
legacy
key from the manifest.json
file.Now your add-on should install in current versions of Thunderbird without issues, but it will do nothing. You need to define one or more entry points as documented in the WebExtensions course on MDN, but for a MailExtension the most common option will be adding a background script to
manifest.json
:"background": {
"scripts": ["background-script.js"]
}
Adding this section to your
manifest.json
will cause the file background-script.js
to be loaded and evaluated by Thunderbird. For bootstrap typed Legacy WebExtensions, the existing bootstrap.js
script itself is a good starting point for a background script – for overlay typed Legacy WebExtensions it may be reasonable to start with an empty script and convert overlays using the guidelines below, gradually building up the background script.Contrary to the bootstrap script in legacy add-ons, the background scripts will not get evaluated in a privileged browser context. Instead it is added to an HTML document (a.k.a the "background page") living in a content process, which only has access to MailExtension APIs and some WebExtension APIs inherited from the underlying Firefox code base (they are listed further down on this page). Any interaction with Thunderbird must occur through these APIs. Whenever code needs to be added to the background script, you need to make sure to migrate calls to XPCOM or other native Thunderbird features to these APIs.
The document "Comparison with XUL/XPCOM extensions" from the Extensions Workshop provides a comprehensive overview about which legacy API can be replaced by which WebExtension API.
Since the WebExtension technology originates in Browsers like Google Chrome and Firefox, the namespace for this new kind of API is
chrome.*
or browser.*
which work in Thunderbird as well. Thunderbird also supports messenger.*
, which is a better fit for MailExtensions.While the Thunderbird team plans to add more APIs with upcoming releases, the current set of APIs will not be sufficient to port most add-ons. To work around this limitation, add-ons can introduce their own, additional APIs as so-called Experiments.
Any feature that was available in previous versions of Thunderbird remains available in Thunderbird 78 inside of Experiment APIs.
As Experiments usually run in the main process and have unrestricted access to any aspect of Thunderbird, they are expected to require updates for each new version of Thunderbird. To reduce the maintenance burden in the future, it is in your own interest to use Experiment APIs only to the extent necessary for the add-on.
Best practice: Try to write APIs that would be useful for a wide range of add-ons, not just the one you're porting. That way, you can later on propose the API you designed for inclusion in Thunderbird, with your add-on serving as reference implementation. If your APIs become a part of Thunderbird, you no longer need to maintain them as part of the add-on.
A more thorough description of Experiment APIs can be found in a separate article:
"options_ui": {
"page": "options.html"
}
Instead of a XUL dialog, the specified HTML document is used, which will be accessible to the user through the add-on manager. From that document, all WebExtension and MailExtension APIs can be accessed in the same way as from the background script.
Under unidentified circumstances, WebExtension APIs invoked from an options page may throw the error "Error: Unknown sender or wrong actor for recvAPICall". Until this issue is fixed, you can call any API through the background page:
(await messenger.runtime.getBackgroundPage()).messenger./* ... */
The settings themselves should be stored using one of the new APIs to store data, such as
storage
, so it may be necessary to add an Experiment to migrate existing settings from nsIPrefBranch or other mechanisms not accessible through modern APIs.The
chrome.manifest
file is no longer supported. Many mechanisms have a more or less direct equivalent in the WebExtension world:- interfaces It should no longer be necessary to use binary interfaces, as binary components are deprecated for a long time. To access custom methods on JS-implemented classes from an Experiment, use
.wrappedJSObject
to get access to the raw JS implementation. - content, skin, resourceWebExtensions mostly access files by specifying a relative path inside the extensions directory structure and not by global
chrome://*
,resource://*
orskin://*
URLs. The WebExtension technology itself does not provide a way to register such URLs anymore. If you need global URLs in a WebExtension, usefile://*
URLs generated byextension.rootURI.resolve()
.
Inside Experiment APIs some calls do not work with
file://*
URLs (e.g. new ChromeWorker()
) , here one needs to manually register a chrome://*
URL, as done in the enigmail add-on.- locale Localization for WebExtensions is handled using the
i18n
API, which uses a singlemessages.json
file to store translations. We have created a tool to convert the legacy DTD and property files. See section 'Converting locale files' below.
There are no direct equivalents to manifest flags, so add-ons now need to provide their own mechanisms to switch code or resources depending on the runtime environment. Relevant information is accessible through the
runtime
API.You may use our python script
localeConverter.py
to convert the legacy DTD and property files into the new JSON format. That script will merge the new entries into a potentially existing messages.json
file.To access the new locales use
messenger.i18n.getMessage()
, from within an Experiment API use context.extension.localeData.localizeMessage()
.There is no automatic replacement of locale placeholder entities like
&myLocaleIdentifier;
in HTML or XHTML files anymore. Instead you can use placeholders like __MSG_myLocaleIdentifier__
and include the i18n.js
script provided by the addon-developer-support repository.XUL overlays are no longer supported and you need to find an alternative:
- Overlays just loading a script without a user interface relationship:
- Move the script's content to a background script.
- Overlays extending the user interface beyond the built-in APIs:
While it would be possible to attempt to re-use existing XUL code in an Experiment, it is probably a better idea to use the more future-proof
windows
API to create a window displaying an HTML dialog:messenger.windows.create({
height: 400,
width: 500,
url: "/path/from/root/of/addon/to/dialog.html",
type: "popup"
});
From these dialogs, all WebExtension and MailExtension APIs can be accessed in the same way as from a background script. To send data to and from the opened window, you can use the message passing concept. An example can be found in this TopicBox thread.
We do not suggest to keep working with XUL dialogs, because we want to protect authors from spending conversion time on a dead technology, which is being removed step by step and requires constant updates to the add-on. However, there might be cases where it is currently reasonable to keep loading a XUL document, which could be done like so:
- 1.Manually inject the button, menuitem or whatever is opening the dialog via a window listener Experiment (see the restart example extension).
- 2.An event handler attached to the injected element runs in privileged chrome context and can call functions like
window.open()
orwindow.openDialog()
. - 3.You need a global path to specify the location of your XUL file, either use a
file://*
orchrome://*
path as described in the section "Replacing chrome.manifest" above. - 4.In TB78 you need to rename your
*.xul
file to*.xhtml
.
Any JavaScript file/module loaded by your XUL dialog also runs in privileged chrome context and does not have direct access to MailExtension or WebExtension APIs.
Components and contract IDs can get registered by calling
Components.manager.registerFactory()
from an Experiment. Remember to also call Components.manager.unregisterFactory()
when the Experiment shuts down.To get a factory implementation, copy the component's existing implementation into an Experiment's implementation script and use its NSGetFactory method to build a factory to register:
// original chrome.manifest:
component {00000000-0000-0000-0000-000000000000} implementation.js
contract @example.com/contract;1 {00000000-0000-0000-0000-000000000000}
// original implementation.js:
var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
let classID = Components.ID("{00000000-0000-0000-0000-000000000000}");
let contractID = "@example.com/contract;1";
function exampleComponent() {
/* ... implementation ... */
}
exampleComponent.prototype = {
/* ... implementation ... */
};
var NSGetFactory = XPCOMUtils.generateNSGetFactory([exampleComponent]);
// new experiment.js:
/* ... */
getAPI(context) {
/* ... do the following in an init() method only called once from your
background page ... */
var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
let classID = Components.ID("{00000000-0000-0000-0000-000000000000}");
let contractID = "@example.com/contract;1";
function exampleComponent() {
/* ... implementation ... */
}
exampleComponent.prototype = {
/* ... implementation ... */
};
let factory = XPCOMUtils.generateNSGetFactory([exampleComponent])(classId);
// WARNING: this assumes that Thunderbird is already running, as
// Components.manager.registerFactory will be unavailable for a few
// milliseconds after startup.
Components.manager.registerFactory(classID, "exampleComponent", contractID,
factory);
context.callOnClose({close(){
Components.manager.unregisterFactory(classID, factory);
}});
/* ... */
}
/* ... */
For complex cases, it might be reasonable to put the implementation and optionally its registration in a separate JavaScript module.
Many parts of XUL are discontinued, and there are some other changes that prevent legacy code to run unchanged in an Experiment API. A separate article deals with these changes: