From 85224f2d4d3557e05bcdb3bcfeb7ed65bd0c203c Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Tue, 15 Jul 2025 22:12:35 -0500 Subject: [PATCH] Add unread-only option for rules --- README.md | 5 +++-- background.js | 7 +++++++ options/options.js | 35 ++++++++++++++++++++++++----------- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3cdc2cc..d8e1bb2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ message meets a specified criterion. - **Markdown conversion** – optionally convert HTML bodies to Markdown before sending them to the AI service. - **Debug logging** – optional colorized logs help troubleshoot interactions with the AI service. - **Light/Dark themes** – automatically match Thunderbird's appearance with optional manual override. -- **Automatic rules** – create rules that tag, move, mark read/unread or flag/unflag messages based on AI classification. +- **Automatic rules** – create rules that tag, move, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages. - **Rule ordering** – drag rules to prioritize them and optionally stop processing after a match. - **Context menu** – apply AI rules from the message list or the message display action button. - **Status icons** – toolbar icons show when classification is in progress and briefly display success or error states. @@ -71,7 +71,8 @@ Sortana is implemented entirely with standard WebExtension scripts—no custom e 1. Open the add-on's options and set the URL of your classification service. 2. Use the **Classification Rules** section to add a criterion and optional actions such as tagging or moving a message when it matches. Drag rules to - reorder them and check *Stop after match* to halt further processing. + reorder them, check *Only apply to unread messages* to skip read mail, and + check *Stop after match* to halt further processing. 3. Save your settings. New mail will be evaluated automatically using the configured rules. diff --git a/background.js b/background.js index 6469288..ad2bbcb 100644 --- a/background.js +++ b/background.js @@ -39,6 +39,7 @@ function normalizeRules(rules) { if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); const rule = { criterion: r.criterion, actions }; if (r.stopProcessing) rule.stopProcessing = true; + if (r.unreadOnly) rule.unreadOnly = true; return rule; }) : []; } @@ -208,14 +209,20 @@ async function processMessage(id) { const full = await messenger.messages.getFull(id); const text = buildEmailText(full); let currentTags = []; + let alreadyRead = false; try { const hdr = await messenger.messages.get(id); currentTags = Array.isArray(hdr.tags) ? [...hdr.tags] : []; + alreadyRead = hdr.read === true; } catch (e) { currentTags = []; + alreadyRead = false; } for (const rule of aiRules) { + if (rule.unreadOnly && alreadyRead) { + continue; + } const cacheKey = await AiClassifier.buildCacheKey(id, rule.criterion); const matched = await AiClassifier.classifyText(text, rule.criterion, cacheKey); if (matched) { diff --git a/options/options.js b/options/options.js index 35d9a2c..ab63644 100644 --- a/options/options.js +++ b/options/options.js @@ -318,20 +318,30 @@ document.addEventListener('DOMContentLoaded', async () => { addAction.className = 'button is-small mb-2'; addAction.addEventListener('click', () => actionsContainer.appendChild(createActionRow())); - const stopLabel = document.createElement('label'); - stopLabel.className = 'checkbox mt-2'; - const stopCheck = document.createElement('input'); - stopCheck.type = 'checkbox'; - stopCheck.className = 'stop-processing'; - stopCheck.checked = rule.stopProcessing === true; - stopLabel.appendChild(stopCheck); - stopLabel.append(' Stop after match'); + const stopLabel = document.createElement('label'); + stopLabel.className = 'checkbox mt-2'; + const stopCheck = document.createElement('input'); + stopCheck.type = 'checkbox'; + stopCheck.className = 'stop-processing'; + stopCheck.checked = rule.stopProcessing === true; + stopLabel.appendChild(stopCheck); + stopLabel.append(' Stop after match'); + + const unreadLabel = document.createElement('label'); + unreadLabel.className = 'checkbox mt-2 ml-4'; + const unreadCheck = document.createElement('input'); + unreadCheck.type = 'checkbox'; + unreadCheck.className = 'unread-only'; + unreadCheck.checked = rule.unreadOnly === true; + unreadLabel.appendChild(unreadCheck); + unreadLabel.append(' Only apply to unread messages'); const body = document.createElement('div'); body.className = 'message-body'; body.appendChild(actionsContainer); body.appendChild(addAction); body.appendChild(stopLabel); + body.appendChild(unreadLabel); article.appendChild(header); article.appendChild(body); @@ -363,9 +373,10 @@ document.addEventListener('DOMContentLoaded', async () => { return { type }; }); const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; - return { criterion, actions, stopProcessing }; + const unreadOnly = ruleEl.querySelector('.unread-only')?.checked; + return { criterion, actions, unreadOnly, stopProcessing }; }); - data.push({ criterion: '', actions: [], stopProcessing: false }); + data.push({ criterion: '', actions: [], unreadOnly: false, stopProcessing: false }); renderRules(data); }); @@ -376,6 +387,7 @@ document.addEventListener('DOMContentLoaded', async () => { if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); const rule = { criterion: r.criterion, actions }; if (r.stopProcessing) rule.stopProcessing = true; + if (r.unreadOnly) rule.unreadOnly = true; return rule; })); @@ -509,7 +521,8 @@ document.addEventListener('DOMContentLoaded', async () => { return { type }; }); const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; - return { criterion, actions, stopProcessing }; + const unreadOnly = ruleEl.querySelector('.unread-only')?.checked; + return { criterion, actions, unreadOnly, stopProcessing }; }).filter(r => r.criterion); const stripUrlParams = stripUrlToggle.checked; const altTextImages = altTextToggle.checked;