12 KiB
Architecture
Extension Structure (Manifest V2)
ThunderAI runs as a standard Thunderbird WebExtension with three main execution contexts:
Background Page → mzta-background.html / mzta-background.js
Popup → popup/mzta-popup.html / popup/mzta-popup.js
Options Page → options/mzta-options.html / options/mzta-options.js
Feature Pages → pages/*/
Content Script → js/lib/diff.js (injected into chatgpt.com)
Web Workers → js/workers/model-worker-*.js (one per API provider)
Data Flow: User Action → AI Response
User clicks popup or presses Ctrl+Alt+A
↓
popup/mzta-popup.js (renders prompt list, handles selection)
↓ (sendMessage to background)
mzta-background.js (orchestrates everything)
↓
js/mzta-placeholders.js (resolves {%placeholder%} values from email data)
↓
js/mzta-prompts.js (builds final prompt string)
↓
┌─────────────────────────────────────────┐
│ Based on connection_type: │
│ chatgpt_web → js/mzta-chatgpt.js │ (opens ChatGPT window)
│ chatgpt_api → Web Worker (openai) │
│ ollama_api → Web Worker (ollama) │
│ google_gemini → Web Worker (gemini) │
│ anthropic → Web Worker (anthropic)│
│ openai_comp → Web Worker (comp) │
└─────────────────────────────────────────┘
↓
Result returned to background
↓
js/mzta-compose-script.js (inserts text into Thunderbird compose window and display window)
Data Flow: Inline Summary on Message Display
The summarize_display_mode preference ('inline' or 'webchat') controls where
the summary is displayed. The summarize_auto preference controls when it is triggered.
summarize_auto = 2(automatic) always generates inline, regardless ofsummarize_display_mode.summarize_auto = 3(on receive) pre-caches the summary silently when the email arrives viaonNewMailReceived. When the user later opens the message, the cache hit triggers an instant display.summarize_auto = 1(manual button) respectssummarize_display_mode:'inline'→ button click triggers inline generation'webchat'→ button click opens the AI chat window via_openSummaryWebchat()
- Context menu summarize also respects
summarize_display_mode:'inline'with a single message → generates inline via_generateSummaryForMessage()'webchat'or multiple messages → opens the AI chat window viaopenChatGPT()
User opens/selects a message in Thunderbird
↓
mzta-compose-script.js (sends "initSummary" to background)
↓
mzta-background.js (checks summarize_auto + summarize_display_mode prefs)
↓
┌──────────────────────────────────────────────────────────┐
│ summarize_auto = 0 → do nothing │
│ summarize_auto = 1 → show "click to generate" button │
│ display_mode = inline → click triggers inline gen │
│ display_mode = webchat → click opens chat window │
│ summarize_auto = 2 → generate immediately (always inline)│
│ summarize_auto = 3 → cache hit (pre-cached on receive) │
└──────────────────────────────────────────────────────────┘
↓ (if generating inline)
taSummaryStore (check cache / set processing)
↓ (cache miss)
mzta-special-commands (via Web Worker, NOT chatgpt_web)
↓
taSummaryStore (save result via taStorage)
↓
mzta-compose-script.js (render summary banner in message body)
Data Flow: Inline Translation on Message Display
The translate_auto preference controls when translation is triggered.
Translation always renders inline (webchat mode has been removed).
The target language is determined by translate_lang (fallback on default_chatgpt_lang).
User opens/selects a message in Thunderbird
↓
mzta-compose-script.js (sends "initTranslation" to background)
↓
mzta-background.js (checks translate + translate_auto prefs)
↓
┌──────────────────────────────────────────────────────────┐
│ translate_auto = 0 → do nothing │
│ translate_auto = 1 → show "click to translate" button │
│ translate_auto = 2 → generate immediately │
└──────────────────────────────────────────────────────────┘
↓
taTranslationStore (check cache / set processing)
↓ (cache miss)
mzta-special-commands (via Web Worker, NOT chatgpt_web)
↓
taTranslationStore (save result via taStorage)
↓
mzta-compose-script.js (render translation banner in message body)
Data Flow: Background Summary on Email Receive (summarize_auto = 3)
When summarize_auto = 3, a summary is generated silently when a new email arrives. The flow mirrors add_tags_auto:
New email arrives
↓
browser.messages.onNewMailReceived
↓
newEmailListener (checks _process_incoming, which includes summarize_auto === 3)
↓
processEmails({ summarizeOnReceive: true })
↓ (single loop — shared with addTagsAuto / spamFilter / translateOnReceive)
_generateSummaryForMessage(headerMessageId, null, { messageData })
← tabId is null → no UI messages sent, silent pre-cache
↓
taSummaryStore.saveSummary()
↓
[later] user opens the message → initSummary → cache hit → showSummary instantly
Data Flow: Background Translation on Email Receive (translate_auto = 3)
When translate_auto = 3, a translation is generated silently when a new email arrives. Mirrors the summarize on-receive flow:
New email arrives
↓
browser.messages.onNewMailReceived
↓
newEmailListener (checks _process_incoming, which includes translate_auto === 3)
↓
processEmails({ translateOnReceive: true })
↓ (single loop — shared with addTagsAuto / spamFilter / summarizeOnReceive)
_generateTranslationForMessage(headerMessageId, null, { messageData })
← tabId is null → no UI messages sent, silent pre-cache
↓
taTranslationStore.saveTranslation()
↓
[later] user opens the message → initTranslation → cache hit → showTranslation instantly
Key Modules
| File | Role |
|---|---|
mzta-background.js |
Main orchestrator: listens for messages, coordinates all features |
js/mzta-menus.js |
Context menu creation and management |
js/mzta-prompts.js |
Prompt definitions (built-in) and custom prompt loading |
js/mzta-placeholders.js |
Placeholder definitions and resolution logic |
js/mzta-utils.js |
General utilities (email parsing, storage helpers, etc.) |
js/mzta-utils-prompt.js |
Prompt-specific utilities (text truncation, lang injection, buildSummaryPrompt() for unified summary prompt assembly, buildTranslationPrompt() for translation prompt assembly) |
js/mzta-compose-script.js |
Content script for compose and message display: injects AI response into compose window, renders unified toolbar (spam badge, summary/translation trigger buttons) and content panels (spam explanation, summary, translation) in message display via #mzta-container |
js/mzta-chatgpt.js |
ChatGPT Web integration (opens browser window, reads DOM) |
js/mzta-special-commands.js |
Handles special prompt actions (add_tags, calendar, task) |
js/mzta-spamreport.js |
Spam filter logic |
js/mzta-i18n.js |
i18n helper (wraps browser.i18n.getMessage) |
js/mzta-logger.js |
Debug logging (gated by do_debug pref) |
js/mzta-store.js |
Storage abstraction helpers |
js/mzta-storage.js |
Unified per-message storage layer (taStorage class) for summary, spam, and translation data |
js/mzta-summarystore.js |
Summary-specific storage wrapper (taSummaryStore class) with caching, truncation, and processing-state tracking |
js/mzta-translationstore.js |
Translation-specific storage wrapper (taTranslationStore class) with caching, truncation, and processing-state tracking |
js/mzta-working-status.js |
Visual status indicator during AI processing |
js/mzta-addtags-exclusion-list.js |
Tag exclusion list management |
js/mzta-placeholders-autocomplete.js |
Autocomplete for placeholders in prompt editor |
API Modules (js/api/)
Each file handles HTTP communication for one provider:
| File | Provider |
|---|---|
anthropic.js |
Claude (Anthropic) API |
google_gemini.js |
Google Gemini API |
ollama.js |
Ollama (self-hosted) |
openai_comp.js |
OpenAI-compatible APIs |
openai_comp_configs.js |
Pre-configured providers (DeepSeek, Grok, Mistral, OpenRouter, Perplexity) |
openai_responses.js |
OpenAI Responses API |
Web Workers (js/workers/)
Each API provider has a dedicated Web Worker so API calls don't block the UI:
model-worker-anthropic.jsmodel-worker-google_gemini.jsmodel-worker-ollama.jsmodel-worker-openai_comp.jsmodel-worker-openai_responses.js
Workers receive a message with the prompt and settings, make the API call, and post back the result.
Feature Pages (pages/)
Each subdirectory is a self-contained settings/UI page for a specific feature:
| Directory | Feature |
|---|---|
addtags/ |
Auto-tagging configuration |
customprompts/ |
Custom prompt editor |
customdataplaceholders/ |
Custom placeholder editor |
get-calendar-event/ |
Calendar event extraction settings |
get-task/ |
Task creation settings |
menu_order/ |
Drag-and-drop reordering and visibility control for popup and context menus |
spamfilter/ |
Spam filter settings |
summarize/ |
Email summarization settings |
translate/ |
Email translation settings |
onboarding/ |
First-run welcome page |
_lib/ |
Shared libraries used by pages |
Storage
All preferences are stored via browser.storage.local. The keys and default values are defined in options/mzta-options-default.js (prefs_default export). Custom prompts and custom placeholders are stored separately in storage under their own keys.
Per-Message Data Storage
Per-message data (summaries, spam reports, translations) is stored via js/mzta-storage.js (taStorage class). Each record is keyed by msg:<headerMessageId> in messenger.storage.local and follows schema version 1. Records contain optional fields: summary, spam, translation, plus metadata (v, ts). The taStorage class provides typed read/write/delete methods per field, automatic record cleanup when all fields are removed, and age-based cleanup.
js/mzta-summarystore.js (taSummaryStore class) wraps taStorage for summary-specific operations: load/save/remove summaries, track in-flight generation state via browser.storage.session, enforce a 100-entry cache limit with oldest-first truncation, and store error states.
js/mzta-translationstore.js (taTranslationStore class) wraps taStorage for translation-specific operations: load/save/remove translations, track in-flight generation state via browser.storage.session, enforce a 100-entry cache limit with oldest-first truncation, and store error states. Each translation record stores translated_text, lang, and optional error information.