14 KiB
Thunderbird WebExtensions Development Guidelines
Purpose: This file provides operative guidelines for Claude when helping develop or modify ThunderAI. These rules override general AI assistant behavior and must be followed strictly.
ThunderAI-Specific Context
- ThunderAI uses Manifest Version 2 — do not suggest or apply any MV3 migration.
- No build tools, no transpilation, no npm — plain ES6 modules loaded directly.
- The manifest already uses
browser_specific_settings(notapplications). - All module imports use relative paths with
.jsextension. - The
mzta-prefix is used for all core module filenames.
Important Guidelines for AI Assistants
1. Always use browser_specific_settings in manifest.json
The applications manifest entry is deprecated. Always use browser_specific_settings:
{
"manifest_version": 2,
"name": "ThunderAI",
"browser_specific_settings": {
"gecko": {
"id": "thunderai@micz.it",
"strict_min_version": "140.0"
}
}
}
2. Do not guess APIs by using Try-Catch
A widespread antipattern in AI-generated Thunderbird extensions:
// WRONG - Never do this!
try {
await browser.someApi.method({ guessedParam: value });
} catch (e) {
try {
await browser.someApi.method({ differentGuess: value });
} catch (e2) {
// Giving up silently — this makes debugging impossible
}
}
Why this is harmful:
- Makes code unmaintainable
- Hides real errors from developers
- Makes debugging extremely difficult
The correct approach:
- Read the API documentation FIRST
- Use the exact parameter names and types specified
- Only use try-catch for expected error conditions with proper handling
- Never suppress errors without logging or handling them
3. Do not use Experiments unnecessarily
// WRONG - Using Experiment when standard API exists
// Don't use Experiment just because you found example code using it
// RIGHT - Check if standard API can do it first
const folders = await browser.folders.query({ name: "Inbox" });
4. Handle file storage correctly
// WRONG - Trying to use raw filesystem APIs
const fs = require('fs'); // Not available!
// RIGHT - Use storage.local with File objects
const file = new File([content], "data.txt", { type: "text/plain" });
await browser.storage.local.set({ file });
5. Do not use async listeners for the runtime.onMessage listener
See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage
6. Parse vCard, vTodo, vEvent and iCal strings using a 3rd party library
Follow https://webextension-api.thunderbird.net/en/mv2/guides/vcard.html to parse vCard, vEvent and vTodo strings.
7. Parse Mailbox Strings using messengerUtilities
Extract email addresses from mailbox strings like "John Doe john@example.com":
const parsed = await browser.messengerUtilities.parseMailboxString(
"John Doe <john@example.com>, Jane <jane@example.com>"
);
// Result:
// [
// { name: "John Doe", email: "john@example.com" },
// { name: "Jane", email: "jane@example.com" }
// ]
// Extract just emails:
const emails = parsed.map(p => p.email);
Documentation: https://webextension-api.thunderbird.net/en/mv2/messengerUtilities.html
Options:
preserveGroups: Keep grouped hierarchiesexpandMailingLists: Expand Thunderbird mailing lists (requiresaddressBookpermission)
8. Set correct strict_min_version entry
Make sure manifest.json has a strict_min_version entry matching the used functions. If a function added in Thunderbird 137 is used, it must be set to 137.0 or higher.
9. Always use background type "module"
Always use type: "module" for background scripts. This allows use of the import directive for ES6 modules, and non-ES6 libraries can still be loaded via the scripts array:
// RIGHT - Always use type: "module"
"background": {
"scripts": ["lib/some-non-ES6-lib.js", "background.js"],
"type": "module"
}
Then in background.js, import libraries explicitly:
// Import ES6 module with default export
import ICAL from "./lib/ical.js";
// Import ES6 module with named exports
import { someFunction, someConstant } from "./lib/somemodule.js";
10. Verify API return types — do not assume array access
Many Thunderbird APIs return wrapped objects, not direct arrays. Always verify the return type in the documentation before accessing the data.
Common pitfall — MessageList:
// WRONG - getDisplayedMessages() returns MessageList, not an array
const [message] = await browser.messageDisplay.getDisplayedMessages(tabId);
// RIGHT - MessageList has a .messages array property
const { messages: [message] } = await browser.messageDisplay.getDisplayedMessages(tabId);
Common pitfall — HeadersDictionary:
// WRONG - headers might not exist or might not be an array
let returnPath = headers["Return-Path"];
// RIGHT - keys are lowercase, values are always arrays
const returnPathArray = headers["return-path"];
const returnPath = returnPathArray?.[0] ?? null;
APIs that return wrapped objects (NOT direct arrays):
| API | Returns | Access Pattern |
|---|---|---|
messageDisplay.getDisplayedMessages() |
MessageList |
result.messages[0] |
messages.list() |
MessageList |
result.messages[0] |
messages.query() |
MessageList |
result.messages[0] |
messages.getHeaders() |
HeadersDictionary |
result["header-name"][0] |
messages.getFull() |
MessagePart |
result.headers["header-name"][0] |
APIs that return direct arrays:
| API | Returns | Access Pattern |
|---|---|---|
tabs.query() |
array of Tab | result[0] |
mailTabs.query() |
array of MailTab | result[0] |
addressBooks.list() |
array of AddressBookNode | result[0] |
contacts.list() |
array of ContactNode | result[0] |
folders.query() |
array of MailFolder | result[0] |
Official API Documentation
Primary resource: https://webextension-api.thunderbird.net/en/mv2/
Documentation exists for different channels:
- Release (mv2): https://webextension-api.thunderbird.net/en/mv2/
- ESR (esr-mv2): https://webextension-api.thunderbird.net/en/esr-mv2/
Key feature: Search functionality and cross-references between types and functions.
Understanding Thunderbird Release Channels
Standard Release Channel (Monthly)
- Update cadence: ~4 weeks
- A new major version with each release (
147.0,148.0, ...) - Gets new features and APIs (and bug fixes & security fixes)
ESR Channel (Extended Support Release)
- Update cadence: 1 major update per year, with lots of versions "missing" in between (
115.*,128.*,140.*,153.*, ...) - Receives bug fixes & security fixes on regular basis alongside the major monthly releases, but as minor releases (
140.1,140.2, ...) - No new features or APIs
For ThunderAI
- Target the Release channel for standard API usage.
- Target the ESR channel for add-ons relying on Experiment APIs. Targeting the Release channel with Experiments is acceptable only when the developer can guarantee required monthly updates.
Experiment APIs
What Are Experiment APIs?
Experiment APIs allow add-ons to access Thunderbird's core internals directly. They have full access to modify everything in Thunderbird.
Critical Rules for Experiments
1. Avoid Experiments Unless Absolutely Necessary
- Standard WebExtension APIs should always be your first choice
- Only use Experiments when standard APIs genuinely cannot accomplish your goal
2. Experiments Require Updates for Each Major Version
- This was manageable with ESR (1x/year)
- Became unsustainable with monthly Release channel (12x/year)
3. When Suggesting Experiments
- Only if standard APIs cannot accomplish the core goal
- Developer must explicitly understand the maintenance burden
- Target ESR channel specifically
- Reference
esr-mv2documentation
Available semi-official Experiment APIs
Calendar Experiment API
Use this instead of creating a custom Experiment for calendar interactions.
Use cases:
- Reading existing event/task items from Thunderbird's calendar
- Listening for item updates
- Creating/updating/deleting items
Setup requirements:
- Temporarily clone the webext-experiments repository.
- Add all
experiment_apisentries found incalendar/manifest.jsonto the project'smanifest.json. - Copy
calendar/experiments/calendar/into the project asexperiments/calendar/. Do not modify these files.
Note: Always request iCal format:
// Always consult schema first, if this example is still correct
browser.calendar.items.onCreated.addListener(
async (calendarItem) => {
if (calendarItem.type === "task") {
console.log("Task in iCal format:", calendarItem.item);
}
},
{ returnFormat: "ical" }
);
Other Experiment Repositories
- https://github.com/thunderbird/webext-support — Helper APIs and modules
- https://github.com/thunderbird/webext-examples — Example extensions (includes some Experiments)
Native File System Access
Current Limitations
Native filesystem access is NOT available in Thunderbird WebExtensions.
Recommended Approach
For data persistence:
await browser.storage.local.set({ myData: someValue });
const data = await browser.storage.local.get("myData");
For user file input:
const file = new File([content], "filename.txt", { type: "text/plain" });
await browser.storage.local.set({ file });
// Retrieve later
const data = await browser.storage.local.get("file");
console.log(data.file.name);
Important: File objects can be stored directly in browser.storage.local without serialization.
Add-on Review Requirements
Review policy: https://thunderbird.github.io/atn-review-policy/
Key Requirements
1. No Build Tools
- Include 3rd party libraries directly (don't use webpack, rollup, etc.)
- Include a
VENDOR.mdfile documenting all 3rd party libraries with links to exact versions (not "latest"). Example: https://webextension-api.thunderbird.net/en/mv2/guides/vcard.html
2. Permissions
- Only request permissions you actually need
- The
tabsandactiveTabpermissions are almost never needed in Thunderbird - Unnecessary permissions may cause rejection during ATN review
Example Repositories
- https://github.com/thunderbird/webext-examples — Official example extensions
- https://github.com/thunderbird/webext-support — Support libraries and helpers
Use these to see proper code structure, learn common patterns, and understand best practices.
Mandatory Checklist Before Providing Code
Before providing any code, verify ALL of these:
- Consulted official API documentation — do NOT guess methods or parameters
- NO try-catch blocks for guessing API parameters
- Used 3rd party libraries or API methods for parsing — minimize manual string parsing or regex
- Used 3rd party libraries are the most recent stable version
- Event listeners registered at file scope (NOT inside init function)
- VENDOR.md includes ALL dependencies with exact version URLs
- Used
browser_specific_settings(NOT deprecatedapplications) - Included proper error handling
- Code has comments explaining the approach
- No hardcoded user-facing strings — use the i18n API (
_locales/en/messages.jsononly) - Add-on fulfills all requirements in the "Add-on Review Requirements" section
- All guidelines in "Important Guidelines for AI Assistants" are followed
- Manifest uses correct
strict_min_version - If using Experiments: manifest has
strict_max_versiontargeting current ESR (fetch https://webextension-api.thunderbird.net/en/esr-mv2/ to get the major version, then use format"<major>.*")
If ANY checkbox is unchecked, DO NOT provide the code. Fix it first.
Mandatory 3rd Party Library Audit
For EACH 3rd party library included in the project:
- Inspect the actual file to determine the export type:
- ES6 default export: Look for
export default→ useimport LibName from "./lib/file.js" - ES6 named exports: Look for
export { name1, name2 }→ useimport { name1, name2 } from "./lib/file.js" - UMD/IIFE (no ES6 exports): Look for
(function(root, factory)or assignments towindow/globalThis→ load viascriptsarray in manifest
- ES6 default export: Look for
- Always prefer the minified module version
- Output a library audit table:
| Library | File | Module Type | Import Statement |
|---|---|---|---|
| ical.js | lib/ical.js | ES6 default | import ICAL from "./lib/ical.js" |
- Update VENDOR.md with the correct file path and version URL
Mandatory API Audit
Before finalizing any code:
- List all used API methods
- For EACH API method, fetch its documentation page:
https://webextension-api.thunderbird.net/en/mv2/<api-name>.html - For EACH API method, verify:
- Parameters: Correct names and types
- Return type: The actual type returned by the Promise
- Access pattern: How to extract data from the return value
- Required permission: What permission is needed in manifest.json
- Output an API audit table:
| API Method | Returns | Access Pattern | Required Permission |
|---|---|---|---|
browser.messageDisplay.getDisplayedMessages() |
MessageList | result.messages[0] |
messagesRead |
browser.messages.getHeaders() |
HeadersDictionary | result["header-name"][0] |
messagesRead |
browser.mailTabs.query() |
array of MailTab | result[0] |
(none) |
browser.storage.local.get |
object | result.keyName |
storage |
browser.i18n.getMessage |
string | direct | (none) |
- Update the permissions entry in manifest.json to include ALL required permissions
Getting Help
- Developer documentation: https://developer.thunderbird.net/
- Support forum: https://thunderbird.topicbox.com/groups/addons
- Matrix chat: #tb-addon-developers:mozilla.org