Add unread-only option for rules

This commit is contained in:
Jordan Wages 2025-07-15 22:12:35 -05:00
commit 85224f2d4d
3 changed files with 34 additions and 13 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, 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. - **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.
@ -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. 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 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 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 3. Save your settings. New mail will be evaluated automatically using the
configured rules. configured rules.

View file

@ -39,6 +39,7 @@ function normalizeRules(rules) {
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
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;
return rule; return rule;
}) : []; }) : [];
} }
@ -208,14 +209,20 @@ async function processMessage(id) {
const full = await messenger.messages.getFull(id); const full = await messenger.messages.getFull(id);
const text = buildEmailText(full); const text = buildEmailText(full);
let currentTags = []; let currentTags = [];
let alreadyRead = false;
try { try {
const hdr = await messenger.messages.get(id); const hdr = await messenger.messages.get(id);
currentTags = Array.isArray(hdr.tags) ? [...hdr.tags] : []; currentTags = Array.isArray(hdr.tags) ? [...hdr.tags] : [];
alreadyRead = hdr.read === true;
} catch (e) { } catch (e) {
currentTags = []; currentTags = [];
alreadyRead = false;
} }
for (const rule of aiRules) { for (const rule of aiRules) {
if (rule.unreadOnly && alreadyRead) {
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

@ -318,20 +318,30 @@ document.addEventListener('DOMContentLoaded', async () => {
addAction.className = 'button is-small mb-2'; addAction.className = 'button is-small mb-2';
addAction.addEventListener('click', () => actionsContainer.appendChild(createActionRow())); addAction.addEventListener('click', () => actionsContainer.appendChild(createActionRow()));
const stopLabel = document.createElement('label'); const stopLabel = document.createElement('label');
stopLabel.className = 'checkbox mt-2'; stopLabel.className = 'checkbox mt-2';
const stopCheck = document.createElement('input'); const stopCheck = document.createElement('input');
stopCheck.type = 'checkbox'; stopCheck.type = 'checkbox';
stopCheck.className = 'stop-processing'; stopCheck.className = 'stop-processing';
stopCheck.checked = rule.stopProcessing === true; stopCheck.checked = rule.stopProcessing === true;
stopLabel.appendChild(stopCheck); stopLabel.appendChild(stopCheck);
stopLabel.append(' Stop after match'); 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'); 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);
article.appendChild(header); article.appendChild(header);
article.appendChild(body); article.appendChild(body);
@ -363,9 +373,10 @@ document.addEventListener('DOMContentLoaded', async () => {
return { type }; return { type };
}); });
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; 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); renderRules(data);
}); });
@ -376,6 +387,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo }); if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
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;
return rule; return rule;
})); }));
@ -509,7 +521,8 @@ document.addEventListener('DOMContentLoaded', async () => {
return { type }; return { type };
}); });
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; 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); }).filter(r => r.criterion);
const stripUrlParams = stripUrlToggle.checked; const stripUrlParams = stripUrlToggle.checked;
const altTextImages = altTextToggle.checked; const altTextImages = altTextToggle.checked;