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
  • Using a Message Display Script
  • 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 Content Scripts

Extending the example extension to use a content script.

PreviousUsing a Background PageNextA Guide to Extensions

Last updated 2 months ago

Was this helpful?

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.

  • compose scripts loaded into the editor of the message composer

  • message display scripts loaded into rendered messages when displayed to the user

// Register the message display script for all newly opened message tabs.
messenger.messageDisplayScripts.register({
    js: [{ file: "messageDisplay/message-content-script.js" }],
    css: [{ file: "messageDisplay/message-content-styles.css" }],
});

// Inject script and CSS in all already open message tabs.
let openTabs = await messenger.tabs.query();
let messageTabs = openTabs.filter(
    tab => ["mail", "messageDisplay"].includes(tab.type)
);
for (let messageTab of messageTabs) {
    // Make sure the tab is displaying a single message. The mail tab could also
    // display a content page or multiple messages, which will cause an error.
    if (messageTab.type == "mail") {
        let messages = await browser.messageDisplay.getDisplayedMessages(
            messageTab.id
        );
        if (messages.length != 1) {
            continue;
        }
    }
    browser.tabs.executeScript(messageTab.id, {
        file: "messageDisplay/message-content-script.js"
    });
    browser.tabs.insertCSS(messageTab.id, {
        file: "messageDisplay/message-content-styles.css"
    });
}

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
.thunderbirdMessageDisplayActionExample {
  background-color: #d70022;
  color: white;
  font-weight: 400;
  padding: 0.25rem 0.5rem;
  margin-bottom: 0.25rem;
  display: flex;
}

.thunderbirdMessageDisplayActionExample_Text {
  flex-grow: 1;
}
message-content-script.js
async function showBanner() {
    let bannerDetails = await browser.runtime.sendMessage({
        command: "getBannerDetails",
    });

    // Get the details back from the formerly serialized content.
    const { text } = bannerDetails;

    // Create the banner element itself.
    const banner = document.createElement("div");
    banner.className = "thunderbirdMessageDisplayActionExample";

    // Create the banner text element.
    const bannerText = document.createElement("div");
    bannerText.className = "thunderbirdMessageDisplayActionExample_Text";
    bannerText.innerText = text;

    // Create a button to display it in the banner.
    const markUnreadButton = document.createElement("button");
    markUnreadButton.innerText = "Mark unread";
    markUnreadButton.addEventListener("click", async () => {
        // Add the button event handler to send the command to the
        // background script.
        browser.runtime.sendMessage({
            command: "markUnread",
        });
    });

    // Add text and button to the banner.
    banner.appendChild(bannerText);
    banner.appendChild(markUnreadButton);

    // Insert it as the very first element in the message.
    document.body.insertBefore(banner, document.body.firstChild);
};

showBanner();

Content scripts cannot yet be loaded as top level ES6 modules. They cannot load other ES6 modules. They can also not use the async keyword in file scope code, we therefore have to create the async wrapper function showBanner().

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.

Sending a runtime message

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 24 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:

/**
 * Add a handler for the communication with other parts of the extension,
 * like our message display script.
 *
 * Note: It is best practice to always define a synchronous listener
 *       function for the runtime.onMessage event.
 *       If defined asynchronously, it will always return a Promise
 *       and therefore answer all messages, even if a different listener
 *       defined elsewhere is supposed to handle these.
 * 
 *       The listener should only return a Promise for messages it is
 *       actually supposed to handle.
 */
messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
    // Check what type of message we have received and invoke the appropriate
    // handler function.
    if (message && message.hasOwnProperty("command")) {
        return commandHandler(message, sender);
    }
    // Return false if the message was not handled by this listener.
    return false;
});

// The actual (asynchronous) handler for command messages.
async function commandHandler(message, sender) {
    // Get the message currently displayed in the sending tab, abort if
    // that failed.
    const messageHeader = await messenger.messageDisplay.getDisplayedMessage(
        sender.tab.id
    );

    if (!messageHeader) {
        return;
    }

    // Check for known commands.
    switch (message.command) {
        case "getBannerDetails":
            // Create the information we want to return to our message display
            // script.
            return { text: `Mail subject is "${messageHeader.subject}"` };
        case "markUnread":
            // Mark the message as unread.
            messenger.messages.update(messageHeader.id, {
                read: false,
            });
            break;
    }
}

The messages.update() function requires the messagesUpdate permission, which needs to be added to the permissions key in our manifest.json file.

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.

Note: It is best practice to always define a synchronous listener function for the runtime.onMessage event. If defined asynchronously, it will always return a Promise and therefore answer all messages, even if a different listener defined elsewhere is supposed to handle these.

The listener should only return a Promise for messages it is actually supposed to handle.

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
  ├── messageDisplay/
      ├── message-content-script.js
      └── message-content-styles.css
  ├── 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": "4.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",
        "messagesUpdate",
        "accountsRead",
        "storage",
        "menus",
        "notifications",
        "messagesModify"
    ],
    "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:

// 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
            });
        }
    }
});

// Register the message display script.
messenger.messageDisplayScripts.register({
    js: [{ file: "messageDisplay/message-content-script.js" }],
    css: [{ file: "messageDisplay/message-content-styles.css" }],
});

/**
 * Add a handler for the communication with other parts of the extension,
 * like our message display script.
 *
 * Note: It is best practice to always define a synchronous listener
 *       function for the runtime.onMessage event.
 *       If defined asynchronously, it will always return a Promise
 *       and therefore answer all messages, even if a different listener
 *       defined elsewhere is supposed to handle these.
 * 
 *       The listener should only return a Promise for messages it is
 *       actually supposed to handle.
 */
messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
    // Check what type of message we have received and invoke the appropriate
    // handler function.
    if (message && message.hasOwnProperty("command")) {
        return commandHandler(message, sender);
    }
    // Return false if the message was not handled by this listener.
    return false;
});

// The actual (asynchronous) handler for command messages.
async function commandHandler(message, sender) {
    // Get the message currently displayed in the sending tab, abort if
    // that failed.
    const messageHeader = await messenger.messageDisplay.getDisplayedMessage(
        sender.tab.id
    );

    if (!messageHeader) {
        return;
    }

    // Check for known commands.
    switch (message.command) {
        case "getBannerDetails":
            // Create the information we want to return to our message display
            // script.
            return { text: `Mail subject is "${messageHeader.subject}"` };
        case "markUnread":
            // Mark the message as unread.
            messenger.messages.update(messageHeader.id, {
                read: false,
            });
            break;
    }
}

Installing

Trying it Out

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

In addition to , Thunderbird supports the following special types of content scripts:

We will be using a message display script in this example. In order to register one, we use the API and add the following code to our background script:

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 . Instead, we have to request this information from the background script using runtime messaging.

The method of the 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 or . The message itself can be a string, an integer, a boolean, an array or an object. It must abide to the .

The message passed to the onMessage listener will be whatever has been sent using sendMessage(). The sender is of type and will include the sending tab.

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.

standard content scripts
messageDisplayScripts
their access is limited
MessageSender
correct changes
sendMessage()
runtime
windows.create()
tabs.create()
structured clone algorithm
first part of the Hello World Extension Tutorial