ThunderAI/js/mzta-compose-script.js

749 lines
No EOL
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2026 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/>.
*/
// CSS selectors for DOM elements injected by ThunderAI.
// These are stripped when retrieving the email body content
// to avoid contaminating placeholder values sent to AI providers.
// Add new selectors here when new UI elements are injected into the email DOM.
const MZTA_INJECTED_SELECTORS = [
'#mzta-spam-check-progress',
'#mzta-spam-report-banner',
'.mzta_dialog',
];
function getCleanBodyClone() {
const clone = document.body.cloneNode(true);
for (const selector of MZTA_INJECTED_SELECTORS) {
for (const el of clone.querySelectorAll(selector)) {
el.remove();
}
}
return clone;
}
browser.runtime.onMessage.addListener((message) => {
switch (message.command) {
case "getSelectedText": {
return Promise.resolve(window.getSelection().toString());
}
case "getSelectedHtml": {
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return Promise.resolve('');
}
const range = selection.getRangeAt(0);
const div = document.createElement('div');
div.appendChild(range.cloneContents());
return Promise.resolve(div.innerHTML);
}
case "replaceSelectedText": {
const selectedText = window.getSelection().toString();
let force_insert = false;
if (selectedText === '') {
if(!confirm(browser.i18n.getMessage("Replace_No_Selected_Text"))) {
return Promise.resolve(false);
}else{
force_insert = true;
}
}
const sel = window.getSelection();
if (!sel || sel.type !== "Range" || !sel.rangeCount) {
if(!force_insert) {
return Promise.resolve(false);
}
}
const r = sel.getRangeAt(0);
r.deleteContents();
const parser = new DOMParser();
const doc = parser.parseFromString(message.text, 'text/html');
r.insertNode(doc.body);
browser.runtime.sendMessage({command: "compose_reloadBody", tabId: message.tabId});
return Promise.resolve(true);
}
case "getText": {
let t = '';
const children = getCleanBodyClone().childNodes;
for (const node of children) {
if (node instanceof Element) {
if (node.classList.contains('moz-signature')) {
continue;
}
}
t += node.textContent;
}
return Promise.resolve(t);
}
case "getTextOnly": {
return Promise.resolve(getCleanBodyClone().innerText);
}
case "getFullHtml": {
return Promise.resolve(getCleanBodyClone().innerHTML);
}
case "getOnlyTypedText": {
let t = '';
const children = window.document.body.childNodes;
const selection = window.getSelection();
let firstNode = null;
let lastNode = null;
for (const node of children) {
if (node instanceof Element) {
if (node.classList.contains('moz-cite-prefix')) { // quoted text in a reply
break;
}
if (node.classList.contains('moz-forward-container')) { // quoted text in a forward
break;
}
}
t += node.textContent + " ";
// Track the first and last nodes for range
if (!firstNode) {
firstNode = node;
}
if(node.textContent.trim() != '') {
lastNode = node;
}
}
if(!lastNode) {
lastNode = firstNode;
}
//if(message.do_autoselect && selection.isCollapsed && firstNode && lastNode) {
if(message.do_autoselect && firstNode && lastNode) {
const range = document.createRange();
range.setStartBefore(firstNode);
range.setEndAfter(lastNode);
selection.removeAllRanges();
selection.addRange(range);
}
return Promise.resolve(t);
}
case "getOnlyQuotedText": {
let t = '';
const children = window.document.body.childNodes;
const selection = window.getSelection();
let firstNode = null;
let lastNode = null;
let foundCitePrefix = false;
for (const node of children) {
if (!foundCitePrefix) {
if (node instanceof Element && (node.classList.contains('moz-cite-prefix') || node.classList.contains('moz-forward-container'))) {
foundCitePrefix = true;
} else {
continue;
}
}
t += node.textContent + " ";
if (!firstNode) {
firstNode = node;
}
if (node.textContent.trim() !== '') {
lastNode = node;
}
}
if (!lastNode) {
lastNode = firstNode;
}
if (message.do_autoselect && firstNode && lastNode) {
const range = document.createRange();
range.setStartBefore(firstNode);
range.setEndAfter(lastNode);
selection.removeAllRanges();
selection.addRange(range);
}
return Promise.resolve(t);
}
case 'sendAlert':
//console.log(">>>>>> message.curr_tab_type: " + JSON.stringify(message.curr_tab_type));
if(message.curr_tab_type == 'mail'){ // workaround for Thunderbird bug not showing the alert
// CSS styles for Thunderbird with support for light and dark themes
const style = document.createElement('style');
style.textContent = `
.mzta_dialog {
display: flex;
flex-direction: column;
border: none;
border-radius: 8px;
padding: 0;
width: 300px;
max-width: 90%;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
background-color: var(--dialog-bg-color, #fff);
color: var(--dialog-text-color, #000);
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1001;
}
.mzta_dialog_content {
position: relative;
padding: 10px;
overflow-y: auto;
overflow-x: clip;
max-height: 80vh;
flex: 1;
margin-bottom: 34px;
}
.mzta_dialog_close {
position: absolute;
bottom: 2px;
right: 2px;
background: #007bff;
border: none;
color: #ffffff;
font-size: 14px;
padding: 4px 6px;
cursor: pointer;
border-radius: 4px;
}
.mzta_dialog_close:hover {
background: #0056b3;
}
.mzta_dialog_message {
margin-bottom: 40px;
}
h2.addtags{
margin:0;
font-size: 1.2em;
text-align: center;
}
.div_btns{
width: 90%;
display: flex;
justify-content: center;
position: fixed;
bottom: 4px;
}
:root {
--dialog-bg-color: #ffffff;
--dialog-text-color: #000000;
}
@media (prefers-color-scheme: dark) {
:root {
--dialog-bg-color: #2e2e2e;
--dialog-text-color: #ffffff;
}
}
`;
document.head.appendChild(style);
// Create dialog elements
const dialog_sendAlert = window.document.createElement('dialog');
dialog_sendAlert.classList.add('mzta_dialog');
const title = document.createElement('h2');
title.className = 'addtags';
if(message.is_error){
title.textContent = browser.i18n.getMessage("thunderai_error_title");
}else{
title.textContent = browser.i18n.getMessage("thunderai_warning_title");
}
dialog_sendAlert.appendChild(title);
const content_sendAlert = window.document.createElement('div');
content_sendAlert.classList.add('mzta_dialog_content');
// Add a div for buttons
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'div_btns';
// Create close button
const closeButton_sendAlert = window.document.createElement('button');
closeButton_sendAlert.classList.add('mzta_dialog_close');
closeButton_sendAlert.id = 'mzta_dialog_close';
closeButton_sendAlert.textContent = 'Close';
buttonsDiv.appendChild(closeButton_sendAlert);
// Create message element
const message_sendAlert = window.document.createElement('div');
message_sendAlert.classList.add('mzta_dialog_message');
message_sendAlert.textContent = message.message;
// Append elements to the dialog
content_sendAlert.appendChild(message_sendAlert);
dialog_sendAlert.appendChild(content_sendAlert);
dialog_sendAlert.appendChild(buttonsDiv);
window.document.body.appendChild(dialog_sendAlert);
// Show the dialog
dialog_sendAlert.showModal();
// Close dialog on button click and remove elements from the DOM
closeButton_sendAlert.onclick = () => {
dialog_sendAlert.close();
dialog_sendAlert.addEventListener('close', () => {
dialog_sendAlert.remove();
style.remove();
});
};
const iframeHeight = window.innerHeight;
const iframeWidth = window.innerWidth;
const dialogRect = dialog_sendAlert.getBoundingClientRect();
const top = Math.max(0, (iframeHeight - dialogRect.height) / 2);
const left = Math.max(0, (iframeWidth - dialogRect.width) / 2);
dialog_sendAlert.style.top = `${top}px`;
dialog_sendAlert.style.left = `${left}px`;
dialog_sendAlert.style.maxHeight = `${iframeHeight - 40}px`;
dialog_sendAlert.style.transform = 'translate(0, 0)';
}else{
alert(message.message);
}
return Promise.resolve(true);
break;
case "getTags":
// console.log(">>>>>>>>>>>>>> getTags: " + JSON.stringify(message.tags));
// ===== These methods are also defined in the file /js/mzta-addatags-exclusion-list.js
async function addTags_getExclusionList() {
let prefs_excluded_tags = await browser.storage.local.get({add_tags_exclusions: []});
// console.log(">>>>>>>>>>>>>>> addTags_getExclusionList prefs_excluded_tags: " + JSON.stringify(prefs_excluded_tags));
return prefs_excluded_tags.add_tags_exclusions;
}
function addTags_setExclusionList(add_tags_exclusions) {
browser.storage.local.set({add_tags_exclusions: add_tags_exclusions});
}
function checkExcludedTag(tag, excluded_word, exact_match = false) {
// Check if the tag is in the exclusion list
if (excluded_word === '') {
return false; // No exclusion word, so no exclusion
}
if(exact_match) {
return tag.toLowerCase() === excluded_word.toLowerCase();
}
return tag.toLowerCase().includes(excluded_word.toLowerCase());
}
// ===================================================================================
// Create and append the styles
const style = document.createElement('style');
style.textContent = `
.mzta_dialog {
display: flex;
flex-direction: column;
border: none;
border-radius: 8px;
width: 300px;
max-width: 90%;
max-height: 90%;
padding: 0.5em;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
background-color: var(--dialog-bg-color, #fff);
color: var(--dialog-text-color, #000);
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1001;
}
.mzta_dialog_content {
position: relative;
padding: 0;
overflow-y: auto;
max-height: 80vh;
flex: 1;
margin-bottom: 20px;
}
.mzta_dialog_btn {
background: #007bff;
border: none;
color: #ffffff;
font-size: 14px;
padding: 4px 6px;
cursor: pointer;
border-radius: 4px;
flex-shrink: 0;
}
.mzta_dialog_btn_margin{
margin-right: 5px;
}
.mzta_dialog_btn:hover {
background: #0056b3;
}
.mzta_dialog_message {
margin-bottom: 40px;
font-size: 14px;
}
.mzta_dialog_message2{
margin: 20px auto;
}
.div_btns{
width: 90%;
display: flex;
justify-content: center;
position: fixed;
bottom: 4px;
}
.tag_excluded{
text-decoration: line-through;
}
h2.addtags{
margin:0;
font-size: 1.2em;
text-align: center;
}
img.exclude-tag-icon{
width: 16px;
height: 16px;
margin-right: 2px;
margin-left: 2px;
position: relative;
top: 3px;
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
:root {
--dialog-bg-color: #2e2e2e;
--dialog-text-color: #ffffff;
}
}
`;
document.head.appendChild(style);
async function createDialog(inputTags, onSubmit) {
// Create the dialog
const tags_dialog = document.createElement('dialog');
tags_dialog.className = 'mzta_dialog';
const title = document.createElement('h2');
title.className = 'addtags';
title.textContent = browser.i18n.getMessage("addtags_dialog_title");
tags_dialog.appendChild(title);
// Create the content
const content = document.createElement('div');
content.className = 'mzta_dialog_content';
let no_submit = false;
// Fix the input array
const words = inputTags.map(word => word.trim()).filter(word => word !== '');
// console.log(">>>>>>>>>>>>> words: " + JSON.stringify(words));
if(words.length == 0){
const message = document.createElement('div');
message.className = 'mzta_dialog_message2';
message.textContent = browser.i18n.getMessage("addtags_no_tags_received");
content.appendChild(message);
no_submit = true;
}
let prefs_tags = await browser.storage.sync.get({add_tags_hide_exclusions: false, add_tags_exclusions_exact_match: false});
let add_tags_exclusions_list = await addTags_getExclusionList();
// console.log(">>>>>>>>>>>>> add_tags_exclusions_list: " + JSON.stringify(add_tags_exclusions_list));
const words_final = words
.filter(word => word !== '')
.map(word => {
const isExcluded = add_tags_exclusions_list.some(exclusion =>
checkExcludedTag(word, exclusion, prefs_tags.add_tags_exclusions_exact_match)
);
return {
tag: word,
excluded: isExcluded ? 1 : 0
};
});
// console.log(">>>>>>>>>>>>> words_final: " + JSON.stringify(words_final));
// Create the form
const form = document.createElement('form');
let tags_shown = 0;
words_final.forEach(word => {
const label = document.createElement('label');
label.style.display = 'block';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = true; // Checked by default
checkbox.value = word.tag;
// Add an image to the checkbox label
const img = document.createElement('img');
img.src = browser.runtime.getURL("/images/exclude-tag.svg");
img.alt = browser.i18n.getMessage("addtags_exclude_tag");
img.title = browser.i18n.getMessage("addtags_exclude_tag");
img.className = 'exclude-tag-icon';
img.addEventListener('click', () => {
if (!label.classList.contains('tag_excluded')) {
label.classList.add('tag_excluded');
if (!add_tags_exclusions_list.includes(word.tag)) {
add_tags_exclusions_list.push(word.tag);
addTags_setExclusionList(add_tags_exclusions_list);
}
} else {
label.classList.remove('tag_excluded');
const idx = add_tags_exclusions_list.indexOf(word.tag);
if (idx > -1) {
add_tags_exclusions_list.splice(idx, 1);
addTags_setExclusionList(add_tags_exclusions_list);
}
}
});
label.appendChild(checkbox);
label.appendChild(img);
label.appendChild(document.createTextNode(` ${word.tag}`));
if(!word.excluded || (word.excluded && !prefs_tags.add_tags_hide_exclusions)){
tags_shown++;
form.appendChild(label);
}
if(word.excluded){
label.className = 'tag_excluded';
checkbox.checked = false;
}
});
if((!no_submit) && (tags_shown == 0)){
const message = document.createElement('div');
message.className = 'mzta_dialog_message2';
message.textContent = browser.i18n.getMessage("addtags_no_valid_tags");
content.appendChild(message);
no_submit = true;
}
// Add a div for buttons
const buttonsDiv = document.createElement('div');
buttonsDiv.className = 'div_btns';
// Add the close button
const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.id = 'mzta_dialog_close';
closeButton.textContent = 'Close';
closeButton.className = 'mzta_dialog_btn mzta_dialog_btn_margin';
closeButton.addEventListener('click', closeDialog);
buttonsDiv.appendChild(closeButton);
if(!no_submit){
// Add the submit button
const submitButton = document.createElement('button');
submitButton.type = 'button';
submitButton.id = 'mzta_dialog_submit';
submitButton.textContent = 'Submit';
submitButton.className = 'mzta_dialog_btn';
submitButton.addEventListener('click', () => {
const selected = Array.from(form.querySelectorAll('input[type=checkbox]:checked')).map(cb => cb.value);
onSubmit(selected);
closeDialog();
});
buttonsDiv.appendChild(submitButton);
}
content.appendChild(form);
tags_dialog.appendChild(content);
tags_dialog.appendChild(buttonsDiv);
document.body.appendChild(tags_dialog);
function closeDialog() {
tags_dialog.close();
tags_dialog.addEventListener('close', () => {
tags_dialog.remove();
style.remove();
});
}
tags_dialog.showModal();
const iframeHeight = window.innerHeight;
const iframeWidth = window.innerWidth;
const dialogRect = tags_dialog.getBoundingClientRect();
const top = Math.max(0, (iframeHeight - dialogRect.height) / 2);
const left = Math.max(0, (iframeWidth - dialogRect.width) / 2);
tags_dialog.style.top = `${top}px`;
tags_dialog.style.left = `${left}px`;
tags_dialog.style.maxHeight = `${iframeHeight - 40}px`;
tags_dialog.style.transform = 'translate(0, 0)';
return true;
}
return createDialog(message.tags, (selected) => {
// console.log('>>>>>>>>>>>> Selected tags:', selected);
browser.runtime.sendMessage({ command: "assign_tags", tags: selected, messageId: message.messageId });
});
break;
case "showSpamCheckInProgress":
const oldBanner = document.getElementById('mzta-spam-report-banner');
if(oldBanner) oldBanner.remove();
if(document.getElementById('mzta-spam-check-progress')) return Promise.resolve(true);
const containerProgress = document.createElement('div');
containerProgress.id = 'mzta-spam-check-progress';
const isDarkProgress = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
let bgColorProgress = isDarkProgress ? '#003366' : '#e6f2ff';
let textColorProgress = isDarkProgress ? '#cce5ff' : '#004085';
let borderColorProgress = isDarkProgress ? '#004085' : '#b8daff';
containerProgress.style.cssText = `background-color: ${bgColorProgress}; color: ${textColorProgress}; border-bottom: 1px solid ${borderColorProgress}; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`;
const textProgress = document.createElement('strong');
textProgress.textContent = browser.i18n.getMessage("spam_check_in_progress");
const loadingImg = document.createElement('img');
loadingImg.src = browser.runtime.getURL("/images/loading.gif");
loadingImg.style.cssText = "height: 16px; width: 16px;";
const brandingProgress = document.createElement('span');
brandingProgress.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI";
brandingProgress.style.cssText = 'margin-left: auto; font-style: italic; font-size: 11px; opacity: 0.7;';
containerProgress.appendChild(loadingImg);
containerProgress.appendChild(textProgress);
containerProgress.appendChild(brandingProgress);
document.body.insertBefore(containerProgress, document.body.firstChild);
return Promise.resolve(true);
case "showSpamReport":
const progressBanner = document.getElementById('mzta-spam-check-progress');
if(progressBanner) progressBanner.remove();
const data = message.data;
if(document.getElementById('mzta-spam-report-banner')) return Promise.resolve(true);
const container = document.createElement('div');
container.id = 'mzta-spam-report-banner';
const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
let bgColor = '#f8f9fa';
let textColor = '#333';
let borderColor = '#ccc';
if (data.spamValue == -999) {
bgColor = isDark ? '#332701' : '#fff3cd';
textColor = isDark ? '#ffeb80' : '#856404';
borderColor = isDark ? '#664d03' : '#ffeeba';
} else if (data.spamValue >= (data.SpamThreshold || 50)) {
bgColor = isDark ? '#5a1a1a' : '#ffe6e6';
textColor = isDark ? '#ffcccc' : '#cc0000';
borderColor = '#cc0000';
} else {
bgColor = isDark ? '#1a401a' : '#e6ffe6';
textColor = isDark ? '#ccffcc' : '#006600';
borderColor = '#006600';
}
container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: start; gap: 15px; width: 100%; box-sizing: border-box;`;
const scoreText = document.createElement('strong');
if (data.spamValue == -999) {
scoreText.textContent = browser.i18n.getMessage("apiwebchat_error");
} else {
scoreText.textContent = ((data.spamValue >= (data.SpamThreshold || 50)) ? "⚠️ " + browser.i18n.getMessage("Spam") : "🛡️ " + browser.i18n.getMessage("Valid")) + " [" + data.spamValue + "/100]";
}
const reasonText = document.createElement('span');
if (data.spamValue == -999) {
reasonText.textContent = data.explanation;
} else {
reasonText.textContent = browser.i18n.getMessage("Explanation") + ": " + data.explanation;
}
const branding = document.createElement('span');
branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI";
branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 11px; opacity: 0.7;';
const closeBtn = document.createElement('span');
closeBtn.textContent = '×';
closeBtn.style.cssText = 'cursor: pointer; font-weight: bold; font-size: 16px; padding: 0 5px;';
closeBtn.title = browser.i18n.getMessage("chatgpt_win_close");
closeBtn.onclick = function() {
container.remove();
browser.runtime.sendMessage({ command: "removeSpamReport", headerMessageId: data.headerMessageId });
};
container.appendChild(scoreText);
container.appendChild(reasonText);
container.appendChild(branding);
container.appendChild(closeBtn);
document.body.insertBefore(container, document.body.firstChild);
return Promise.resolve(true);
default:
// do nothing
return Promise.resolve(false);
break;
}
});
browser.runtime.sendMessage({ command: "checkSpamReport" });