Using a Background Page
Extending the example extension to use a background page.
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 24h.

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.
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 described in the MailExtension guide, there are two ways to load background scripts:
  1. 1.
    Actually defining a background HTML page, that uses script tags to load the JavaScript files.
  2. 2.
    Just defining the to-be-loaded JavaScript files and let Thunderbird create a background page on-the-fly.
The author of this example prefers the first option, as it allows declaring the loaded JavaScript file (in the script tag) as type="module", which enables support for using ES6 modules. We therefore add the following section to our manifest.json file:
1
"background": {
2
"page": "background.html"
3
},
Copied!
We place the following background.html file into our hello-world project folder:
background.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset="utf-8"/>
5
<script src="background.js" type="module"></script>
6
</head>
7
</html>
Copied!
Let's create the following barebone background.js script in the hello-world project folder:
background.js
1
async function load() {
2
3
}
4
5
document.addEventListener("DOMContentLoaded", load);
Copied!
We use an event listener at the end of this script, which runs the load function once the background page is fully loaded. Avoid placing function calls outside of the load function.

Listening for New Messages

In order to listen for new messages, we have to add a listener for the onNewMessageReceived event:
1
messenger.messages.onNewMailReceived.addListener(async (folder, messages) => {
2
// Do something with folder and messages.
3
})
Copied!
The above code is using an inline arrow function to define the callback function, which is called for each onNewMailReceived event. This is identical to the following implicit function definition:
1
async function onNewMailReceivedCallback(folder, messages) {
2
// Do something with folder and messages.
3
}
4
messenger.messages.onNewMailReceived.addListener(onNewMailReceivedCallback)
Copied!
The author of this example prefers to use inline arrow functions, if the function is not used elsewhere.
The onNewMessageReceived event requires the accountsRead permission, which needs to be added to the permissions key in our manifest.json file.
The callback function of the onNewMailReceived event receives two parameters: folder being a MailFolder and messages being a MessageList. The updated background script, which stores the folder and the message information of the new received mail, could look as follows:
1
// A wrapper function returning an async iterator for a MessageList. Derived from
2
// https://webextension-api.thunderbird.net/en/91/how-to/messageLists.html
3
async function* iterateMessagePages(page) {
4
for (let message of page.messages) {
5
yield message;
6
}
7
8
while (page.id) {
9
page = await messenger.messages.continueList(page.id);
10
for (let message of page.messages) {
11
yield message;
12
}
13
}
14
}
15
16
async function load() {
17
18
// Add a listener for the onNewMailReceived events.
19
await messenger.messages.onNewMailReceived.addListener(async (folder, messages) => {
20
let { messageLog } = await messenger.storage.local.get({ messageLog: [] });
21
22
for await (let message of iterateMessagePages(messages)) {
23
messageLog.push({
24
folder: folder.name,
25
time: Date.now(),
26
message: message
27
})
28
}
29
30
await messenger.storage.local.set({ messageLog });
31
})
32
33
}
34
35
document.addEventListener("DOMContentLoaded", load);
Copied!

messenger.storage.local.get()

In line 20 of the shown script, we request the current messageLog entry from the WebExtensions local storage. The used syntax allows defining the default value of [](an empty Array), if there currently is no messageLog entry stored.
We could also request multiple values from the local storage:
1
let rv = await messenger.storage.local.get({
2
messageLog: [],
3
aBoolValue: true,
4
aStringValue: "none"
5
});
6
console.log(rv);
Copied!
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:
1
{
2
messageLog: [],
3
aBoolValue: false,
4
aStringValue: "The sky is the limit."
5
}
Copied!
To access the content of the messageLog member, one would have to use rv.messageLog. That is sometimes not the desired behavior, and instead one could store the requested value directly in a variable, as shown in line 20 of our background script. This is called object destructering 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.

iterateMessagePages()

Since Thunderbird's WebExtension API potentially has to handle a lot of messages, the MessageList data type is paginated. Please check the Working with Message Lists tutorial for more information.
The provided iterateMessagePages() wrapper function is doing most of the heavy lifting and allows to asynchronously loop over the returned messages in line 22 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 30 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:
1
await messenger.storage.local.set({ messageLog: messageLog });
Copied!

Adding Menu Entries and their Actions

