Merge pull request #261 from micz/spamfilter

Spamfilter
This commit is contained in:
Mic 2025-02-14 23:24:57 +01:00 committed by GitHub
commit a9acf76333
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 802 additions and 35 deletions

View file

@ -8,6 +8,7 @@
<h2>Version 3.2.0 - ??/??/2025</h2>
<ul>
<li><i>[ChatGPT API][Ollama API][OpenAI Comp API][Gemini API]</i> Added an option to automatically tag incoming emails [<a href="https://github.com/micz/ThunderAI/issues/237">#237</a>].</li>
<li><i>[ChatGPT API][Ollama API][OpenAI Comp API][Gemini API]</i> Added an configurable antispam filter for incoming emails [<a href="https://github.com/micz/ThunderAI/issues/231">#231</a>].</li>
<li>Added the <i>{%thunderai_def_sign%}</i> placeholder to get the default signature as defined in the options [<a href="https://github.com/micz/ThunderAI/issues/248">#248</a>].</li>
<li>Added the <i>{%thunderai_def_lang%}</i> placeholder to get the default language as defined in the options [<a href="https://github.com/micz/ThunderAI/issues/248">#248</a>].</li>
<li>Various minor improvements.</li>

View file

@ -1050,5 +1050,105 @@
"thunderai_def_lang": {
"message": "Default language as defined in ThunderAI options.",
"description": ""
},
"prefs_OptionText_spamfilter": {
"message": "Automatic Spam Filter",
"description": ""
},
"prefs_OptionText_spamfilter_Info": {
"message": "If checked, ThunderAI will automatically move spam emails to the spam folder.",
"description": ""
},
"prefs_OptionText_btnManageSpamFilterInfo": {
"message": "Manage spam filter settings",
"description": ""
},
"SpamFilter_PageTitle": {
"message": "Manage Spam Filter Settings",
"description": ""
},
"SpamFilter_info_default": {
"message": "In this page you can modify the default prompt used to detect spam emails.",
"description": ""
},
"SpamFilter_prompt_text_title": {
"message": "Current prompt text",
"description": ""
},
"prefs_OptionText_spamfilter_infoline": {
"message": "You can change the prompt as you wish, but the response received from the AI must be in JSON format as specified in the default prompt!",
"description": ""
},
"prompt_spamfilter" : {
"message" : "Detect spam emails",
"description" : ""
},
"prompt_spamfilter_full_text" : {
"message" : "Analyze the following email and determine if it is spam or not. Consider factors such as suspicious keywords, excessive promotional language, misleading subject lines, requests for personal information, and unusual sender addresses.\nProvide a value from 0 (not spam) to 100 (spam) and an explaination of no more than 10 words.\nGenerate a response in JSON format only. Do not include any additional text or explanation; provide only the JSON. Here is the format to be used:\n{\n\"spamValue\": <integer from 0 to 100>,\n\"explanation\": \"Brief explanation of your reasoning\",\n}\nHere there are the mail information:\nSender: \"{%author%}\"\nSubject: \"{%mail_subject%}\"\nHtml body: \"{%mail_html_body%}\"",
"description" : ""
},
"SpamFilter_prompt_prefs_title": {
"message": "Spam Filter Options",
"description": ""
},
"prefs_OptionText_spamfilter_threshold": {
"message": "Spam threshold",
"description": ""
},
"prefs_OptionText_spamfilter_threshold_Info": {
"message": "If the value returned by the AI is above this threshold, the email will be moved to the spam folder.",
"description": ""
},
"spamfilter_threshold_too_low": {
"message": "The spam threshold is too low! You will likely mark too much mails as spam!",
"description": ""
},
"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": ""
}
}

View file

@ -318,6 +318,8 @@ export const placeholdersUtils = {
break;
}
}
return finalSubs;
}
}

View file

