Thunderbird MailExtension Development
Complete reference for building, testing, and publishing email extensions for Mozilla Thunderbird.
Overview
Thunderbird extensions use the MailExtension API (based on WebExtensions) with the messenger.* namespace. Thunderbird supports both Manifest V2 and V3 since version 128.
Key Characteristics:
- Global namespace:
messenger(Thunderbird-specific) +browser(standard WebExtensions) - Both MV2 and MV3 supported (Thunderbird 128+)
- Thunderbird-specific APIs:
accounts,addressBooks,compose,folders,mailTabs,messages,messageDisplay - Submission via ATN (addons.thunderbird.net)
Version Requirements
| Version | Status | Notes | |---------|--------|-------| | 128.x (ESR) | Current | Full MV2 + MV3 support | | 115.x | Legacy | End of support | | < 115 | Deprecated | Not recommended |
Best Practice: Set strict_min_version to "128.0"
Manifest Structure
Manifest V3 (Recommended, Thunderbird 128+)
{
"manifest_version": 3,
"name": "My Thunderbird Extension",
"version": "1.0.0",
"description": "Extension description",
"author": "Your Name",
"browser_specific_settings": {
"gecko": {
"id": "extension@example.com",
"strict_min_version": "128.0"
}
},
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"64": "icons/icon-64.png"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_popup": "popup.html",
"default_title": "My Extension",
"default_icon": "icons/icon-32.png"
},
"permissions": [
"storage",
"messagesRead",
"addressBooks"
]
}
Manifest V2 (Still Supported)
{
"manifest_version": 2,
"name": "My Thunderbird Extension",
"version": "1.0.0",
"author": "Your Name",
"browser_specific_settings": {
"gecko": {
"id": "extension@example.com",
"strict_min_version": "128.0"
}
},
"background": {
"scripts": ["background.js"],
"type": "module"
},
"browser_action": {
"default_popup": "popup.html",
"default_title": "My Extension"
},
"permissions": [
"storage",
"messagesRead",
"addressBooks"
]
}
MV2 vs MV3 Key Differences
| Feature | MV2 | MV3 |
|---------|-----|-----|
| Toolbar button | browser_action | action |
| Background | background.scripts | background.service_worker |
| Execute script | tabs.executeScript | messenger.scripting.messageDisplay.executeScript |
| Compose scripts | composeScripts | scripting.compose |
| Contacts API | messenger.contacts.* | messenger.addressBooks.contacts.* (vCard only) |
All Manifest Keys Reference
Metadata:
name(required) - Extension nameversion(required) - Version stringdescription- Short descriptionauthor- Author nameicons- Extension icons
Thunderbird-Specific:
browser_specific_settings.gecko.id- Required for ATNbrowser_specific_settings.gecko.strict_min_version- Minimum version
Background & Scripts:
background- Service worker (MV3) or scripts (MV2)message_display_scripts(MV2) - Scripts for displayed messages
UI Components:
action(MV3) /browser_action(MV2) - Main toolbar buttoncompose_action- Compose window toolbar buttonmessage_display_action- Message view toolbar button
Permissions:
permissions- API permissionsexperiment_apis- Custom Experiment APIs
Other:
commands- Keyboard shortcutsoptions_ui- Options page
Messenger APIs
Complete API Namespace List
| API | Permission | Description |
|-----|------------|-------------|
| accounts | accountsRead | Email accounts and identities |
| addressBooks | addressBooks | Address books management |
| compose | compose | Compose windows and events |
| contacts | addressBooks | Contact management (use addressBooks.contacts in MV3) |
| folders | accountsFolders | Mail folders management |
| identities | accountsIdentities | Account identities |
| mailTabs | - | Main Thunderbird window |
| messages | messagesRead, messagesMove | Message operations |
| messageDisplay | messagesRead | Displayed message events |
| messageDisplayAction | - | Message toolbar button |
| tabs | - | Tab management |
| windows | - | Window management |
| runtime | - | Extension runtime |
| storage | storage | Data storage |
| i18n | - | Internationalization |
Standard WebExtension APIs (also available)
browser.runtime- Messaging, lifecyclebrowser.storage- Data persistencebrowser.i18n- Localizationbrowser.tabs- Tab managementbrowser.windows- Window managementbrowser.commands- Keyboard shortcuts
Accounts API
// List all accounts
const accounts = await messenger.accounts.list();
// Get specific account
const account = await messenger.accounts.get(accountId);
// Get account details
console.log(account.name, account.type, account.identities);
// List folders in account
const folders = await messenger.folders.getSubFolders(account);
Messages API
// List messages in folder
const messages = await messenger.messages.list(folderId);
// Get specific message
const message = await messenger.messages.get(messageId);
// Message properties
console.log(message.subject, message.from, message.to, message.date);
// Get full message with body
const fullMessage = await messenger.messages.getFull(messageId);
console.log(fullMessage.parts[0].body);
// Query messages
const results = await messenger.messages.query({
from: "sender@example.com",
unread: true,
limit: 50
});
// Move messages
await messenger.messages.move([messageId], destinationFolderId);
// Copy messages
await messenger.messages.copy([messageId], destinationFolderId);
// Delete messages
await messenger.messages.delete([messageId], true); // true = skip trash
// Mark as read/unread
await messenger.messages.update(messageId, { read: true });
// Archive messages
await messenger.messages.archive([messageId]);
// Import message
const importedId = await messenger.messages.import(
file, // File object
folderId,
{ read: true, flagged: false }
);
Folders API
// Get folder
const folder = await messenger.folders.get(folderId);
// List subfolders
const subfolders = await messenger.folders.getSubFolders(parentFolder);
// Create folder
const newFolder = await messenger.folders.create(parentAccountId, "New Folder");
// Rename folder
await messenger.folders.rename(folderId, "New Name");
// Delete folder
await messenger.folders.delete(folderId);
// Mark folder as read
await messenger.folders.markAsRead(folderId);
// Get folder properties
console.log(folder.name, folder.path, folder.unreadCount, folder.totalCount);
Address Books & Contacts API (MV3)
// List address books
const addressBooks = await messenger.addressBooks.list();
// Get address book
const book = await messenger.addressBooks.get(addressBookId);
// Create contact (vCard format)
const contactId = await messenger.addressBooks.contacts.create(addressBookId, {
vCard: `BEGIN:VCARD
VERSION:4.0
FN:John Doe
EMAIL:john@example.com
TEL:+1-555-0100
END:VCARD`
});
// Get contact
const contact = await messenger.addressBooks.contacts.get(contactId);
console.log(contact.vCard);
// Update contact
await messenger.addressBooks.contacts.update(contactId, {
vCard: updatedVCard
});
// Delete contact
await messenger.addressBooks.contacts.delete(contactId);
// Quick search contacts
const results = await messenger.addressBooks.contacts.quickSearch("john");
// Search in specific address book
const results = await messenger.addressBooks.contacts.query({
addressBookId: addressBookId,
searchText: "john"
});
// Create mailing list
const listId = await messenger.addressBooks.mailingLists.create(addressBookId, {
name: "Team",
nickName: "team",
description: "Team members"
});
// Add contact to mailing list
await messenger.addressBooks.mailingLists.addMember(listId, contactId);
Compose API
// Open compose window
const tab = await messenger.compose.beginNew({
to: ["recipient@example.com"],
cc: ["cc@example.com"],
subject: "Hello",
body: "Message content",
isPlainText: false
});
// Compose with attachments
await messenger.compose.beginNew({
to: ["recipient@example.com"],
attachments: [{
file: new File(["content"], "file.txt", { type: "text/plain" })
}]
});
// Reply to message
await messenger.compose.beginReply(messageId, "replyToAll");
// Forward message
await messenger.compose.beginForward(messageId, "forwardInline");
// Get compose details
const details = await messenger.compose.getComposeDetails(tabId);
console.log(details.to, details.subject, details.body);
// Set compose details
await messenger.compose.setComposeDetails(tabId, {
subject: "Updated Subject"
});
// Listen for compose events
messenger.compose.onBeforeSend.addListener((tab, details) => {
// Modify message before sending
details.body += "\n\n-- Sent via MyExtension";
return { details };
});
// Listen for compose window open
messenger.compose.onComposeCreated.addListener((tab) => {
console.log("Compose window created:", tab.id);
});
Message Display API
// Listen for message displayed
messenger.messageDisplay.onMessageDisplayed.addListener((tab, message) => {
console.log("Message displayed:", message.subject);
});
// Get displayed message
const message = await messenger.messageDisplay.getDisplayedMessage(tabId);
// Listen for messages displayed (batch)
messenger.messageDisplay.onMessagesDisplayed.addListener((tab, messages) => {
console.log(`${messages.length} messages displayed`);
});
Mail Tabs API
// Get current mail tab
const mailTab = await messenger.mailTabs.getCurrent();
// Get displayed folder
const folder = await messenger.mailTabs.getDisplayedFolder(tabId);
// Set displayed folder
await messenger.mailTabs.update(tabId, {
displayedFolderId: folderId
});
// Get selected messages
const selection = await messenger.mailTabs.getSelectedMessages(tabId);
// Listen for folder changes
messenger.mailTabs.onSelectedMessagesChanged.addListener((tab, selection) => {
console.log("Selection changed:", selection.messages);
});
UI Actions (Toolbar Buttons)
Main Toolbar (action / browser_action)
{
"action": {
"default_popup": "popup.html",
"default_title": "My Extension",
"default_icon": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png"
}
}
}
// Listen for clicks (if no popup)
messenger.action.onClicked.addListener((tab) => {
console.log("Action clicked");
});
// Update badge
await messenger.action.setBadgeText({ text: "5" });
await messenger.action.setBadgeBackgroundColor({ color: "#ff0000" });
// Update icon
await messenger.action.setIcon({ path: "icons/icon-active.png" });
Compose Window (compose_action)
{
"compose_action": {
"default_popup": "compose_popup.html",
"default_title": "Compose Tool",
"default_icon": "icons/compose-icon.png"
}
}
// Listen for clicks in compose window
messenger.composeAction.onClicked.addListener((tab) => {
const details = await messenger.compose.getComposeDetails(tab.id);
console.log("Compose action clicked:", details.subject);
});
Message Display (message_display_action)
{
"message_display_action": {
"default_popup": "message_popup.html",
"default_title": "Message Tool",
"default_icon": "icons/message-icon.png"
}
}
// Listen for clicks on message
messenger.messageDisplayAction.onClicked.addListener(async (tab) => {
const message = await messenger.messageDisplay.getDisplayedMessage(tab.id);
console.log("Message action clicked:", message.subject);
});
Message Display Scripts
MV2 Configuration
{
"message_display_scripts": [
{
"matches": ["<all_urls>"],
"js": ["message_content.js"],
"css": ["message_styles.css"]
}
]
}
MV3 Configuration
// In background.js
await messenger.scripting.messageDisplay.executeScript({
tabId: tabId,
files: ["message_content.js"]
});
Available APIs in Display Scripts
Limited APIs available:
messenger.runtime.connect(),messenger.runtime.sendMessage()messenger.runtime.onConnect,messenger.runtime.onMessagemessenger.i18n.getMessage(),messenger.i18n.getAcceptLanguages()messenger.storage.*
// message_content.js
// Send message to background
const response = await messenger.runtime.sendMessage({
action: "processMessage",
content: document.body.innerText
});
// Listen for messages from background
messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "highlight") {
// Highlight text in message
document.body.innerHTML = document.body.innerHTML.replace(
message.text,
`<mark>${message.text}</mark>`
);
}
});
Experiment APIs
Experiments provide access to Thunderbird internals not exposed via WebExtension APIs.
When to Use Experiments
- Need access to internal Thunderbird services
- API functionality not yet available in MailExtension API
- Complex integrations with core features
⚠️ Warning: Experiments grant full, unrestricted access. Users see:
"Have full, unrestricted access to Thunderbird, and your computer"
Experiment Structure
{
"experiment_apis": {
"myapi": {
"schema": "api/myapi/schema.json",
"parent": {
"scopes": ["addon_parent"],
"paths": [["myapi"]],
"script": "api/myapi/implementation.js",
"events": ["startup"]
}
}
}
}
Schema Definition (schema.json)
[
{
"namespace": "myapi",
"functions": [
{
"name": "doSomething",
"type": "function",
"async": true,
"parameters": [
{
"name": "param",
"type": "string"
}
]
}
],
"events": [
{
"name": "onSomething",
"type": "function"
}
]
}
]
Implementation (implementation.js)
class MyAPI extends ExtensionAPI {
getAPI(context) {
return {
myapi: {
async doSomething(param) {
// Access Thunderbird internals via Services
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm"
);
// Do something with internal APIs
return Services.someService.process(param);
},
onSomething: new ExtensionCommon.EventManager({
context,
name: "myapi.onSomething",
register: (fire) => {
const callback = (data) => fire.async(data);
// Register with internal service
someInternalService.addListener(callback);
return () => {
someInternalService.removeListener(callback);
};
}
}).api()
}
};
}
onStartup() {
console.log("Extension starting up");
}
onShutdown(reason) {
console.log("Extension shutting down:", reason);
// Cleanup required
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
}
}
Using Experiment API
// In background.js
const result = await messenger.myapi.doSomething("param");
// Listen for experiment events
messenger.myapi.onSomething.addListener((data) => {
console.log("Event received:", data);
});
Available Community Experiments
| Experiment | Description | Repository | |------------|-------------|------------| | Calendar | Calendar API | webext-experiments/calendar | | FileSystem | File system access | webext-support/FileSystem | | LegacyPrefs | Preferences access | webext-support/LegacyPrefs | | NotificationBox | Notification bars | webext-experiments/NotificationBox | | WindowListener | Window events | webext-support/WindowListener |
ATN Submission Process
Pre-Submission Checklist
- [ ] Extension ID in
browser_specific_settings.gecko.id - [ ] Works with Thunderbird 128+
- [ ] All permissions are necessary
- [ ] Privacy policy included (if collecting data)
- [ ] No obfuscated code
- [ ] Source code available (if using build tools)
- [ ] Icons: 32x32 and 64x64 minimum
- [ ] Screenshots for listed extensions
- [ ] Clear description
Submission Steps
-
Build extension:
zip -r extension.zip manifest.json background.js icons/ popup.html -
Create developer account:
- Visit https://addons.thunderbird.net/developers/
- Sign up and complete profile
-
Submit:
- Go to Developer Hub → "Submit a New Add-on"
- Upload
.zipor.xpifile - Choose distribution: Listed (public) or Unlisted (direct)
-
Fill listing:
- Name, description, categories
- Screenshots, icons
- Privacy policy (inline, not external link)
- Support email/URL
-
Review process:
- Automated validation: immediate
- Manual review: 1-7 days for listed extensions
- Respond to reviewer comments within 10 days
Review Criteria
- Extension works with supported Thunderbird versions
- Uses only necessary permissions
- No remote code execution
- Uses HTTPS for sensitive data
- Clear privacy policy disclosure
- No Experiment API when built-in API exists
- No hidden functionality
Privacy Policy Requirements
Must include (inline, not external):
# Privacy Policy for [Add-on Name]
## Data Collection
[Specific description of what data is collected]
## Purpose
[Why data is collected]
## Storage
[How and where data is stored]
## Sharing
[Whether data is shared with third parties]
## User Control
[How users can delete their data]
Common Rejection Reasons
| Reason | Solution | |--------|----------| | Doesn't work with supported versions | Test on Thunderbird 128+ | | Uses Experiment when built-in API exists | Use MailExtension API | | No response to reviewer comments | Check email, respond within 10 days | | Unclear privacy policy | Be specific about data collection | | Excessive permissions | Remove unused permissions | | Missing source code | Provide if using minification |
Testing & Debugging
Temporary Installation
- Open Thunderbird
- Go to Tools → Add-ons and Themes
- Click gear icon → Debug Add-ons
- Click Load Temporary Add-on
- Select
manifest.json
Note: Temporary add-ons are removed when Thunderbird closes.
Debugging Tools
Access Developer Tools:
- In Debug Add-ons page
- Click "Inspect" next to extension
- Console opens for background scripts
Debug specific components:
- Background: Console in debug page
- Popup: Right-click popup → "Inspect"
- Content scripts: Message window DevTools
Debug Commands
// Check manifest
messenger.runtime.getManifest();
// Check permissions
messenger.permissions.contains({ permissions: ['messagesRead'] });
// Get extension URL
messenger.runtime.getURL('/path/to/resource');
// Reload extension
messenger.runtime.reload();
// Check last error
if (messenger.runtime.lastError) {
console.error(messenger.runtime.lastError);
}
Testing Workflow
# 1. Create extension
# 2. Load temporarily in Thunderbird
# 3. Test functionality
# 4. Check console for errors
# 5. Fix issues
# 6. Reload extension (click Reload in debug page)
# 7. Repeat until working
# 8. Build and submit to ATN
Logging
// Use console for debugging
console.log("Extension loaded");
console.log("Message received:", message);
// Structured logging
console.table([
{ id: 1, name: "First" },
{ id: 2, name: "Second" }
]);
// Timing
console.time("operation");
// ... operation
console.timeEnd("operation");
Migration from Legacy Extensions
Key Changes in Thunderbird 128
| Change | Impact |
|--------|--------|
| Services.jsm removed | Use ChromeUtils.importESModule() |
| JSM → ES modules | Use .sys.mjs files |
| mailWindowOverlay.js removed | Use MailExtension APIs |
| Overlay extensions deprecated | Use MailExtensions only |
Migration Checklist
- [ ] Convert to WebExtension/MailExtension format
- [ ] Replace XUL overlays with HTML/CSS
- [ ] Replace
Services.jsmwith ES modules - [ ] Use
messenger.*APIs instead of direct XPCOM - [ ] Implement Experiment APIs for missing functionality
- [ ] Test thoroughly on Thunderbird 128+
Common Migration Patterns
Before (Legacy):
Components.utils.import("resource:///modules/mailServices.js");
MailServices.compose.OpenComposeWindow(...);
After (MailExtension):
messenger.compose.beginNew({
to: ["recipient@example.com"],
subject: "Hello"
});
Best Practices
Code Organization
my-extension/
├── manifest.json
├── background.js
├── popup.html
├── popup.js
├── compose_popup.html
├── compose_popup.js
├── api/
│ └── myapi/
│ ├── schema.json
│ └── implementation.js
├── icons/
│ ├── icon-16.png
│ ├── icon-32.png
│ └── icon-64.png
├── _locales/
│ ├── en/
│ │ └── messages.json
│ └── it/
│ └── messages.json
└── README.md
Error Handling
async function safeAsync(fn) {
try {
return await fn();
} catch (error) {
console.error("Error:", error);
return { error: error.message };
}
}
// Usage
const result = await safeAsync(() => messenger.messages.get(messageId));
if (result.error) {
console.error("Failed to get message:", result.error);
}
Performance
- Use pagination for large message lists
- Cache frequently accessed data
- Debounce rapid events
- Use
messages.query()with filters instead oflist()+ filter manually
Security
- Validate all user input
- Sanitize HTML before display
- Use minimal permissions
- Don't store sensitive data in
storage.localunencrypted - Validate message content before processing
Troubleshooting
Common Issues
| Error | Cause | Solution |
|-------|-------|----------|
| messenger is not defined | Script not in extension context | Check manifest script paths |
| Permission denied | Missing permission | Add to manifest permissions |
| API not available | Wrong Thunderbird version | Check strict_min_version |
| Contacts API fails in MV3 | Using old API | Use messenger.addressBooks.contacts.* |
| Experiment not loading | Path error | Check schema and implementation paths |
| Message scripts not working | Limited API access | Only runtime/storage/i18n available |
Debug Commands
// Check Thunderbird version
const info = await messenger.runtime.getBrowserInfo();
console.log(info.version);
// Check platform info
const platform = await messenger.runtime.getPlatformInfo();
console.log(platform.os, platform.arch);
// List all listeners
// (Add logging to all addListener calls)
// Check storage
const all = await messenger.storage.local.get(null);
console.log("Stored data:", all);
Differences from Firefox WebExtensions
| Feature | Firefox | Thunderbird |
|---------|---------|-------------|
| Namespace | browser.* | messenger.* (mail) + browser.* (common) |
| Context | Web browser | Email client |
| Content scripts | Work on web pages | Only in web tabs, not email content |
| Main action | browser_action / action | Same + compose_action, message_display_action |
| Mail APIs | None | accounts, compose, messages, etc. |
| Experiments | Limited | Common for email-specific features |
| Store | AMO | ATN |
File Structure Template
my-thunderbird-extension/
├── manifest.json
├── background.js
├── popup.html
├── popup.js
├── compose_popup.html
├── compose_popup.js
├── message_popup.html
├── message_popup.js
├── message_content.js
├── styles/
│ └── popup.css
├── icons/
│ ├── icon-16.png
│ ├── icon-32.png
│ └── icon-64.png
├── api/
│ └── myapi/
│ ├── schema.json
│ └── implementation.js
├── _locales/
│ ├── en/
│ │ └── messages.json
│ └── it/
│ └── messages.json
└── README.md
Quick Reference
Essential Permissions
{
"permissions": [
"storage", // Data storage
"messagesRead", // Read messages
"messagesMove", // Move/copy/delete messages
"addressBooks", // Access contacts
"compose", // Compose windows
"accountsRead", // Read accounts
"accountsFolders" // Access folders
]
}
Essential APIs
// Messages
messenger.messages.list(folderId)
messenger.messages.get(messageId)
messenger.messages.query({ from, unread })
messenger.messages.update(messageId, { read: true })
// Folders
messenger.folders.get(folderId)
messenger.folders.getSubFolders(account)
// Compose
messenger.compose.beginNew({ to, subject, body })
messenger.compose.getComposeDetails(tabId)
// Address Books
messenger.addressBooks.list()
messenger.addressBooks.contacts.create(addressBookId, { vCard })
// Display
messenger.messageDisplay.getDisplayedMessage(tabId)
messenger.messageDisplayAction.onClicked
Workflow Summary
- Develop: Write code, load temporarily
- Debug: Use Debug Add-ons → Inspect
- Test: Test all functionality
- Build: Create .zip with manifest and scripts
- Submit: Upload to ATN
- Review: Respond to reviewer feedback
- Publish: Extension goes live