Let's add the following code to our load() function, which will add both menu entries and will react to them being clicked:
1
// Create the menu entries.
2
let menu_id = await messenger.menus.create({
3
title: "Show received email",
4
contexts: [
5
"browser_action",
6
"tools_menu"
7
],
8
});
9
10
// Register a listener for the menus.onClicked event.
11
await messenger.menus.onClicked.addListener(async (info, tab) => {
12
if (info.menuItemId == menu_id) {
13
// Our menu entry was clicked
14
let { messageLog } = await messenger.storage.local.get({ messageLog: [] });
15
16
let now = Date.now();
17
let last24h = messageLog.filter(e => (now - e.time) < 24 * 60 * 1000);
18
19
for (let entry of last24h) {
20
messenger.notifications.create({
21
"type": "basic",
22
"iconUrl": "images/internet.png",
23
"title": `${entry.folder}: ${entry.message.author}`,
24
"message": entry.message.subject
25
});
26
}
27
}
28
});
Copied!

messenger.menus.create()

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 Supported UI Elements page.
The menus.create() 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.
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()

In order to do something when our menu is clicked, we add a listener for the onClicked 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).

messenger.notifications.create()

After we have retrieved the current messageLog from the local storage, we loop over all entries and create a notification for each entry in line 20.
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

Let's double-check that we have all the files in the right places:
1
hello-world/
2
├── manifest.json
3
├── background.html
4
├── background.js
5
├── mainPopup/
6
├── popup.html
7
├── popup.css
8
└── popup.js
9
├── messagePopup/
10
├── popup.html
11
├── popup.css
12
└── popup.js
13
└── images/
14
├── internet.png
15
├── internet-32px.png
16
└── internet-16px.png
Copied!
This is how our manifest.json should now look like:
manifest.json
1
{
2
"manifest_version": 2,
3
"name": "Hello World",
4
"description": "Your basic Hello World extension!",
5
"version": "3.0",
6
"author": "[Your Name Here]",
7
"applications": {
8
"gecko": {
10
"strict_min_version": "78.0"
11
}
12
},
13
"browser_action": {
14
"default_popup": "mainPopup/popup.html",
15
"default_title": "Hello World",
16
"default_icon": "images/internet-32px.png"
17
},
18
"message_display_action": {
19
"default_popup": "messagePopup/popup.html",
20
"default_title": "Details",
21
"default_icon": "images/internet-32px.png"
22
},
23
"permissions": [
24
"messagesRead",
25
"accountsRead",
26
"storage",
27
"menus",
28
"notifications"
29
],
30
"background": {
31
"page": "background.html"
32
},
33
"icons": {
34
"64": "images/internet.png",
35
"32": "images/internet-32px.png",
36
"16": "images/internet-16px.png"
37
}
38
}
Copied!
Our background script should look as follows:
background.js
1
// A wrapper function returning an async iterator for a MessageList. Derived from
2
// https://webextension-api.thunderbird.net/en/91/how-to/messageLists.html
3
async function* iterateMessagePages(page) {
4
for (let message of page.messages) {
5
yield message;
6
}
7
8
while (page.id) {
9
page = await messenger.messages.continueList(page.id);
10
for (let message of page.messages) {
11
yield message;
12
}
13
}
14
}
15
16
async function load() {
17
18
// Add a listener for the onNewMailReceived events.
19
await messenger.messages.onNewMailReceived.addListener(async (folder, messages) => {
20
let { messageLog } = await messenger.storage.local.get({ messageLog: [] });
21
22
for await (let message of iterateMessagePages(messages)) {
23
messageLog.push({
24
folder: folder.name,
25
time: Date.now(),
26
message: message
27
})
28
}
29
30
await messenger.storage.local.set({ messageLog });
31
})
32
33
// Create the menu entries.
34
let menu_id = await messenger.menus.create({
35
title: "Show received email",
36
contexts: [
37
"browser_action",
38
"tools_menu"
39
],
40
});
41
42
// Register a listener for the menus.onClicked event.
43
await messenger.menus.onClicked.addListener(async (info, tab) => {
44
if (info.menuItemId == menu_id) {
45
// Our menu entry was clicked
46
let { messageLog } = await messenger.storage.local.get({ messageLog: [] });
47
48
let now = Date.now();
49
let last24h = messageLog.filter(e => (now - e.time) < 24 * 60 * 1000);
50
51
for (let entry of last24h) {
52
messenger.notifications.create({
53
"type": "basic",
54
"iconUrl": "images/internet.png",
55
"title": `${entry.folder}: ${entry.message.author}`,
56
"message": entry.message.subject
57
});
58
}
59
}
60
});
61
}
62
63
document.addEventListener("DOMContentLoaded", load);
Copied!

Installing

As described in the first part of the Hello World Extension Tutorial, go to the Add-ons Manager to open the Debug Add-on Page and temporarily install the extension.

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.