From 6fd6da8a1247ee7255e33d0a13fd41c3d21c65fc Mon Sep 17 00:00:00 2001 From: Jordan Wages Date: Tue, 15 Jul 2025 22:50:50 -0500 Subject: [PATCH] Add age filters to rules --- README.md | 5 +++-- background.js | 14 ++++++++++++++ options/options.js | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f9a92f7..de61261 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, copy, forward, reply, delete, archive, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages. +- **Automatic rules** – create rules that tag, move, copy, forward, reply, delete, archive, mark read/unread or flag/unflag messages based on AI classification. Rules can optionally apply only to unread messages and can ignore messages outside a chosen age range. - **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. @@ -72,7 +72,8 @@ Sortana is implemented entirely with standard WebExtension scripts—no custom e 2. Use the **Classification Rules** section to add a criterion and optional actions such as tagging, moving, copying, forwarding, replying, deleting or archiving a message when it matches. Drag rules to - reorder them, check *Only apply to unread messages* to skip read mail, and + reorder them, check *Only apply to unread messages* to skip read mail, + set optional minimum or maximum message age limits, and check *Stop after match* to halt further processing. Forward and reply actions open a compose window using the account that received the message. 3. Save your settings. New mail will be evaluated automatically using the diff --git a/background.js b/background.js index 9a230e1..6636f60 100644 --- a/background.js +++ b/background.js @@ -41,6 +41,8 @@ function normalizeRules(rules) { const rule = { criterion: r.criterion, actions }; if (r.stopProcessing) rule.stopProcessing = true; if (r.unreadOnly) rule.unreadOnly = true; + if (typeof r.minAgeDays === 'number') rule.minAgeDays = r.minAgeDays; + if (typeof r.maxAgeDays === 'number') rule.maxAgeDays = r.maxAgeDays; return rule; }) : []; } @@ -229,6 +231,18 @@ async function processMessage(id) { if (rule.unreadOnly && alreadyRead) { continue; } + if (hdr && (typeof rule.minAgeDays === 'number' || typeof rule.maxAgeDays === 'number')) { + const msgTime = new Date(hdr.date).getTime(); + if (!isNaN(msgTime)) { + const ageDays = (Date.now() - msgTime) / 86400000; + if (typeof rule.minAgeDays === 'number' && ageDays < rule.minAgeDays) { + continue; + } + if (typeof rule.maxAgeDays === 'number' && ageDays > rule.maxAgeDays) { + 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 951c135..b089568 100644 --- a/options/options.js +++ b/options/options.js @@ -355,12 +355,30 @@ document.addEventListener('DOMContentLoaded', async () => { unreadLabel.appendChild(unreadCheck); unreadLabel.append(' Only apply to unread messages'); + const ageBox = document.createElement('div'); + ageBox.className = 'field is-grouped mt-2'; + const minInput = document.createElement('input'); + minInput.type = 'number'; + minInput.placeholder = 'Min days'; + minInput.className = 'input is-small min-age mr-2'; + minInput.style.width = '6em'; + if (typeof rule.minAgeDays === 'number') minInput.value = rule.minAgeDays; + const maxInput = document.createElement('input'); + maxInput.type = 'number'; + maxInput.placeholder = 'Max days'; + maxInput.className = 'input is-small max-age'; + maxInput.style.width = '6em'; + if (typeof rule.maxAgeDays === 'number') maxInput.value = rule.maxAgeDays; + ageBox.appendChild(minInput); + ageBox.appendChild(maxInput); + const body = document.createElement('div'); body.className = 'message-body'; body.appendChild(actionsContainer); body.appendChild(addAction); body.appendChild(stopLabel); body.appendChild(unreadLabel); + body.appendChild(ageBox); article.appendChild(header); article.appendChild(body); @@ -399,7 +417,12 @@ document.addEventListener('DOMContentLoaded', async () => { }); const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; const unreadOnly = ruleEl.querySelector('.unread-only')?.checked; - return { criterion, actions, unreadOnly, stopProcessing }; + const minAgeDays = parseFloat(ruleEl.querySelector('.min-age')?.value); + const maxAgeDays = parseFloat(ruleEl.querySelector('.max-age')?.value); + const rule = { criterion, actions, unreadOnly, stopProcessing }; + if (!isNaN(minAgeDays)) rule.minAgeDays = minAgeDays; + if (!isNaN(maxAgeDays)) rule.maxAgeDays = maxAgeDays; + return rule; }); data.push({ criterion: '', actions: [], unreadOnly: false, stopProcessing: false }); renderRules(data); @@ -414,6 +437,8 @@ document.addEventListener('DOMContentLoaded', async () => { const rule = { criterion: r.criterion, actions }; if (r.stopProcessing) rule.stopProcessing = true; if (r.unreadOnly) rule.unreadOnly = true; + if (typeof r.minAgeDays === 'number') rule.minAgeDays = r.minAgeDays; + if (typeof r.maxAgeDays === 'number') rule.maxAgeDays = r.maxAgeDays; return rule; })); @@ -557,7 +582,12 @@ document.addEventListener('DOMContentLoaded', async () => { }); const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; const unreadOnly = ruleEl.querySelector('.unread-only')?.checked; - return { criterion, actions, unreadOnly, stopProcessing }; + const minAgeDays = parseFloat(ruleEl.querySelector('.min-age')?.value); + const maxAgeDays = parseFloat(ruleEl.querySelector('.max-age')?.value); + const rule = { criterion, actions, unreadOnly, stopProcessing }; + if (!isNaN(minAgeDays)) rule.minAgeDays = minAgeDays; + if (!isNaN(maxAgeDays)) rule.maxAgeDays = maxAgeDays; + return rule; }).filter(r => r.criterion); const stripUrlParams = stripUrlToggle.checked; const altTextImages = altTextToggle.checked;