Add age filters to rules
This commit is contained in:
parent
0bd397560d
commit
6fd6da8a12
3 changed files with 49 additions and 4 deletions
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue