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
  • Background Page and Background Scripts
  • Listening for New Messages
  • Adding Menu Entries and their Actions
  • Testing the Extension
  • Installing
  • Trying it Out

Was this helpful?

Edit on GitHub
Export as PDF
  1. Add-on Development
  2. A "Hello World" Extension Tutorial

Using a Background Page

Extending the example extension to use a background page.

PreviousUsing WebExtension APIsNextUsing Content Scripts

Last updated 3 months ago

Was this helpful?

In the third part of the Hello World Extension Tutorial, we will introduce the concept of the WebExtension background page.

We will keep track of incoming mails, add a menu entry to the tools menu and also a context menu entry to our button in Thunderbird's main toolbar and a click on both will open notifications with the collected information from the last 24 hours.

Background Page and Background Scripts

In the first two parts of the Hello World Extension Tutorial, we used well-defined UI hooks to load HTML pages when the user opened one of our popups. In contrast, the background page - if defined - is automatically loaded when the add-on is enabled during Thunderbird start or after the add-on has been manually enabled or installed. It is automatically destroyed when the add-on is shutting down.

  1. Actually defining a background HTML page, that uses script tags to load the JavaScript files.

  2. Just defining the to-be-loaded JavaScript files and let Thunderbird create a background page on-the-fly.

"background": {
    "page": "background.html"
},

We place the following background.html file into our hello-world project folder:

background.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <script type="module" src="background.js"></script>
</head>

</html>

Let's also create an empty background.js script file in the hello-world project folder.

Listening for New Messages

background.js
// Import all functions defined in the messageTools module.
import * as messageTools from '/modules/messageTools.mjs';

messenger.messages.onNewMailReceived.addListener(async (folder, messages) => {
    let { messageLog } = await messenger.storage.local.get({ messageLog: [] });

    for await (let message of messageTools.iterateMessagePages(messages)) {
        messageLog.push({
            folder: folder.name,
            time: Date.now(),
            message: message
        })
    }

    await messenger.storage.local.set({ messageLog });
})

The above code is using an inline arrow function to define the callback function for the event listener (which is called for each onNewMailReceived event). This is identical to the following implicit function definition:

async function onNewMailReceivedCallback(folder, messages) { ... } messenger.messages.onNewMailReceived.addListener( onNewMailReceivedCallback );

The onNewMessageReceived event requires the accountsRead permission, which needs to be added to the permissions key in our manifest.json file.

messenger.storage.local.get()

In line 5 of the shown background script, we request the current messageLog entry from the WebExtensions local storage. The used syntax allows to define a default value of [](an empty Array), if there is currently no messageLog entry stored.

We could also request multiple values from the local storage:

let rv = await messenger.storage.local.get({
  messageLog: [],
  aBoolValue: true,
  aStringValue: "none"
});
console.log(rv);

The call to storage.local.get() returns a Promise for a single object with the requested entries, for example the above console.log(rv) could produce the following output:

{
  messageLog: [],
  aBoolValue: false,
  aStringValue: "The sky is the limit."
}

To access the content of the messageLog member, one would have to use rv.messageLog. That is sometimes not the desired behaviour, and instead we store the requested value directly in a variable. This is called object destructuring, and it maps the content of the messageLog member of the returned object to the messageLog variable. Any other non-matching returned member is ignored.

Access to the local storage requires the storage permission, which needs to be added to the permissions key in our manifest.json file.

messageTools.iterateMessagePages()

The listener is using a helper function to be able to loop over the received messages. The iterateMessagePages function is defined in an ES6 module, which is loaded in line 2 of the background script shown above. So, we create a modules subfolder into our hello-world project folder. Then, we place the messageTools.mjs file within. Here follows what it should contain.

messageTools.mjs
// A wrapper function returning an async iterator for a MessageList.
export async function* iterateMessagePages(page) {
    for (let message of page.messages) {
        yield message;
    }

    while (page.id) {
        page = await messenger.messages.continueList(page.id);
        for (let message of page.messages) {
            yield message;
        }
    }
}

To identify the script file as an ES6 module, which does not include file scope code, but only defines functions, we use the *.mjs file extension.

The provided iterateMessagePages() wrapper function is doing most of the heavy lifting and allows to asynchronously loop over the returned messages in line 7 of the shown background script. For each received message, we subsequently push a new entry into the messageLog Array.

messenger.storage.local.set()

In line 15 we store the updated messageLog Array back into the local storage. We use the object shorthand notation, which allows leaving out the actual value definition, if the value is stored in a variable with the same name as the object's member name. If the shorthand notation is unwanted, one could instead write the following:

await messenger.storage.local.set({ messageLog: messageLog });

Adding Menu Entries and their Actions

Let's add the following code to our background script, which will add two menu entries and will react to them being clicked:

// Create the menu entries.
let menu_id = await messenger.menus.create({
    title: "Show received email",
    contexts: [
        "browser_action",
        "tools_menu"
    ],
});

// Register a listener for the menus.onClicked event.
messenger.menus.onClicked.addListener(async (info, tab) => {
    if (info.menuItemId == menu_id) {
        // Our menu entry was clicked
        let { messageLog } = await messenger.storage.local.get({ 
           messageLog: []
        });

        let now = Date.now();
        let last24h = messageLog.filter(e => (now - e.time) < 24 * 60 * 1000);

        for (let entry of last24h) {
            messenger.notifications.create({
                "type": "basic",
                "iconUrl": "images/internet.png",
                "title": `${entry.folder}: ${entry.message.author}`,
                "message": entry.message.subject
                });                
        }
    }
});

