diff --git a/README.md b/README.md index e6b8e41..bda2023 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ message meets a specified criterion. - **Persistent result caching** – classification results are saved to disk so messages aren't re-evaluated across restarts. - **Advanced parameters** – tune generation settings like temperature, top‑p and more from the options page. - **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service. +- **Automatic rules** – create rules that tag or move new messages based on AI classification. - **Packaging script** – `build-xpi.ps1` builds an XPI ready for installation. ## Architecture Overview @@ -45,7 +46,7 @@ APIs: | `modules/ExpressionSearchFilter.jsm` | Custom filter term and AI request logic. | | `experiment/api.js` | Bridges WebExtension code with privileged APIs.| | `content/filterEditor.js` | Patches the filter editor interface. | -| `options/options.html` and `options.js` | Endpoint configuration UI. | +| `options/options.html` and `options.js` | Endpoint and rule configuration UI. | | `logger.js` and `modules/logger.jsm` | Colorized logging with optional debug mode. | ## Building diff --git a/background.js b/background.js index a3577b7..a51028a 100644 --- a/background.js +++ b/background.js @@ -12,6 +12,12 @@ let logger; let AiClassifier; +let aiRules = []; + +async function sha256Hex(str) { + const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)); + return Array.from(new Uint8Array(buf), b => b.toString(16).padStart(2, '0')).join(''); +} // Startup (async () => { logger = await import(browser.runtime.getURL("logger.js")); @@ -23,9 +29,10 @@ let AiClassifier; logger.aiLog("failed to import AiClassifier", {level: 'error'}, e); } try { - const store = await browser.storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging"]); + const store = await browser.storage.local.get(["endpoint", "templateName", "customTemplate", "customSystemPrompt", "aiParams", "debugLogging", "aiRules"]); logger.setDebug(store.debugLogging); AiClassifier.setConfig(store); + aiRules = Array.isArray(store.aiRules) ? store.aiRules : []; logger.aiLog("configuration loaded", {debug: true}, store); } catch (err) { logger.aiLog("failed to load config", {level: 'error'}, err); @@ -62,15 +69,26 @@ browser.runtime.onMessage.addListener(async (msg) => { if (typeof messenger !== "undefined" && messenger.messages?.onNewMailReceived) { messenger.messages.onNewMailReceived.addListener(async (folder, messages) => { logger.aiLog("onNewMailReceived", {debug: true}, messages); + if (!aiRules.length) { + const { aiRules: stored } = await browser.storage.local.get("aiRules"); + aiRules = Array.isArray(stored) ? stored : []; + } for (const msg of (messages?.messages || messages || [])) { const id = msg.id ?? msg; try { const full = await messenger.messages.getFull(id); const text = full?.parts?.[0]?.body || ""; - const criterion = (await browser.storage.local.get("autoCriterion")).autoCriterion || ""; - const matched = await AiClassifier.classifyText(text, criterion); - if (matched) { - await messenger.messages.update(id, {tags: ["$label1"]}); + for (const rule of aiRules) { + const cacheKey = await sha256Hex(`${id}|${rule.criterion}`); + const matched = await AiClassifier.classifyText(text, rule.criterion, cacheKey); + if (matched) { + if (rule.tag) { + await messenger.messages.update(id, {tags: [rule.tag]}); + } + if (rule.moveTo) { + await messenger.messages.move([id], rule.moveTo); + } + } } } catch (e) { logger.aiLog("failed to classify new mail", {level: 'error'}, e); diff --git a/options/options.html b/options/options.html index b758309..ed239cd 100644 --- a/options/options.html +++ b/options/options.html @@ -78,6 +78,21 @@ flex-wrap: wrap; } + #rules-container { + margin-top: 10px; + } + + .rule { + border: 1px solid #ccc; + padding: 10px; + margin-bottom: 10px; + border-radius: 4px; + } + + .rule-actions { + margin-top: 10px; + } + button { padding: 10px 20px; border: none; @@ -197,8 +212,13 @@ + +