@ -195,6 +195,19 @@ const specialPrompts = [
is_default: "1",
is_special: "1",
},
{
id: 'prompt_spamfilter',
name: "__MSG_prompt_spamfilter__",
text: "prompt_spamfilter_full_text",
type: "1",
action: "0",
need_selected: "0",
need_signature: "0",
need_custom_text: "0",
define_response_lang: "0",
is_default: "1",
is_special: "1",
},
];
@ -359,4 +372,8 @@ export async function getSpecialPrompts(){
export async function setSpecialPrompts(prompts) {
// console.log(">>>>>>>>>>>> setSpecialPrompts prompts: " + JSON.stringify(prompts));
await browser.storage.local.set({_special_prompts: prompts});
}
export async function getSpamFilterPrompt(){
return (await getSpecialPrompts()).find(prompt => prompt.id == 'prompt_spamfilter');
}

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

@ -37,7 +37,7 @@ export const taPromptUtils = {
fullPrompt = curr_prompt.text + (String(curr_prompt.need_signature) == "1" ? " " + await taPromptUtils.getDefaultSignature():"") + " " + chatgpt_lang + " \"" + (selection_text=='' ? body_text : selection_text) + "\" ";
}else{
// we have at least a placeholder, do the magic!
let finalSubs = placeholdersUtils.getPlaceholdersValues(curr_prompt.text, curr_message, subject_text, body_text, msg_text, only_typed_text, selection_text, tags_full_list);
let finalSubs = await placeholdersUtils.getPlaceholdersValues(curr_prompt.text, curr_message, subject_text, body_text, msg_text, only_typed_text, selection_text, tags_full_list);
// console.log(">>>>>>>>>> finalSubs: " + JSON.stringify(finalSubs));
let prefs_ph = await browser.storage.sync.get({placeholders_use_default_value: false});
fullPrompt = (placeholdersUtils.replacePlaceholders(curr_prompt.text, finalSubs, prefs_ph.placeholders_use_default_value, true) + (String(curr_prompt.need_signature) == "1" ? " " + await taPromptUtils.getDefaultSignature():"") + " " + chatgpt_lang).trim();

View file

@ -302,6 +302,23 @@ export function getActiveSpecialPromptsIDs(addtags = false, get_calendar_event =
return output;
}
export function extractJsonObject(inputString) {
try {
const jsonMatch = inputString.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const jsonObject = JSON.parse(jsonMatch[0]);
// console.log(">>>>>>>>>> Extracted JSON object:", jsonObject);
return jsonObject;
} else {
console.error("[ThunderAI] No JSON object found in the input string.");
return null;
}
} catch (error) {
console.error("[ThunderAI] Error extracting JSON object:", error);
return null;
}
}
export async function checkSparksPresence() {
try {
return (await browser.runtime.sendMessage('thunderai-sparks@micz.it',{action: "checkPresence"}) === 'ok');

View file

@ -36,6 +36,9 @@
"messagesUpdate",
"https://*.chatgpt.com/*"
],
"optional_permissions": [
"messagesMove"
],
"background": {
"page": "mzta-background.html"
},

View file

@ -20,9 +20,13 @@ import { mzta_script } from './js/mzta-chatgpt.js';
import { prefs_default } from './options/mzta-options-default.js';
import { mzta_Menus } from './js/mzta-menus.js';
import { taLogger } from './js/mzta-logger.js';
import { getCurrentIdentity, getOriginalBody, replaceBody, setBody, i18nConditionalGet, generateCallID, migrateCustomPromptsStorage, migrateDefaultPromptsPropStorage, getGPTWebModelString, getTagsList, createTag, assignTagsToMessage, checkIfTagExists, getActiveSpecialPromptsIDs, checkSparksPresence, getMessages, getMailBody } from './js/mzta-utils.js';
import { getCurrentIdentity, getOriginalBody, replaceBody, setBody, i18nConditionalGet, generateCallID, migrateCustomPromptsStorage, migrateDefaultPromptsPropStorage, getGPTWebModelString, getTagsList, createTag, assignTagsToMessage, checkIfTagExists, getActiveSpecialPromptsIDs, checkSparksPresence, getMessages, getMailBody, extractJsonObject } from './js/mzta-utils.js';
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);
await migrateCustomPromptsStorage();
await migrateDefaultPromptsPropStorage();
@ -601,7 +605,7 @@ async function doGetCalendarEvent(get_calendar_event) {
}
async function reload_pref_init(){
prefs_init = await browser.storage.sync.get({do_debug: false, add_tags: true, get_calendar_event: true, connection_type: 'chatgpt_web', add_tags_auto: false, add_tags_auto_force_existing: false, add_tags_auto_only_inbox: true});
prefs_init = await browser.storage.sync.get({do_debug: false, add_tags: true, get_calendar_event: true, connection_type: 'chatgpt_web', add_tags_auto: false, add_tags_auto_force_existing: false, add_tags_auto_only_inbox: true, spamfilter: false, spamfilter_threshold: 70, dynamic_menu_force_enter: false});
}
@ -657,52 +661,54 @@ 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;
let body_text = '';
// if some auto feature is active prepare some data
// this will be used when will be implemented the spam filter (https://github.com/micz/ThunderAI/issues/231)
// or the auto translator https://github.com/micz/ThunderAI/issues/247
if(prefs_init.add_tags_auto){
// this will be used when will be implemented also the auto translator https://github.com/micz/ThunderAI/issues/247
if(prefs_init.add_tags_auto || prefs_init.spamfilter){
curr_fullMessage = await browser.messages.getFull(message.id);
msg_text = await getMailBody(curr_fullMessage);
body_text = msg_text.text.replace(/\s+/g, ' ').trim();
}
// Auto add_tags
if(prefs_init.add_tags_auto){
let specialFullPrompt_add_tags = '';
let curr_prompt_add_tags = menus.allPrompts.find(p => p.id === 'prompt_add_tags');
let chatgpt_lang = taPromptUtils.getDefaultLang(curr_prompt_add_tags);
let msg_text = await getMailBody(curr_fullMessage);
let body_text = msg_text.text.replace(/\s+/g, ' ').trim();
let tags_full_list = await getTagsList();
let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_add_tags);
specialFullPrompt_add_tags = await taPromptUtils.preparePrompt(curr_prompt_add_tags, message, chatgpt_lang, /*selection_text*/ '', body_text, curr_fullMessage.headers.subject, msg_text, /*only_typed_text*/ '', tags_full_list);
let prefs_aat = await browser.storage.sync.get({add_tags_maxnum: 3, connection_type: '', add_tags_force_lang: true, default_chatgpt_lang: '', add_tags_auto_force_existing: false});
specialFullPrompt_add_tags = taPromptUtils.finalizePrompt_add_tags(specialFullPrompt_add_tags, prefs_aat.add_tags_maxnum, prefs_aat.add_tags_force_lang, prefs_aat.default_chatgpt_lang);
@ -719,6 +725,73 @@ const newEmailListener = (folder, messagesList) => {
let _data = {messageId: message.id, tags: tags_current_email.split(/,\s*/)};
_assign_tags(_data, !prefs_aat.add_tags_auto_force_existing);
}
// Spam filter
if(prefs_init.spamfilter){
let curr_prompt_spamfilter = await getSpamFilterPrompt();
//console.log(">>>>>>>>>>>> curr_prompt_spamfilter: " + JSON.stringify(curr_prompt_spamfilter));
let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_spamfilter);
let specialFullPrompt_spamfilter = await taPromptUtils.preparePrompt(curr_prompt_spamfilter, message, chatgpt_lang, /*selection_text*/ '', body_text, curr_fullMessage.headers.subject, msg_text, /*only_typed_text*/ '', '');
taLog.log("Special prompt: " + specialFullPrompt_spamfilter);
let cmd_spamfilter = new mzta_specialCommand(specialFullPrompt_spamfilter,prefs_init.connection_type,prefs_init.do_debug);
await cmd_spamfilter.initWorker();
/* We expect to receive from the AI a JSON object like this:
* {
* "spamValue": <integer from 0 to 100>,
* "explanation": "Brief explanation of your reasoning",
* }
*/
let spamfilter_result = '';
taLog.log("Sending the prompt...");
try{
spamfilter_result = (await cmd_spamfilter.sendPrompt()).trim();
}catch(err){
console.error("[ThunderAI | SpamFilter] Error getting spamfilter: ", err);
}
taLog.log("spamfilter_result: " + spamfilter_result);
let jsonObj = {};
taLog.log("Decoding the AI response...");
try{
jsonObj = extractJsonObject(spamfilter_result);
}catch(e){
console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e);
}
taLog.log("SpamFilter jsonObj: " + JSON.stringify(jsonObj));
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
let spamFolder = await messenger.folders.query({accountId: message.folder.accountId, specialUse: ['junk']})
//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();
}
}

