spam report added. see #231

This commit is contained in:
mic 2025-02-14 23:22:47 +01:00
parent 943d997f72
commit be6510f5fe
6 changed files with 282 additions and 39 deletions

View file

@ -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": ""
}
}

91
js/mzta-spamreport.js Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View file

@ -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();

View file

@ -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;
}
}

View file

@ -17,7 +17,7 @@
<td><span>__MSG_prefs_OptionText_spamfilter_threshold__</span></td>
<td>
<label>
<input type="number" id="spamfilter_threshold" name="spamfilter_threshold" min="0" max="100" class="option-input" /><span id="spam_threshold_too_low">__MSG_spam_threshold_too_low__</span>
<input type="number" id="spamfilter_threshold" name="spamfilter_threshold" min="0" max="100" class="option-input" /><span id="spamfilter_threshold_too_low">__MSG_spamfilter_threshold_too_low__</span>
<br>__MSG_prefs_OptionText_spamfilter_threshold_Info__
</label>
</td>
@ -35,6 +35,24 @@
<div id="spamfilter_info_additional_statements"></div>
<div class="btn_div"><button id="btn_reset_prompt" disabled>__MSG_reset_default__</button><button id="btn_save_prompt" disabled>__MSG_save__</button></div>
</div>
<h2>__MSG_SpamReport_Title__</h2>
<table id="report_data">
<thead>
<tr>
<th>Message_ID</th>
<th>__MSG_Date__</th>
<th>__MSG_From__</th>
<th>__MSG_Subject__</th>
<th>__MSG_Spam_Value__</th>
<th>__MSG_Moved_to_Spam__</th>
<th>__MSG_Explanation__</th>
<th>__MSG_Report_Date__</th>
</tr>
</thead>
<tbody id="report_data_body">
<!-- Rows will be inserted dynamically -->
</tbody>
</table>
<script src="mzta-spamfilter.js" type="module"></script>
<script src="../../js/mzta-i18n.js"></script>
</body>

View file

@ -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 = `
<td>${report.headerMessageId}</td>
<td>${new Date(report.message_date).toLocaleString()}</td>
<td>${report.from.join(", ")}</td>
<td>${report.subject.join(", ")}</td>
<td>${report.spamValue}</td>
<td>${report.moved?browser.i18n.getMessage("spamfilter_moved"):browser.i18n.getMessage("spamfilter_not_moved")} (${report.SpamThreshold})</td>
<td>${report.explanation}</td>
<td>${new Date(report.report_date).toLocaleString()}</td>
`;
// 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 = {};