--- 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+) ```json { "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) ```json { "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 name - `version` (required) - Version string - `description` - Short description - `author` - Author name - `icons` - Extension icons **Thunderbird-Specific:** - `browser_specific_settings.gecko.id` - **Required for ATN** - `browser_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 button - `compose_action` - Compose window toolbar button - `message_display_action` - Message view toolbar button **Permissions:** - `permissions` - API permissions - `experiment_apis` - Custom Experiment APIs **Other:** - `commands` - Keyboard shortcuts - `options_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, lifecycle - `browser.storage` - Data persistence - `browser.i18n` - Localization - `browser.tabs` - Tab management - `browser.windows` - Window management - `browser.commands` - Keyboard shortcuts ### Accounts API ```javascript // 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 ```javascript // 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 ```javascript // 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) ```javascript // 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 ```javascript // 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 ```javascript // 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 ```javascript // 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) ```json { "action": { "default_popup": "popup.html", "default_title": "My Extension", "default_icon": { "16": "icons/icon-16.png", "32": "icons/icon-32.png" } } } ``` ```javascript // 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) ```json { "compose_action": { "default_popup": "compose_popup.html", "default_title": "Compose Tool", "default_icon": "icons/compose-icon.png" } } ``` ```javascript // 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) ```json { "message_display_action": { "default_popup": "message_popup.html", "default_title": "Message Tool", "default_icon": "icons/message-icon.png" } } ``` ```javascript // 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 ```json { "message_display_scripts": [ { "matches": [""], "js": ["message_content.js"], "css": ["message_styles.css"] } ] } ``` ### MV3 Configuration ```javascript // 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.onMessage` - `messenger.i18n.getMessage()`, `messenger.i18n.getAcceptLanguages()` - `messenger.storage.*` ```javascript // 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, `${message.text}` ); } }); ``` ## 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 ```json { "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) ```json [ { "namespace": "myapi", "functions": [ { "name": "doSomething", "type": "function", "async": true, "parameters": [ { "name": "param", "type": "string" } ] } ], "events": [ { "name": "onSomething", "type": "function" } ] } ] ``` ### Implementation (implementation.js) ```javascript 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 ```javascript // 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](https://github.com/thunderbird/webext-experiments/tree/main/calendar) | | FileSystem | File system access | [webext-support/FileSystem](https://github.com/thunderbird/webext-support/tree/master/experiments/FileSystem) | | LegacyPrefs | Preferences access | [webext-support/LegacyPrefs](https://github.com/thunderbird/webext-support/tree/master/experiments/LegacyPrefs) | | NotificationBox | Notification bars | [webext-experiments/NotificationBox](https://github.com/thunderbird/webext-experiments/tree/main/NotificationBox) | | WindowListener | Window events | [webext-support/WindowListener](https://github.com/thunderbird/webext-support/tree/master/experiments/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 1. **Build extension:** ```bash zip -r extension.zip manifest.json background.js icons/ popup.html ``` 2. **Create developer account:** - Visit https://addons.thunderbird.net/developers/ - Sign up and complete profile 3. **Submit:** - Go to Developer Hub → "Submit a New Add-on" - Upload `.zip` or `.xpi` file - Choose distribution: Listed (public) or Unlisted (direct) 4. **Fill listing:** - Name, description, categories - Screenshots, icons - Privacy policy (inline, not external link) - Support email/URL 5. **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): ```markdown # 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 1. Open Thunderbird 2. Go to **Tools → Add-ons and Themes** 3. Click gear icon → **Debug Add-ons** 4. Click **Load Temporary Add-on** 5. Select `manifest.json` **Note:** Temporary add-ons are removed when Thunderbird closes. ### Debugging Tools **Access Developer Tools:** 1. In Debug Add-ons page 2. Click "Inspect" next to extension 3. 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 ```javascript // 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 ```bash # 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 ```javascript // 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.jsm` with 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):** ```javascript Components.utils.import("resource:///modules/mailServices.js"); MailServices.compose.OpenComposeWindow(...); ``` **After (MailExtension):** ```javascript 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 ```javascript 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 of `list()` + filter manually ### Security - Validate all user input - Sanitize HTML before display - Use minimal permissions - Don't store sensitive data in `storage.local` unencrypted - 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 ```javascript // 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 ```json { "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 ```javascript // 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 1. **Develop:** Write code, load temporarily 2. **Debug:** Use Debug Add-ons → Inspect 3. **Test:** Test all functionality 4. **Build:** Create .zip with manifest and scripts 5. **Submit:** Upload to ATN 6. **Review:** Respond to reviewer feedback 7. **Publish:** Extension goes live ## References - [Thunderbird Developer Hub](https://developer.thunderbird.net/add-ons/) - [WebExtension API Reference](https://webextension-api.thunderbird.net/) - [Supported APIs](https://developer.thunderbird.net/add-ons/mailextensions/supported-webextension-api) - [Manifest V3 Guide](https://github.com/thunderbird/webext-docs/blob/beta-mv2/guides/manifestV3.rst) - [Example Extensions](https://github.com/thunderbird/webext-examples) - [Experiment Support](https://github.com/thunderbird/webext-support) - [ATN Developer Hub](https://addons.thunderbird.net/developers/) - [ATN Review Policy](https://thunderbird.github.io/atn-review-policy/)