View file

@ -51,4 +51,6 @@ export const prefs_default = {
add_tags_auto_force_existing: false,
add_tags_auto_only_inbox: true,
get_calendar_event: true,
spamfilter: false,
spamfilter_threshold: 70,
}

View file

@ -334,6 +334,16 @@
</label>
</td>
</tr>
<tr class="spamfilter_tr">
<td><span>__MSG_prefs_OptionText_spamfilter__</span>
<br><button id="btnManageSpamFilterInfo" class="btn_small">__MSG_prefs_OptionText_btnManageSpamFilterInfo__</button></td>
<td>
<label>
<input type="checkbox" id="spamfilter" name="spamfilter" class="option-input" />
__MSG_prefs_OptionText_spamfilter_Info__
</label>
</td>
</tr>
<tr class="get_calendar_event_tr">
<td><span>__MSG_prefs_OptionText_get_calendar_event__</span>
<br><button id="btnManageCalendarEventInfo" class="btn_small">__MSG_prefs_OptionText_btnManageCalendarEventInfo__</button></td>

View file

@ -304,6 +304,23 @@ document.addEventListener('DOMContentLoaded', async () => {
});
addtags_info_btn.disabled = addtags_el.checked ? '' : 'disabled';
let spamfilter_el = document.getElementById('spamfilter');
let spamfilter_info_btn = document.getElementById('btnManageSpamFilterInfo');
spamfilter_el.addEventListener('click', (event) => {
async function _spamfilter_el_change() {
if (event.target.checked) {
const granted = await messenger.permissions.request({ permissions: ["messagesMove"] });
if (!granted) {
event.target.checked = false;
spamfilter_info_btn.disabled = 'disabled';
}
}
}
_spamfilter_el_change();
spamfilter_info_btn.disabled = event.target.checked ? '' : 'disabled';
});
spamfilter_info_btn.disabled = spamfilter_el.checked ? '' : 'disabled';
let get_calendar_event_el = document.getElementById('get_calendar_event');
let get_calendar_event_info_btn = document.getElementById('btnManageCalendarEventInfo');
get_calendar_event_el.addEventListener('click', (event) => {
@ -337,6 +354,19 @@ document.addEventListener('DOMContentLoaded', async () => {
})
});
document.getElementById('btnManageSpamFilterInfo').addEventListener('click', () => {
// check if the tab is already there
browser.tabs.query({url: browser.runtime.getURL('../pages/spamfilter/mzta-spamfilter.html')}).then((tabs) => {
if (tabs.length > 0) {
// if the tab is already there, focus it
browser.tabs.update(tabs[0].id, {active: true});
} else {
// if the tab is not there, create it
browser.tabs.create({url: browser.runtime.getURL('../pages/spamfilter/mzta-spamfilter.html')});
}
})
});
document.getElementById('btnManageCalendarEventInfo').addEventListener('click', () => {
// check if the tab is already there
browser.tabs.query({url: browser.runtime.getURL('../pages/get-calendar-event/mzta-get-calendar-event.html')}).then((tabs) => {

View file

@ -10,6 +10,7 @@
<h2>Version 3.2.0 - ??/??/2025</h2>
<ul>
<li><i>[ChatGPT API][Ollama API][OpenAI Comp API][Gemini API]</i> Added an option to automatically tag incoming emails [<a href="https://github.com/micz/ThunderAI/issues/237">#237</a>].</li>
<li><i>[ChatGPT API][Ollama API][OpenAI Comp API][Gemini API]</i> Added an configurable antispam filter for incoming emails [<a href="https://github.com/micz/ThunderAI/issues/231">#231</a>].</li>
<li>Added the <i>{%thunderai_def_sign%}</i> placeholder to get the default signature as defined in the options [<a href="https://github.com/micz/ThunderAI/issues/248">#248</a>].</li>
<li>Added the <i>{%thunderai_def_lang%}</i> placeholder to get the default language as defined in the options [<a href="https://github.com/micz/ThunderAI/issues/248">#248</a>].</li>
<li>Various minor improvements.</li>

View file

@ -0,0 +1,154 @@
#spamfilter_prompt_container{
width: 90%;
margin: 20px auto;
}
#spamfilter_prompt_text{
width: 100%;
}
.infoline{
font-size: 0.8em;
font-style: italic;
}
.btn_div{
width: 100%;
display: flex;
justify-content: space-between;
}
table#miczPrefs {
padding: 10px;
border-spacing: 0px;
width: 90%;
margin: 0px auto 40px auto;
border: 1px solid #ccc;
}
.spamfilter_table_title{
width: 90%;
margin: auto;
}
.section_title{
font-weight: bold;
}
table#miczPrefs td{
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
vertical-align: top;
padding: 2px;
}
table#miczPrefs tr:first-child td {
border-top: none;
}
table#miczPrefs tr:last-child td {
border-bottom: none;
}
.autocomplete-container {
position: relative;
}
.autocomplete-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #ccc;
z-index: 1000;
max-height: 200px;
overflow-y: auto;
padding: 0;
margin: 0;
list-style: none;
font-size: small;
}
.autocomplete-list li {
padding: 8px;
cursor: pointer;
}
.autocomplete-list li:hover {
background-color: #f0f0f0;
}
.autocomplete-list li.active {
background-color: #ddd;
}
.hidden {
display: none;
}
.unsaved{
color: red;
}
#spamfilter_threshold_too_low{
display: none;
color:red;
font-weight: bold;
font-size: 1.1em;
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 {
background-color: #1C1B22;
color: rgb(251, 251, 254);
}
a:link { color: #409EFF; }
a:visited { color: #409EFF; }
a:hover { color: #66B1FF; }
a:active { color: #66B1FF; }
a:active { color: #66B1FF; }
.autocomplete-list {
background-color: #2E2F36;
border: 1px solid #2E2F36;
}
.autocomplete-list li:hover {
background-color: #4c4e58;
}
.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

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ThunderAI - __MSG_SpamFilter_PageTitle__</title>
<link rel="stylesheet" href="mzta-spamfilter.css">
<link rel="icon" href="../../images/icon-16px.png">
</head>
<body>
<div>
<h1 class="page_title">__MSG_SpamFilter_PageTitle__</h1>
<p>__MSG_SpamFilter_info_default__</p>
</div>
<div class="spamfilter_table_title section_title">__MSG_SpamFilter_prompt_prefs_title__</div>
<table id="miczPrefs">
<tr class="spamfilter_tr">
<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="spamfilter_threshold_too_low">__MSG_spamfilter_threshold_too_low__</span>
<br>__MSG_prefs_OptionText_spamfilter_threshold_Info__
</label>
</td>
</tr>
</table>
<div id="spamfilter_container">
<span class="section_title">__MSG_GetCalendarEvent_prompt_text_title__</span><span id="spamfilter_event_prompt_unsaved" class="unsaved hidden"> __MSG_customPrompts_unsaved_changes__</span>
<br><span class="infoline">__MSG_prefs_OptionText_btnManagePrompts_infoline__ <a href="https://micz.it/thunderbird-addon-thunderai/data-placeholders/">__MSG_customPrompts_managePrompts_help__</a>
<br><b>__MSG_prefs_OptionText_GetCalendarEvent_infoline2__</b></span>
<br>
<div class="autocomplete-container">
<textarea id="spamfilter_prompt_text" rows="15"></textarea>
<ul class="autocomplete-list hidden"></ul>
</div>
<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>
</html>

View file

@ -0,0 +1,207 @@
/*
* 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/>.
*/
import { prefs_default } from '../../options/mzta-options-default.js';
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 () => {
i18n.updateDocument();
await restoreOptions();
document.querySelectorAll(".option-input").forEach(element => {
element.addEventListener("change", saveOptions);
});
document.getElementById("spamfilter_threshold").addEventListener("input", check_spamfilter_threshold);
check_spamfilter_threshold({target: document.getElementById("spamfilter_threshold")});
let spamfilter_textarea = document.getElementById('spamfilter_prompt_text');
let spamfilter_save_btn = document.getElementById('btn_save_prompt');
let spamfilter_reset_btn = document.getElementById('btn_reset_prompt');
let specialPrompts = await getSpecialPrompts();
let spamfilter_prompt = specialPrompts.find(prompt => prompt.id === 'prompt_spamfilter');
spamfilter_textarea.addEventListener('input', (event) => {
spamfilter_reset_btn.disabled = (event.target.value === browser.i18n.getMessage('prompt_spamfilter_full_text'));
spamfilter_save_btn.disabled = (event.target.value === spamfilter_prompt.text);
if(spamfilter_save_btn.disabled){
document.getElementById('spamfilter_prompt_unsaved').classList.add('hidden');
} else {
document.getElementById('spamfilter_prompt_unsaved').classList.remove('hidden');
}
});
spamfilter_reset_btn.addEventListener('click', () => {
spamfilter_textarea.value = browser.i18n.getMessage('prompt_spamfilter_full_text');
spamfilter_reset_btn.disabled = true;
let event = new Event('input', { bubbles: true, cancelable: true });
spamfilter_textarea.dispatchEvent(event);
});
spamfilter_save_btn.addEventListener('click', () => {
specialPrompts.find(prompt => prompt.id === 'prompt_spamfilter').text = spamfilter_textarea.value;
setSpecialPrompts(specialPrompts);
spamfilter_save_btn.disabled = true;
document.getElementById('spamfilter_prompt_unsaved').classList.add('hidden');
browser.runtime.sendMessage({command: "reload_menus"});
});
if(spamfilter_prompt.text === 'prompt_spamfilter_full_text'){
spamfilter_prompt.text = browser.i18n.getMessage(spamfilter_prompt.text);
}
spamfilter_textarea.value = spamfilter_prompt.text;
spamfilter_reset_btn.disabled = (spamfilter_textarea.value === browser.i18n.getMessage('prompt_spamfilter_full_text'));
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 spamfilter_threshold_too_low = document.getElementById("spamfilter_threshold_too_low");
if(event.target.value < 50){
spamfilter_threshold_too_low.style.display = "inline";
if(event.target.value == 0){
spamfilter_threshold_too_low.textContent = browser.i18n.getMessage('spamfilter_threshold_zero');
spamfilter_threshold_too_low.style.fontSize = "1.5em";
}else{
spamfilter_threshold_too_low.textContent = browser.i18n.getMessage('spamfilter_threshold_too_low');
spamfilter_threshold_too_low.style.fontSize = "1.1em";
}
}else{
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 = {};
let element = e.target;
switch (element.type) {
case 'checkbox':
options[element.id] = element.checked;
break;
case 'number':
options[element.id] = element.valueAsNumber;
break;
case 'text':
case 'password':
options[element.id] = element.value.trim();
break;
default:
if (element.tagName === 'SELECT') {
options[element.id] = element.value;
}else{
console.error("[ThunderAI] Unhandled input type:", element.type);
}
}
browser.storage.sync.set(options);
}
async function restoreOptions() {
function setCurrentChoice(result) {
document.querySelectorAll(".option-input").forEach(element => {
taLog.log("Options restoring " + element.id + " = " + (element.id=="chatgpt_api_key" || element.id=="openai_comp_api_key" ? "****************" : result[element.id]));
switch (element.type) {
case 'checkbox':
element.checked = result[element.id] || false;
break;
case 'number':
let default_number_value = 0;
if(element.id == 'chatgpt_win_height') default_number_value = prefs_default.chatgpt_win_height;
if(element.id == 'chatgpt_win_width') default_number_value = prefs_default.chatgpt_win_width;
element.value = result[element.id] ?? default_number_value;
break;
case 'text':
case 'password':
let default_text_value = '';
if(element.id == 'default_chatgpt_lang') default_text_value = prefs_default.default_chatgpt_lang;
element.value = result[element.id] || default_text_value;
break;
default:
if (element.tagName === 'SELECT') {
let default_select_value = '';
if(element.id == 'reply_type') default_select_value = 'reply_all';
if(element.id == 'connection_type') default_select_value = 'chatgpt_web';
element.value = result[element.id] || default_select_value;
if (element.value === '') {
element.selectedIndex = -1;
}
}else{
console.error("[ThunderAI] Unhandled input type:", element.type);
}
}
});
}
let getting = await browser.storage.sync.get(prefs_default);
setCurrentChoice(getting);
}