messenger.menus.create()

Using the menus API requires the menus permission, which needs to be added to the permissions key in our manifest.json file.

messenger.menus.onClicked()

messenger.notifications.create()

Using the notifications API requires the notifications permission, which needs to be added to the permissions key in our manifest.json file.

Testing the Extension

hello-world/
  ├── background.html
  ├── background.js
  ├── manifest.json
  ├── images/
      ├── internet.png
      ├── internet-16px.png
      └── internet-32px.png
  ├── mainPopup/
      ├── popup.css
      ├── popup.html
      └── popup.js
  ├── messagePopup/
      ├── popup.css
      ├── popup.html
      └── popup.js
  └── modules/
      └── messageTools.mjs

This is how our manifest.json should now look like:

manifest.json
{
    "manifest_version": 2,
    "name": "Hello World Example",
    "description": "A basic Hello World example extension!",
    "version": "3.0",
    "author": "Thunderbird Team",
    "browser_specific_settings": {
        "gecko": {
            "id": "helloworld@yoursite.com",
            "strict_min_version": "128.0"
        }
    },
    "browser_action": {
        "default_popup": "mainPopup/popup.html",
        "default_title": "Hello World",
        "default_icon": "images/internet-32px.png"
    },
    "message_display_action": {
        "default_popup": "messagePopup/popup.html",
        "default_title": "Details",
        "default_icon": "images/internet-32px.png"
    },
    "permissions": [
        "messagesRead",
        "accountsRead",
        "storage",
        "menus",
        "notifications"
    ],
    "background": {
        "page": "background.html"
    },
    "icons": {
        "64": "images/internet.png",
        "32": "images/internet-32px.png",
        "16": "images/internet-16px.png"
    }
}

Our background script should look as follows:

background.js
// Import all functions defined in the messageTools module.
import * as messageTools from '/modules/messageTools.mjs';

// Add a listener for the onNewMailReceived events.
messenger.messages.onNewMailReceived.addListener(async (folder, messages) => {
    let { messageLog } = await messenger.storage.local.get({ messageLog: [] });

    for await (let message of messageTools.iterateMessagePages(messages)) {
        messageLog.push({
            folder: folder.name,
            time: Date.now(),
            message: message
        })
    }

    await messenger.storage.local.set({ messageLog });
})

// Create the menu entries.
let menu_id = await messenger.menus.create({
    title: "Show received email",
    contexts: [
        "browser_action",
        "tools_menu"
    ],
});

// Register a listener for the menus.onClicked event.
await messenger.menus.onClicked.addListener(async (info, tab) => {
    if (info.menuItemId == menu_id) {
        // Our menu entry was clicked
        let { messageLog } = await messenger.storage.local.get({ messageLog: [] });

        let now = Date.now();
        let last24h = messageLog.filter(e => (now - e.time) < 24 * 60 * 1000);

        for (let entry of last24h) {
            messenger.notifications.create({
                "type": "basic",
                "iconUrl": "images/internet.png",
                "title": `${entry.folder}: ${entry.message.author}`,
                "message": entry.message.subject
            });
        }
    }
});

Installing

Trying it Out

After you have received one or more new messages, while the add-on has been active, open the context menu of our browser_action button in Thunderbird's main toolbar and click on "Show received emails". For each received message, you should see a notification.

The background page is a standard HTML page, supporting the same technologies as ordinary HTML pages, but it is never shown to the user. Its main purpose is to load one or more JavaScript files into the background. Those background scripts can be used to listen for events or to initialize and properly set up the add-on. As , there are two ways to load background scripts:

The author of this example prefers the first option. It allows to use the await keyword in file scope code and it allows to load other . We therefore add the following section to our manifest.json file:

In order to listen for new messages, we have to add a listener for the event to our background script:

The callback function of the onNewMailReceived event receives two parameters: folder being a and messages being a . The defined event listener stores the folder and the message information of the new received mail in the extensions storage.

Since Thunderbird's WebExtension API potentially has to handle a lot of messages, the data type is paginated. Please check the tutorial for more information.

In line 2 we create a new menu entry. We use the title Show received email and we add it to the browser_action context and to the tools_menu context. A list of other available contexts can be found on the page.

The function returns the id of the new menu, which we can use to identify our menu, or - for example - add submenus by using the id as the parentId for other menu entries.

In order to do something when our menu is clicked, we add a listener for the event. We check the id of the clicked menu to see which of our menus was clicked (we only added one, but checking here anyhow).

After we have retrieved the current messageLog from the local storage, we loop over all entries and for each entry in line 20.

Let's double-check that we made the and have all the files in the right places:

As described in the , go to the Add-ons Manager to open the Debug Add-on Page and temporarily install the extension.

ES6 modules
onNewMessageReceived
MailFolder
MessageList
MessageList
Working with Message Lists
menus.create()
onClicked
create a notification
correct changes
Supported UI Elements
first part of the Hello World Extension Tutorial
described in the MailExtension guide