Neuer forcedPrefs-WebExtension-Experiment (Services.prefs) setzt beim Start globale Prefs (Startseite aus, Kartenansicht, nicht gruppiert, absteigend, 3-Spalten-Layout, nicht auto-gelesen) sowie pro Identität die Signatur-/ Antwort-Einstellungen. Pro Setting im Options-Dialog (⚙-Tab) abschaltbar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015PZHd9vRsBiH8cKYdQo1Yv
338 lines
13 KiB
JavaScript
338 lines
13 KiB
JavaScript
// ── 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 = '<!-- SIG_FOOTER_START -->';
|
|
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); // "-- <br>" + 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: use insertText API to go through the editor's rendering pipeline
|
|
const htmlContent = isHtmlTemplate ? msg.text : msg.text.replace(/\n/g, '<br>');
|
|
|
|
// Move cursor to beginning of body first via script injection
|
|
await browser.tabs.executeScript(tab.id, {
|
|
code: `
|
|
const editor = document.getElementById('messageEditor');
|
|
const editorDoc = editor ? editor.contentDocument : document;
|
|
const body = editorDoc.body;
|
|
if (body) {
|
|
const range = editorDoc.createRange();
|
|
range.setStart(body, 0);
|
|
range.collapse(true);
|
|
const sel = editorDoc.getSelection();
|
|
sel.removeAllRanges();
|
|
sel.addRange(range);
|
|
}
|
|
`
|
|
}).catch(() => {
|
|
// Fallback: some TB versions structure the editor differently
|
|
});
|
|
|
|
// Insert HTML at cursor position - this goes through the editor's render pipeline
|
|
await browser.compose.insertText(tab.id, htmlContent + '<br>', { 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, '<br>');
|
|
const bodyIdx = old.indexOf('<body');
|
|
let newBody;
|
|
if (bodyIdx !== -1) {
|
|
const insertAt = old.indexOf('>', bodyIdx) + 1;
|
|
newBody = old.slice(0, insertAt) + '\n' + htmlTpl + '\n' + old.slice(insertAt);
|
|
} else {
|
|
newBody = htmlTpl + '<br>' + 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,
|
|
});
|
|
}
|
|
}
|
|
}
|