Using Content Scripts
Extending the example extension to use a content script.
In the fourth part of the Hello World Extension Tutorial, we will introduce the concept of content scripts.
We will add a banner to the top of the message display area, displaying some information about the currently viewed message. The banner will also include a button to mark the currently viewed message as unread.

Using a Message Display Script

Content Scripts are JavaScript files that are loaded and executed in content pages. This technology was mainly developed for browsers, where it is used to interact with the currently viewed web page.
In addition to standard content scripts, Thunderbird supports the following special types of content scripts:
  • compose scripts loaded into the editor of the message composer
  • message display scripts loaded into rendered messages when displayed to the user
We will be using a message display script in this example. In order to register one, we use the messageDisplayScripts API and add the following code to our background script:
1
// Register the message display script.
2
messenger.messageDisplayScripts.register({
3
js: [{ file: "messageDisplay/message-content-script.js" }],
4
css: [{ file: "messageDisplay/message-content-styles.css" }],
5
});
Copied!
The messageDisplayScripts API requires the messagesModify permission, which needs to be added to the permissions key in our manifest.json file.
Whenever a message is displayed to the user, the registered CSS file will be added and the registered JavaScript file will be injected and executed. Let's create a messageDisplay directory inside our hello-world project folder with the following two files:
message-content-styles.css
1
.thunderbirdMessageDisplayActionExample {
2
background-color: #d70022;
3
color: white;
4
font-weight: 400;
5
padding: 0.25rem 0.5rem;
6
margin-bottom: 0.25rem;
7
display: flex;
8
}
9
10
.thunderbirdMessageDisplayActionExample_Text {
11
flex-grow: 1;
12
}
Copied!
message-content-script.js
1
const showBanner = async () => {
2
let bannerDetails = await browser.runtime.sendMessage({
3
command: "getBannerDetails",
4
});
5
6
// get the details back from the formerly serialized content
7
const { text } = bannerDetails;
8
9
// create the banner element itself
10
const banner = document.createElement("div");
11
banner.className = "thunderbirdMessageDisplayActionExample";
12
13
// create the banner text element
14
const bannerText = document.createElement("div");
15
bannerText.className = "thunderbirdMessageDisplayActionExample_Text";
16
bannerText.innerText = text;
17
18
// create a button to display it in the banner
19
const markUnreadButton = document.createElement("button");
20
markUnreadButton.innerText = "Mark unread";
21
markUnreadButton.addEventListener("click", async () => {
22
// add the button event handler to send the command to the background script
23
browser.runtime.sendMessage({
24
command: "markUnread",
25
});
26
});
27
28
// add text and button to the banner
29
banner.appendChild(bannerText);
30
banner.appendChild(markUnreadButton);
31
32
// and insert it as the very first element in the message
33
document.body.insertBefore(banner, document.body.firstChild);
34
};
35
36
showBanner();
Copied!
The main purpose of the message-content-script.js file is to manipulate the rendered message and add a banner at its top. We use basic DOM manipulation techniques.
What is special however is how the displayed information is retrieved. In the second part of this tutorial, we used the tabs API and the messageDisplay API from our background page, to learn which message is currently displayed and then used the messages API to get the required information. This does not work for content scripts, as their access is limited. Instead, we have to request this information from the background script using runtime messaging.

Sending a runtime message

The sendMessage() method of the runtime API will send a message to each active page, including the background page, the options page, popup pages and other HTML pages of our extension loaded using windows.create() or tabs.create(). The message itself can be a string, an integer, a boolean, an array or an object. It must abide to the structured clone algorithm.
In line 2 of message-content-script.js, we send the message object {command: "getBannerDetails"}, to request the display details from the background page. In line 23 we send the message object {command: "markUnread"}, to request the background page to mark the currently viewed message as unread.

Receiving a runtime message

The background page can listen for runtime messages, by registering the following listener:
1
/**
2
* Add a handler for communication with other parts of the extension,
3
* like our message display script.
4
*
5
* Note: If this handler is defined async, there should be only one such
6
* handler in the background script for all incoming messages.
7
*/
8
messenger.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
9
// Check if the message includes our command member.
10
if (message && message.hasOwnProperty("command")) {
11
// Get the message currently displayed in the sending tab, abort if
12
// that failed.
13
const messageHeader = await messenger.messageDisplay.getDisplayedMessage(sender.tab.id);
14
if (!messageHeader) {
15
return;
16
}
17
// Check for known commands.
18
switch (message.command) {
19
case "getBannerDetails":
20
// Create the information we want to return to our message display script.
21
return { text: `Mail subject is "${messageHeader.subject}"` };
22
case "markUnread":
23
// mark the message as unread
24
messenger.messages.update(messageHeader.id, {
25
read: false,
26
});
27
break;
28
}
29
}
30
});
Copied!
The message passed to the onMessage listener will be whatever has been sent using sendMessage(), the sender is of type MessageSender and will include the sending tab.
In this example, we check if the runtime message includes our command and based on its value either return the banner details or mark the viewed message as unread.
Note: The onMessage listener has a third parameter sendResponse, which is a callback function to send a synchronous response back to the sending tab. For Thunderbird however the preferred way is to return an asynchronous response using a Promise instead.
If only one onMessage listener is used in the entire extension, the listener can be declared async, which will always return a Promise. Even if no return statement is explicitly defined, it will return a Promise for undefiend.
If multiple onMessage listeners are used and each should react on a different set of messages, the listeners must not be defined as async, but only return a Promsise for the requested messages. See the examples ans explenations given on MDN.

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
├── displayMessage/
14
├── message-content-script.js
15
└── message-content-styles.css
16
└── images/
17
├── internet.png
18
├── internet-32px.png
19
└── 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": "4.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
"messagesModify"
30
],
31
"background": {
32
"page": "background.html"
33
},
34
"icons": {
35
"64": "images/internet.png",
36
"32": "images/internet-32px.png",
37
"16": "images/internet-16px.png"
38
}
39
}
Copied!
Our background script should 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
// 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
* Add a handler for communication with other parts of the extension,
64
* like our message display script.
65
*
66
* Note: If this handler is defined async, there should be only one such
67
* handler in the background script for all incoming messages.
68
*/
69
messenger.runtime.onMessage.addListener(async (message, sender) => {
70
// Check if the message includes our command member.
71
if (message && message.hasOwnProperty("command")) {
72
// Get the message currently displayed in the sending tab, abort if
73
// that failed.
74
const messageHeader = await messenger.messageDisplay.getDisplayedMessage(sender.tab.id);
75
if (!messageHeader) {
76
return;
77
}
78
// Check for known commands.
79
switch (message.command) {
80
case "getBannerDetails":
81
// Create the information we want to return to our message display script.
82
return { text: `Mail subject is "${messageHeader.subject}"` };
83
case "markUnread":
84
// mark the message as unread
85
messenger.messages.update(messageHeader.id, {
86
read: false,
87
});
88
break;
89
}
90
}
91
});
92
93
// Register the message display script.
94
messenger.messageDisplayScripts.register({
95
js: [{ file: "messageDisplay/message-content-script.js" }],
96
css: [{ file: "messageDisplay/message-content-styles.css" }],
97
});
98
}
99
100
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

Open a message. There should be a red banner added at the top of it with its subject and a button labeled "Mark unread". Clicking that button should mark the message as unread