diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ea758ff0..c325d267 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1099,12 +1099,56 @@ "message": "If the value returned by the AI is above this threshold, the email will be moved to the spam folder.", "description": "" }, - "spam_threshold_too_low": { + "spamfilter_threshold_too_low": { "message": "The spam threshold is too low! You will likely mark too much mails as spam!", "description": "" }, - "spam_threshold_zero": { + "spamfilter_threshold_zero": { "message": "The spam threshold is zero! You will mark all mails as spam!", "description": "" + }, + "spamfilter_no_reports": { + "message": "No messages have screened for spam yet. Here you'll find a list of the last 100 spam reports for the current session only.", + "description": "" + }, + "SpamReport_Title": { + "message": "Spam Filter Reports", + "description": "" + }, + "Date": { + "message": "Date", + "description": "" + }, + "From": { + "message": "From", + "description": "" + }, + "Subject": { + "message": "Subject", + "description": "" + }, + "Spam_Value": { + "message": "Spam Value", + "description": "" + }, + "Moved_to_Spam": { + "message": "Moved to Spam", + "description": "" + }, + "Explanation": { + "message": "Explanation", + "description": "" + }, + "Report_Date": { + "message": "Report Date", + "description": "" + }, + "spamfilter_moved": { + "message": "Yes", + "description": "" + }, + "spamfilter_not_moved": { + "message": "No", + "description": "" } } \ No newline at end of file diff --git a/js/mzta-spamreport.js b/js/mzta-spamreport.js new file mode 100644 index 00000000..308dad7b --- /dev/null +++ b/js/mzta-spamreport.js @@ -0,0 +1,91 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 - 2025 Mic (m@micz.it) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export const taSpamReport = { + + logger: console, + _internal_data_id: 'mzta-spam-report-data', + _max_reports: 100, + + async saveReportData(data, data_id){ + let obj = await browser.storage.session.get(taSpamReport._internal_data_id); + //console.log(">>>>>>>> saveReportData obj 1: " + JSON.stringify(obj)); + if(obj == undefined){ + obj = {}; + obj[taSpamReport._internal_data_id] = {}; + //console.log(">>>>>>>> saveReportData obj 2a: " + JSON.stringify(obj)); + } + if(Object.keys(obj).length === 0){ + obj[taSpamReport._internal_data_id] = {}; + //console.log(">>>>>>>> saveReportData obj 2b: " + JSON.stringify(obj)); + } + + obj[taSpamReport._internal_data_id][data_id] = data; + //console.log(">>>>>>>> saveReportData obj 3: " + JSON.stringify(obj)); + await browser.storage.session.set(obj); + }, + + async loadReportData(data_id){ + let output = await browser.storage.session.get(taSpamReport._internal_data_id); + return output[taSpamReport._internal_data_id][data_id]; + }, + + async getAllReportData(){ + let data = await browser.storage.session.get(taSpamReport._internal_data_id); + //console.log(">>>>>>>>>>> getAllReportData: " + JSON.stringify(data)); + return data[taSpamReport._internal_data_id]; + }, + + saveAllData(data){ + browser.storage.session.set({[taSpamReport._internal_data_id]: data}); + }, + + clearReportData(){ + browser.storage.session.clear(); + }, + + async truncReportData(){ + let data = taSpamReport.sortReportsByDate(await taSpamReport.getAllReportData()); + if(data == undefined) return; + //console.log(">>>>>>>>>> truncReportData: " + JSON.stringify(data)); + if(Object.keys(data).length > taSpamReport._max_reports){ + let keys = Object.keys(data); + for(let i = taSpamReport._max_reports; i < keys.length; i++){ + // console.log(">>>>>>>>>> truncReportData: " + keys[i]); + // console.log(">>>>>>>>>> truncReportData i: " + i); + delete data[keys[i]]; + } + } + taSpamReport.saveAllData(data); + }, + + sortReportsByDate(data) { + if(data == undefined) return undefined; + const reportKeys = Object.keys(data); + reportKeys.sort((a, b) => { + const dateA = new Date(data[a].report_date); + const dateB = new Date(data[b].report_date); + return dateB - dateA; + }); + const sortedReports = {}; + reportKeys.forEach((key) => { + sortedReports[key] = data[key]; + }); + return sortedReports; + } +} \ No newline at end of file diff --git a/mzta-background.js b/mzta-background.js index 97fa7649..fe4e6878 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -24,6 +24,7 @@ import { getCurrentIdentity, getOriginalBody, replaceBody, setBody, i18nConditio import { taPromptUtils } from './js/mzta-utils-prompt.js'; import { mzta_specialCommand } from './js/mzta-special-commands.js'; import { getSpamFilterPrompt } from './js/mzta-prompts.js'; +import { taSpamReport } from './js/mzta-spamreport.js'; //browser.permissions.remove({ permissions: ["messagesMove"] }).then(console.log); @@ -660,34 +661,35 @@ const newEmailListener = (folder, messagesList) => { async function _newEmailListener(){ let messages = getMessages(messagesList); + taSpamReport.logger = taLog; + for await (let message of messages) { - let fullMessage = await browser.messages.getFull(message.id); + // let fullMessage = await browser.messages.getFull(message.id); + // taLog.log(`From: ${fullMessage.headers.from}`); + // taLog.log(`Subject: ${fullMessage.headers.subject}`); + // taLog.log(`Name: ${fullMessage.name}`); + // taLog.log(`Body: ${fullMessage.body}`); + // taLog.log(`contentType: [${fullMessage.contentType}]`); + // taLog.log(`fullMessage.parts.length: ${fullMessage.parts.length}`); - taLog.log(`From: ${fullMessage.headers.from}`); - taLog.log(`Subject: ${fullMessage.headers.subject}`); - taLog.log(`Name: ${fullMessage.name}`); - taLog.log(`Body: ${fullMessage.body}`); - taLog.log(`contentType: [${fullMessage.contentType}]`); - taLog.log(`fullMessage.parts.length: ${fullMessage.parts.length}`); - - if (fullMessage.parts.length > 0) { - for (let part of fullMessage.parts) { - taLog.log(`From: ${part.headers.from}`); - taLog.log(`Subject: ${part.headers.subject}`); - taLog.log(`Name: ${part.name}`); - taLog.log(`Body: ${part.body}`); - taLog.log(`contentType: [${part.contentType}]`); - taLog.log('contentType JSON: '+JSON.stringify(part.contentType)); - if (part.contentType.trim().toLowerCase() === "text/plain") { - taLog.log("Body (text/plain):" + part.body); - // return part.body; - } - if (part.contentType.trim().toLowerCase() === "text/html") { - taLog.log("Body (text/html):" + part.body); - // return part.body; - } - } - } + // if (fullMessage.parts.length > 0) { + // for (let part of fullMessage.parts) { + // taLog.log(`From: ${part.headers.from}`); + // taLog.log(`Subject: ${part.headers.subject}`); + // taLog.log(`Name: ${part.name}`); + // taLog.log(`Body: ${part.body}`); + // taLog.log(`contentType: [${part.contentType}]`); + // taLog.log('contentType JSON: '+JSON.stringify(part.contentType)); + // if (part.contentType.trim().toLowerCase() === "text/plain") { + // taLog.log("Body (text/plain):" + part.body); + // // return part.body; + // } + // if (part.contentType.trim().toLowerCase() === "text/html") { + // taLog.log("Body (text/html):" + part.body); + // // return part.body; + // } + // } + // } let curr_fullMessage = null; let msg_text = null; @@ -756,10 +758,22 @@ const newEmailListener = (folder, messagesList) => { console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e); } taLog.log("SpamFilter jsonObj: " + JSON.stringify(jsonObj)); - //TODO save log + + let report_data = {}; + report_data.report_date = new Date(); + report_data.headerMessageId = message.headerMessageId; + report_data.spamValue = jsonObj.spamValue; + report_data.explanation = jsonObj.explanation; + report_data.subject = curr_fullMessage.headers.subject; + report_data.from = curr_fullMessage.headers.from; + report_data.message_date = new Date(message.date); + report_data.moved = false; + report_data.SpamThreshold = prefs_init.spamfilter_threshold; + //console.log(">>>>>>>>>>>> report_data.SpamThreshold: " + JSON.stringify(report_data.SpamThreshold)); //TODO move email if it's spam if(jsonObj.spamValue >= prefs_init.spamfilter_threshold){ + taLog.log("Marking as spam ["+message.headerMessageId+"]"); // mark as spam messenger.messages.update(message.id, { junk: true }); //get the spam folder @@ -767,9 +781,18 @@ const newEmailListener = (folder, messagesList) => { //console.log(">>>>>>>>>>>> spamFolder: " + JSON.stringify(spamFolder)); //move the message messenger.messages.move([message.id], spamFolder[0].id); + report_data.moved = true; + taLog.log("Marked as spam ["+message.headerMessageId+"]"); } + + // Save the operation log + taSpamReport.saveReportData(report_data,message.headerMessageId); } } + + if(prefs_init.spamfilter){ + taSpamReport.truncReportData(); + } } return _newEmailListener(); diff --git a/pages/spamfilter/mzta-spamfilter.css b/pages/spamfilter/mzta-spamfilter.css index 6a9bacc0..e62e300d 100644 --- a/pages/spamfilter/mzta-spamfilter.css +++ b/pages/spamfilter/mzta-spamfilter.css @@ -91,7 +91,7 @@ table#miczPrefs tr:last-child td { color: red; } -#spam_threshold_too_low{ +#spamfilter_threshold_too_low{ display: none; color:red; font-weight: bold; @@ -99,6 +99,19 @@ table#miczPrefs tr:last-child td { margin-left: 10px; } +table#report_data { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} +table#report_data th, table#report_data td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; +} +table#report_data th { + background-color: #f4f4f4; +} @media (prefers-color-scheme: dark) { body { @@ -124,4 +137,18 @@ table#miczPrefs tr:last-child td { .autocomplete-list li.active { background-color: #4c4e58; } + + table#report_data th, + table#report_data td { + border-color: #444; + color: #ddd; + } + + table#report_data th { + background-color: #333; + } + + table#report_data td { + background-color: #222; + } } \ No newline at end of file diff --git a/pages/spamfilter/mzta-spamfilter.html b/pages/spamfilter/mzta-spamfilter.html index 293cf3fb..2a2f2846 100644 --- a/pages/spamfilter/mzta-spamfilter.html +++ b/pages/spamfilter/mzta-spamfilter.html @@ -17,7 +17,7 @@ __MSG_prefs_OptionText_spamfilter_threshold__ @@ -35,6 +35,24 @@
+

__MSG_SpamReport_Title__

+ + + + + + + + + + + + + + + + +
Message_ID__MSG_Date____MSG_From____MSG_Subject____MSG_Spam_Value____MSG_Moved_to_Spam____MSG_Explanation____MSG_Report_Date__
diff --git a/pages/spamfilter/mzta-spamfilter.js b/pages/spamfilter/mzta-spamfilter.js index bec1afdb..4b979b94 100644 --- a/pages/spamfilter/mzta-spamfilter.js +++ b/pages/spamfilter/mzta-spamfilter.js @@ -21,9 +21,11 @@ import { taLogger } from '../../js/mzta-logger.js'; import { getSpecialPrompts, setSpecialPrompts } from "../../js/mzta-prompts.js"; import { getPlaceholders } from "../../js/mzta-placeholders.js"; import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js"; +import { taSpamReport } from '../../js/mzta-spamreport.js'; let autocompleteSuggestions = []; let taLog = new taLogger("mzta-spamfilter-page",true); +taSpamReport.logger = taLog; document.addEventListener('DOMContentLoaded', async () => { @@ -78,27 +80,65 @@ document.addEventListener('DOMContentLoaded', async () => { autocompleteSuggestions = (await getPlaceholders(true)).filter(p => !(p.id === 'additional_text')).map(p => ({command: '{%'+p.id+'%}', type: p.type})); textareaAutocomplete(spamfilter_textarea, autocompleteSuggestions, 1); // type_value = 1, only when reading an email + loadSpamReport() }); function check_spamfilter_threshold(event) { - let spam_threshold_too_low = document.getElementById("spam_threshold_too_low"); + let spamfilter_threshold_too_low = document.getElementById("spamfilter_threshold_too_low"); if(event.target.value < 50){ - spam_threshold_too_low.style.display = "inline"; + spamfilter_threshold_too_low.style.display = "inline"; if(event.target.value == 0){ - spam_threshold_too_low.textContent = browser.i18n.getMessage('spam_threshold_zero'); - spam_threshold_too_low.style.fontSize = "1.5em"; + spamfilter_threshold_too_low.textContent = browser.i18n.getMessage('spamfilter_threshold_zero'); + spamfilter_threshold_too_low.style.fontSize = "1.5em"; }else{ - spam_threshold_too_low.textContent = browser.i18n.getMessage('spam_threshold_too_low'); - spam_threshold_too_low.style.fontSize = "1.1em"; + spamfilter_threshold_too_low.textContent = browser.i18n.getMessage('spamfilter_threshold_too_low'); + spamfilter_threshold_too_low.style.fontSize = "1.1em"; } }else{ - spam_threshold_too_low.style.display = "none"; + spamfilter_threshold_too_low.style.display = "none"; } } +async function loadSpamReport(){ + let report_data = await taSpamReport.getAllReportData(); + console.log(">>>>>>>>>>>> loadSpamReport: " + JSON.stringify(report_data)); + //document.getElementById("report_data").textContent = JSON.stringify(report_data, null, 2); + if(report_data == undefined){ + document.getElementById("report_data").innerText = browser.i18n.getMessage("spamfilter_no_reports"); + }else{ + populateTable(report_data); + } +} + + // Function to populate the table + function populateTable(data) { + const tableBody = document.getElementById("report_data_body"); + tableBody.innerHTML = ""; // Clear table before inserting new data + + Object.keys(data).forEach(email => { + const report = data[email]; + + // Create a new row + const row = document.createElement("tr"); + + row.innerHTML = ` + ${report.headerMessageId} + ${new Date(report.message_date).toLocaleString()} + ${report.from.join(", ")} + ${report.subject.join(", ")} + ${report.spamValue} + ${report.moved?browser.i18n.getMessage("spamfilter_moved"):browser.i18n.getMessage("spamfilter_not_moved")} (${report.SpamThreshold}) + ${report.explanation} + ${new Date(report.report_date).toLocaleString()} + `; + + // Append the row to the table + tableBody.appendChild(row); + }); +} + // Methods to manage options, derived from: /options/mzta-options.js - function saveOptions(e) { e.preventDefault(); let options = {};