// 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 }; // Mapping DocuWare-Feldname -> Funktion, die einen Vorbefüllwert aus meta liefert. const PREFILL = { EML_SENDER: (m) => m.senderEmail, EML_SENDER_DISPLAYNAME: (m) => m.senderName, EML_SENDER_NAME: (m) => m.senderName, EML_RECEIVER: (m) => m.to, EML_CC: (m) => m.cc, EML_BCC: (m) => m.bcc, EML_SUBJECT: (m) => m.subject, DOC_SUBJECT: (m) => m.subject, EML_BODY: (m) => m.bodyText, EML_SENDINGDATE: (m) => m.date, EML_DISPLAYDATE: (m) => m.date, EML_RECEIVINGDATE: (m) => m.date, EML_DIRECTION: (m) => m.direction, EML_SIZE: (m) => m.sizeBytes, EML_ACCOUNT: (m) => m.account, DOC_DATE: (m) => m.date, // Naheliegende generische Feldnamen (Schränke ohne EML_*-Schema) SUBJECT: (m) => m.subject, BETREFF: (m) => m.subject, EMAIL: (m) => m.senderEmail, E_MAIL: (m) => m.senderEmail, DATE: (m) => m.date, DATUM: (m) => m.date, }; 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 Auth.logon(STATE.settings); await loadCabinets(); status(""); } catch (e) { status(`Verbindung fehlgeschlagen: ${e.message}`, "err"); } } function applyDefaults() { $("optPdf").checked = STATE.settings.storePdf; const hasAtt = STATE.attachments.length > 0; // Standard-Umfang aus den Einstellungen ableiten. let scope = "both"; if (STATE.settings.storeEml && !STATE.settings.storeAttachments) scope = "eml"; else if (!STATE.settings.storeEml && STATE.settings.storeAttachments) scope = "att"; if (!hasAtt) scope = "eml"; // ohne Anhänge nur E-Mail sinnvoll setScope(scope); // Optionen, die Anhänge erfordern, deaktivieren wenn keine vorhanden. document.querySelectorAll('input[name="scope"]').forEach((r) => { if ((r.value === "att" || r.value === "both") && !hasAtt) r.disabled = true; }); } function setScope(value) { const el = document.querySelector(`input[name="scope"][value="${value}"]`); if (el) el.checked = true; } function getScope() { const el = document.querySelector('input[name="scope"]:checked'); return el ? el.value : "both"; } 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 = []; 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); } catch (e) { $("fields").innerHTML = `
${e.message}
`; } } // 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 fn = PREFILL[f.name]; if (!fn) return; const val = fn(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 scope = getScope(); const wantEml = scope !== "att"; const wantPdf = $("optPdf").checked; const wantAttScope = scope !== "eml"; if (!wantEml && !wantAttScope) { status("Nichts ausgewählt zum Ablegen.", "err"); return; } await saveOptionPrefs(scope, wantPdf); // Sticky: Umfang merken $("submit").disabled = true; const multi = STATE.messageIds.length > 1; let tagFailed = false; try { 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); 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; } } } 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 fn = PREFILL[f.name]; if (!fn) return; const val = fn(meta); if (val === undefined || val === null || val === "") return; result[f.name] = { value: val, type: f.type }; }); return result; } // Sticky: zuletzt genutzten Ablage-Umfang in den Einstellungen merken. async function saveOptionPrefs(scope, wantPdf) { const storeEml = scope !== "att"; const storeAttachments = scope !== "eml"; try { STATE.settings = await Settings.set({ storeEml, storeAttachments, storePdf: wantPdf }); } 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); $("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="scope"]').forEach((r) => r.addEventListener("change", () => saveOptionPrefs(getScope(), $("optPdf").checked)) ); $("optPdf").addEventListener("change", () => saveOptionPrefs(getScope(), $("optPdf").checked) ); init();