LogoLogo
  • About Thunderbird
  • Contributing to Thunderbird
    • Getting Started Contributing
    • Setting Up A Build Environment
    • Building Thunderbird
      • Windows Build Prerequisites
      • Linux Build Prerequisites
      • macOS Build Prerequisites
      • Artifact Builds
    • Codebase Overview
      • Account Configuration
      • Address Book
      • Chat Core
        • Notifications
        • Message Styles
        • Keyboard shortcuts
        • Chat Core Protocols
        • Contacts
      • Mail Front-End
    • Tutorials and Examples
      • Hello World Example
      • Thunderbird Live Development Videos
    • Fixing a Bug
      • Bug Triaging 101
        • Bug Status Classicification
        • Bug Types
        • Garbage Collection
        • Narrow the Scope
      • Using Mercurial Bookmarks
      • Using Mercurial Queues
      • Lint and Format Code
      • Using ESLint to Format Javascript Code
      • Try Server
      • Landing a Patch
      • Care and Feeding of the Tree
    • Testing
      • Running Tests
      • Adding Tests
      • Writing Mochitest Tests
  • Planning
    • Roadmap
    • Android Roadmap
    • Supported Standards
  • Add-on Development
    • Introduction
    • What's new?
      • Manifest Version 3
    • A "Hello World" Extension Tutorial
      • Using WebExtension APIs
      • Using a Background Page
      • Using Content Scripts
    • A Guide to Extensions
      • Supported Manifest Keys
      • Supported UI Elements
      • Supported WebExtension APIs
      • Thunderbird's WebExtension API Documentation
      • Thunderbird WebExtension Examples
      • Introducing Experiments
    • A Guide to Themes
    • Developer Community
    • Documentation & Resources
      • Tips and Tricks
    • Add-on Update Guides
      • Update for Thunderbird 128
      • Update for Thunderbird 115
        • Adapt to Changes in Thunderbird 103-115
      • Update for Thunderbird 102
        • Adapt to Changes in Thunderbird 92-102
      • Update for Thunderbird 91
        • Adapt to Changes in Thunderbird 79-91
      • Update for Thunderbird 78
        • Adapt to Changes in Thunderbird 69-78
      • Update for Thunderbird 68
        • Adapt to Changes in Thunderbird 61-68
      • How to convert legacy extensions?
        • Convert wrapped WebExtensions to modern WebExtensions
        • Convert legacy WebExtensions to modern WebExtensions
        • Convert legacy overlay extension to legacy WebExtension
        • Convert legacy bootstrapped extension to legacy WebExtension
  • Releases
    • Thunderbird Channels
    • Release Cadence
    • Uplifting Fixes
    • Feature Flags
    • Tracking Fixes for Releases
    • Contributing to Release Notes
Powered by GitBook
On this page
  • Designing the API
  • Building the Structure
  • Managing your Experiment's lifecycle
  • Passing data to and from a WebExtension
  • Parent implementations
  • Child implementations
  • Structuring Experiment code
  • Accessing WebExtensions directly from an Experiment

Was this helpful?

Edit on GitHub
Export as PDF
  1. Add-on Development
  2. A Guide to Extensions

Introducing Experiments

PreviousSupported WebExtension APIsNextA Guide to Themes

Last updated 7 months ago

Was this helpful?

This document aggregates information on topics that commonly arise when developing a new Experiment. For a complete documentation on each individual topic, refer to the linked articles.

Since Experiments directly interact with Thunderbird's core functions, it is necessary to get used to the source code of Thunderbird itself. We gathered the most useful resources on the page.

Thunderbird does contain a few useful features related to Experiments . Especially if you're writing an Experiment with complex interactions between the WebExtension and your Experiment, it may be helpful to read the documentation blocks within and possibly other modules in the same source code folder.

For reference, the parent implementations of all built-in APIs can be found in .

Experiment APIs have full access to Thunderbird's core functions and can bypass the WebExtension permission system entirely. Including one or more Experiment APIs will therefore disable the individual permission prompt and instead prompt the user only for the permission.

The use of optional permissions is not supported for the same reason.

Designing the API

