// Ablage-Dialog: Schrankwahl, dynamische Felder, Upload. const $ = (id) => document.getElementById(id); let STATE = { messageId: null, messageIds: [], // alle ausgewählten Nachrichten settings: null, meta: null, attachments: [], cabinets: [], // alle Ziele (Archive + Briefkörbe) cabinetId: "", isBasket: false, // gewähltes Ziel ist ein Briefkorb? dialogId: null, fields: [], // Felddefinitionen des Store-Dialogs existingDoc: null, // Treffer der Dedup-Suche im gewählten Schrank (oder null) cabinetFieldsByCab: {}, // Cache: cabinetId -> [Feldnamen] (für EML_ID-Erkennung) }; // Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert. // Welches DocuWare-Feld welchen Token bekommt, steht im Mapping (s. effectiveFieldMap). const TOKEN_FN = { senderEmail: (m) => m.senderEmail, senderName: (m) => m.senderName, to: (m) => m.to, cc: (m) => m.cc, bcc: (m) => m.bcc, subject: (m) => m.subject, body: (m) => m.bodyText, date: (m) => m.date, direction: (m) => m.direction, size: (m) => m.sizeBytes, account: (m) => m.account, }; // Aktives Feld-Mapping: eigenes (aus den Einstellungen) wenn gesetzt, sonst Default. function effectiveFieldMap() { const m = STATE.settings && STATE.settings.fieldMapping; return m && Object.keys(m).length ? m : DEFAULT_FIELD_MAP; } // Vorbefüllwert für ein DocuWare-Feld (oder undefined). Das Mapping kann je Feld // entweder einen Mail-Token (String) ODER einen festen Wert ({ const: "NEU" }) tragen. function prefillFor(fieldName, meta) { const m = effectiveFieldMap()[fieldName]; if (!m) return undefined; if (typeof m === "object") { if (m.const !== undefined && m.const !== null && m.const !== "") return m.const; if (m.token && TOKEN_FN[m.token]) return TOKEN_FN[m.token](meta); return undefined; } const fn = TOKEN_FN[m]; return fn ? fn(meta) : undefined; } function status(msg, kind) { const el = $("status"); el.textContent = msg || ""; el.className = kind || "muted"; } function qs(name) { return new URLSearchParams(location.search).get(name); } async function init() { // messageIds (Mehrfachauswahl) bevorzugt, sonst einzelnes messageId. const idsParam = qs("messageIds"); STATE.messageIds = (idsParam ? idsParam.split(",") : [qs("messageId")]) .map((x) => parseInt(x, 10)) .filter((n) => !isNaN(n)); STATE.messageId = STATE.messageIds[0]; STATE.settings = await Settings.get(); if (!STATE.settings.serverUrl) { status("Bitte zuerst in den Einstellungen Server & Login hinterlegen.", "err"); $("submit").disabled = true; return; } try { // Anzeige/Vorbefüllung anhand der ersten Nachricht. STATE.meta = await Mail.getMeta(STATE.messageId); STATE.attachments = await Mail.listAttachments(STATE.messageId); } catch (e) { status(`Mail konnte nicht gelesen werden: ${e.message}`, "err"); return; } if (STATE.messageIds.length > 1) { $("multiNote").textContent = `${STATE.messageIds.length} E-Mails werden abgelegt. ` + "Eingetragene Werte gelten für alle; Absender/Betreff/Datum werden je E-Mail einzeln übernommen."; } renderAttachments(); applyDefaults(); try { status("Verbinde mit DocuWare …"); await ensureAuth(); await loadCabinets(); status(""); } catch (e) { status(`Verbindung fehlgeschlagen: ${e.message}`, "err"); } } // Holt den Sitzungs-Token vom Hintergrundskript. Ist keiner vorhanden, wird das // Passwort einmalig abgefragt (nur im RAM, nicht gespeichert) und ein Token geholt. async function ensureAuth() { const st = await browser.runtime.sendMessage({ type: "auth:status" }); if (st && st.hasToken && st.token) { Auth.setToken(st.token); return; } let err = ""; while (true) { const pw = await askPassword(err); if (pw === null) throw new Error("Anmeldung abgebrochen."); status("Anmeldung …"); const res = await browser.runtime.sendMessage({ type: "auth:logon", password: pw }); if (res && res.ok) { Auth.setToken(res.token); return; } err = (res && res.error) || "Anmeldung fehlgeschlagen."; } } // Zeigt das Passwort-Overlay und löst mit dem eingegebenen Passwort (oder null // bei Abbruch) auf. Das Passwort verlässt diese Funktion nur Richtung Broker. function askPassword(errMsg) { return new Promise((resolve) => { const ov = $("pwOverlay"); const input = $("pwInput"); $("pwWho").textContent = [STATE.settings.username, STATE.settings.organization] .filter(Boolean) .join(" @ "); $("pwErr").textContent = errMsg || ""; input.value = ""; ov.hidden = false; input.focus(); const cleanup = (val) => { ov.hidden = true; $("pwOk").removeEventListener("click", onOk); $("pwCancel").removeEventListener("click", onCancel); input.removeEventListener("keydown", onKey); resolve(val); }; const onOk = () => { if (input.value) cleanup(input.value); }; const onCancel = () => cleanup(null); const onKey = (e) => { if (e.key === "Enter") { e.preventDefault(); onOk(); } else if (e.key === "Escape") { e.preventDefault(); onCancel(); } }; $("pwOk").addEventListener("click", onOk); $("pwCancel").addEventListener("click", onCancel); input.addEventListener("keydown", onKey); }); } function applyDefaults() { // Mail-Format (.eml ODER PDF) aus den Einstellungen. setMailFormat(STATE.settings.mailFormat === "pdf" ? "pdf" : "eml"); // Anhänge-Haken nur sinnvoll, wenn die Mail überhaupt Anhänge hat. const hasAtt = STATE.attachments.length > 0; const att = $("optAtt"); att.checked = hasAtt && STATE.settings.storeAttachments !== false; att.disabled = !hasAtt; $("attList").style.display = hasAtt && att.checked ? "" : "none"; } function setMailFormat(value) { const el = document.querySelector(`input[name="mailFormat"][value="${value}"]`); if (el) el.checked = true; } function getMailFormat() { const el = document.querySelector('input[name="mailFormat"]:checked'); return el ? el.value : "eml"; } function renderAttachments() { const ul = $("attList"); ul.innerHTML = ""; STATE.attachments.forEach((a, i) => { const li = document.createElement("li"); li.innerHTML = ` ` + ``; ul.appendChild(li); }); } async function loadCabinets() { let cabinets = (await browser.storage.local.get("cabinets")).cabinets; if (!cabinets || cabinets.length === 0) { cabinets = await DocuWare.listCabinets(STATE.settings); await browser.storage.local.set({ cabinets }); } STATE.cabinets = cabinets; const sel = $("cabinet"); sel.innerHTML = ''; const addGroup = (label, list) => { if (!list.length) return; const g = document.createElement("optgroup"); g.label = label; list.forEach((c) => { const o = document.createElement("option"); o.value = c.id; o.textContent = c.name; g.appendChild(o); }); sel.appendChild(g); }; addGroup("Archive", cabinets.filter((c) => !c.isBasket)); addGroup("Briefkörbe", cabinets.filter((c) => c.isBasket)); if (STATE.settings.defaultCabinetId) { sel.value = STATE.settings.defaultCabinetId; if (sel.value) await onCabinetChange(); } } async function onCabinetChange() { STATE.cabinetId = $("cabinet").value; STATE.dialogId = null; STATE.fields = []; STATE.existingDoc = null; hideArchivedBanner(); const target = STATE.cabinets.find((c) => String(c.id) === String(STATE.cabinetId)); STATE.isBasket = !!(target && target.isBasket); $("cabHeading").textContent = target ? `Ablegen in „${target.name}"` : "Ablage"; // Sticky: zuletzt gewählten Schrank als Standard merken. if (STATE.cabinetId) { Settings.set({ defaultCabinetId: STATE.cabinetId }).then((s) => (STATE.settings = s)); } if (!STATE.cabinetId) { $("fields").innerHTML = '
Ziel wählen …
'; return; } // Briefkörbe haben keinen Store-Dialog -> keine Indexfelder. $("dialogRow").style.display = "none"; if (STATE.isBasket) { $("fields").innerHTML = '
Briefkorb: keine Indexfelder. Die Dokumente werden ' + "unindiziert im Briefkorb abgelegt und können dort später archiviert werden.
"; return; } $("fields").innerHTML = '
Lade Ablagedialog …
'; try { const cands = await DocuWare.listFieldsDialogs(STATE.settings, STATE.cabinetId); STATE.dialogCands = cands; if (!cands.length) throw new Error("Kein Store- oder Suchdialog für diesen Schrank gefunden."); // Auswahl-Dropdown nur zeigen, wenn es mehrere Dialoge gibt. const ds = $("storeDialog"); ds.innerHTML = ""; cands.forEach((c) => { const o = document.createElement("option"); o.value = c.id; o.textContent = c.type === "search" ? `${c.name} (Suchdialog)` : c.name; ds.appendChild(o); }); $("dialogRow").style.display = cands.length > 1 ? "grid" : "none"; // Gemerkte Wahl pro Schrank, sonst erster Dialog. const remembered = (STATE.settings.dialogByCabinet || {})[STATE.cabinetId]; const pick = cands.find((c) => c.id === remembered) || cands[0]; ds.value = pick.id; await loadDialogFields(pick); checkExisting(); // „bereits abgelegt?" im gewählten Schrank prüfen (nicht blockierend) } catch (e) { $("fields").innerHTML = `
${e.message}
`; } } // Sucht im gewählten Schrank nach der Message-ID und zeigt ggf. den Banner. // Setzt STATE.existingDoc als Grundlage für die Dubletten-Warnung beim Ablegen. async function checkExisting() { STATE.existingDoc = null; hideArchivedBanner(); if (STATE.isBasket || !STATE.cabinetId || !STATE.meta || !STATE.meta.rfcMessageId) return; try { const names = await cabinetFieldNames(STATE.cabinetId); if (!names.includes("EML_ID")) return; // Schrank ohne Message-ID-Feld -> kein Dedup const hit = await DocuWare.findByMessageId( STATE.settings, STATE.cabinetId, STATE.meta.rfcMessageId ); if (hit) { STATE.existingDoc = hit; showArchivedBanner(); } } catch (_) { /* Dedup ist optional – Fehler ignorieren */ } } // Feldnamen eines Schranks (gecacht), um EML_ID-Unterstützung zu erkennen. async function cabinetFieldNames(cabinetId) { if (STATE.cabinetFieldsByCab[cabinetId]) return STATE.cabinetFieldsByCab[cabinetId]; const names = await DocuWare.cabinetFieldNames(STATE.settings, cabinetId).catch(() => []); STATE.cabinetFieldsByCab[cabinetId] = names; return names; } function showArchivedBanner() { $("archivedBanner").hidden = false; } function hideArchivedBanner() { const el = $("archivedBanner"); if (el) el.hidden = true; } // Öffnet das vorhandene DocuWare-Dokument im Browser/Webclient. function openExistingDoc() { if (!STATE.existingDoc) return; const url = DocuWare.documentViewUrl(STATE.settings, STATE.cabinetId, STATE.existingDoc.id); browser.tabs.create({ url }).catch(() => { try { window.open(url, "_blank"); } catch (_) {} }); } // Dubletten-Abfrage: resolve(true) = trotzdem ablegen, resolve(false) = abbrechen. function confirmDuplicate() { return new Promise((resolve) => { const ov = $("dupOverlay"); ov.hidden = false; const cleanup = (val) => { ov.hidden = true; $("dupProceed").removeEventListener("click", onProceed); $("dupCancel").removeEventListener("click", onCancel); $("dupOpen").removeEventListener("click", openExistingDoc); resolve(val); }; const onProceed = () => cleanup(true); const onCancel = () => cleanup(false); $("dupProceed").addEventListener("click", onProceed); $("dupCancel").addEventListener("click", onCancel); $("dupOpen").addEventListener("click", openExistingDoc); // öffnet Doc, Overlay bleibt offen }); } // Lädt und rendert die Felder für einen konkreten Dialog. async function loadDialogFields(pick) { STATE.dialogId = pick.id; STATE.dialogType = pick.type; STATE.fields = []; $("fields").innerHTML = '
Lade Felder …
'; STATE.fields = await DocuWare.getStoreFields(STATE.settings, STATE.cabinetId, STATE.dialogId); renderFields(); if (pick.type === "search") { const note = document.createElement("div"); note.className = "muted"; note.style.marginTop = "6px"; note.textContent = "Hinweis: Kein Store-Dialog verfügbar – Felder stammen aus dem Suchdialog."; $("fields").appendChild(note); } } // Nutzer wählt einen anderen Ablagedialog -> merken (pro Schrank) und neu laden. async function onStoreDialogChange() { const id = $("storeDialog").value; const pick = (STATE.dialogCands || []).find((c) => c.id === id); if (!pick) return; const map = { ...(STATE.settings.dialogByCabinet || {}), [STATE.cabinetId]: id }; STATE.settings = await Settings.set({ dialogByCabinet: map }); try { await loadDialogFields(pick); } catch (e) { $("fields").innerHTML = `
${e.message}
`; } } function renderFields() { const container = $("fields"); container.innerHTML = ""; for (const f of STATE.fields) { const wrap = document.createElement("div"); wrap.className = "field" + (isMemo(f) ? " full" : ""); wrap.dataset.name = f.name; wrap.dataset.type = f.type; wrap.dataset.required = f.required ? "1" : ""; const label = document.createElement("label"); label.textContent = f.label; if (f.required) { const r = document.createElement("span"); r.className = "req"; r.textContent = " *"; label.appendChild(r); } wrap.appendChild(label); const input = buildInput(f); wrap.appendChild(input); if (input._datalist) wrap.appendChild(input._datalist); container.appendChild(wrap); } applyFieldDefaults(); // vordefinierte Werte (PrefillValue) zuerst prefillFields(); // Mail-Vorbefüllung überschreibt gemappte Felder populateSelectLists(); // Auswahllisten/Vorschläge asynchron nachladen } // Baut das Eingabe-Element (synchron). Auswahllisten werden später befüllt. function buildInput(f) { const t = (f.type || "").toLowerCase(); let el; if (isMemo(f)) { el = document.createElement("textarea"); } else if (f.isSelectList && f.selectListOnly) { // Nur Listenwerte erlaubt -> starres Dropdown. el = document.createElement("select"); el.innerHTML = ''; } else if (t.includes("date")) { el = document.createElement("input"); el.type = "date"; } else if (t.includes("decimal") || t.includes("numeric") || t.includes("int")) { el = document.createElement("input"); el.type = "number"; el.step = "any"; } else { el = document.createElement("input"); el.type = "text"; if (f.isSelectList) { // Combobox: frei tippbar mit Vorschlagsliste (wie in Connect-to-Outlook). const dl = document.createElement("datalist"); dl.id = "dl-" + f.name; el.setAttribute("list", dl.id); el._datalist = dl; } } el.dataset.role = "value"; if (f.readOnly && el.tagName !== "SELECT") el.readOnly = true; return el; } // Vordefinierte Werte (PrefillValue aus dem Dialog) eintragen. function applyFieldDefaults() { STATE.fields.forEach((f) => { if (f.prefill === undefined || f.prefill === null || f.prefill === "") return; setFieldValue(f.name, f, f.prefill); }); } function prefillFields() { STATE.fields.forEach((f) => { const val = prefillFor(f.name, STATE.meta); if (val === undefined || val === null || val === "") return; setFieldValue(f.name, f, val); }); } // Setzt einen Wert auf das Control eines Feldes (kümmert sich um Datum/Select). function setFieldValue(name, f, val) { const wrap = document.querySelector(`.field[data-name="${name}"]`); const el = wrap && wrap.querySelector("[data-role='value']"); if (!el) return; if (el.type === "date") { const d = val instanceof Date ? val : new Date(val); if (!isNaN(d)) el.value = toDateInput(d); } else if (el.tagName === "SELECT") { let opt = Array.from(el.options).find((o) => o.value === String(val)); if (!opt) { opt = document.createElement("option"); opt.value = String(val); opt.textContent = String(val); el.appendChild(opt); } el.value = String(val); } else { el.value = val instanceof Date ? toDateInput(val) : String(val); } wrap.classList.add("prefilled"); } // Lädt die Auswahllisten-Werte und füllt Dropdowns/Vorschlagslisten. async function populateSelectLists() { for (const f of STATE.fields) { if (!f.isSelectList) continue; let vals = []; try { vals = await DocuWare.getSelectList( STATE.settings, STATE.cabinetId, STATE.dialogId, f.name ); } catch (_) { /* leere Liste ist ok */ } if (!vals.length) continue; const wrap = document.querySelector(`.field[data-name="${f.name}"]`); const el = wrap && wrap.querySelector("[data-role='value']"); if (!el) continue; if (el.tagName === "SELECT") { const current = el.value; vals.forEach((v) => { if (!Array.from(el.options).some((o) => o.value === v)) { const o = document.createElement("option"); o.value = v; o.textContent = v; el.appendChild(o); } }); if (current) el.value = current; } else { const dl = el._datalist || document.getElementById(el.getAttribute("list")); if (dl) { dl.innerHTML = ""; vals.forEach((v) => { const o = document.createElement("option"); o.value = v; dl.appendChild(o); }); } } } } function collectFieldValues() { const result = {}; let firstInvalid = null; document.querySelectorAll(".field").forEach((wrap) => { wrap.classList.remove("invalid"); const el = wrap.querySelector("[data-role='value']"); const name = wrap.dataset.name; const type = wrap.dataset.type; const required = wrap.dataset.required === "1"; const raw = el ? el.value : ""; if (required && !String(raw).trim()) { wrap.classList.add("invalid"); if (!firstInvalid) firstInvalid = name; } if (String(raw).trim() !== "") { result[name] = { value: el.type === "date" ? new Date(raw) : raw, type }; } }); return { result, firstInvalid }; } async function submit() { if (!STATE.cabinetId) { status("Bitte ein Ziel wählen.", "err"); return; } let result = {}; if (!STATE.isBasket) { const collected = collectFieldValues(); if (collected.firstInvalid) { status(`Pflichtfeld fehlt: ${collected.firstInvalid}`, "err"); return; } result = collected.result; } const fmt = getMailFormat(); // "eml" ODER "pdf" – nie beides const wantEml = fmt === "eml"; const wantPdf = fmt === "pdf"; const att = $("optAtt"); const wantAttScope = att.checked && !att.disabled; // Dubletten-Warnung (nur Einzelmail): liegt die Mail im Ziel schon, warnen – // „Trotzdem ablegen" bleibt aber möglich. if (STATE.messageIds.length === 1 && !STATE.isBasket && STATE.meta.rfcMessageId) { let hit = STATE.existingDoc; if (!hit) { const names = await cabinetFieldNames(STATE.cabinetId); if (names.includes("EML_ID")) { hit = await DocuWare.findByMessageId( STATE.settings, STATE.cabinetId, STATE.meta.rfcMessageId ).catch(() => null); } } if (hit) { STATE.existingDoc = hit; showArchivedBanner(); const proceed = await confirmDuplicate(); if (!proceed) return; } } await saveOptionPrefs(fmt, wantAttScope); // Sticky: Format + Anhänge-Wahl merken $("submit").disabled = true; const multi = STATE.messageIds.length > 1; let tagFailed = false; try { const fieldNames = STATE.isBasket ? [] : await cabinetFieldNames(STATE.cabinetId); let mi = 0; for (const mid of STATE.messageIds) { mi++; const prefix = multi ? `Mail ${mi}/${STATE.messageIds.length}: ` : ""; // Meta/Anhänge der jeweiligen Nachricht. const meta = mid === STATE.messageId ? STATE.meta : await Mail.getMeta(mid); const atts = mid === STATE.messageId ? STATE.attachments : await Mail.listAttachments(mid); // Indexfelder: manuelle Werte für alle gleich; gemappte Felder je Mail neu. const fields = STATE.isBasket ? {} : fieldsForMessage(result, meta, multi); // Message-ID für Dedup/„bereits abgelegt" mitschreiben (wenn Schrank EML_ID hat). if (!STATE.isBasket && meta.rfcMessageId && fieldNames.includes("EML_ID")) { fields.EML_ID = { value: DocuWare.normalizeMessageId(meta.rfcMessageId), type: "Text" }; } const wantAtt = wantAttScope && atts.length > 0; if (wantEml) { status(`${prefix}E-Mail …`); const eml = await Mail.getEmlFile(mid, meta.subject); await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, fields); } if (wantPdf) { status(`${prefix}PDF …`); const pdf = Pdf.fromMail(meta); await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, fields); } if (wantAtt) { // Bei Einzelmail: angehakte Anhänge. Bei Mehrfach: alle Anhänge je Mail. const indices = multi ? atts.map((_, i) => i) : selectedAttachmentIndices(); for (const i of indices) { const a = atts[i]; if (!a) continue; status(`${prefix}Anhang ${a.name} …`); const file = await Mail.getAttachmentFile(mid, a.partName); const af = STATE.isBasket ? {} : { ...fields, DOC_FILE_NAME: { value: a.name, type: "Text" } }; await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, file, a.name, af); } } if (STATE.settings.tagOnSuccess) { try { await tagMessage(mid); } catch (te) { console.warn("DocuWare-Markierung fehlgeschlagen:", te); tagFailed = true; } } // Background-Badge aktualisieren (Mail gilt jetzt als abgelegt). if (meta.rfcMessageId) { browser.runtime .sendMessage({ type: "archive:done", rfcMessageId: meta.rfcMessageId, cabinetId: STATE.cabinetId }) .catch(() => {}); } } const base = multi ? `${STATE.messageIds.length} E-Mails abgelegt.` : "Erfolgreich abgelegt."; status(tagFailed ? `${base} (Markierung nicht möglich)` : base, "ok"); setTimeout(() => window.close(), tagFailed ? 2500 : 1200); } catch (e) { status(`Fehler beim Ablegen: ${e.message}`, "err"); $("submit").disabled = false; } } // Indexfelder für eine konkrete Nachricht: gemappte Felder (EML_*/generisch) // werden je Mail aus deren Metadaten neu berechnet, manuelle Werte bleiben. function fieldsForMessage(baseResult, meta, multi) { if (!multi) return baseResult; // Einzelmail: manuelle Edits respektieren const result = { ...baseResult }; STATE.fields.forEach((f) => { const val = prefillFor(f.name, meta); if (val === undefined || val === null || val === "") return; result[f.name] = { value: val, type: f.type }; }); return result; } // Sticky: zuletzt genutztes Format + Anhänge-Wahl in den Einstellungen merken. async function saveOptionPrefs(mailFormat, storeAttachments) { try { STATE.settings = await Settings.set({ mailFormat, storeAttachments }); } catch (_) { /* nicht kritisch */ } } function selectedAttachmentIndices() { return STATE.attachments .map((_, i) => i) .filter((i) => { const c = $(`att-${i}`); return c && c.checked; }); } // Markiert die Mail mit dem Tag "DocuWare". Wirft bei Fehler (Aufrufer fängt). async function tagMessage(messageId) { const KEY = "docuware"; const NAME = "DocuWare"; const COLOR = "#0e8a8a"; // Tags-API unterscheidet sich je nach Thunderbird-Version. const api = browser.messages.tags; let list = []; if (api && api.list) list = await api.list(); else if (browser.messages.listTags) list = await browser.messages.listTags(); let tag = list.find((t) => t.key === KEY || t.tag === NAME); if (!tag) { if (api && api.create) await api.create(KEY, NAME, COLOR); else if (browser.messages.createTag) await browser.messages.createTag(KEY, NAME, COLOR); tag = { key: KEY }; } const msg = await browser.messages.get(messageId); const current = msg.tags || []; if (!current.includes(tag.key)) { await browser.messages.update(messageId, { tags: [...current, tag.key] }); } } // --- Helfer --- const isMemo = (f) => /memo/i.test(f.type) || f.name === "EML_BODY"; const toDateInput = (d) => d.toISOString().slice(0, 10); function formatSize(b) { if (!b) return "–"; if (b < 1024) return `${b} B`; if (b < 1024 * 1024) return `${(b / 1024).toFixed(0)} KB`; return `${(b / 1024 / 1024).toFixed(1)} MB`; } function resetForm() { document.querySelectorAll(".field").forEach((wrap) => { const el = wrap.querySelector("[data-role='value']"); if (el) el.value = ""; wrap.classList.remove("prefilled", "invalid"); }); applyFieldDefaults(); prefillFields(); applyDefaults(); status(""); } $("cabinet").addEventListener("change", onCabinetChange); $("storeDialog").addEventListener("change", onStoreDialogChange); $("openExisting").addEventListener("click", openExistingDoc); $("submit").addEventListener("click", submit); $("reset").addEventListener("click", resetForm); $("cancel").addEventListener("click", () => window.close()); // Sticky: Optionen sofort bei Änderung merken (nicht erst beim Ablegen). document.querySelectorAll('input[name="mailFormat"]').forEach((r) => r.addEventListener("change", () => saveOptionPrefs(getMailFormat(), $("optAtt").checked)) ); $("optAtt").addEventListener("change", () => { const show = $("optAtt").checked && STATE.attachments.length > 0; $("attList").style.display = show ? "" : "none"; saveOptionPrefs(getMailFormat(), $("optAtt").checked); }); init();