Fix: Correctly adding tags with non-ASCII characters [#689].
+
Improved the spacing between lines when displaying the AI response in the API webchat [#686].
+
Some minor improvments.
+
+
Version 4.0.1 - 27/02/2026
+
+
Fix: Correctly handling additional text without a placeholder [#681].
+
+
Version 4.0.0 - 24/02/2026
ThunderAI is now compatible only with Thunderbird 140 and later [#616].
[All APIs] It's now possibile to define a specific API integration for calendar and task recognition [#498].
@@ -24,7 +43,6 @@
Fix: Now it's possibile to use multiple additional_text placeholders in a single prompt, also using custom placeholders [#554].
When using the additional_text placeholder is now possibile to specify an ID that will be shown in the form asking for the text [#525].
[ChatGPT Web] Added an option to define a custom time to wait for the page load. Sometimes, on slow PCs, the ChatGPT page loads slowly and ThunderAI inject its content too early. With this option you can adjust the waiting time [#634].
-
...
Version 3.8.5 - 22/02/2026
diff --git a/CLAUDE.md b/CLAUDE.md
index 69cf47a3..b43e45f5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,39 +1,65 @@
-# ThunderAI Project Agent Context
+# ThunderAI - Claude Code Guide
## Project Overview
-ThunderAI is a Mozilla Thunderbird add-on that integrates Large Language Models (LLMs) such as ChatGPT, Gemini, Claude, and Ollama for advanced email management. It allows users to analyze, draft, correct, tag, and create calendar events directly from the email client.
+ThunderAI is a **Thunderbird WebExtension (Manifest V2)** that integrates multiple AI providers (ChatGPT Web, OpenAI API, Google Gemini, Claude/Anthropic, Ollama, and OpenAI-compatible APIs) directly into the Thunderbird email client.
-## Technical Stack
-- **Languages:** JavaScript (ES6+), HTML5, CSS3.
-- **Environment:** Thunderbird MailExtension API (based on WebExtensions).
-- **Core Logic:** Primarily located in `background.js` and UI-specific scripts.
-- **API Integrations:** Specific logic is contained within dedicated API provider files (e.g., `api_openai.js`, `api_google_gemini.js`).
+- **Extension ID:** `thunderai@micz.it`
+- **Min Thunderbird:** 140.0+
+- **Language:** Plain ES6+ JavaScript modules — no build tools, no transpilation, no npm
+- **License:** GPLv3
-## Development Rules & Style Guide
-- **Code Style:** Use modern JavaScript. Prefer `async/await` over chained Promises.
-- **Naming Conventions:** Use `camelCase` for variables and functions.
-- **Security:** - Never hardcode API keys in the source code.
- - Use `messenger.storage.local` for data persistence.
- - Strictly adhere to CORS policies for external API calls.
-- **i18n:**
- - The project supports multiple languages via `_locales/`. When adding UI strings, always use `messenger.i18n.getMessage()`.
- - When you need to modify language file, modify only the english version.
-- **Logging:**
- - When possibile use the taLog object to log errors, otherwise use the console.error() method.
- - If it's useful to log add debug logs using taLog.log() method.
+## Key Rules
-## Files & Directories Structure
-- `manifest.json`: Extension entry point and permission definitions.
-- `api_*.js`: Modules for integration with different AI providers.
-- `options/`: Configuration and settings pages.
-- `_locales/`: Translation files (JSON format).
-- `graphics/`: Visual assets and icons.
+1. **Localization:** Modify ONLY `_locales/en/messages.json`. All other locale files are managed via Weblate — never touch them.
+2. **No build system:** There is no bundler, compiler, or package manager. All JS files are plain ES6 modules loaded directly by the browser engine.
+3. **Module imports:** Use relative paths with `.js` extension (e.g., `import { foo } from '../js/mzta-utils.js'`).
+4. **Placeholder format:** Placeholders in prompt text use the `{%placeholder_id%}` syntax (e.g., `{%mail_text_body_or_selected%}`).
+5. **No test suite:** There is no automated test framework. Testing is done manually in Thunderbird.
+6. **Settings defaults:** All new preferences must be added to `options/mzta-options-default.js` in `prefs_default`.
+7. **Keep spec files up to date:** When making code changes that affect a subsystem described in claude-spec/, update the relevant spec file to reflect the new behavior. Read the spec before modifying, update it after.
-## Important Commands & Workflow
-- **Testing:** Since this is a Thunderbird add-on, testing is performed by loading the extension as a "Temporary Add-on" via Thunderbird's `Debug Add-ons` menu.
-- **Build:** The project does not use complex build tools (no Webpack/Vite by default); it is a "pure" MailExtension.
+## Directory Map
-## Agent Instructions
-1. **Context Awareness:** When modifying API-related files, ensure compatibility with the existing placeholder system (e.g., `{%mail_body%}`, `{%additional_text%}`).
-2. **Permission Review:** Before modifying `manifest.json`, verify that any new permissions requested are strictly necessary for the feature.
-3. **Compatibility:** Ensure code is compatible with the latest Thunderbird ESR (Extended Support Release) versions.
\ No newline at end of file
+```
+/
+├── mzta-background.js # Background script (main entry point)
+├── mzta-background.html # Loads the background script
+├── manifest.json # Extension manifest
+├── js/ # Core modules
+│ ├── api/ # AI API integration modules
+│ ├── workers/ # Web Workers (one per API provider)
+│ ├── lib/ # Third-party libraries (diff.js)
+│ └── mzta-*.js # Core utilities, menus, prompts, placeholders
+├── options/ # Settings UI
+│ ├── mzta-options.html/.js/.css
+│ ├── mzta-options-default.js # ALL default preference values
+│ └── mzta-release-notes.html
+├── pages/ # Feature-specific settings pages
+│ ├── addtags/
+│ ├── customprompts/
+│ ├── customdataplaceholders/
+│ ├── get-calendar-event/
+│ ├── get-task/
+│ ├── spamfilter/
+│ ├── summarize/
+│ └── onboarding/
+├── popup/ # Popup menu (shown on toolbar click)
+│ └── mzta-popup.html/.js/.css
+├── _locales/ # Localization
+│ ├── en/messages.json # ← ONLY THIS FILE is edited directly
+│ └── [15 other languages managed by Weblate]
+├── images/ # Icons and graphical assets
+└── api_webchat/ # Web chat API interface
+```
+
+## Spec Files
+
+For detailed documentation see [`claude-spec/`](claude-spec/):
+
+- [01-architecture.md](claude-spec/01-architecture.md) — Module structure and data flow
+- [02-prompts.md](claude-spec/02-prompts.md) — Prompt system (types, actions, properties)
+- [03-placeholders.md](claude-spec/03-placeholders.md) — Placeholder system
+- [04-api-integrations.md](claude-spec/04-api-integrations.md) — AI provider integrations
+- [05-options.md](claude-spec/05-options.md) — Settings and preferences system
+- [06-localization.md](claude-spec/06-localization.md) — i18n rules and workflow
+- [99-thunderbird-team-spec.md](claude-spec/99-thunderbird-team-spec.md) — Thunderbird WebExtensions development guidelines (API usage, experiments, review requirements)
diff --git a/README.md b/README.md
index e4847667..5ece8058 100644
--- a/README.md
+++ b/README.md
@@ -88,20 +88,20 @@ Are you using this addon in your Thunderbird?
## Attributions
### Translations
-- Chinese (Simplified) (zh_Hans): [jeklau](https://github.com/jeklau)
-- Chinese (Traditional) (zh_Hant): [evez](https://github.com/evez)
-- Croatian (hr): Petar Jedvaj
-- Czech (cs): [Fjuro](https://hosted.weblate.org/user/Fjuro/), [Jaroslav Staněk](https://hosted.weblate.org/user/jaroush/)
-- French (fr): Generated automatically, [Noam](https://github.com/noam-sc)
-- German (de): Generated automatically
-- Greek (el): [ChristosK.](https://github.com/christoskaterini)
-- Italian (it): [Mic](https://github.com/micz)
-- Japanese (ja): [Taichi Ito](https://github.com/watya1)
-- Polski (pl): [neexpl](https://github.com/neexpl), [makkacprzak](https://github.com/makkacprzak)
-- Português Brasileiro (pt-br): Bruno Pereira de Souza
-- Russian (ru): [Maksim](https://hosted.weblate.org/user/law820314/)
-- Spanish (es): [Gerardo Sobarzo](https://hosted.weblate.org/user/gerardo.sobarzo/), [Andrés Rendón Hernández](https://hosted.weblate.org/user/arendon/), [Erick Limon](https://hosted.weblate.org/user/ErickLimonG/)
-- Swedish (sv): [Andreas Pettersson](https://hosted.weblate.org/user/Andy_tb/) , [Luna Jernberg](https://hosted.weblate.org/user/bittin1ddc447d824349b2/)
+- Brazilian Portuguese - Português Brasileiro (pt-br): Bruno Pereira de Souza
+- Chinese (Simplified) - Jiǎntǐ Zhōngwén (简体中文) (zh_Hans): [jeklau](https://github.com/jeklau), [Min9X1n](https://github.com/Min9X1n)
+- Chinese (Traditional) - Fántǐ Zhōngwén (繁體中文) (zh_Hant): [evez](https://github.com/evez)
+- Croatian - Hrvatski (hr): Petar Jedvaj
+- Czech - Čeština (cs): [Fjuro](https://hosted.weblate.org/user/Fjuro/), [Jaroslav Staněk](https://hosted.weblate.org/user/jaroush/)
+- French - Français (fr): Generated automatically, [Noam](https://github.com/noam-sc)
+- German - Deutsch (de): Generated automatically
+- Greek - Elliniká (Ελληνικά) (el): [ChristosK.](https://github.com/christoskaterini)
+- Italian - Italiano (it): [Mic](https://github.com/micz)
+- Japanese - Nihongo (日本語) (ja): [Taichi Ito](https://github.com/watya1)
+- Polish - Polski (pl): [neexpl](https://github.com/neexpl), [makkacprzak](https://github.com/makkacprzak)
+- Russian - Russkiy (русский) (ru): [Maksim](https://hosted.weblate.org/user/law820314/)
+- Spanish - Español (es): [Gerardo Sobarzo](https://hosted.weblate.org/user/gerardo.sobarzo/), [Andrés Rendón Hernández](https://hosted.weblate.org/user/arendon/), [Erick Limon](https://hosted.weblate.org/user/ErickLimonG/)
+- Swedish - Svenska (sv): [Andreas Pettersson](https://hosted.weblate.org/user/Andy_tb/) , [Luna Jernberg](https://hosted.weblate.org/user/bittin1ddc447d824349b2/)
Do you want to help translate this addon? [Find out how!](https://micz.it/thunderbird-addon-thunderai/translate/)
diff --git a/VENDORS.md b/VENDORS.md
index cd5cd651..a30565a3 100644
--- a/VENDORS.md
+++ b/VENDORS.md
@@ -2,7 +2,10 @@ file: pages\_lib\list.js
source: https://raw.githubusercontent.com/javve/list.js/v2.3.1/dist/list.js
file: pages\_lib\tom-select.base.js
-source: https://cdn.jsdelivr.net/npm/tom-select@2.5.1/dist/js/tom-select.base.js
+source: https://cdn.jsdelivr.net/npm/tom-select@2.5.2/dist/js/tom-select.base.js
+
+file: pages\_lib\tom-select.default.min.css
+source: https://cdn.jsdelivr.net/npm/tom-select@v2.5.2/dist/css/tom-select.default.min.css
file: js\lib\diff.js
source: https://cdnjs.cloudflare.com/ajax/libs/jsdiff/7.0.0/diff.js
\ No newline at end of file
diff --git a/_locales/de/messages.json b/_locales/de/messages.json
index 66367494..e0f76e4c 100644
--- a/_locales/de/messages.json
+++ b/_locales/de/messages.json
@@ -1214,7 +1214,7 @@
"message": "Antworten Sie auf die folgende E-Mail „{%mail_text_body%}“. {%additional_text%}. Antworten Sie nur mit dem erforderlichen Text und ohne zusätzliche Kommentare oder anderen Text."
},
"prompt_reply_custom_command": {
- "message": "Mit Befehl antworten"
+ "message": "Mit Befehl antworten..."
},
"prefs_OptionText_chatgpt_web_br_replace_info": {
"message": "Bitte beachten Sie, dass alle -Tags in der Antwort der KI durch Zeilenumbrüche ersetzt werden."
@@ -1449,5 +1449,32 @@
},
"antispam_by": {
"message": "Antispam von"
+ },
+ "prefs_doc_title": {
+ "message": "Dokumentation"
+ },
+ "prefs_doc_setup_guide": {
+ "message": "Einrichtungsleitfäden"
+ },
+ "prefs_doc_custom_prompt_tutorial": {
+ "message": "Tutorial für benutzerdefinierte Prompts"
+ },
+ "prefs_doc_open_welcome": {
+ "message": "Die Willkommensseite öffnen"
+ },
+ "prefs_OptionText_chatgpt_win_pos_text": {
+ "message": "Position des KI-Chatfensters"
+ },
+ "prefs_OptionText_chatgpt_win_top": {
+ "message": "Oben"
+ },
+ "prefs_OptionText_chatgpt_win_left": {
+ "message": "Links"
+ },
+ "prefs_chatgpt_win_save_position": {
+ "message": "Fensterposition beim Schließen automatisch speichern."
+ },
+ "prefs_chatgpt_win_position_info": {
+ "message": "Leer lassen, um die Standardposition zu verwenden."
}
}
diff --git a/_locales/el/messages.json b/_locales/el/messages.json
index 5f6e056f..f3dbb337 100644
--- a/_locales/el/messages.json
+++ b/_locales/el/messages.json
@@ -15,7 +15,7 @@
"message": "Απάντηση σε αυτό το νήμα"
},
"prompt_reply_custom_command": {
- "message": "Απάντηση με εντολή"
+ "message": "Απάντηση με εντολή..."
},
"prompt_rewrite_polite": {
"message": "Ξαναγράψε ευγενικά"
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index e2abc0c0..4a527e1f 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -20,7 +20,7 @@
"description": ""
},
"prompt_reply_custom_command": {
- "message": "Reply with command",
+ "message": "Reply with command...",
"description": ""
},
"prompt_rewrite_polite": {
@@ -931,6 +931,22 @@
"message": "Click here, it only takes a minute!",
"description": ""
},
+ "prefs_doc_title": {
+ "message": "Documentation",
+ "description": ""
+ },
+ "prefs_doc_setup_guide": {
+ "message": "Setup Guides",
+ "description": ""
+ },
+ "prefs_doc_custom_prompt_tutorial": {
+ "message": "Custom Prompt Tutorial",
+ "description": ""
+ },
+ "prefs_doc_open_welcome": {
+ "message": "Open the Welcome Page",
+ "description": ""
+ },
"prefs_OpenAIComp_ForceModel": {
"message": "Manually insert model",
"description": ""
@@ -1970,5 +1986,25 @@
"prefs_THStats_2": {
"message": "Click here! Try ThunderStats!",
"description": ""
+ },
+ "prefs_OptionText_chatgpt_win_pos_text":{
+ "message": "AI chat window position",
+ "description": ""
+ },
+ "prefs_OptionText_chatgpt_win_top":{
+ "message": "Top",
+ "description": ""
+ },
+ "prefs_OptionText_chatgpt_win_left":{
+ "message": "Left",
+ "description": ""
+ },
+ "prefs_chatgpt_win_save_position": {
+ "message": "Automatically save the window position when using the close button.",
+ "description": ""
+ },
+ "prefs_chatgpt_win_position_info":{
+ "message": "Leave empty to use the default position.",
+ "description": ""
}
}
\ No newline at end of file
diff --git a/_locales/eo/messages.json b/_locales/eo/messages.json
index 34747628..e9e375ad 100644
--- a/_locales/eo/messages.json
+++ b/_locales/eo/messages.json
@@ -168,7 +168,7 @@
"message": "Malelekti ĉion"
},
"prompt_reply_custom_command": {
- "message": "Respondi per komando"
+ "message": "Respondi per komando..."
},
"customPrompts_substitute_text": {
"message": "Anstataŭigi tekston"
diff --git a/_locales/es/messages.json b/_locales/es/messages.json
index e0c082c6..37105671 100644
--- a/_locales/es/messages.json
+++ b/_locales/es/messages.json
@@ -30,7 +30,7 @@
"message": "Clasificar"
},
"prompt_reply_custom_command": {
- "message": "Responder con comando"
+ "message": "Responder con comando..."
},
"prompt_this": {
"message": "Crea un prompt de esto"
diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json
index dc41a8e9..71bb28ce 100644
--- a/_locales/fr/messages.json
+++ b/_locales/fr/messages.json
@@ -1211,7 +1211,7 @@
"message": "Gérer les espaces réservés de données"
},
"prompt_reply_custom_command": {
- "message": "Répondre avec une commande"
+ "message": "Répondre avec une commande..."
},
"prompt_reply_custom_command_full_text": {
"message": "Répondez à l'e-mail suivant « {%mail_text_body%} ». {%additional_text%}. Répondez uniquement avec le texte nécessaire, sans commentaires ou texte supplémentaire."
@@ -1449,5 +1449,32 @@
},
"antispam_by": {
"message": "Antispam par"
+ },
+ "prefs_doc_title": {
+ "message": "Documentation"
+ },
+ "prefs_doc_setup_guide": {
+ "message": "Guides de configuration"
+ },
+ "prefs_doc_custom_prompt_tutorial": {
+ "message": "Tutoriel sur les prompts personnalisés"
+ },
+ "prefs_doc_open_welcome": {
+ "message": "Ouvrir la page d'accueil"
+ },
+ "prefs_OptionText_chatgpt_win_pos_text": {
+ "message": "Position de la fenêtre de chat IA"
+ },
+ "prefs_OptionText_chatgpt_win_top": {
+ "message": "Haut"
+ },
+ "prefs_OptionText_chatgpt_win_left": {
+ "message": "Gauche"
+ },
+ "prefs_chatgpt_win_save_position": {
+ "message": "Enregistrer automatiquement la position de la fenêtre lors de la fermeture."
+ },
+ "prefs_chatgpt_win_position_info": {
+ "message": "Laisser vide pour utiliser la position par défaut."
}
}
diff --git a/_locales/it/messages.json b/_locales/it/messages.json
index 5bd0eb19..2a762996 100644
--- a/_locales/it/messages.json
+++ b/_locales/it/messages.json
@@ -1211,7 +1211,7 @@
"message": "I segnaposto di dati esistenti con lo stesso ID verranno sovrascritti. I segnaposto con ID nuovi verranno aggiunti."
},
"prompt_reply_custom_command": {
- "message": "Rispondi con istruzioni aggiuntive"
+ "message": "Rispondi con istruzioni aggiuntive..."
},
"prompt_reply_custom_command_full_text": {
"message": "Rispondi alla seguente email \"{%mail_text_body%}\". {%additional_text%}. Rispondi solo con il testo necessario, senza commenti aggiuntivi o altro testo."
@@ -1449,5 +1449,32 @@
},
"antispam_by": {
"message": "Antispam di"
+ },
+ "prefs_doc_title": {
+ "message": "Documentazione"
+ },
+ "prefs_doc_setup_guide": {
+ "message": "Guide di configurazione"
+ },
+ "prefs_doc_custom_prompt_tutorial": {
+ "message": "Tutorial Prompt Personalizzato"
+ },
+ "prefs_doc_open_welcome": {
+ "message": "Apri la Pagina di Benvenuto"
+ },
+ "prefs_OptionText_chatgpt_win_pos_text": {
+ "message": "Posizione finestra della chat AI"
+ },
+ "prefs_OptionText_chatgpt_win_top": {
+ "message": "Alto"
+ },
+ "prefs_OptionText_chatgpt_win_left": {
+ "message": "Sinistra"
+ },
+ "prefs_chatgpt_win_save_position": {
+ "message": "Salva automaticamente la posizione della finestra quando si usa il pulsante chiudi."
+ },
+ "prefs_chatgpt_win_position_info": {
+ "message": "Lascia vuoto per usare la posizione di default."
}
}
diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json
new file mode 100644
index 00000000..79cc2215
--- /dev/null
+++ b/_locales/nl/messages.json
@@ -0,0 +1,5 @@
+{
+ "extensionDescription": {
+ "message": "Gebruik ChatGPT, Google Gemini, Claude of Ollama om uw emails te verbeteren."
+ }
+}
diff --git a/_locales/pt/messages.json b/_locales/pt/messages.json
index f6656907..c088c55a 100644
--- a/_locales/pt/messages.json
+++ b/_locales/pt/messages.json
@@ -156,7 +156,7 @@
"message": "Responder"
},
"prompt_reply_custom_command": {
- "message": "Responder com o comando"
+ "message": "Responder com o comando..."
},
"chatgpt_win_close": {
"message": "Fechar"
diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json
index dbecf17f..2fe54878 100644
--- a/_locales/ro/messages.json
+++ b/_locales/ro/messages.json
@@ -24,7 +24,7 @@
"message": "Răspunde la această conversație"
},
"prompt_reply_custom_command": {
- "message": "Răspunde cu instrucțiuni suplimentare"
+ "message": "Răspunde cu instrucțiuni suplimentare..."
},
"prompt_rewrite_polite": {
"message": "Rescrie politicos"
diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json
index 90f0ea9f..ffc560a9 100644
--- a/_locales/sv/messages.json
+++ b/_locales/sv/messages.json
@@ -186,7 +186,7 @@
"message": "Svara på denna tråd"
},
"prompt_reply_custom_command": {
- "message": "Svara med kommando"
+ "message": "Svara med kommando..."
},
"prompt_rewrite_polite": {
"message": "Skriv om artigt"
@@ -1433,5 +1433,32 @@
},
"prefs_OptionText_calendar_no_selection_missing_placeholder": {
"message": "Prompten måste innehålla platshållaren {%mail_text_body_or_selected%} eller {%mail_html_body_or_selected%} för att aktivera det här alternativet. Lägg till en av dessa platshållare i prompten eller återställ den till standard."
+ },
+ "prefs_doc_title": {
+ "message": "Dokumentation"
+ },
+ "prefs_doc_setup_guide": {
+ "message": "Installationsguider"
+ },
+ "prefs_doc_custom_prompt_tutorial": {
+ "message": "Handledning för anpassad prompt"
+ },
+ "prefs_doc_open_welcome": {
+ "message": "Öppna välkomstsidan"
+ },
+ "prefs_OptionText_chatgpt_win_pos_text": {
+ "message": "AI-chattfönstrets position"
+ },
+ "prefs_OptionText_chatgpt_win_top": {
+ "message": "Överst"
+ },
+ "prefs_OptionText_chatgpt_win_left": {
+ "message": "Vänster"
+ },
+ "prefs_chatgpt_win_save_position": {
+ "message": "Spara automatiskt fönstrets position när stängningsknappen används."
+ },
+ "prefs_chatgpt_win_position_info": {
+ "message": "Lämna tomt för att använda standardpositionen."
}
}
diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json
index 5c187541..22d6bd28 100644
--- a/_locales/zh_Hans/messages.json
+++ b/_locales/zh_Hans/messages.json
@@ -843,7 +843,7 @@
"message": "未找到可编辑的日历!"
},
"customPrompts_form_label_use_diff_viewer": {
- "message": "启用差异查看器"
+ "message": "启用文本差异查看器"
},
"get_calendar_event_prompt_prefs_title": {
"message": "日历事件选项"
@@ -888,6 +888,27 @@
"message": "说明"
},
"customPrompts_form_label_use_diff_viewer_title": {
- "message": "当操作设置为“替换文本”时,可以选择差异查看器。"
+ "message": "当操作设置为“替换文本”时,可以选择文本差异查看器。"
+ },
+ "prompt_reply_custom_command": {
+ "message": "使用附加的提示词指令..."
+ },
+ "prompt_string": {
+ "message": "提示词"
+ },
+ "prefs_OptionText_reply_type_Info": {
+ "message": "此即回复电子邮件时默认采用的回复类型。您可在后续的回信对话框中另行选择其他选项。"
+ },
+ "prefs_OptionText_btnManageCustomDataPH": {
+ "message": "管理您的数据占位符"
+ },
+ "OpenChatGPTTab_Info2": {
+ "message": "从此处打开 ChatGPT Web 版将应用已配置好的模型、项目及自定义 GPT 相关设置。"
+ },
+ "placeholder_mail_headers": {
+ "message": "邮件头"
+ },
+ "placeholder_selected_html": {
+ "message": "已选中的 HTML"
}
}
diff --git a/api_webchat/controller.js b/api_webchat/controller.js
index c6f349c9..e5138da0 100644
--- a/api_webchat/controller.js
+++ b/api_webchat/controller.js
@@ -115,6 +115,8 @@ if (worker) {
case 'anthropic': llmName = "Claude"; break;
}
messagesArea.setLLMName(llmName);
+
+ document.title += " [" + llmName + " | " + decodeURIComponent(prompt_name) + "]";
let workerInitMessage = {
type: 'init',
@@ -266,7 +268,8 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if(userInput !== null) {
if(!placeholdersUtils.hasPlaceholder(promptData.prompt, 'additional_text')){
// no additional_text placeholder, do as usual
- promptData.prompt += " " + userInput;
+ const inputText = Array.isArray(userInput) ? userInput.map(obj => obj.custom_text).join(' ') : userInput;
+ promptData.prompt += " " + inputText;
}else{
// we have the additional_text placeholder, do the magic!
let finalSubs = {};
diff --git a/api_webchat/index.html b/api_webchat/index.html
index 5d803548..509c7017 100644
--- a/api_webchat/index.html
+++ b/api_webchat/index.html
@@ -3,7 +3,7 @@
-
+ ThunderAI Assistant
diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js
index 7036c30f..5d9b5bc4 100644
--- a/api_webchat/messagesArea.js
+++ b/api_webchat/messagesArea.js
@@ -534,7 +534,7 @@ class MessagesArea extends HTMLElement {
// Convert Markdown to DOM nodes using the markdown-it library
const md = window.markdownit();
- const html = convertNewlinesToBr(md.render(fullText));
+ const html = md.render(fullText);
this.fullTextHTML += html;
@@ -543,6 +543,7 @@ class MessagesArea extends HTMLElement {
// Create a new DOM parser
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
+ convertTextNodeNewlinesToBr(doc.body);
// Remove existing tokens
while (this.accumulatingMessageEl.firstChild) {
@@ -586,8 +587,23 @@ function htmlStringToFragment(htmlString) {
return fragment;
}
-function convertNewlinesToBr(text) {
- return text.replace(/\n/g, ' ');
+function convertTextNodeNewlinesToBr(element) {
+ element.childNodes.forEach(node => {
+ if (node.nodeType === Node.TEXT_NODE) {
+ if (node.textContent.includes('\n') && node.textContent.trim() !== '') {
+ const fragment = document.createDocumentFragment();
+ node.textContent.split('\n').forEach((part, idx, arr) => {
+ fragment.appendChild(document.createTextNode(part));
+ if (idx < arr.length - 1) {
+ fragment.appendChild(document.createElement('br'));
+ }
+ });
+ node.parentNode.replaceChild(fragment, node);
+ }
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ convertTextNodeNewlinesToBr(node);
+ }
+ });
}
function removeAloneBRs(htmlString) {
diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md
new file mode 100644
index 00000000..94dcdead
--- /dev/null
+++ b/claude-spec/01-architecture.md
@@ -0,0 +1,108 @@
+# 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)
+```
+
+## 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) |
+| `js/mzta-compose-script.js` | Injects AI response into Thunderbird compose window |
+| `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-working-status.js` | Visual status indicator during AI processing |
+| `js/mzta-addatags-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 |
+| `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.
diff --git a/claude-spec/02-prompts.md b/claude-spec/02-prompts.md
new file mode 100644
index 00000000..2e37e189
--- /dev/null
+++ b/claude-spec/02-prompts.md
@@ -0,0 +1,86 @@
+# Prompts System
+
+## Overview
+
+Prompts are the core user-facing feature of ThunderAI. Each prompt defines an AI instruction and how it behaves. There are two kinds:
+
+- **Built-in prompts** — defined in `js/mzta-prompts.js`
+- **Custom prompts** — created by the user and stored in `browser.storage.local`
+
+## Prompt Properties
+
+### Base Properties (built-in only)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | string | Unique identifier |
+| `name` | string | `__MSG_key__` i18n reference or plain text |
+| `prompt` | string | The prompt template text (may contain `{%placeholder%}` tokens) |
+| `type` | number | `0` = always visible, `1` = reading email only, `2` = composing only |
+| `action` | number | `0` = close, `1` = reply (open compose), `2` = substitute text in-place |
+| `need_selected` | number | `0` = use full message body, `1` = requires text selection |
+| `need_signature` | number | `0` = no signature, `1` = include signature |
+| `need_custom_text` | number | `0` = no custom input, `1` = show custom text input field |
+| `define_response_lang` | number | `0` = no language hint, `1` = append response language instruction |
+| `use_diff_viewer` | number | `0` = normal output, `1` = show diff viewer (ChatGPT Web only) |
+
+### User Properties (stored per-prompt in storage)
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `enabled` | number | `0` = hidden, `1` = shown in popup |
+| `position_display` | number | Sort order in reading view |
+| `position_compose` | number | Sort order in compose view |
+
+### Per-Prompt API Override Properties
+
+Each prompt can override the global API connection. These mirror the keys in `integration_options_config` and `prefs_default`:
+
+| Property | Description |
+|----------|-------------|
+| `connection_type` | Override API type for this prompt |
+| `chatgpt_web_model` | Override ChatGPT Web model |
+| `chatgpt_web_project` | Override ChatGPT Web project |
+| `chatgpt_web_custom_gpt` | Override custom GPT |
+| All `chatgpt_*`, `ollama_*`, `openai_comp_*`, `google_gemini_*`, `anthropic_*` keys | Override specific API settings |
+
+## Special Prompts
+
+Some prompts trigger additional Thunderbird actions beyond just sending text to the AI. They are identified by their `id`:
+
+| ID | Feature |
+|----|---------|
+| `add_tags` | Auto-tag the email after AI response |
+| `spamfilter` | Classify as spam and optionally move email |
+| `summarize` | Summarize email content |
+| `get_calendar_event` | Extract and create a calendar event |
+| `get_task` | Extract and create a task |
+
+These special prompts can have their own dedicated API integration settings (configured in the Options page). The list of these special prompts is in `options/mzta-options-default.js` as `special_prompts_with_integration`.
+
+## Prompt Types Reference
+
+```
+type 0 → shown when reading AND composing
+type 1 → shown only when reading an email (message display)
+type 2 → shown only when composing an email
+```
+
+## Action Types Reference
+
+```
+action 0 → no output, just close (e.g. for tag/spam actions handled in background)
+action 1 → open a reply compose window with AI response
+action 2 → replace selected text (or insert) in compose window
+```
+
+## Adding a New Built-in Prompt
+
+1. Add the prompt object to the `defaultPrompts` array in `js/mzta-prompts.js`
+2. Add the `name` string key to `_locales/en/messages.json`
+3. If the prompt text needs a localized string, add it to `_locales/en/messages.json` as well
+4. Reference any needed placeholders using `{%placeholder_id%}` syntax in the `prompt` field
+
+## Custom Prompts
+
+Custom prompts are stored in `browser.storage.local` and managed via `pages/customprompts/`. They follow the same property structure as built-in prompts but are created/edited/deleted by the user through the UI. Custom placeholders can also be referenced in custom prompt text.
diff --git a/claude-spec/03-placeholders.md b/claude-spec/03-placeholders.md
new file mode 100644
index 00000000..17d1c201
--- /dev/null
+++ b/claude-spec/03-placeholders.md
@@ -0,0 +1,78 @@
+# Placeholders System
+
+## Overview
+
+Placeholders are dynamic tokens embedded in prompt text that get replaced with real data at runtime (email content, headers, user input, etc.).
+
+**Format:** `{%placeholder_id%}`
+
+Example in a prompt: `"Summarize this email: {%mail_text_body_or_selected%}"`
+
+## Placeholder Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `id` | string | Unique identifier used in `{%id%}` tokens |
+| `name` | string | Display name (i18n `__MSG_key__` or plain text) |
+| `default_value` | string | Value used if placeholder cannot be resolved |
+| `type` | number | `0` = always, `1` = reading only, `2` = composing only |
+| `is_default` | string | `"1"` = built-in (not editable/deletable), `"0"` = custom |
+| `is_dynamic` | string | `"0"` = fixed value, `"1"` = dynamic (takes a parameter after `:`) |
+| `enabled` | number | `0` = disabled, `1` = enabled |
+| `text` | string | Content for custom placeholders only |
+
+## Built-in Placeholders (defined in `js/mzta-placeholders.js`)
+
+| ID | Description | Type |
+|----|-------------|------|
+| `mail_text_body` | Full plain text of the email | 0 |
+| `mail_html_body` | Full HTML of the email | 0 |
+| `mail_typed_text` | Text typed so far in compose window | 2 |
+| `mail_text_body_or_selected` | Plain text body, or selected text if any | 1 |
+| `mail_html_body_or_selected` | HTML body, or selected HTML if any | 1 |
+| `mail_selected_text` | Only the selected text | 1 |
+| `mail_selected_html` | Only the selected HTML | 1 |
+| `mail_subject` | Email subject line | 0 |
+| `mail_date` | Email date | 1 |
+| `mail_author` | Email sender | 0 |
+| `mail_recipients` | Email recipients | 0 |
+| `mail_tags` | Current tags on the email | 1 |
+| `mail_available_tags` | All available tags in Thunderbird | 1 |
+| `identity_name` | Current identity display name | 0 |
+| `identity_email` | Current identity email address | 0 |
+| `identity_signature` | Current identity signature | 0 |
+| `additional_text[id]` | User input field (dynamic, shows input in popup) | 0 |
+| `mail_header:name` | Any email header by name (dynamic) | 1 |
+
+## Dynamic Placeholders
+
+Dynamic placeholders use a colon separator to pass a parameter:
+
+```
+{%additional_text:my_field_id%} → shows an input field labelled "my_field_id" in the popup
+{%mail_header:x-spam-score%} → fetches the X-Spam-Score header value
+```
+
+The `is_dynamic: "1"` property signals this behavior in the placeholder definition.
+
+## Custom Placeholders
+
+Users can define their own placeholders via `pages/customdataplaceholders/`. Custom placeholders:
+- Have `is_default: "0"`
+- Have a `text` property containing the replacement value
+- Are stored in `browser.storage.local`
+- Are merged with default placeholders at runtime before prompt processing
+
+## Placeholder Resolution Order
+
+1. Built-in placeholders are defined in `js/mzta-placeholders.js`
+2. Custom placeholders are loaded from storage
+3. At runtime, `mzta-background.js` gathers email data (via Thunderbird APIs)
+4. Each `{%id%}` token in the prompt string is replaced with the resolved value
+5. If a value cannot be resolved, `default_value` is used as fallback
+
+## Adding a New Built-in Placeholder
+
+1. Add the object to the `defaultPlaceholders` array in `js/mzta-placeholders.js`
+2. Add the `name` i18n key to `_locales/en/messages.json` as `placeholder_` (or choose a descriptive key)
+3. Implement the resolution logic in the relevant section of `mzta-background.js`
diff --git a/claude-spec/04-api-integrations.md b/claude-spec/04-api-integrations.md
new file mode 100644
index 00000000..318eadbf
--- /dev/null
+++ b/claude-spec/04-api-integrations.md
@@ -0,0 +1,86 @@
+# API Integrations
+
+## Connection Types
+
+The active AI provider is controlled by the `connection_type` preference. Possible values:
+
+| `connection_type` value | Provider |
+|------------------------|----------|
+| `chatgpt_web` | ChatGPT Web (no API key, opens browser window) |
+| `chatgpt_api` | OpenAI API (ChatGPT via API key) |
+| `ollama_api` | Ollama (self-hosted LLM) |
+| `openai_comp_api` | OpenAI-compatible API |
+| `google_gemini_api` | Google Gemini API |
+| `anthropic_api` | Claude (Anthropic) API |
+
+The global default is `chatgpt_web`. Each special prompt (`add_tags`, `spamfilter`, etc.) can independently override this via its own `{prefix}_connection_type` pref.
+
+## Provider Configuration
+
+Each provider has its own settings block in `integration_options_config` (`options/mzta-options-default.js`):
+
+### ChatGPT Web
+Controlled via `js/mzta-chatgpt.js`. Opens a browser window to `chatgpt.com`, injects the prompt via DOM automation, and reads back the response. Settings: `chatgpt_web_model`, `chatgpt_web_tempchat`, `chatgpt_web_project`, `chatgpt_web_custom_gpt`, `chatgpt_web_load_wait_time`.
+
+Content script `js/lib/diff.js` is injected into ChatGPT pages for diff-view support.
+
+### OpenAI API (`chatgpt_api`)
+- Module: `js/api/openai_responses.js`
+- Worker: `js/workers/model-worker-openai_responses.js`
+- Settings keys: `chatgpt_api_key`, `chatgpt_model`, `chatgpt_developer_messages`, `chatgpt_temperature`, `chatgpt_store`
+
+### Ollama (`ollama_api`)
+- Module: `js/api/ollama.js`
+- Worker: `js/workers/model-worker-ollama.js`
+- Settings keys: `ollama_host`, `ollama_model`, `ollama_num_ctx`, `ollama_temperature`, `ollama_think`
+- Requires CORS to be configured on the Ollama server
+
+### OpenAI-Compatible (`openai_comp_api`)
+- Module: `js/api/openai_comp.js`
+- Worker: `js/workers/model-worker-openai_comp.js`
+- Settings keys: `openai_comp_host`, `openai_comp_model`, `openai_comp_api_key`, `openai_comp_use_v1`, `openai_comp_chat_name`, `openai_comp_temperature`
+- Pre-configured providers: `js/api/openai_comp_configs.js` (DeepSeek, Grok, Mistral, OpenRouter, Perplexity)
+
+### Google Gemini (`google_gemini_api`)
+- Module: `js/api/google_gemini.js`
+- Worker: `js/workers/model-worker-google_gemini.js`
+- Settings keys: `google_gemini_api_key`, `google_gemini_model`, `google_gemini_system_instruction`, `google_gemini_thinking_budget`, `google_gemini_temperature`
+
+### Anthropic / Claude (`anthropic_api`)
+- Module: `js/api/anthropic.js`
+- Worker: `js/workers/model-worker-anthropic.js`
+- Settings keys: `anthropic_api_key`, `anthropic_model`, `anthropic_version`, `anthropic_max_tokens`, `anthropic_system_prompt`, `anthropic_temperature`
+
+## Web Worker Pattern
+
+For all API-based providers (everything except ChatGPT Web), the call goes through a Web Worker:
+
+```
+mzta-background.js
+ → creates new Worker('js/workers/model-worker-.js')
+ → postMessage({ prompt, settings })
+ → worker makes HTTP fetch to provider API
+ → worker postMessage({ result }) back
+ → background handles result
+```
+
+This keeps API calls off the main thread and avoids blocking the Thunderbird UI.
+
+## Optional Permissions
+
+API calls require host permissions. These are declared as `optional_permissions` in `manifest.json` and requested at runtime:
+
+- `https://*.chatgpt.com/*` and `https://*.openai.com/*` for ChatGPT
+- `https://*.anthropic.com/*` for Claude
+- `https://*/*` and `http://*/*` for Ollama and OpenAI-compatible endpoints
+
+## Adding a New Provider
+
+1. Create `js/api/.js` with the API call logic
+2. Create `js/workers/model-worker-.js` that imports and calls the API module
+3. Add a new `connection_type` value constant
+4. Add settings keys to `integration_options_config` in `options/mzta-options-default.js`
+5. Add UI controls to `options/mzta-options.html` and `options/mzta-options.js`
+6. Add the new `connection_type` case to the dispatch logic in `mzta-background.js`
+7. Add required host permissions to `manifest.json` optional_permissions
+8. Add i18n strings to `_locales/en/messages.json`
diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md
new file mode 100644
index 00000000..f1dc571c
--- /dev/null
+++ b/claude-spec/05-options.md
@@ -0,0 +1,113 @@
+# Options & Settings System
+
+## Overview
+
+All extension preferences are stored in `browser.storage.local`. Defaults and the full list of valid keys are defined in `options/mzta-options-default.js`.
+
+## Key Exports from `mzta-options-default.js`
+
+| Export | Description |
+|--------|-------------|
+| `prefs_default` | All preference keys with their default values |
+| `integration_options_config` | Per-provider API settings structure |
+| `getDynamicSettingsDefaults(keysFilter)` | Returns per-special-prompt integration defaults |
+| `getDynamicSettingValue(prefs, prefix, settingName)` | Reads a prefixed setting for a special prompt |
+
+## Settings Structure
+
+### Global Integration Settings
+
+Stored flat in `prefs_default` with `{provider}_{key}` naming:
+
+```
+chatgpt_api_key, chatgpt_model, chatgpt_developer_messages, chatgpt_temperature, chatgpt_store
+ollama_host, ollama_model, ollama_num_ctx, ollama_temperature, ollama_think
+openai_comp_host, openai_comp_model, openai_comp_api_key, openai_comp_use_v1, openai_comp_chat_name, openai_comp_temperature
+google_gemini_api_key, google_gemini_model, google_gemini_system_instruction, google_gemini_thinking_budget, google_gemini_temperature
+anthropic_api_key, anthropic_model, anthropic_version, anthropic_max_tokens, anthropic_system_prompt, anthropic_temperature
+```
+
+Plus the global connection selector:
+```
+connection_type (default: 'chatgpt_web')
+use_specific_integration (default: false)
+```
+
+### Special Prompt Integration Overrides
+
+The 5 special prompts (`add_tags`, `spamfilter`, `summarize`, `get_calendar_event`, `get_task`) each get their own `use_specific_integration` and `connection_type` keys:
+
+```
+{prefix}_use_specific_integration (default: false)
+{prefix}_connection_type (default: 'chatgpt_api')
+```
+
+These are generated programmatically at the bottom of `mzta-options-default.js` using `special_prompts_with_integration` array.
+
+### UI & Feature Preferences
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `do_debug` | `false` | Enable debug logging |
+| `chatgpt_win_height` | `800` | ChatGPT window height |
+| `chatgpt_win_width` | `700` | ChatGPT window width |
+| `chatgpt_win_top` | `''` | Window top position |
+| `chatgpt_win_left` | `''` | Window left position |
+| `chatgpt_win_save_position` | `false` | Remember window position |
+| `default_chatgpt_lang` | `''` | Force response language |
+| `default_sign_name` | `''` | Default signature name |
+| `reply_type` | `'reply_all'` | Default reply type |
+| `composing_plain_text` | `false` | Use plain text in compose |
+| `chatgpt_web_model` | `''` | ChatGPT Web model override |
+| `chatgpt_web_tempchat` | `false` | Use temporary chat |
+| `chatgpt_web_project` | `''` | ChatGPT Web project |
+| `chatgpt_web_custom_gpt` | `''` | Custom GPT URL |
+| `chatgpt_web_load_wait_time` | `1000` | Wait time (ms) for ChatGPT page |
+| `dynamic_menu_force_enter` | `false` | Force Enter to submit in popup |
+| `dynamic_menu_order_alphabet` | `true` | Sort prompts alphabetically |
+| `placeholders_use_default_value` | `false` | Use placeholder defaults when empty |
+| `max_prompt_length` | `30000` | Max prompt string length |
+
+### Feature Flags
+
+| Key | Default | Description |
+|-----|---------|-------------|
+| `add_tags` | `false` | Enable auto-tagging feature |
+| `add_tags_maxnum` | `3` | Max tags to apply |
+| `add_tags_hide_exclusions` | `false` | Hide excluded tags from menu |
+| `add_tags_exclusions_exact_match` | `false` | Exact match for exclusions |
+| `add_tags_first_uppercase` | `true` | Capitalize first letter of tags |
+| `add_tags_force_lang` | `true` | Force language for tags |
+| `add_tags_auto` | `false` | Auto-tag on message open |
+| `add_tags_auto_force_existing` | `false` | Only use existing tags |
+| `add_tags_auto_only_inbox` | `true` | Auto-tag only inbox messages |
+| `add_tags_auto_uselist` | `false` | Use tag allow-list |
+| `add_tags_auto_uselist_list` | `''` | Tag allow-list content |
+| `add_tags_enabled_accounts` | `[]` | Accounts where auto-tag is active |
+| `get_calendar_event` | `true` | Enable calendar event extraction |
+| `get_calendar_event_from_clipboard` | `false` | Enable calendar from clipboard |
+| `get_task` | `true` | Enable task creation |
+| `calendar_enforce_timezone` | `false` | Force specific timezone |
+| `calendar_timezone` | `''` | Timezone to enforce |
+| `calendar_no_selection` | `false` | Skip selection prompt |
+| `spamfilter` | `false` | Enable spam filter |
+| `spamfilter_threshold` | `70` | Spam confidence threshold (%) |
+| `spamfilter_enabled_accounts` | `[]` | Accounts where spam filter is active |
+| `spamfilter_show_msg_panel` | `true` | Show info panel on spam detection |
+| `summarize` | `false` | Enable email summarization |
+
+## Adding a New Preference
+
+1. Add the key and default value to `prefs_default` in `options/mzta-options-default.js`
+2. Add UI control to `options/mzta-options.html`
+3. Add load/save logic to `options/mzta-options.js`
+4. Add i18n label to `_locales/en/messages.json`
+5. Read the pref in the relevant module via `browser.storage.local.get()`
+
+## Reading Preferences at Runtime
+
+```javascript
+const prefs = await browser.storage.local.get(prefs_default);
+// prefs now contains all keys with defaults for any unset values
+const myPref = prefs.my_new_pref;
+```
diff --git a/claude-spec/06-localization.md b/claude-spec/06-localization.md
new file mode 100644
index 00000000..7578fd25
--- /dev/null
+++ b/claude-spec/06-localization.md
@@ -0,0 +1,91 @@
+# Localization
+
+## Golden Rule
+
+**Only ever modify `_locales/en/messages.json`.**
+
+All other locale files (`de`, `fr`, `it`, `es`, `zh_Hans`, `zh_Hant`, `pl`, `ru`, `pt-br`, `sv`, `el`, `cs`, `hr`, `ja`, `nb_NO`) are managed by translators through [Weblate](https://hosted.weblate.org/). Never edit them manually.
+
+## Message File Format
+
+Each entry in `_locales/en/messages.json` follows the standard WebExtension i18n format:
+
+```json
+"key_name": {
+ "message": "The English text",
+ "description": "Context for translators explaining where/how this string is used"
+}
+```
+
+The `description` field is important — it helps Weblate translators understand the context.
+
+## Using Strings in Code
+
+### In JavaScript
+```javascript
+import { i18n } from './mzta-i18n.js';
+const text = i18n('key_name');
+// or directly:
+const text = browser.i18n.getMessage('key_name');
+```
+
+### In HTML
+```html
+
+
+__MSG_key_name__
+```
+
+### In manifest.json
+```json
+"description": "__MSG_extensionDescription__"
+```
+
+## Naming Conventions
+
+| Prefix | Usage |
+|--------|-------|
+| `menu_*` | Context menu and popup menu labels |
+| `prompt_*` | Built-in prompt names |
+| `placeholder_*` | Placeholder display names |
+| `options_*` | Settings page labels |
+| `pages_*` | Feature page labels |
+| `error_*` | Error messages |
+| `info_*` | Informational messages |
+| `btn_*` | Button labels |
+
+## Adding a New String
+
+1. Open `_locales/en/messages.json`
+2. Add the new key in alphabetical order within the file (or near related keys)
+3. Include both `message` and `description` fields
+4. Use the string in code via `browser.i18n.getMessage('key_name')` or `__MSG_key_name__`
+
+Example:
+```json
+"my_new_feature_label": {
+ "message": "My New Feature",
+ "description": "Label for the new feature button in the options page"
+}
+```
+
+## Supported Languages (16)
+
+| Code | Language |
+|------|----------|
+| `en` | English (source) |
+| `de` | German |
+| `es` | Spanish |
+| `fr` | French |
+| `it` | Italian |
+| `pl` | Polish |
+| `ru` | Russian |
+| `pt-br` | Brazilian Portuguese |
+| `sv` | Swedish |
+| `el` | Greek |
+| `cs` | Czech |
+| `hr` | Croatian |
+| `ja` | Japanese |
+| `nb_NO` | Norwegian Bokmål |
+| `zh_Hans` | Chinese Simplified |
+| `zh_Hant` | Chinese Traditional |
diff --git a/claude-spec/99-thunderbird-team-spec.md b/claude-spec/99-thunderbird-team-spec.md
new file mode 100644
index 00000000..c3be0040
--- /dev/null
+++ b/claude-spec/99-thunderbird-team-spec.md
@@ -0,0 +1,398 @@
+# 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` (not `applications`).
+- All module imports use relative paths with `.js` extension.
+- 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`:
+
+```json
+{
+ "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:
+
+```javascript
+// 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:**
+1. Read the API documentation FIRST
+2. Use the exact parameter names and types specified
+3. Only use try-catch for expected error conditions with proper handling
+4. Never suppress errors without logging or handling them
+
+### 3. Do not use Experiments unnecessarily
+
+```javascript
+// 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
+
+```javascript
+// 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 ":
+
+```javascript
+const parsed = await browser.messengerUtilities.parseMailboxString(
+ "John Doe , Jane "
+);
+
+// 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 hierarchies
+- `expandMailingLists`: Expand Thunderbird mailing lists (requires `addressBook` permission)
+
+### 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:
+
+```json
+// RIGHT - Always use type: "module"
+"background": {
+ "scripts": ["lib/some-non-ES6-lib.js", "background.js"],
+ "type": "module"
+}
+```
+
+Then in `background.js`, import libraries explicitly:
+```javascript
+// 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:**
+```javascript
+// 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:**
+```javascript
+// 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-mv2` documentation
+
+### 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:**
+1. Temporarily clone the [webext-experiments](https://github.com/thunderbird/webext-experiments/) repository.
+2. Add all `experiment_apis` entries found in `calendar/manifest.json` to the project's `manifest.json`.
+3. Copy `calendar/experiments/calendar/` into the project as `experiments/calendar/`. Do not modify these files.
+
+**Note:** Always request iCal format:
+
+```javascript
+// 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:**
+```javascript
+await browser.storage.local.set({ myData: someValue });
+const data = await browser.storage.local.get("myData");
+```
+
+**For user file input:**
+```javascript
+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.md` file 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 `tabs` and `activeTab` permissions 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 deprecated `applications`)
+- [ ] Included proper error handling
+- [ ] Code has comments explaining the approach
+- [ ] No hardcoded user-facing strings — use the i18n API (`_locales/en/messages.json` only)
+- [ ] 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_version` targeting current ESR (fetch https://webextension-api.thunderbird.net/en/esr-mv2/ to get the major version, then use format `".*"`)
+
+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` → use `import LibName from "./lib/file.js"`
+ - **ES6 named exports:** Look for `export { name1, name2 }` → use `import { name1, name2 } from "./lib/file.js"`
+ - **UMD/IIFE (no ES6 exports):** Look for `(function(root, factory)` or assignments to `window`/`globalThis` → load via `scripts` array in manifest
+- [ ] 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/.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
diff --git a/js/mzta-chatgpt.js b/js/mzta-chatgpt.js
index 0a354d35..bf12533e 100644
--- a/js/mzta-chatgpt.js
+++ b/js/mzta-chatgpt.js
@@ -614,11 +614,21 @@ async function doProceed(message, customText = ''){
let final_prompt = message.prompt;
if (Array.isArray(customText)) {
+ let anyReplaced = false;
customText.forEach(obj => {
- let escapedPH = obj.placeholder.replace(/[.*+?^{$}()|[\\]\\\\]/g, '\\\\$&');
- let regex = new RegExp(escapedPH, 'g');
- final_prompt = final_prompt.replace(regex, obj.custom_text);
+ if (final_prompt.includes(obj.placeholder)) {
+ anyReplaced = true;
+ let escapedPH = obj.placeholder.replace(/[.*+?^{$}()|[\\]\\\\]/g, '\\\\$&');
+ let regex = new RegExp(escapedPH, 'g');
+ final_prompt = final_prompt.replace(regex, obj.custom_text);
+ }
});
+ if (!anyReplaced) {
+ const inputText = customText.map(obj => obj.custom_text).join(' ');
+ if (inputText !== '') {
+ final_prompt += ' ' + inputText;
+ }
+ }
} else {
//check if there is the additional_text placeholder
if(final_prompt.includes('{%additional_text%}')){
diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js
index b5751a2c..56b706ee 100644
--- a/js/mzta-compose-script.js
+++ b/js/mzta-compose-script.js
@@ -682,13 +682,13 @@ switch (message.command) {
borderColor = '#006600';
}
- container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: start; gap: 15px; width: 100%; box-sizing: border-box;`;
+ container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`;
const scoreText = document.createElement('strong');
if (data.spamValue == -999) {
scoreText.textContent = browser.i18n.getMessage("apiwebchat_error");
} else {
- scoreText.textContent = ((data.spamValue >= (data.SpamThreshold || 50)) ? browser.i18n.getMessage("Spam") : browser.i18n.getMessage("Valid")) + " [" + data.spamValue + "/100]";
+ scoreText.textContent = ((data.spamValue >= (data.SpamThreshold || 50)) ? "⚠️ " + browser.i18n.getMessage("Spam") : "🛡️ " + browser.i18n.getMessage("Valid")) + " [" + data.spamValue + "/100]";
}
const reasonText = document.createElement('span');
@@ -700,7 +700,7 @@ switch (message.command) {
const branding = document.createElement('span');
branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI";
- branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 11px; opacity: 0.7;';
+ branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5;';
const closeBtn = document.createElement('span');
closeBtn.textContent = '×';
diff --git a/js/mzta-spamreport.js b/js/mzta-spamreport.js
index f18a59f1..2f4200e1 100644
--- a/js/mzta-spamreport.js
+++ b/js/mzta-spamreport.js
@@ -16,30 +16,49 @@
* along with this program. If not, see .
*/
-export const taSpamReport = {
- logger: console,
- _data_prefix: 'mzta-spam-report-',
- _processing_prefix: 'mzta-spam-processing-',
- _max_reports: 100,
+import { taStorage } from './mzta-storage.js';
+import { taLogger } from './mzta-logger.js';
+
+export class taSpamReport {
+
+ _processing_prefix = 'mzta-spam-processing-';
+ _max_reports = 100;
+ _storage = null;
+ taLog = null;
+
+ constructor(do_debug = false) {
+ this._storage = new taStorage(do_debug);
+ this.taLog = new taLogger('mzta-spamreport', do_debug);
+ }
async setProcessing(data_id) {
+ this.taLog.log("[setProcessing] data_id: " + data_id);
const key = this._processing_prefix + data_id;
await browser.storage.session.set({ [key]: true });
- },
+ }
async isProcessing(data_id) {
+ this.taLog.log("[isProcessing] data_id: " + data_id);
const key = this._processing_prefix + data_id;
let output = await browser.storage.session.get(key);
- return output[key] || false;
- },
+ let result = output[key] || false;
+ this.taLog.log("[isProcessing] result: " + result);
+ return result;
+ }
async saveReportData(data, data_id) {
- const key = this._data_prefix + data_id;
- await browser.storage.session.set({ [key]: data });
- await browser.storage.session.remove(this._processing_prefix + data_id);
- },
+ this.taLog.log("[saveReportData] data_id: " + data_id);
+ try {
+ await this._storage.writeSpam(data_id, data, true);
+ await browser.storage.session.remove(this._processing_prefix + data_id);
+ } catch (e) {
+ this.taLog.error("[saveReportData] error: " + e);
+ throw e;
+ }
+ }
async saveError(data_id, error_message) {
+ this.taLog.log("[saveError] data_id: " + data_id + ", error_message: " + error_message);
let data = {
spamValue: -999,
explanation: error_message,
@@ -48,53 +67,71 @@ export const taSpamReport = {
};
await this.saveReportData(data, data_id);
return data;
- },
+ }
async loadReportData(data_id) {
- const key = this._data_prefix + data_id;
- let output = await browser.storage.session.get(key);
- return output[key] || null;
- },
+ this.taLog.log("[loadReportData] data_id: " + data_id);
+ let record = await this._storage.getRecord(data_id);
+ if (!record || !this._storage.hasField(record, taStorage.FIELD_SPAM)) {
+ this.taLog.log("[loadReportData] no record found for data_id: " + data_id);
+ return null;
+ }
+ let spam = record.spam;
+ return {
+ headerMessageId: data_id,
+ spamValue: spam.spamValue,
+ explanation: spam.explanation,
+ report_date: new Date(spam.ts),
+ subject: spam.subject,
+ from: spam.from,
+ message_date: spam.message_date,
+ moved: spam.moved,
+ SpamThreshold: spam.SpamThreshold,
+ };
+ }
async removeReportData(data_id) {
- const key = this._data_prefix + data_id;
- await browser.storage.session.remove(key);
+ this.taLog.log("[removeReportData] data_id: " + data_id);
+ await this._storage.deleteSpamField(data_id);
await browser.storage.session.remove(this._processing_prefix + data_id);
- },
+ }
async getAllReportData() {
- let allData = await browser.storage.session.get(null);
- let reportData = {};
-
- for (const [key, value] of Object.entries(allData)) {
- if (key.startsWith(this._data_prefix)) {
- reportData[key.replace(this._data_prefix, '')] = value;
- }
- }
-
- return reportData;
- },
+ this.taLog.log("[getAllReportData] loading all reports");
+ return await this._storage.getAllSpamRecords();
+ }
async clearReportData() {
- let allData = await browser.storage.session.get(null);
- let keysToDelete = Object.keys(allData).filter(key => key.startsWith(this._data_prefix) || key.startsWith(this._processing_prefix));
-
+ this.taLog.log("[clearReportData] clearing all report data");
+ let allSpam = await this._storage.getAllSpamRecords();
+ let spamKeys = Object.keys(allSpam);
+ this.taLog.log("[clearReportData] deleting " + spamKeys.length + " spam records");
+ for (let messageId of spamKeys) {
+ await this._storage.deleteSpamField(messageId);
+ }
+ let allSession = await browser.storage.session.get(null);
+ let keysToDelete = Object.keys(allSession).filter(k => k.startsWith(this._processing_prefix));
+ this.taLog.log("[clearReportData] deleting " + keysToDelete.length + " session keys");
for (let key of keysToDelete) {
await browser.storage.session.remove(key);
}
- },
+ }
async truncReportData() {
- let data = await this.getAllReportData();
+ this.taLog.log("[truncReportData] checking report count");
+ let data = await this._storage.getAllSpamRecords();
let sortedData = this.sortReportsByDate(data);
let keys = Object.keys(sortedData);
+ this.taLog.log("[truncReportData] total reports: " + keys.length + ", max: " + this._max_reports);
if (keys.length > this._max_reports) {
+ let toDelete = keys.length - this._max_reports;
+ this.taLog.log("[truncReportData] truncating " + toDelete + " oldest reports");
for (let i = this._max_reports; i < keys.length; i++) {
- await browser.storage.session.remove(this._data_prefix + keys[i]);
+ await this._storage.deleteSpamField(keys[i]);
}
}
- },
+ }
sortReportsByDate(data) {
if (!data) return {};
@@ -112,4 +149,4 @@ export const taSpamReport = {
return sortedReports;
}
-};
+}
diff --git a/js/mzta-storage.js b/js/mzta-storage.js
new file mode 100644
index 00000000..983a3451
--- /dev/null
+++ b/js/mzta-storage.js
@@ -0,0 +1,276 @@
+/*
+ * ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
+ * Copyright (C) 2024 - 2026 Mic (m@micz.it)
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import { taLogger } from './mzta-logger.js';
+
+export class taStorage {
+
+ static STORAGE_KEY_PREFIX = 'msg:';
+ static SCHEMA_VERSION = 1;
+ static FIELD_SPAM = 'spam';
+ static FIELD_SUMMARY = 'summary';
+ static FIELD_TRANSLATION = 'translation';
+
+ taLog = null;
+
+ constructor(do_debug = false) {
+ this.taLog = new taLogger("mzta-storage", do_debug);
+ }
+
+ /**
+ * Build the storage key for a given Message-ID.
+ * @param {string} messageId - The Message-ID header string.
+ * @returns {string} The prefixed storage key.
+ */
+ _buildKey(messageId) {
+ return taStorage.STORAGE_KEY_PREFIX + messageId;
+ }
+
+ /**
+ * Read the full record for a given Message-ID.
+ * @param {string} messageId - The Message-ID header string.
+ * @returns {Promise