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.
- **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, 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.
- **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.
@ -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.
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
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
configured rules.
@ -116,6 +118,7 @@ Sortana requests the following Thunderbird permissions:
- `accountsRead` list accounts and folders for move or copy actions.
- `menus` add context menu commands.
- `tabs` open new tabs and query the active tab.
- `compose` create reply and forward compose windows for matching rules.
## Thunderbird Add-on Store Disclosures

View file

@ -24,8 +24,13 @@
,"action.copy": { "message": "copy" }
,"action.delete": { "message": "delete" }
,"action.archive": { "message": "archive" }
,"action.forward": { "message": "forward" }
,"action.reply": { "message": "reply" }
,"param.markRead": { "message": "mark read" }
,"param.markUnread": { "message": "mark unread" }
,"param.flag": { "message": "flag" }
,"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 {
const full = await messenger.messages.getFull(id);
const text = buildEmailText(full);
let hdr;
let currentTags = [];
let alreadyRead = false;
let identityId = null;
try {
const hdr = await messenger.messages.get(id);
hdr = await messenger.messages.get(id);
currentTags = Array.isArray(hdr.tags) ? [...hdr.tags] : [];
alreadyRead = hdr.read === true;
const ids = await messenger.identities.list(hdr.folder.accountId);
identityId = ids[0]?.id || null;
} catch (e) {
currentTags = [];
alreadyRead = false;
identityId = null;
}
for (const rule of aiRules) {
@ -247,6 +252,10 @@ async function processMessage(id) {
await messenger.messages.delete([id]);
} else if (act.type === 'archive') {
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) {

View file

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

View file

@ -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','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');
opt.value = t;
opt.textContent = t;
@ -242,6 +242,23 @@ document.addEventListener('DOMContentLoaded', async () => {
sel.value = String(action.flagged ?? true);
wrap.appendChild(sel);
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') {
paramSpan.appendChild(document.createElement('span'));
}
@ -530,6 +547,12 @@ document.addEventListener('DOMContentLoaded', async () => {
if (type === 'flag') {
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 };
});
const stopProcessing = ruleEl.querySelector('.stop-processing')?.checked;