Skill v1.0.1
currentAutomated scan100/100+1 new
version: "1.0.1" name: thunderbird-extension description: Comprehensive guide for developing MailExtensions for Mozilla Thunderbird, including Manifest V2/V3 configuration, all messenger.* APIs, UI actions, Experiment APIs, and ATN submission. metadata: author: mte90 version: 1.0.0 tags:
- thunderbird
- mailextension
- email-extension
- mozilla
- atn
- messenger-api
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 accountsconst accounts = await messenger.accounts.list();// Get specific accountconst account = await messenger.accounts.get(accountId);// Get account detailsconsole.log(account.name, account.type, account.identities);// List folders in accountconst folders = await messenger.folders.getSubFolders(account);
Messages API
// List messages in folderconst messages = await messenger.messages.list(folderId);// Get specific messageconst message = await messenger.messages.get(messageId);// Message propertiesconsole.log(message.subject, message.from, message.to, message.date);// Get full message with bodyconst fullMessage = await messenger.messages.getFull(messageId);console.log(fullMessage.parts[0].body);// Query messagesconst results = await messenger.messages.query({from: "sender@example.com",unread: true,limit: 50});// Move messagesawait messenger.messages.move([messageId], destinationFolderId);// Copy messagesawait messenger.messages.copy([messageId], destinationFolderId);// Delete messagesawait messenger.messages.delete([messageId], true); // true = skip trash// Mark as read/unreadawait messenger.messages.update(messageId, { read: true });// Archive messagesawait messenger.messages.archive([messageId]);// Import messageconst importedId = await messenger.messages.import(file, // File objectfolderId,{ read: true, flagged: false });
Folders API
// Get folderconst folder = await messenger.folders.get(folderId);// List subfoldersconst subfolders = await messenger.folders.getSubFolders(parentFolder);// Create folderconst newFolder = await messenger.folders.create(parentAccountId, "New Folder");// Rename folderawait messenger.folders.rename(folderId, "New Name");// Delete folderawait messenger.folders.delete(folderId);// Mark folder as readawait messenger.folders.markAsRead(folderId);// Get folder propertiesconsole.log(folder.name, folder.path, folder.unreadCount, folder.totalCount);
Address Books & Contacts API (MV3)
// List address booksconst addressBooks = await messenger.addressBooks.list();// Get address bookconst book = await messenger.addressBooks.get(addressBookId);// Create contact (vCard format)const contactId = await messenger.addressBooks.contacts.create(addressBookId, {vCard: `BEGIN:VCARDVERSION:4.0FN:John DoeEMAIL:john@example.comTEL:+1-555-0100END:VCARD`});// Get contactconst contact = await messenger.addressBooks.contacts.get(contactId);console.log(contact.vCard);// Update contactawait messenger.addressBooks.contacts.update(contactId, {vCard: updatedVCard});// Delete contactawait messenger.addressBooks.contacts.delete(contactId);// Quick search contactsconst results = await messenger.addressBooks.contacts.quickSearch("john");// Search in specific address bookconst results = await messenger.addressBooks.contacts.query({addressBookId: addressBookId,searchText: "john"});// Create mailing listconst listId = await messenger.addressBooks.mailingLists.create(addressBookId, {name: "Team",nickName: "team",description: "Team members"});// Add contact to mailing listawait messenger.addressBooks.mailingLists.addMember(listId, contactId);
Compose API
// Open compose windowconst tab = await messenger.compose.beginNew({to: ["recipient@example.com"],cc: ["cc@example.com"],subject: "Hello",body: "Message content",isPlainText: false});// Compose with attachmentsawait messenger.compose.beginNew({to: ["recipient@example.com"],attachments: [{file: new File(["content"], "file.txt", { type: "text/plain" })}]});// Reply to messageawait messenger.compose.beginReply(messageId, "replyToAll");// Forward messageawait messenger.compose.beginForward(messageId, "forwardInline");// Get compose detailsconst details = await messenger.compose.getComposeDetails(tabId);console.log(details.to, details.subject, details.body);// Set compose detailsawait messenger.compose.setComposeDetails(tabId, {subject: "Updated Subject"});// Listen for compose eventsmessenger.compose.onBeforeSend.addListener((tab, details) => {// Modify message before sendingdetails.body += "\n\n-- Sent via MyExtension";return { details };});// Listen for compose window openmessenger.compose.onComposeCreated.addListener((tab) => {console.log("Compose window created:", tab.id);});
Message Display API
// Listen for message displayedmessenger.messageDisplay.onMessageDisplayed.addListener((tab, message) => {console.log("Message displayed:", message.subject);});// Get displayed messageconst 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 tabconst mailTab = await messenger.mailTabs.getCurrent();// Get displayed folderconst folder = await messenger.mailTabs.getDisplayedFolder(tabId);// Set displayed folderawait messenger.mailTabs.update(tabId, {displayedFolderId: folderId});// Get selected messagesconst selection = await messenger.mailTabs.getSelectedMessages(tabId);// Listen for folder changesmessenger.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 badgeawait messenger.action.setBadgeText({ text: "5" });await messenger.action.setBadgeBackgroundColor({ color: "#ff0000" });// Update iconawait 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 windowmessenger.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 messagemessenger.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.jsawait 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 backgroundconst response = await messenger.runtime.sendMessage({action: "processMessage",content: document.body.innerText});// Listen for messages from backgroundmessenger.runtime.onMessage.addListener((message, sender, sendResponse) => {if (message.action === "highlight") {// Highlight text in messagedocument.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 Servicesconst { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");// Do something with internal APIsreturn Services.someService.process(param);},onSomething: new ExtensionCommon.EventManager({context,name: "myapi.onSomething",register: (fire) => {const callback = (data) => fire.async(data);// Register with internal servicesomeInternalService.addListener(callback);return () => {someInternalService.removeListener(callback);};}}).api()}};}onStartup() {console.log("Extension starting up");}onShutdown(reason) {console.log("Extension shutting down:", reason);// Cleanup requiredServices.obs.notifyObservers(null, "startupcache-invalidate", null);}}
Using Experiment API
// In background.jsconst result = await messenger.myapi.doSomething("param");// Listen for experiment eventsmessenger.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:
``bash 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 manifestmessenger.runtime.getManifest();// Check permissionsmessenger.permissions.contains({ permissions: ['messagesRead'] });// Get extension URLmessenger.runtime.getURL('/path/to/resource');// Reload extensionmessenger.runtime.reload();// Check last errorif (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 debuggingconsole.log("Extension loaded");console.log("Message received:", message);// Structured loggingconsole.table([{ id: 1, name: "First" },{ id: 2, name: "Second" }]);// Timingconsole.time("operation");// ... operationconsole.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 };}}// Usageconst 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 versionconst info = await messenger.runtime.getBrowserInfo();console.log(info.version);// Check platform infoconst platform = await messenger.runtime.getPlatformInfo();console.log(platform.os, platform.arch);// List all listeners// (Add logging to all addListener calls)// Check storageconst 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
// Messagesmessenger.messages.list(folderId)messenger.messages.get(messageId)messenger.messages.query({ from, unread })messenger.messages.update(messageId, { read: true })// Foldersmessenger.folders.get(folderId)messenger.folders.getSubFolders(account)// Composemessenger.compose.beginNew({ to, subject, body })messenger.compose.getComposeDetails(tabId)// Address Booksmessenger.addressBooks.list()messenger.addressBooks.contacts.create(addressBookId, { vCard })// Displaymessenger.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