diff --git a/README.md b/README.md index d8e1bb2..4bcef29 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ message meets a specified criterion. - **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. - **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. - **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. @@ -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. 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 @@ -110,10 +110,10 @@ Sortana requests the following Thunderbird permissions: - `storage` – store configuration and cached classification results. - `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. - `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. - `tabs` – open new tabs and query the active tab. diff --git a/_locales/en-US/messages.json b/_locales/en-US/messages.json index 01d6c92..6fc5b61 100644 --- a/_locales/en-US/messages.json +++ b/_locales/en-US/messages.json @@ -21,6 +21,7 @@ "options.collapseWhitespace": { "message": "Collapse long whitespace" } ,"action.read": { "message": "read" } ,"action.flag": { "message": "flag" } + ,"action.copy": { "message": "copy" } ,"param.markRead": { "message": "mark read" } ,"param.markUnread": { "message": "mark unread" } ,"param.flag": { "message": "flag" } diff --git a/background.js b/background.js index ad2bbcb..8a26971 100644 --- a/background.js +++ b/background.js @@ -37,6 +37,7 @@ function normalizeRules(rules) { const actions = []; if (r.tag) actions.push({ type: 'tag', tagKey: r.tag }); 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 }; if (r.stopProcessing) rule.stopProcessing = true; if (r.unreadOnly) rule.unreadOnly = true; @@ -234,6 +235,8 @@ async function processMessage(id) { } } else if (act.type === 'move' && 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') { await messenger.messages.update(id, { junk: !!act.junk }); } else if (act.type === 'read') { diff --git a/options/options.js b/options/options.js index ab63644..7ec57ee 100644 --- a/options/options.js +++ b/options/options.js @@ -171,7 +171,7 @@ document.addEventListener('DOMContentLoaded', async () => { const typeWrapper = document.createElement('div'); typeWrapper.className = 'select is-small mr-2'; 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'); opt.value = t; opt.textContent = t; @@ -198,7 +198,7 @@ document.addEventListener('DOMContentLoaded', async () => { sel.value = action.tagKey || ''; wrap.appendChild(sel); paramSpan.appendChild(wrap); - } else if (typeSelect.value === 'move') { + } else if (typeSelect.value === 'move' || typeSelect.value === 'copy') { const wrap = document.createElement('div'); wrap.className = 'select is-small'; const sel = document.createElement('select'); @@ -209,7 +209,7 @@ document.addEventListener('DOMContentLoaded', async () => { opt.textContent = f.name; sel.appendChild(opt); } - sel.value = action.folder || ''; + sel.value = action.folder || action.copyTarget || ''; wrap.appendChild(sel); paramSpan.appendChild(wrap); } else if (typeSelect.value === 'junk') { @@ -361,6 +361,9 @@ document.addEventListener('DOMContentLoaded', async () => { if (type === 'move') { return { type, folder: row.querySelector('.folder-select').value }; } + if (type === 'copy') { + return { type, copyTarget: row.querySelector('.folder-select').value }; + } if (type === 'junk') { return { type, junk: row.querySelector('.junk-select').value === 'true' }; } @@ -385,6 +388,7 @@ document.addEventListener('DOMContentLoaded', async () => { const actions = []; if (r.tag) actions.push({ type: 'tag', tagKey: r.tag }); 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 }; if (r.stopProcessing) rule.stopProcessing = true; if (r.unreadOnly) rule.unreadOnly = true; @@ -509,6 +513,9 @@ document.addEventListener('DOMContentLoaded', async () => { if (type === 'move') { return { type, folder: row.querySelector('.folder-select').value }; } + if (type === 'copy') { + return { type, copyTarget: row.querySelector('.folder-select').value }; + } if (type === 'junk') { return { type, junk: row.querySelector('.junk-select').value === 'true' }; }