Add age filters to rules

This commit is contained in:
Jordan Wages 2025-07-15 22:50:50 -05:00
commit 6fd6da8a12
3 changed files with 49 additions and 4 deletions

View file

@ -17,7 +17,7 @@ message meets a specified criterion.
- **Markdown conversion** optionally convert HTML bodies to Markdown before sending them to the AI service. - **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. - **Debug logging** optional colorized logs help troubleshoot interactions with the AI service.
- **Light/Dark themes** automatically match Thunderbird's appearance with optional manual override. - **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. - **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. - **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. - **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 2. Use the **Classification Rules** section to add a criterion and optional
actions such as tagging, moving, copying, forwarding, replying, actions such as tagging, moving, copying, forwarding, replying,
deleting or archiving a message when it matches. Drag rules to 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 check *Stop after match* to halt further processing. Forward and reply actions
open a compose window using the account that received the message. open a compose window using the account that received the message.
3. Save your settings. New mail will be evaluated automatically using the 3. Save your settings. New mail will be evaluated automatically using the

View file

@ -41,6 +41,8 @@ function normalizeRules(rules) {
const rule = { criterion: r.criterion, actions }; const rule = { criterion: r.criterion, actions };
if (r.stopProcessing) rule.stopProcessing = true; if (r.stopProcessing) rule.stopProcessing = true;
if (r.unreadOnly) rule.unreadOnly = 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; return rule;
}) : []; }) : [];
} }
@ -229,6 +231,18 @@ async function processMessage(id) {
if (rule.unreadOnly && alreadyRead) { if (rule.unreadOnly && alreadyRead) {
continue; 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 cacheKey = await AiClassifier.buildCacheKey(id, rule.criterion);
const matched = await AiClassifier.classifyText(text, rule.criterion, cacheKey); const matched = await AiClassifier.classifyText(text, rule.criterion, cacheKey);
if (matched) { if (matched) {

View file

@ -355,12 +355,30 @@ document.addEventListener('DOMContentLoaded', async () => {
unreadLabel.appendChild(unreadCheck); unreadLabel.appendChild(unreadCheck);
unreadLabel.append(' Only apply to unread messages'); 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'); const body = document.createElement('div');
body.className = 'message-body'; body.className = 'message-body';
body.appendChild(actionsContainer); body.appendChild(actionsContainer);
body.appendChild(addAction); body.appendChild(addAction);
body.appendChild(stopLabel); body.appendChild(stopLabel);
body.appendChild(unreadLabel); body.appendChild(unreadLabel);
body.appendChild(ageBox);
article.appendChild(header); article.appendChild(header);
article.appendChild(body); article.appendChild(body);
@ -399,7 +417,12 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;
const unreadOnly = ruleEl.querySelector('.unread-only')?.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 }); data.push({ criterion: '', actions: [], unreadOnly: false, stopProcessing: false });
renderRules(data); renderRules(data);
@ -414,6 +437,8 @@ document.addEventListener('DOMContentLoaded', async () => {
const rule = { criterion: r.criterion, actions }; const rule = { criterion: r.criterion, actions };
if (r.stopProcessing) rule.stopProcessing = true; if (r.stopProcessing) rule.stopProcessing = true;
if (r.unreadOnly) rule.unreadOnly = 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; return rule;
})); }));
@ -557,7 +582,12 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;
const unreadOnly = ruleEl.querySelector('.unread-only')?.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); }).filter(r => r.criterion);
const stripUrlParams = stripUrlToggle.checked; const stripUrlParams = stripUrlToggle.checked;
const altTextImages = altTextToggle.checked; const altTextImages = altTextToggle.checked;