When you encounter a feature that cannot get implemented with existing WebExtension APIs, it may be helpful to first read through the documentation of some built-in APIs, this document and some linked documentation to get a feeling about how existing APIs encapsulate common problems (i.e. listener registration through events, excessive use of Promise, ...) and what limitations your API will have to live with (i.e. potential complexity when passing functions or raw DOM elements).

Afterwards, think about (hypothetical or real) add-ons that would have similar needs and try to design an API that would be useful for a wide range of add-ons:

  • A good API provides a single feature with as little complexity as possible (smaller is usually better).

    Example: an API adding both a menu item and a toolbar item for a single callback function might be useful, but it would be better to split it in two separate operations and combine them in the WebExtension.

  • A good API can be used in a generic way and is not tied to your add-on's logic or functionality.

    Example: an API to add a menu item that will perform a fixed action might solve the specific issue you're working on, but it would be better to permit WebExtension code to dynamically specify the action to perform.

  • A good API adheres to the conventions of WebExtension APIs.

    Example: a registerListener function might be a good idea in isolation, but it would be better to use events (that also simplifies the implementation!).

Depending on your time constraints, experience, and the concrete feature you're trying to write an API for, it is not necessarily reasonable to satisfy all three criteria, they are just a guideline to aim for.

Building the Structure

  1. A parent implementation implements the API in Thunderbird's main process. All features that were available to a bootstrapped legacy extension can be used here.

  2. A child implementation implements the API in the content process(es). This permits more complex interactions with WebExtension code and potentially improves performance, at the cost of not being able to access the main process.

A typical entry in the manifest.json file to register an Experiment:

    "experiment_apis": {
        "LegacyPrefs": {
            "schema": "api/LegacyPrefs/schema.json",
            "parent": {
                "scopes": ["addon_parent"],
                "paths": [["LegacyPrefs"]],
                "script": "api/LegacyPrefs/implementation.js",
                "events": ["startup"]
            }
        }
    }

Technically speaking, Thunderbird is not actually using multiple processes (yet). However, the APIs were designed with multiple processes in mind and enforce at least some constraints as if the parts were in different processes.

In most cases, you can start by formalizing your API draft into a schema and adding a parent implementation using one of the linked articles as base. Add or switch to a child implementation if you have performance considerations or need to pass more complex data (see below).

Check out the Example Experiments:

Avoid declaring global variables in the implementation of your Experiment, as that can cause collisions with other Experiments loaded. Instead declare them as members of your API, or use a closure.

Managing your Experiment's lifecycle

