/* * 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 . */ // 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-container', '.mzta_dialog', ]; function getCleanBodyHtml() { const clone = document.body.cloneNode(true); for (const selector of MZTA_INJECTED_SELECTORS) { for (const el of clone.querySelectorAll(selector)) { el.remove(); } } for (const table of clone.querySelectorAll('table.moz-main-header')) { let sibling = table.previousElementSibling; while (sibling && sibling.tagName === 'DIV') { const toRemove = sibling; sibling = sibling.previousElementSibling; toRemove.remove(); } table.remove(); } return clone; } // ── Theme colors ──────────────────────────────────────────────────── function _getThemeColors(spamValue, spamThreshold) { const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches; const colors = { isDark, toolbar: { bg: isDark ? '#1e1e1e' : '#f5f5f5', text: isDark ? '#ddd' : '#333', border: isDark ? '#444' : '#ddd' }, spamLoading: { bg: isDark ? '#003366' : '#e6f2ff', text: isDark ? '#cce5ff' : '#004085', border: isDark ? '#004085' : '#b8daff' }, summary: { bg: isDark ? '#2a2a2a' : '#f0f0f0', text: isDark ? '#e0e0e0' : '#333', border: isDark ? '#444' : '#ddd' }, summaryErr: { bg: isDark ? '#3a1a1a' : '#f7e6e6', text: isDark ? '#ffcccc' : '#660000', border: '#660000' }, translation: { bg: isDark ? '#1a2e2a' : '#e8f5e9', text: isDark ? '#c8e6c9' : '#1b5e20', border: isDark ? '#2e5740' : '#a5d6a7' }, translErr: { bg: isDark ? '#3a1a1a' : '#f7e6e6', text: isDark ? '#ffcccc' : '#660000', border: '#660000' }, linkColor: isDark ? '#6db3f2' : '#1a5fa8', }; // Spam colors depend on the score if (spamValue !== undefined) { const threshold = spamThreshold || 50; if (spamValue == -999) { colors.spam = { bg: isDark ? '#332701' : '#fff3cd', text: isDark ? '#ffeb80' : '#856404', border: isDark ? '#664d03' : '#ffeeba' }; } else if (spamValue >= threshold) { colors.spam = { bg: isDark ? '#5a1a1a' : '#ffe6e6', text: isDark ? '#ffcccc' : '#cc0000', border: '#cc0000' }; } else { colors.spam = { bg: isDark ? '#1a401a' : '#e6ffe6', text: isDark ? '#ccffcc' : '#006600', border: '#006600' }; } } return colors; } // ── Container / Toolbar / Panels management ───────────────────────── function _ensureContainer() { let container = document.getElementById('mzta-container'); if (!container) { const colors = _getThemeColors(); container = document.createElement('div'); container.id = 'mzta-container'; container.style.cssText = 'font-family: system-ui, -apple-system, sans-serif;'; document.body.insertBefore(container, document.body.firstChild); const toolbar = document.createElement('div'); toolbar.id = 'mzta-toolbar'; toolbar.style.cssText = `display: none; align-items: center; gap: 8px; padding: 6px 0.5rem; background-color: ${colors.toolbar.bg}; border-bottom: 1px solid ${colors.toolbar.border}; font-size: 13px; color: ${colors.toolbar.text};`; container.appendChild(toolbar); const panels = document.createElement('div'); panels.id = 'mzta-panels'; panels.style.cssText = 'display: flex; flex-direction: column; gap: 4px; padding: 4px 0;'; container.appendChild(panels); } return { toolbar: document.getElementById('mzta-toolbar'), panels: document.getElementById('mzta-panels'), }; } function _updateToolbarVisibility() { const toolbar = document.getElementById('mzta-toolbar'); if (!toolbar) return; const hasItems = toolbar.querySelector('#mzta-toolbar-spam, #mzta-toolbar-summary, #mzta-toolbar-translation'); toolbar.style.display = hasItems ? 'flex' : 'none'; // Push the first button (summary or translation) to the right const summary = document.getElementById('mzta-toolbar-summary'); const translation = document.getElementById('mzta-toolbar-translation'); const firstBtn = summary || translation; if (summary) summary.style.marginLeft = (firstBtn === summary) ? 'auto' : ''; if (translation) translation.style.marginLeft = (firstBtn === translation) ? 'auto' : ''; } const _TOOLBAR_SLOT_ORDER = ['mzta-toolbar-spam', 'mzta-toolbar-summary', 'mzta-toolbar-translation']; function _addToolbarItem(id, element) { const { toolbar } = _ensureContainer(); const existing = document.getElementById(id); if (existing) existing.remove(); element.id = id; const myIndex = _TOOLBAR_SLOT_ORDER.indexOf(id); let insertBefore = null; for (let i = myIndex + 1; i < _TOOLBAR_SLOT_ORDER.length; i++) { const later = document.getElementById(_TOOLBAR_SLOT_ORDER[i]); if (later) { insertBefore = later; break; } } if (insertBefore) toolbar.insertBefore(element, insertBefore); else toolbar.appendChild(element); _updateToolbarVisibility(); } function _removeToolbarItem(id) { const el = document.getElementById(id); if (el) el.remove(); _updateToolbarVisibility(); } const _PANEL_ORDER = [ 'mzta-spam-check-progress', 'mzta-spam-report-banner', 'mzta-translation-generating', 'mzta-translation-banner', 'mzta-summary-generating', 'mzta-summary-banner' ]; function _addPanel(id, element) { const { panels } = _ensureContainer(); const existing = document.getElementById(id); if (existing) existing.remove(); element.id = id; const myIndex = _PANEL_ORDER.indexOf(id); let insertBefore = null; for (let i = myIndex + 1; i < _PANEL_ORDER.length; i++) { const later = document.getElementById(_PANEL_ORDER[i]); if (later) { insertBefore = later; break; } } if (insertBefore) panels.insertBefore(element, insertBefore); else panels.appendChild(element); _updatePanelMargins(); } function _removePanel(id) { const el = document.getElementById(id); if (el) el.remove(); _updatePanelMargins(); } function _updatePanelMargins() { const panels = document.getElementById('mzta-panels'); if (!panels) return; let lastPanel = null; for (const child of panels.children) { child.style.marginBottom = ''; lastPanel = child; } if (lastPanel) lastPanel.style.marginBottom = '1rem'; } function createThreeDotsMenu(isDark, menuItems, panelColors) { const wrapper = document.createElement('div'); wrapper.style.cssText = 'position: relative; display: inline-flex; align-items: center;'; const dotsBtn = document.createElement('span'); dotsBtn.textContent = '\u22EE'; dotsBtn.style.cssText = 'cursor: pointer; opacity: 0.7; font-size: 18px; padding: 2px 7px; line-height: 1; user-select: none; transition: opacity 0.2s; display: flex; align-items: center;'; dotsBtn.onmouseover = () => dotsBtn.style.opacity = '1'; dotsBtn.onmouseout = () => dotsBtn.style.opacity = '0.7'; const dropdown = document.createElement('div'); const dropdownBg = panelColors.bg; const dropdownBorder = panelColors.border; const defaultTextColor = panelColors.text; dropdown.style.cssText = `display: none; position: absolute; right: 0; top: 100%; z-index: 9999; min-width: 180px; background-color: ${dropdownBg}; border: 1px solid ${dropdownBorder}; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); overflow: hidden;`; menuItems.forEach(item => { const row = document.createElement('div'); row.style.cssText = `display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; font-size: 13px; color: ${defaultTextColor}; transition: background-color 0.15s, color 0.15s;`; const iconSpan = document.createElement('span'); iconSpan.textContent = item.icon; iconSpan.style.cssText = 'font-size: 15px; width: 18px; text-align: center;'; const labelSpan = document.createElement('span'); labelSpan.textContent = item.label; row.appendChild(iconSpan); row.appendChild(labelSpan); const hoverBg = item.hoverColor === '#cc0000' ? (isDark ? 'rgba(204,0,0,0.2)' : 'rgba(204,0,0,0.1)') : (isDark ? 'rgba(77,157,224,0.2)' : 'rgba(26,95,168,0.1)'); row.onmouseover = () => { row.style.backgroundColor = hoverBg; row.style.color = item.hoverColor; }; row.onmouseout = () => { row.style.backgroundColor = ''; row.style.color = defaultTextColor; }; row.onclick = (e) => { e.stopPropagation(); dropdown.style.display = 'none'; if (item.disableAfterClick) { row.onclick = null; row.style.opacity = '0.5'; row.style.pointerEvents = 'none'; } item.onClick(); }; dropdown.appendChild(row); }); dotsBtn.onclick = (e) => { e.stopPropagation(); dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; }; document.addEventListener('click', (e) => { if (!wrapper.contains(e.target)) { dropdown.style.display = 'none'; } }, true); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && dropdown.style.display !== 'none') { dropdown.style.display = 'none'; } }); wrapper.appendChild(dotsBtn); wrapper.appendChild(dropdown); return wrapper; } 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 = getCleanBodyHtml().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(getCleanBodyHtml().innerText); } case "getFullHtml": { return Promise.resolve(getCleanBodyHtml().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": { _removePanel('mzta-spam-report-banner'); _removeToolbarItem('mzta-toolbar-spam'); if (document.getElementById('mzta-spam-check-progress')) return Promise.resolve(true); const colors = _getThemeColors(); // Loading badge in toolbar const badge = document.createElement('div'); badge.style.cssText = `background-color: ${colors.spamLoading.bg}; color: ${colors.spamLoading.text}; border: 1px solid ${colors.spamLoading.border}; border-radius: 4px; padding: 2px 8px; font-size: 12px; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;`; const badgeLoading = document.createElement('img'); badgeLoading.src = browser.runtime.getURL("/images/loading.gif"); badgeLoading.style.cssText = "height: 14px; width: 14px;"; badge.appendChild(badgeLoading); const badgeText = document.createElement('span'); badgeText.textContent = browser.i18n.getMessage("spam_check_in_progress"); badge.appendChild(badgeText); _addToolbarItem('mzta-toolbar-spam', badge); return Promise.resolve(true); } case "showSpamReport": { _removePanel('mzta-spam-check-progress'); _removeToolbarItem('mzta-toolbar-spam'); const data = message.data; if (document.getElementById('mzta-spam-report-banner')) return Promise.resolve(true); const colors = _getThemeColors(data.spamValue, data.SpamThreshold); const sc = colors.spam; // Spam badge in toolbar (clickable to toggle explanation panel) const badge = document.createElement('div'); badge.title = browser.i18n.getMessage("spam_badge_tooltip"); badge.style.cssText = `background-color: ${sc.bg}; color: ${sc.text}; border: 1px solid ${sc.border}; border-radius: 4px; padding: 2px 8px; font-size: 12px; font-weight: bold; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; white-space: nowrap; transition: opacity 0.2s;`; if (data.spamValue == -999) { badge.textContent = browser.i18n.getMessage("apiwebchat_error"); } else { badge.textContent = ((data.spamValue >= (data.SpamThreshold || 50)) ? "\u26A0\uFE0F " + browser.i18n.getMessage("Spam") : "\uD83D\uDEE1\uFE0F " + browser.i18n.getMessage("Valid")) + " [" + data.spamValue + "/100]"; } const chevron = document.createElement('span'); chevron.textContent = ' \u25BC'; chevron.style.cssText = 'font-size: 10px; transition: transform 0.2s; transform: rotate(-90deg);'; badge.appendChild(chevron); let spamExpanded = false; badge.onclick = () => { const panel = document.getElementById('mzta-spam-report-banner'); if (panel) { spamExpanded = !spamExpanded; panel.style.display = spamExpanded ? 'flex' : 'none'; chevron.style.transform = spamExpanded ? '' : 'rotate(-90deg)'; } }; badge.onmouseover = () => { badge.style.opacity = '0.8'; }; badge.onmouseout = () => { badge.style.opacity = '1'; }; _addToolbarItem('mzta-toolbar-spam', badge); // Explanation panel (collapsed by default) const panel = document.createElement('div'); panel.style.cssText = `background-color: ${sc.bg}; color: ${sc.text}; border: 1px solid ${sc.border}; border-radius: 4px; padding: 8px 0.5rem; font-size: 13px; display: none; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`; const reasonText = document.createElement('span'); if (data.spamValue == -999) { reasonText.textContent = data.explanation; } else { reasonText.textContent = browser.i18n.getMessage("Explanation") + ": " + data.explanation; } const spamMenu = createThreeDotsMenu(colors.isDark, [ { icon: '\u21BB', label: browser.i18n.getMessage("spamfilter_refresh") || 'Refresh spam report', hoverColor: colors.isDark ? '#4d9de0' : '#1a5fa8', disableAfterClick: true, onClick: () => { browser.runtime.sendMessage({ command: "refreshSpamReport", headerMessageId: data.headerMessageId }); } }, { icon: '\u00D7', label: browser.i18n.getMessage("spamfilter_delete") || 'Delete spam report', hoverColor: '#cc0000', onClick: () => { _removePanel('mzta-spam-report-banner'); _removeToolbarItem('mzta-toolbar-spam'); browser.runtime.sendMessage({ command: "removeSpamReport", headerMessageId: data.headerMessageId }); } } ], { bg: sc.bg, border: sc.border, text: sc.text }); const rightGroup = document.createElement('span'); rightGroup.style.cssText = 'margin-left: auto; display: flex; align-items: center; gap: 5px;'; const branding = document.createElement('span'); branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI"; branding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5;'; rightGroup.appendChild(branding); rightGroup.appendChild(spamMenu); panel.appendChild(reasonText); panel.appendChild(rightGroup); _addPanel('mzta-spam-report-banner', panel); return Promise.resolve(true); } case "showSummary": { _removePanel('mzta-summary-generating'); _removePanel('mzta-summary-banner'); _removeToolbarItem('mzta-toolbar-summary'); const summaryData = message.data; const colors = _getThemeColors(); const sc = summaryData.error ? colors.summaryErr : colors.summary; const summaryContainer = document.createElement('div'); summaryContainer.className = 'thunderai-summary-pane'; summaryContainer.style.cssText = `background-color: ${sc.bg}; color: ${sc.text}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${sc.border}; font-size: 14px;`; const summaryMenu = createThreeDotsMenu(colors.isDark, [ { icon: '\u21BB', label: browser.i18n.getMessage("summarize_refresh") || 'Refresh summary', hoverColor: colors.isDark ? '#4d9de0' : '#1a5fa8', disableAfterClick: true, onClick: () => { browser.runtime.sendMessage({ command: "refreshSummary", headerMessageId: summaryData.headerMessageId }); } }, { icon: '\u00D7', label: browser.i18n.getMessage("summarize_delete") || 'Delete summary', hoverColor: '#cc0000', onClick: () => { _removePanel('mzta-summary-banner'); browser.runtime.sendMessage({ command: "removeSummary", headerMessageId: summaryData.headerMessageId }); } } ], { bg: sc.bg, border: sc.border, text: sc.text }); const summaryBranding = document.createElement('span'); summaryBranding.textContent = browser.i18n.getMessage("summary_by") + " ThunderAI"; summaryBranding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5; white-space: nowrap;'; const summaryRightGroup = document.createElement('span'); summaryRightGroup.style.cssText = 'display: flex; align-items: center; gap: 5px; float: right; margin-left: 10px;'; summaryRightGroup.appendChild(summaryBranding); summaryRightGroup.appendChild(summaryMenu); const summaryIcon = document.createElement('img'); summaryIcon.src = browser.runtime.getURL("/images/ai_summary.png"); summaryIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0; margin-top: 2px;${colors.isDark ? ' filter: invert(1);' : ''}`; const summaryTextWrapper = document.createElement('div'); summaryTextWrapper.style.cssText = 'flex: 1; min-width: 0;'; summaryTextWrapper.appendChild(summaryRightGroup); const summaryText = document.createElement('div'); summaryText.className = 'thunderai-summary-content'; const hasHtml = !!summaryData.summary_html; function setSummaryHtml(element, html) { element.textContent = ''; const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); while (doc.body.firstChild) { element.appendChild(doc.body.firstChild); } element.querySelectorAll('p').forEach(p => { p.style.marginBlockStart = '0'; }); } if (summaryData.error) { summaryText.textContent = summaryData.message || browser.i18n.getMessage("summarize_error"); } else if (hasHtml) { setSummaryHtml(summaryText, summaryData.summary_html); } else { summaryText.textContent = summaryData.summary; } summaryText.style.cssText = 'font-size: 14px; line-height: 1.4;'; summaryTextWrapper.appendChild(summaryText); const maxLen = summaryData.maxDisplayLength || 0; const fullText = summaryData.summary; if (!summaryData.error && maxLen > 0 && fullText && fullText.length > maxLen) { summaryText.style.overflow = 'hidden'; summaryText.style.transition = 'max-height 0.2s ease'; if (!hasHtml) { let cutPos = fullText.lastIndexOf(' ', maxLen); if (cutPos <= 0) cutPos = maxLen; const truncated = fullText.substring(0, cutPos) + '\u2026'; summaryText.textContent = truncated; requestAnimationFrame(() => { summaryText.style.maxHeight = summaryText.scrollHeight + 'px'; }); const toggleLink = document.createElement('a'); toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; toggleLink.href = '#'; toggleLink.style.cssText = `display: inline-block; margin-top: 4px; font-size: 13px; color: ${colors.linkColor}; cursor: pointer; text-decoration: underline;`; let expanded = false; toggleLink.addEventListener('click', (e) => { e.preventDefault(); if (!expanded) { summaryText.textContent = fullText; summaryText.style.maxHeight = summaryText.scrollHeight + 'px'; toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less"; } else { summaryText.textContent = truncated; const collapsedHeight = summaryText.scrollHeight; summaryText.textContent = fullText; summaryText.style.maxHeight = summaryText.scrollHeight + 'px'; requestAnimationFrame(() => { summaryText.style.maxHeight = collapsedHeight + 'px'; }); summaryText.addEventListener('transitionend', function handler() { summaryText.removeEventListener('transitionend', handler); summaryText.textContent = truncated; }); toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; } expanded = !expanded; }); summaryTextWrapper.appendChild(toggleLink); } else { const collapsedMaxHeight = '4.2em'; summaryText.style.maxHeight = collapsedMaxHeight; const toggleLink = document.createElement('a'); toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; toggleLink.href = '#'; toggleLink.style.cssText = `display: inline-block; margin-top: 4px; font-size: 13px; color: ${colors.linkColor}; cursor: pointer; text-decoration: underline;`; let expanded = false; toggleLink.addEventListener('click', (e) => { e.preventDefault(); if (!expanded) { summaryText.style.maxHeight = summaryText.scrollHeight + 'px'; toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less"; } else { summaryText.style.maxHeight = collapsedMaxHeight; toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; } expanded = !expanded; }); requestAnimationFrame(() => { if (summaryText.scrollHeight > summaryText.clientHeight) { summaryTextWrapper.appendChild(toggleLink); } else { summaryText.style.maxHeight = ''; summaryText.style.overflow = ''; } }); } } const summaryBody = document.createElement('div'); summaryBody.style.cssText = 'display: flex; gap: 8px; align-items: flex-start;'; summaryBody.appendChild(summaryIcon); summaryBody.appendChild(summaryTextWrapper); summaryContainer.appendChild(summaryBody); _addPanel('mzta-summary-banner', summaryContainer); return Promise.resolve(true); } case "showSummaryGenerating": { if (document.getElementById('mzta-summary-generating')) return Promise.resolve(true); _removePanel('mzta-summary-banner'); _removeToolbarItem('mzta-toolbar-summary'); const colors = _getThemeColors(); const genContainer = document.createElement('div'); genContainer.className = 'thunderai-summary-pane'; genContainer.style.cssText = `background-color: ${colors.summary.bg}; color: ${colors.summary.text}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${colors.summary.border}; font-size: 14px; display: flex; align-items: center; gap: 10px;`; const genIcon = document.createElement('img'); genIcon.src = browser.runtime.getURL("/images/ai_summary.png"); genIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${colors.isDark ? ' filter: invert(1);' : ''}`; const genLoading = document.createElement('img'); genLoading.src = browser.runtime.getURL("/images/loading.gif"); genLoading.style.cssText = "height: 16px; width: 16px;"; const genTitle = document.createElement('span'); genTitle.className = 'thunderai-summary-title'; genTitle.textContent = browser.i18n.getMessage("summarize_generating"); genTitle.style.cssText = 'font-size: 14px;'; genContainer.appendChild(genIcon); genContainer.appendChild(genLoading); genContainer.appendChild(genTitle); _addPanel('mzta-summary-generating', genContainer); return Promise.resolve(true); } case "showSummaryButton": { if (document.getElementById('mzta-toolbar-summary')) return Promise.resolve(true); const colors = _getThemeColors(); const triggerBtn = document.createElement('div'); triggerBtn.title = browser.i18n.getMessage("summarize_click_to_generate"); triggerBtn.style.cssText = `background-color: ${colors.summary.bg}; border: 1px solid ${colors.summary.border}; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${colors.summary.text}; display: inline-flex; align-items: center; gap: 6px;`; const triggerIcon = document.createElement('img'); triggerIcon.src = browser.runtime.getURL("/images/ai_summary.png"); triggerIcon.style.cssText = `height: 14px; width: 14px;${colors.isDark ? ' filter: invert(1);' : ''}`; triggerBtn.appendChild(triggerIcon); const triggerLabel = document.createElement('span'); triggerLabel.textContent = browser.i18n.getMessage("get_ai_summary"); triggerBtn.appendChild(triggerLabel); triggerBtn.onmouseover = () => { triggerBtn.style.opacity = '1'; }; triggerBtn.onmouseout = () => { triggerBtn.style.opacity = '0.7'; }; triggerBtn.onclick = () => { _removeToolbarItem('mzta-toolbar-summary'); browser.runtime.sendMessage({ command: message.webchat ? "triggerSummaryWebchat" : "triggerSummaryGeneration", headerMessageId: message.headerMessageId }); }; _addToolbarItem('mzta-toolbar-summary', triggerBtn); return Promise.resolve(true); } case "showTranslation": { _removePanel('mzta-translation-generating'); _removePanel('mzta-translation-banner'); _removeToolbarItem('mzta-toolbar-translation'); const translationData = message.data; const colors = _getThemeColors(); const tc = translationData.error ? colors.translErr : colors.translation; const translationContainer = document.createElement('div'); translationContainer.className = 'thunderai-translation-pane'; translationContainer.style.cssText = `background-color: ${tc.bg}; color: ${tc.text}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${tc.border}; font-size: 14px;`; const translationHeader = document.createElement('div'); translationHeader.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 6px;'; const translationIcon = document.createElement('img'); translationIcon.src = browser.runtime.getURL("/images/ai_translate.png"); translationIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${colors.isDark ? ' filter: invert(1);' : ''}`; const translationTitleSpan = document.createElement('span'); translationTitleSpan.style.cssText = `font-weight: bold; font-size: 14px; color: ${tc.text}; flex-grow: 1;`; translationTitleSpan.textContent = browser.i18n.getMessage("translate_banner_title") || "AI Translation"; if (translationData.lang) { translationTitleSpan.textContent += ' (' + translationData.lang + ')'; } const translationMenu = createThreeDotsMenu(colors.isDark, [ { icon: '\u21BB', label: browser.i18n.getMessage("translate_refresh") || 'Refresh translation', hoverColor: colors.isDark ? '#4d9de0' : '#1a5fa8', disableAfterClick: true, onClick: () => { browser.runtime.sendMessage({ command: "refreshTranslation", headerMessageId: translationData.headerMessageId }); } }, { icon: '\u00D7', label: browser.i18n.getMessage("translate_delete") || 'Delete translation', hoverColor: '#cc0000', onClick: () => { _removePanel('mzta-translation-banner'); browser.runtime.sendMessage({ command: "removeTranslation", headerMessageId: translationData.headerMessageId }); } } ], { bg: tc.bg, border: tc.border, text: tc.text }); const translationBranding = document.createElement('span'); translationBranding.textContent = browser.i18n.getMessage("translate_by") + " ThunderAI"; translationBranding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5; white-space: nowrap;'; translationHeader.appendChild(translationIcon); translationHeader.appendChild(translationTitleSpan); translationHeader.appendChild(translationBranding); translationHeader.appendChild(translationMenu); translationContainer.appendChild(translationHeader); const translationTextWrapper = document.createElement('div'); translationTextWrapper.style.cssText = 'flex-grow: 1;'; const translationText = document.createElement('div'); translationText.style.cssText = 'white-space: pre-wrap; line-height: 1.5;'; if (translationData.error) { translationText.textContent = translationData.message || browser.i18n.getMessage("translate_error"); } else if (translationData.translation_status === '-1') { translationText.textContent = browser.i18n.getMessage("translate_skipped"); } else { if (translationData.translated_subject) { const subjectEl = document.createElement('div'); subjectEl.style.cssText = 'font-weight: bold; margin-bottom: 4px;'; subjectEl.textContent = translationData.translated_subject; translationTextWrapper.appendChild(subjectEl); } translationText.textContent = translationData.translated_text || ''; } translationTextWrapper.appendChild(translationText); const maxLenTranslation = translationData.maxDisplayLength || 0; const fullTranslationText = translationData.translated_text || ''; if (!translationData.error && translationData.translation_status !== '-1' && maxLenTranslation > 0 && fullTranslationText.length > maxLenTranslation) { translationText.style.overflow = 'hidden'; translationText.style.transition = 'max-height 0.2s ease'; let cutPos = fullTranslationText.lastIndexOf(' ', maxLenTranslation); if (cutPos <= 0) cutPos = maxLenTranslation; const truncatedTranslation = fullTranslationText.substring(0, cutPos) + '\u2026'; translationText.textContent = truncatedTranslation; requestAnimationFrame(() => { translationText.style.maxHeight = translationText.scrollHeight + 'px'; }); const toggleLink = document.createElement('a'); toggleLink.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; toggleLink.href = '#'; toggleLink.style.cssText = `display: inline-block; margin-top: 4px; font-size: 13px; color: ${colors.linkColor}; cursor: pointer; text-decoration: underline;`; let expanded = false; toggleLink.addEventListener('click', (e) => { e.preventDefault(); if (!expanded) { translationText.textContent = fullTranslationText; translationText.style.maxHeight = translationText.scrollHeight + 'px'; toggleLink.textContent = browser.i18n.getMessage("translate_see_less") || "See less"; } else { translationText.textContent = truncatedTranslation; const collapsedHeight = translationText.scrollHeight; translationText.textContent = fullTranslationText; translationText.style.maxHeight = translationText.scrollHeight + 'px'; requestAnimationFrame(() => { translationText.style.maxHeight = collapsedHeight + 'px'; }); translationText.addEventListener('transitionend', function handler() { translationText.removeEventListener('transitionend', handler); translationText.textContent = truncatedTranslation; }); toggleLink.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; } expanded = !expanded; }); translationTextWrapper.appendChild(toggleLink); } translationContainer.appendChild(translationTextWrapper); _addPanel('mzta-translation-banner', translationContainer); return Promise.resolve(true); } case "showTranslationGenerating": { if (document.getElementById('mzta-translation-generating')) return Promise.resolve(true); _removePanel('mzta-translation-banner'); _removeToolbarItem('mzta-toolbar-translation'); const colors = _getThemeColors(); const genContainer = document.createElement('div'); genContainer.className = 'thunderai-translation-pane'; genContainer.style.cssText = `background-color: ${colors.translation.bg}; color: ${colors.translation.text}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${colors.translation.border}; font-size: 14px; display: flex; align-items: center; gap: 10px;`; const genIcon = document.createElement('img'); genIcon.src = browser.runtime.getURL("/images/ai_translate.png"); genIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${colors.isDark ? ' filter: invert(1);' : ''}`; const genLoading = document.createElement('img'); genLoading.src = browser.runtime.getURL("/images/loading.gif"); genLoading.style.cssText = "height: 16px; width: 16px;"; const genTitle = document.createElement('span'); genTitle.textContent = browser.i18n.getMessage("translate_generating") || "Translating..."; genTitle.style.cssText = 'font-size: 14px;'; genContainer.appendChild(genIcon); genContainer.appendChild(genLoading); genContainer.appendChild(genTitle); _addPanel('mzta-translation-generating', genContainer); return Promise.resolve(true); } case "showTranslationButton": { if (document.getElementById('mzta-toolbar-translation')) return Promise.resolve(true); const colors = _getThemeColors(); const triggerBtn = document.createElement('div'); triggerBtn.title = browser.i18n.getMessage("translate_click_to_generate") || "Click to translate this email"; triggerBtn.style.cssText = `background-color: ${colors.translation.bg}; border: 1px solid ${colors.translation.border}; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${colors.translation.text}; display: inline-flex; align-items: center; gap: 6px;`; const triggerIcon = document.createElement('img'); triggerIcon.src = browser.runtime.getURL("/images/ai_translate.png"); triggerIcon.style.cssText = `height: 14px; width: 14px;${colors.isDark ? ' filter: invert(1);' : ''}`; triggerBtn.appendChild(triggerIcon); const triggerLabel = document.createElement('span'); triggerLabel.textContent = browser.i18n.getMessage("get_ai_translation") || "Get AI Translation"; triggerBtn.appendChild(triggerLabel); triggerBtn.onmouseover = () => { triggerBtn.style.opacity = '1'; }; triggerBtn.onmouseout = () => { triggerBtn.style.opacity = '0.7'; }; triggerBtn.onclick = () => { _removeToolbarItem('mzta-toolbar-translation'); browser.runtime.sendMessage({ command: message.webchat ? "triggerTranslationWebchat" : "triggerTranslationGeneration", headerMessageId: message.headerMessageId }); }; _addToolbarItem('mzta-toolbar-translation', triggerBtn); return Promise.resolve(true); } default: // do nothing return Promise.resolve(false); break; } }); browser.runtime.sendMessage({ command: "checkSpamReport" }); browser.runtime.sendMessage({ command: "initSummary" }); browser.runtime.sendMessage({ command: "initTranslation" });