Merge pull request #89 from wagesj45/codex/extend-action-selector-with-read-and-flag

Add read and flag rule actions
This commit is contained in:
Jordan Wages 2025-07-15 21:57:22 -05:00 committed by GitHub
commit 3eef24d2dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 46 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 or move new messages based on AI classification. - **Automatic rules** create rules that tag, move, mark read/unread or flag/unflag messages based on AI classification.
- **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.
@ -101,7 +101,7 @@ Here are some useful and fun example criteria you can use in your filters. Filte
For when you're ready to filter based on vibes. For when you're ready to filter based on vibes.
You can define as many filters as you'd like, each using a different prompt and You can define as many filters as you'd like, each using a different prompt and
triggering tags, moves, or actions based on the model's classification. triggering tags, moves, read/unread changes or flag updates based on the model's classification.
## Required Permissions ## Required Permissions
@ -110,7 +110,7 @@ Sortana requests the following Thunderbird permissions:
- `storage` store configuration and cached classification results. - `storage` store configuration and cached classification results.
- `messagesRead` read message contents for classification. - `messagesRead` read message contents for classification.
- `messagesMove` move messages when a rule specifies a target folder. - `messagesMove` move messages when a rule specifies a target folder.
- `messagesUpdate` change message properties such as tags and junk status. - `messagesUpdate` change message properties such as tags, junk status, read/unread state and flags.
- `messagesTagsList` retrieve existing message tags for rule actions. - `messagesTagsList` retrieve existing message tags for rule actions.
- `accountsRead` list accounts and folders for move actions. - `accountsRead` list accounts and folders for move actions.
- `menus` add context menu commands. - `menus` add context menu commands.

View file

@ -19,4 +19,10 @@
"options.stripUrlParams": { "message": "Remove URL tracking parameters" }, "options.stripUrlParams": { "message": "Remove URL tracking parameters" },
"options.altTextImages": { "message": "Replace images with alt text" }, "options.altTextImages": { "message": "Replace images with alt text" },
"options.collapseWhitespace": { "message": "Collapse long whitespace" } "options.collapseWhitespace": { "message": "Collapse long whitespace" }
,"action.read": { "message": "read" }
,"action.flag": { "message": "flag" }
,"param.markRead": { "message": "mark read" }
,"param.markUnread": { "message": "mark unread" }
,"param.flag": { "message": "flag" }
,"param.unflag": { "message": "unflag" }
} }

View file

@ -229,6 +229,10 @@ async function processMessage(id) {
await messenger.messages.move([id], act.folder); await messenger.messages.move([id], act.folder);
} else if (act.type === 'junk') { } else if (act.type === 'junk') {
await messenger.messages.update(id, { junk: !!act.junk }); await messenger.messages.update(id, { junk: !!act.junk });
} else if (act.type === 'read') {
await messenger.messages.update(id, { read: !!act.read });
} else if (act.type === 'flag') {
await messenger.messages.update(id, { flagged: !!act.flagged });
} }
} }
if (rule.stopProcessing) { if (rule.stopProcessing) {

View file

@ -171,7 +171,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const typeWrapper = document.createElement('div'); const typeWrapper = document.createElement('div');
typeWrapper.className = 'select is-small mr-2'; typeWrapper.className = 'select is-small mr-2';
const typeSelect = document.createElement('select'); const typeSelect = document.createElement('select');
['tag','move','junk'].forEach(t => { ['tag','move','junk','read','flag'].forEach(t => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = t; opt.value = t;
opt.textContent = t; opt.textContent = t;
@ -222,6 +222,26 @@ document.addEventListener('DOMContentLoaded', async () => {
sel.value = String(action.junk ?? true); sel.value = String(action.junk ?? true);
wrap.appendChild(sel); wrap.appendChild(sel);
paramSpan.appendChild(wrap); paramSpan.appendChild(wrap);
} else if (typeSelect.value === 'read') {
const wrap = document.createElement('div');
wrap.className = 'select is-small';
const sel = document.createElement('select');
sel.className = 'read-select';
sel.appendChild(new Option('mark read','true'));
sel.appendChild(new Option('mark unread','false'));
sel.value = String(action.read ?? true);
wrap.appendChild(sel);
paramSpan.appendChild(wrap);
} else if (typeSelect.value === 'flag') {
const wrap = document.createElement('div');
wrap.className = 'select is-small';
const sel = document.createElement('select');
sel.className = 'flag-select';
sel.appendChild(new Option('flag','true'));
sel.appendChild(new Option('unflag','false'));
sel.value = String(action.flagged ?? true);
wrap.appendChild(sel);
paramSpan.appendChild(wrap);
} }
} }
@ -334,6 +354,12 @@ document.addEventListener('DOMContentLoaded', async () => {
if (type === 'junk') { if (type === 'junk') {
return { type, junk: row.querySelector('.junk-select').value === 'true' }; return { type, junk: row.querySelector('.junk-select').value === 'true' };
} }
if (type === 'read') {
return { type, read: row.querySelector('.read-select').value === 'true' };
}
if (type === 'flag') {
return { type, flagged: row.querySelector('.flag-select').value === 'true' };
}
return { type }; return { type };
}); });
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;
@ -474,6 +500,12 @@ document.addEventListener('DOMContentLoaded', async () => {
if (type === 'junk') { if (type === 'junk') {
return { type, junk: row.querySelector('.junk-select').value === 'true' }; return { type, junk: row.querySelector('.junk-select').value === 'true' };
} }
if (type === 'read') {
return { type, read: row.querySelector('.read-select').value === 'true' };
}
if (type === 'flag') {
return { type, flagged: row.querySelector('.flag-select').value === 'true' };
}
return { type }; return { type };
}); });
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;