Experiments are loaded on demand. In order for an Experiment to get loaded, you thus need to either use the API from the WebExtension or register an implementation for the startup event (which calls the onStartup() method of that implementation's ExtensionAPI object once your add-on is loaded).

If your add-on is unloaded, Thunderbird will call the onShutdown() method of each loaded implementation's ExtensionAPI object. You should perform any cleanup tasks in that method, for example you must invalidate Thunderbird's startup cache whenever your add-on is unloaded for a non-shutdown reason:

Services.obs.notifyObservers(null, "startupcache-invalidate", null);​

Failure to invalidate caches may cause parts of the add-on's Experiment APIs to be cached across updates of the Add-on, even if they are changed in the update. It is thus usually a good idea to execute the code above in the onShutdown() method of an Experiment that is always loaded.

In addition to your Experiment being loaded and unloaded as a whole, that Experiment's API will get loaded into each WebExtension context independently. As there can be multiple contexts at the same time, an Experiment may have multiple loaded APIs in parallel. You can perform context-specific loading tasks directly in getAPI(), and register context-specific unloading code through context.callOnClose().

Passing data to and from a WebExtension

Parent implementations

If your API function is supposed to return a value, it must be defined as async in the schema file.

Child implementations

If you want to pass more complex data structures, especially functions or instances of custom classes, you can do so from a child implementation. There is a big caveat, though: the Experiment's implementation scripts are privileged relative to WebExtension scripts, which causes their scopes to be disjunct:

  • Accessing data that belongs to an Experiment from a WebExtension: it is not possible to directly access chrome-scoped objects from a WebExtension (but it can hold references on it).

    There are two options to work around that: either the Experiment clones the object into the unprivileged scope of the WebExtension or it directly constructs an unprivileged object.

    The second option is using the constructors in context.cloneScope directly from the Experiment. Their results can be used from the WebExtension without further cloning.

Common pitfall: async functions return a Promise in the scope of the function, so you need to wrap such functions before cloning them into a WebExtension scope.

Structuring Experiment code

// Register a resource:// url with a custom namespace, which points to the
// "modules" folder. The namespace should be unique to avoid conflicts with
// other add-ons.
await messenger.LegacyHelper.registerGlobalUrls([
    ["resource", "myaddon", "modules/"],
]);

The file TestModule.sys.mjs in the modules folder will then be accessible via resource://myaddon/TestModule.sys.mjs.

Since system modules cannot be unloaded, we have to append a unique query, to make sure cached files are not re-used after an update. In the following example the version key from manifest.json is used. This allows us to use the same identifier throughout the entire add-on, but load the new version whenever the add-on has been updated:

const { ExtensionParent } = ChromeUtils.importESModule(
    "resource://gre/modules/ExtensionParent.sys.mjs"
);
const extension = ExtensionParent.GlobalManager.getExtension(
    "<id-of-your-extensions>"
);
const query = extension.manifest.version;

// Load TestModule.sys.mjs.
var { TestModule } = ChromeUtils.importESModule(
  "resource://myaddon/TestModule.sys.mjs?" + query
)

Accessing WebExtensions directly from an Experiment

Experiments should be standalone and have no dependencies to any WebExtension components. If possible, design your Experiments as if they were a feature of Thunderbird that does not know about your add-on, and keep the scope as narrow as possible. If you design your APIs correctly, you will not need to access the WebExtension part of your add-on.

In some rare cases, it may be possible that that is not feasible to follow this recommended practice for the current update cycle. In that case, it is possible to tear down the separation between the Experiment and the WebExtension. That can permit you to treat an Experiment as if it were a part of the WebExtension, except for the restrictions regarding passing data outlined above.

In a child implementation, you can directly access the real WebExtension scope via Components.utils.waiveXrays(context.cloneScope).

Outside of a child implementation, or if you need the scope of the background page in particular, you can extract the background page's scope from an extension object:

const webextScope = Array.from(extension.views).find(
    view => view.viewType === "background").xulBrowser.contentWindow
    .wrappedJSObject;

This hack only works because Thunderbird is internally not (yet) using multiple processes. Again, it is highly recommended to design your APIs in a way that these interactions are not necessary as it is likely that this technique will stop working in future versions of Thunderbird.

Once you have a draft for your API, you can start to build the Experiment. Experiments consist of three parts, which are registered through :

A describes the API that can be accessed by the WebExtension part of the add-on.

Either parent or child implementation may be omitted. Full examples for and are available in the Firefox source documentation.

Check out the , which creates all the needed files.

In general, you can always pass simple data structures as function parameters and return values of an API. Thunderbird will automatically adapt them using the , so you do not need to worry about them.

Accessing data that belongs to the WebExtension from an Experiment: the code in the Experiment gains , permitting it to directly access the chrome implementation of the given object. Usually, you don't need to worry about that and things work out just fine – but if they don't, you can opt-out via Components.utils.waiveXrays().

The first option usually boils down to invoking or a related function with context.cloneScope as target scope. A notable exception is returning data from async API functions or wrapping Promises via , which causes automatic cloning of the result (unless the result is wrapped into a ExtensionCommon.NoCloneSpreadArgs).

If your Experiment API is so complex that it does not reasonably fit into a single source file, you can load your own system modules with some additional boilerplate: you need to define a custom global URL. The Experiment can be used to define custom global URLs. In this example, we are registering a custom resource:// URL with the namespace myaddon:

manifest.json
schema
a simple function with parent and child implementations
events add-ons can listen for
Experiment Generator
Activity Manager Experiment
Open SearchDialog Experiment
Remove Attachments If Junk Experiment
Restart Experiment
structured clone algorithm
xray vision
Components.utils.cloneInto()
context.wrapPromise()
LegacyHelper
whose documentation is no longer generated
ExtensionCommon.sys.mjs
/comm/mail/components/extensions/parent
Have full, unrestricted access to Thunderbird, and your computer
Documentation & Resources