# 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 of `summarize_display_mode`. - `summarize_auto = 3` (on receive) pre-caches the summary silently when the email arrives via `onNewMailReceived`. When the user later opens the message, the cache hit triggers an instant display. - `summarize_auto = 1` (manual button) respects `summarize_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 via `openChatGPT()` ``` 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.js` - `model-worker-google_gemini.js` - `model-worker-ollama.js` - `model-worker-openai_comp.js` - `model-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 | | `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:` 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.