Merge pull request #91 from wagesj45/codex/add-copy-option-to-action-selector

Enable copy rule action
This commit is contained in:
Jordan Wages 2025-07-15 22:34:04 -05:00 committed by GitHub
commit d992ad9c55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 18 additions and 7 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. Rules can optionally apply only to unread messages. - **Automatic rules** create rules that tag, move, copy, 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.
@ -102,7 +102,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, read/unread changes or flag updates based on the model's classification. triggering tags, moves, copies, read/unread changes or flag updates based on the model's classification.
## Required Permissions ## Required Permissions
@ -110,10 +110,10 @@ 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 or copy messages when a rule specifies a target folder.
- `messagesUpdate` change message properties such as tags, junk status, read/unread state and flags. - `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 or copy actions.
- `menus` add context menu commands. - `menus` add context menu commands.
- `tabs` open new tabs and query the active tab. - `tabs` open new tabs and query the active tab.

View file

@ -21,6 +21,7 @@
"options.collapseWhitespace": { "message": "Collapse long whitespace" } "options.collapseWhitespace": { "message": "Collapse long whitespace" }
,"action.read": { "message": "read" } ,"action.read": { "message": "read" }
,"action.flag": { "message": "flag" } ,"action.flag": { "message": "flag" }
,"action.copy": { "message": "copy" }
,"param.markRead": { "message": "mark read" } ,"param.markRead": { "message": "mark read" }
,"param.markUnread": { "message": "mark unread" } ,"param.markUnread": { "message": "mark unread" }
,"param.flag": { "message": "flag" } ,"param.flag": { "message": "flag" }

View file

@ -37,6 +37,7 @@ function normalizeRules(rules) {
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 });
if (r.copyTarget || r.copyTo) actions.push({ type: 'copy', copyTarget: r.copyTarget || r.copyTo });
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;
@ -234,6 +235,8 @@ async function processMessage(id) {
} }
} else if (act.type === 'move' && act.folder) { } else if (act.type === 'move' && act.folder) {
await messenger.messages.move([id], act.folder); await messenger.messages.move([id], act.folder);
} else if (act.type === 'copy' && act.copyTarget) {
await messenger.messages.copy([id], act.copyTarget);
} 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') { } else if (act.type === 'read') {

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','read','flag'].forEach(t => { ['tag','move','copy','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;
@ -198,7 +198,7 @@ document.addEventListener('DOMContentLoaded', async () => {
sel.value = action.tagKey || ''; sel.value = action.tagKey || '';
wrap.appendChild(sel); wrap.appendChild(sel);
paramSpan.appendChild(wrap); paramSpan.appendChild(wrap);
} else if (typeSelect.value === 'move') { } else if (typeSelect.value === 'move' || typeSelect.value === 'copy') {
const wrap = document.createElement('div'); const wrap = document.createElement('div');
wrap.className = 'select is-small'; wrap.className = 'select is-small';
const sel = document.createElement('select'); const sel = document.createElement('select');
@ -209,7 +209,7 @@ document.addEventListener('DOMContentLoaded', async () => {
opt.textContent = f.name; opt.textContent = f.name;
sel.appendChild(opt); sel.appendChild(opt);
} }
sel.value = action.folder || ''; sel.value = action.folder || action.copyTarget || '';
wrap.appendChild(sel); wrap.appendChild(sel);
paramSpan.appendChild(wrap); paramSpan.appendChild(wrap);
} else if (typeSelect.value === 'junk') { } else if (typeSelect.value === 'junk') {
@ -361,6 +361,9 @@ document.addEventListener('DOMContentLoaded', async () => {
if (type === 'move') { if (type === 'move') {
return { type, folder: row.querySelector('.folder-select').value }; return { type, folder: row.querySelector('.folder-select').value };
} }
if (type === 'copy') {
return { type, copyTarget: row.querySelector('.folder-select').value };
}
if (type === 'junk') { if (type === 'junk') {
return { type, junk: row.querySelector('.junk-select').value === 'true' }; return { type, junk: row.querySelector('.junk-select').value === 'true' };
} }
@ -385,6 +388,7 @@ document.addEventListener('DOMContentLoaded', async () => {
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 });
if (r.copyTarget || r.copyTo) actions.push({ type: 'copy', copyTarget: r.copyTarget || r.copyTo });
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;
@ -509,6 +513,9 @@ document.addEventListener('DOMContentLoaded', async () => {
if (type === 'move') { if (type === 'move') {
return { type, folder: row.querySelector('.folder-select').value }; return { type, folder: row.querySelector('.folder-select').value };
} }
if (type === 'copy') {
return { type, copyTarget: row.querySelector('.folder-select').value };
}
if (type === 'junk') { if (type === 'junk') {
return { type, junk: row.querySelector('.junk-select').value === 'true' }; return { type, junk: row.querySelector('.junk-select').value === 'true' };
} }