Merge pull request #93 from wagesj45/codex/add-forward-and-reply-actions

Add forward and reply actions
This commit is contained in:
Jordan Wages 2025-07-15 22:45:04 -05:00 committed by GitHub
commit 0bd397560d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 48 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, copy, 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.
- **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.
@ -70,9 +70,11 @@ Sortana is implemented entirely with standard WebExtension scripts—no custom e
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, deleting or archiving a message when it matches. Drag rules to actions such as tagging, moving, copying, forwarding, replying,
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, and
check *Stop after match* to halt further processing. check *Stop after match* to halt further processing. Forward and reply actions
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
configured rules. configured rules.
@ -116,6 +118,7 @@ Sortana requests the following Thunderbird permissions:
- `accountsRead` list accounts and folders for move or copy 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.
- `compose` create reply and forward compose windows for matching rules.
## Thunderbird Add-on Store Disclosures ## Thunderbird Add-on Store Disclosures

View file

@ -23,9 +23,14 @@
,"action.flag": { "message": "flag" } ,"action.flag": { "message": "flag" }
,"action.copy": { "message": "copy" } ,"action.copy": { "message": "copy" }
,"action.delete": { "message": "delete" } ,"action.delete": { "message": "delete" }
,"action.archive": { "message": "archive" } ,"action.archive": { "message": "archive" }
,"action.forward": { "message": "forward" }
,"action.reply": { "message": "reply" }
,"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" }
,"param.unflag": { "message": "unflag" } ,"param.unflag": { "message": "unflag" }
,"param.address": { "message": "address" }
,"param.replyAll": { "message": "reply all" }
,"param.replySender": { "message": "reply sender" }
} }

View file

@ -209,15 +209,20 @@ async function processMessage(id) {
try { try {
const full = await messenger.messages.getFull(id); const full = await messenger.messages.getFull(id);
const text = buildEmailText(full); const text = buildEmailText(full);
let hdr;
let currentTags = []; let currentTags = [];
let alreadyRead = false; let alreadyRead = false;
let identityId = null;
try { try {
const hdr = await messenger.messages.get(id); hdr = await messenger.messages.get(id);
currentTags = Array.isArray(hdr.tags) ? [...hdr.tags] : []; currentTags = Array.isArray(hdr.tags) ? [...hdr.tags] : [];
alreadyRead = hdr.read === true; alreadyRead = hdr.read === true;
const ids = await messenger.identities.list(hdr.folder.accountId);
identityId = ids[0]?.id || null;
} catch (e) { } catch (e) {
currentTags = []; currentTags = [];
alreadyRead = false; alreadyRead = false;
identityId = null;
} }
for (const rule of aiRules) { for (const rule of aiRules) {
@ -247,6 +252,10 @@ async function processMessage(id) {
await messenger.messages.delete([id]); await messenger.messages.delete([id]);
} else if (act.type === 'archive') { } else if (act.type === 'archive') {
await messenger.messages.archive([id]); await messenger.messages.archive([id]);
} else if (act.type === 'forward' && act.address && identityId) {
await browser.compose.beginForward(id, { to: [act.address], identityId });
} else if (act.type === 'reply' && act.replyType && identityId) {
await browser.compose.beginReply(id, { replyType: act.replyType, identityId });
} }
} }
if (rule.stopProcessing) { if (rule.stopProcessing) {

View file

@ -42,6 +42,7 @@
"menus", "menus",
"scripting", "scripting",
"tabs", "tabs",
"theme" "theme",
"compose"
] ]
} }

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','copy','junk','read','flag','delete','archive'].forEach(t => { ['tag','move','copy','junk','read','flag','delete','archive','forward','reply'].forEach(t => {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = t; opt.value = t;
opt.textContent = t; opt.textContent = t;
@ -242,6 +242,23 @@ document.addEventListener('DOMContentLoaded', async () => {
sel.value = String(action.flagged ?? true); sel.value = String(action.flagged ?? true);
wrap.appendChild(sel); wrap.appendChild(sel);
paramSpan.appendChild(wrap); paramSpan.appendChild(wrap);
} else if (typeSelect.value === 'forward') {
const input = document.createElement('input');
input.type = 'text';
input.className = 'input is-small forward-input';
input.placeholder = 'address@example.com';
input.value = action.address || '';
paramSpan.appendChild(input);
} else if (typeSelect.value === 'reply') {
const wrap = document.createElement('div');
wrap.className = 'select is-small';
const sel = document.createElement('select');
sel.className = 'reply-select';
sel.appendChild(new Option('all','all'));
sel.appendChild(new Option('sender','sender'));
sel.value = action.replyType || 'all';
wrap.appendChild(sel);
paramSpan.appendChild(wrap);
} else if (typeSelect.value === 'delete' || typeSelect.value === 'archive') { } else if (typeSelect.value === 'delete' || typeSelect.value === 'archive') {
paramSpan.appendChild(document.createElement('span')); paramSpan.appendChild(document.createElement('span'));
} }
@ -530,6 +547,12 @@ document.addEventListener('DOMContentLoaded', async () => {
if (type === 'flag') { if (type === 'flag') {
return { type, flagged: row.querySelector('.flag-select').value === 'true' }; return { type, flagged: row.querySelector('.flag-select').value === 'true' };
} }
if (type === 'forward') {
return { type, address: row.querySelector('.forward-input').value.trim() };
}
if (type === 'reply') {
return { type, replyType: row.querySelector('.reply-select').value };
}
return { type }; return { type };
}); });
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked; const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;