Add account and folder filters for rules
This commit is contained in:
parent
1c3ced5134
commit
cd0a31ed98
3 changed files with 87 additions and 5 deletions
|
@ -19,6 +19,7 @@ message meets a specified criterion.
|
||||||
- **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 and can ignore messages outside a chosen age range.
|
- **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.
|
||||||
|
- **Account & folder filters** – limit rules to specific accounts or folders.
|
||||||
- **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.
|
||||||
- **View reasoning** – inspect why rules matched via the Details popup.
|
- **View reasoning** – inspect why rules matched via the Details popup.
|
||||||
|
@ -69,11 +70,12 @@ Sortana is implemented entirely with standard WebExtension scripts—no custom e
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
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, 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,
|
reorder them, check *Only apply to unread messages* to skip read mail,
|
||||||
set optional minimum or maximum message age limits, and
|
set optional minimum or maximum message age limits, select the accounts or
|
||||||
|
folders a rule should apply to, 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
|
||||||
|
|
|
@ -33,7 +33,11 @@ let detectSystemTheme;
|
||||||
|
|
||||||
function normalizeRules(rules) {
|
function normalizeRules(rules) {
|
||||||
return Array.isArray(rules) ? rules.map(r => {
|
return Array.isArray(rules) ? rules.map(r => {
|
||||||
if (r.actions) return r;
|
if (r.actions) {
|
||||||
|
if (!Array.isArray(r.accounts)) r.accounts = [];
|
||||||
|
if (!Array.isArray(r.folders)) r.folders = [];
|
||||||
|
return r;
|
||||||
|
}
|
||||||
const actions = [];
|
const actions = [];
|
||||||
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
|
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
|
||||||
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
|
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
|
||||||
|
@ -43,6 +47,8 @@ function normalizeRules(rules) {
|
||||||
if (r.unreadOnly) rule.unreadOnly = true;
|
if (r.unreadOnly) rule.unreadOnly = true;
|
||||||
if (typeof r.minAgeDays === 'number') rule.minAgeDays = r.minAgeDays;
|
if (typeof r.minAgeDays === 'number') rule.minAgeDays = r.minAgeDays;
|
||||||
if (typeof r.maxAgeDays === 'number') rule.maxAgeDays = r.maxAgeDays;
|
if (typeof r.maxAgeDays === 'number') rule.maxAgeDays = r.maxAgeDays;
|
||||||
|
if (Array.isArray(r.accounts)) rule.accounts = r.accounts;
|
||||||
|
if (Array.isArray(r.folders)) rule.folders = r.folders;
|
||||||
return rule;
|
return rule;
|
||||||
}) : [];
|
}) : [];
|
||||||
}
|
}
|
||||||
|
@ -228,6 +234,14 @@ async function processMessage(id) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const rule of aiRules) {
|
for (const rule of aiRules) {
|
||||||
|
if (hdr && Array.isArray(rule.accounts) && rule.accounts.length &&
|
||||||
|
!rule.accounts.includes(hdr.folder.accountId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (hdr && Array.isArray(rule.folders) && rule.folders.length &&
|
||||||
|
!rule.folders.includes(hdr.folder.path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (rule.unreadOnly && alreadyRead) {
|
if (rule.unreadOnly && alreadyRead) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,6 +123,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
let tagList = [];
|
let tagList = [];
|
||||||
let folderList = [];
|
let folderList = [];
|
||||||
|
let accountList = [];
|
||||||
try {
|
try {
|
||||||
tagList = await messenger.messages.tags.list();
|
tagList = await messenger.messages.tags.list();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -130,6 +131,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const accounts = await messenger.accounts.list(true);
|
const accounts = await messenger.accounts.list(true);
|
||||||
|
accountList = accounts.map(a => ({ id: a.id, name: a.name }));
|
||||||
const collect = (f, prefix='') => {
|
const collect = (f, prefix='') => {
|
||||||
folderList.push({ id: f.id ?? f.path, name: prefix + f.name });
|
folderList.push({ id: f.id ?? f.path, name: prefix + f.name });
|
||||||
(f.subFolders || []).forEach(sf => collect(sf, prefix + f.name + '/'));
|
(f.subFolders || []).forEach(sf => collect(sf, prefix + f.name + '/'));
|
||||||
|
@ -372,6 +374,54 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
ageBox.appendChild(minInput);
|
ageBox.appendChild(minInput);
|
||||||
ageBox.appendChild(maxInput);
|
ageBox.appendChild(maxInput);
|
||||||
|
|
||||||
|
const acctBox = document.createElement('div');
|
||||||
|
acctBox.className = 'field mt-2';
|
||||||
|
const acctLabel = document.createElement('label');
|
||||||
|
acctLabel.className = 'label';
|
||||||
|
acctLabel.textContent = 'Accounts';
|
||||||
|
const acctControl = document.createElement('div');
|
||||||
|
const acctWrap = document.createElement('div');
|
||||||
|
acctWrap.className = 'select is-multiple is-small';
|
||||||
|
const acctSel = document.createElement('select');
|
||||||
|
acctSel.className = 'account-select';
|
||||||
|
acctSel.multiple = true;
|
||||||
|
acctSel.size = Math.min(accountList.length, 4) || 1;
|
||||||
|
for (const a of accountList) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = a.id;
|
||||||
|
opt.textContent = a.name;
|
||||||
|
if ((rule.accounts || []).includes(a.id)) opt.selected = true;
|
||||||
|
acctSel.appendChild(opt);
|
||||||
|
}
|
||||||
|
acctWrap.appendChild(acctSel);
|
||||||
|
acctControl.appendChild(acctWrap);
|
||||||
|
acctBox.appendChild(acctLabel);
|
||||||
|
acctBox.appendChild(acctControl);
|
||||||
|
|
||||||
|
const folderBox = document.createElement('div');
|
||||||
|
folderBox.className = 'field mt-2';
|
||||||
|
const folderLabel = document.createElement('label');
|
||||||
|
folderLabel.className = 'label';
|
||||||
|
folderLabel.textContent = 'Folders';
|
||||||
|
const folderControl = document.createElement('div');
|
||||||
|
const folderWrap = document.createElement('div');
|
||||||
|
folderWrap.className = 'select is-multiple is-small';
|
||||||
|
const folderSel = document.createElement('select');
|
||||||
|
folderSel.className = 'folder-filter-select';
|
||||||
|
folderSel.multiple = true;
|
||||||
|
folderSel.size = Math.min(folderList.length, 6) || 1;
|
||||||
|
for (const f of folderList) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = f.id;
|
||||||
|
opt.textContent = f.name;
|
||||||
|
if ((rule.folders || []).includes(f.id)) opt.selected = true;
|
||||||
|
folderSel.appendChild(opt);
|
||||||
|
}
|
||||||
|
folderWrap.appendChild(folderSel);
|
||||||
|
folderControl.appendChild(folderWrap);
|
||||||
|
folderBox.appendChild(folderLabel);
|
||||||
|
folderBox.appendChild(folderControl);
|
||||||
|
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'message-body';
|
body.className = 'message-body';
|
||||||
body.appendChild(actionsContainer);
|
body.appendChild(actionsContainer);
|
||||||
|
@ -379,6 +429,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
body.appendChild(stopLabel);
|
body.appendChild(stopLabel);
|
||||||
body.appendChild(unreadLabel);
|
body.appendChild(unreadLabel);
|
||||||
body.appendChild(ageBox);
|
body.appendChild(ageBox);
|
||||||
|
body.appendChild(acctBox);
|
||||||
|
body.appendChild(folderBox);
|
||||||
|
|
||||||
article.appendChild(header);
|
article.appendChild(header);
|
||||||
article.appendChild(body);
|
article.appendChild(body);
|
||||||
|
@ -419,17 +471,25 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const unreadOnly = ruleEl.querySelector('.unread-only')?.checked;
|
const unreadOnly = ruleEl.querySelector('.unread-only')?.checked;
|
||||||
const minAgeDays = parseFloat(ruleEl.querySelector('.min-age')?.value);
|
const minAgeDays = parseFloat(ruleEl.querySelector('.min-age')?.value);
|
||||||
const maxAgeDays = parseFloat(ruleEl.querySelector('.max-age')?.value);
|
const maxAgeDays = parseFloat(ruleEl.querySelector('.max-age')?.value);
|
||||||
|
const accounts = [...(ruleEl.querySelector('.account-select')?.selectedOptions || [])].map(o => o.value);
|
||||||
|
const folders = [...(ruleEl.querySelector('.folder-filter-select')?.selectedOptions || [])].map(o => o.value);
|
||||||
const rule = { criterion, actions, unreadOnly, stopProcessing };
|
const rule = { criterion, actions, unreadOnly, stopProcessing };
|
||||||
if (!isNaN(minAgeDays)) rule.minAgeDays = minAgeDays;
|
if (!isNaN(minAgeDays)) rule.minAgeDays = minAgeDays;
|
||||||
if (!isNaN(maxAgeDays)) rule.maxAgeDays = maxAgeDays;
|
if (!isNaN(maxAgeDays)) rule.maxAgeDays = maxAgeDays;
|
||||||
|
if (accounts.length) rule.accounts = accounts;
|
||||||
|
if (folders.length) rule.folders = folders;
|
||||||
return rule;
|
return rule;
|
||||||
});
|
});
|
||||||
data.push({ criterion: '', actions: [], unreadOnly: false, stopProcessing: false });
|
data.push({ criterion: '', actions: [], unreadOnly: false, stopProcessing: false, accounts: [], folders: [] });
|
||||||
renderRules(data);
|
renderRules(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
renderRules((defaults.aiRules || []).map(r => {
|
renderRules((defaults.aiRules || []).map(r => {
|
||||||
if (r.actions) return r;
|
if (r.actions) {
|
||||||
|
if (!Array.isArray(r.accounts)) r.accounts = [];
|
||||||
|
if (!Array.isArray(r.folders)) r.folders = [];
|
||||||
|
return r;
|
||||||
|
}
|
||||||
const actions = [];
|
const actions = [];
|
||||||
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
|
if (r.tag) actions.push({ type: 'tag', tagKey: r.tag });
|
||||||
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
|
if (r.moveTo) actions.push({ type: 'move', folder: r.moveTo });
|
||||||
|
@ -439,6 +499,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (r.unreadOnly) rule.unreadOnly = true;
|
if (r.unreadOnly) rule.unreadOnly = true;
|
||||||
if (typeof r.minAgeDays === 'number') rule.minAgeDays = r.minAgeDays;
|
if (typeof r.minAgeDays === 'number') rule.minAgeDays = r.minAgeDays;
|
||||||
if (typeof r.maxAgeDays === 'number') rule.maxAgeDays = r.maxAgeDays;
|
if (typeof r.maxAgeDays === 'number') rule.maxAgeDays = r.maxAgeDays;
|
||||||
|
if (Array.isArray(r.accounts)) rule.accounts = r.accounts;
|
||||||
|
if (Array.isArray(r.folders)) rule.folders = r.folders;
|
||||||
return rule;
|
return rule;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -584,9 +646,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const unreadOnly = ruleEl.querySelector('.unread-only')?.checked;
|
const unreadOnly = ruleEl.querySelector('.unread-only')?.checked;
|
||||||
const minAgeDays = parseFloat(ruleEl.querySelector('.min-age')?.value);
|
const minAgeDays = parseFloat(ruleEl.querySelector('.min-age')?.value);
|
||||||
const maxAgeDays = parseFloat(ruleEl.querySelector('.max-age')?.value);
|
const maxAgeDays = parseFloat(ruleEl.querySelector('.max-age')?.value);
|
||||||
|
const accounts = [...(ruleEl.querySelector('.account-select')?.selectedOptions || [])].map(o => o.value);
|
||||||
|
const folders = [...(ruleEl.querySelector('.folder-filter-select')?.selectedOptions || [])].map(o => o.value);
|
||||||
const rule = { criterion, actions, unreadOnly, stopProcessing };
|
const rule = { criterion, actions, unreadOnly, stopProcessing };
|
||||||
if (!isNaN(minAgeDays)) rule.minAgeDays = minAgeDays;
|
if (!isNaN(minAgeDays)) rule.minAgeDays = minAgeDays;
|
||||||
if (!isNaN(maxAgeDays)) rule.maxAgeDays = maxAgeDays;
|
if (!isNaN(maxAgeDays)) rule.maxAgeDays = maxAgeDays;
|
||||||
|
if (accounts.length) rule.accounts = accounts;
|
||||||
|
if (folders.length) rule.folders = folders;
|
||||||
return rule;
|
return rule;
|
||||||
}).filter(r => r.criterion);
|
}).filter(r => r.criterion);
|
||||||
const stripUrlParams = stripUrlToggle.checked;
|
const stripUrlParams = stripUrlToggle.checked;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue