// ── Toolbar button: open settings page ── browser.browserAction.onClicked.addListener(() => { browser.runtime.openOptionsPage(); }); // ── "Erledigt" button in message display ── async function executeErledigtAction(tab, actionConfig) { const message = await messenger.messageDisplay.getDisplayedMessage(tab.id); if (!message) { browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL('icons/icon.png'), title: 'Fehler', message: 'Keine Nachricht ausgewählt.' }); return; } const storage = await browser.storage.local.get(['gitea_config', 'schlagwoerter_cache']); const config = storage.gitea_config || {}; const schlagwoerter = storage.schlagwoerter_cache; // "Als gelesen markieren" — pro Aktion konfigurierbar, standardmäßig an. const markRead = (actionConfig || {}).markRead !== false; // Apply user's tag let tagKey = null; if (Array.isArray(schlagwoerter) && config.authorName) { const match = schlagwoerter.find(u => u.name.toLowerCase() === config.authorName.toLowerCase()); if (match) { tagKey = `$hps_${match.name.toLowerCase().replace(/\s+/g, '_')}`; } } if (tagKey) { const currentTags = message.tags || []; if (!currentTags.includes(tagKey)) { await messenger.messages.update(message.id, { tags: [...currentTags, tagKey] }); } } // Mark as read (before the move, while the id is still valid in this folder) if (markRead && !message.read) { await messenger.messages.update(message.id, { read: true }); } // Move to target folder if (actionConfig.targetFolder) { const folderInfo = JSON.parse(actionConfig.targetFolder); await messenger.messages.move([message.id], folderInfo); } // Feedback const parts = []; if (tagKey) parts.push('markiert'); if (markRead && !message.read) parts.push('gelesen'); if (actionConfig.targetFolder) parts.push('verschoben'); const title = actionConfig.name || 'Erledigt'; browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL('icons/icon.png'), title, message: parts.length ? `Nachricht ${parts.join(' & ')}.` : 'Kein Schlagwort oder Zielordner konfiguriert.' }); } // Single action: direct click without popup messenger.messageDisplayAction.onClicked.addListener(async (tab) => { try { const result = await browser.storage.local.get('erledigt_config'); const actions = (result.erledigt_config || {}).actions || []; await executeErledigtAction(tab, actions[0] || {}); } catch (e) { console.error('Erledigt-Button Fehler:', e); browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL('icons/icon.png'), title: 'Fehler', message: e.message }); } }); // Toggle popup vs direct click based on action count async function updateErledigtPopup() { const result = await browser.storage.local.get('erledigt_config'); const actions = (result.erledigt_config || {}).actions || []; const setLabel = (label) => { // setLabel ist Thunderbird-spezifisch — defensiv prüfen, damit der Start nie bricht. if (messenger.messageDisplayAction.setLabel) return messenger.messageDisplayAction.setLabel({ label }); }; if (actions.length > 1) { await messenger.messageDisplayAction.setPopup({ popup: 'message_popup.html' }); await messenger.messageDisplayAction.setTitle({ title: 'Aktion wählen' }); await setLabel('QuickMove'); } else { const name = actions[0]?.name || 'QuickMove'; await messenger.messageDisplayAction.setPopup({ popup: '' }); await messenger.messageDisplayAction.setTitle({ title: name }); await setLabel(name); // Button heißt wie die (einzige) Aktion } } // Update on config change browser.storage.onChanged.addListener((changes, area) => { if (area === 'local' && changes.erledigt_config) updateErledigtPopup(); }); updateErledigtPopup(); // ── about:config-Prefs bei jedem Start erzwingen ── // // Manche Thunderbird-Einstellungen (Startseite, Ansicht, Layout, pro Konto das // Signatur-/Antwort-Verhalten) „springen zurück" und müssen sonst von Hand // nachgezogen werden. Wir setzen sie bei JEDEM Start neu über den privilegierten // forcedPrefs-Experiment (Services.prefs). Die Liste liegt in // lib/forced-prefs-list.js (globale FORCED_PREFS / FORCED_IDENTITY_PREFS). // Pro Eintrag kann der Client im Options-Dialog opt-outen (forced_prefs_optout). const FORCED_PREFS_OPTOUT_KEY = 'forced_prefs_optout'; async function enforceForcedPrefs() { const stored = await browser.storage.local.get(FORCED_PREFS_OPTOUT_KEY); const disabled = new Set(stored[FORCED_PREFS_OPTOUT_KEY] || []); // Globale Prefs for (const p of FORCED_PREFS) { if (disabled.has(p.id)) continue; try { await browser.forcedPrefs.setPref(p.name, p.type, p.value); } catch (e) { console.error('[forcedPrefs] konnte', p.name, 'nicht setzen:', e); } } // Pro-Identität-Prefs — über alle Konten/Identitäten iterieren const accounts = await browser.accounts.list(); for (const acc of accounts) { for (const ident of (acc.identities || [])) { for (const p of FORCED_IDENTITY_PREFS) { if (disabled.has(p.id)) continue; const name = `mail.identity.${ident.id}.${p.suffix}`; try { await browser.forcedPrefs.setPref(name, p.type, p.value); } catch (e) { console.error('[forcedPrefs] konnte', name, 'nicht setzen:', e); } } } } } enforceForcedPrefs(); // Top-Level = läuft einmal pro TB-Start // Wird eine Pref im Options-Dialog wieder AKTIVIERT, sofort anwenden // (statt bis zum nächsten Neustart zu warten). browser.storage.onChanged.addListener((changes, area) => { if (area === 'local' && changes[FORCED_PREFS_OPTOUT_KEY]) enforceForcedPrefs(); }); // ── Signatur in neuen Compose-Fenstern automatisch aktualisieren ── // // Thunderbird merkt sich die Signatur einer Identität pro Sitzung. Öffnet der // Benutzer nach einem Hintergrund-Sync ein neues Verfassen-Fenster, steckt darin // noch die ALTE Signatur (alter Footer) — `identities.update({signature})` // schreibt zwar den Pref, aber das Compose-Fenster fügt die gecachte Fassung ein. // Ein Event „neues Compose-Fenster" gibt es in der WebExtension-API nicht, daher // hängen wir uns an tabs.onCreated und tauschen NUR den Footer-Teil (ab dem // SIG_FOOTER_START-Marker) im eingefügten Signaturblock gegen den frisch // gesyncten aus. Header, Trenner ("-- ") und Struktur bleiben unangetastet. // Vollautomatisch, ohne jede Benutzeraktion. const SIG_FOOTER_MARK = ''; const sigSleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function getFreshFooter() { const c = await browser.storage.local.get('sig_footer_cache'); return c.sig_footer_cache || ''; } async function refreshComposeSignature(tabId, tabType) { // Nicht-Compose-Tabs sofort ignorieren, wenn der Typ bekannt ist. if (tabType && tabType !== 'messageCompose') return; const footer = await getFreshFooter(); if (!footer) return; // Läuft IM Compose-Dokument: findet den Signaturblock, ersetzt nur den Footer. const code = `(function(){ const editor = document.getElementById('messageEditor'); const doc = editor ? editor.contentDocument : document; const sig = doc.querySelector('.moz-signature'); if (!sig) return 'notready'; const MARK = ${JSON.stringify(SIG_FOOTER_MARK)}; const cur = sig.innerHTML; const i = cur.indexOf(MARK); if (i === -1) return 'unmanaged'; // keine vom Plugin verwaltete Signatur const before = cur.slice(0, i + MARK.length); // "--
" + Header + Marker const curFooter = cur.slice(i + MARK.length).replace(/^\\s+/, ''); const NEW = ${JSON.stringify(footer)}; if (curFooter === NEW) return 'current'; // schon aktuell, nichts tun sig.innerHTML = before + '\\n' + NEW; return 'updated'; })();`; // Thunderbird fügt die Signatur evtl. erst kurz nach dem Öffnen ein → kurz pollen. let throwCount = 0; for (let attempt = 0; attempt < 20; attempt++) { let details; try { details = await browser.compose.getComposeDetails(tabId); } catch (_) { // Kein Compose-Tab oder noch nicht bereit. Bei bekanntem Typ weiter pollen, // sonst nach ein paar Fehlversuchen aufgeben. if (tabType !== 'messageCompose' && ++throwCount > 3) return; await sigSleep(150); continue; } if (!details) { await sigSleep(150); continue; } if (details.isPlainText) return; // Footer ist HTML — Plaintext nicht anfassen if (details.type === 'draft') return; // gespeicherte Entwürfe nicht überschreiben let res; try { const out = await browser.tabs.executeScript(tabId, { code }); res = Array.isArray(out) ? out[0] : out; } catch (_) { return; // Fenster bereits geschlossen o. Ä. } if (res !== 'notready') return; // fertig: updated / current / unmanaged await sigSleep(150); } } browser.tabs.onCreated.addListener((tab) => { refreshComposeSignature(tab.id, tab && tab.type).catch((err) => console.error('[Sig] Compose-Refresh fehlgeschlagen:', err)); }); // ── Template insertion ── browser.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.action === 'erledigtAction') { (async () => { try { const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); const result = await browser.storage.local.get('erledigt_config'); const actions = (result.erledigt_config || {}).actions || []; const action = actions[msg.index] || {}; await executeErledigtAction(tab, action); sendResponse({ success: true }); } catch (e) { console.error('Erledigt-Action Fehler:', e); browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL('icons/icon.png'), title: 'Fehler', message: e.message }); sendResponse({ success: false, error: e.message }); } })(); return true; } if (msg.action !== 'insertTemplate') return; handleInsertTemplate(msg).then(() => sendResponse()) .catch(err => sendResponse({ error: err.message })); return true; // keep channel open for async response }); async function handleInsertTemplate(msg) { try { const [tab] = await browser.tabs.query({ active: true, currentWindow: true, windowType: 'messageCompose', }); if (!tab) throw new Error('Kein Compose-Fenster gefunden'); const details = await browser.compose.getComposeDetails(tab.id); const isHtmlTemplate = msg.text.includes('<'); // If compose is plain text but template is HTML, switch to HTML mode first if (details.isPlainText && isHtmlTemplate) { await browser.compose.setComposeDetails(tab.id, { isPlainText: false }); } if (details.isPlainText && !isHtmlTemplate) { // Plain text template in plain text mode - use old method const old = details.plainTextBody || ''; const newBody = msg.text + (old ? '\n' + old : ''); await browser.compose.setComposeDetails(tab.id, { plainTextBody: newBody }); } else { // HTML mode: wrap the template in a marker element (id=hps-vorlage) and // replace any previously inserted one — so durchswitchen ersetzt statt stapelt. const htmlContent = isHtmlTemplate ? msg.text : msg.text.replace(/\n/g, '
'); // Directly manipulate the editor DOM: remove the old template block (if any), // then insert the new one at the top of the body. Returns true on success. const injected = await browser.tabs.executeScript(tab.id, { code: ` (function () { const editor = document.getElementById('messageEditor'); const editorDoc = editor ? editor.contentDocument : document; const body = editorDoc && editorDoc.body; if (!body) return false; // Remove a previously inserted template block, so switching replaces it const prev = editorDoc.getElementById('hps-vorlage'); if (prev) prev.remove(); // Wrap the new template in a marker element with a stable id const wrapper = editorDoc.createElement('div'); wrapper.id = 'hps-vorlage'; wrapper.innerHTML = ${JSON.stringify(htmlContent)}; // Insert at the very top of the body (above quote/signature/typed text) body.insertBefore(wrapper, body.firstChild); // Place the cursor right after the inserted block const range = editorDoc.createRange(); range.setStartAfter(wrapper); range.collapse(true); const sel = editorDoc.getSelection(); sel.removeAllRanges(); sel.addRange(range); return true; })(); ` }).then(r => (Array.isArray(r) ? r[0] : r)).catch(() => false); if (!injected) { // Fallback: editor DOM not reachable (TB version differences) — use the // insertText pipeline. Note: this variant cannot replace, only prepend. await browser.compose.insertText(tab.id, htmlContent + '
', { insertAsText: false }); } } } catch (e) { console.error('background.js error:', e); // Fallback: if insertText fails, try setComposeDetails try { const [tab] = await browser.tabs.query({ active: true, currentWindow: true, windowType: 'messageCompose', }); if (tab) { const details = await browser.compose.getComposeDetails(tab.id); const old = details.body || ''; const htmlTpl = msg.text.includes('<') ? msg.text : msg.text.replace(/\n/g, '
'); const bodyIdx = old.indexOf('', bodyIdx) + 1; newBody = old.slice(0, insertAt) + '\n' + htmlTpl + '\n' + old.slice(insertAt); } else { newBody = htmlTpl + '
' + old; } await browser.compose.setComposeDetails(tab.id, { body: newBody }); } } catch (e2) { console.error('background.js fallback error:', e2); browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL('icons/icon.png'), title: 'Fehler beim Einfügen', message: e2.message, }); } } }