thunderbird2docuware/dialog/dialog.js
sylyx 81964c76d3 Mapping: feste Werte ({const}) + Teilen via JSON-Copy/Paste und Pull von raw-JSON-URL (mappingUrl)
- prefillFor löst Token-String ODER {const:...} auf; Options-Editor mit
  'Fester Wert'-Option + Konstanten-Eingabe.
- Feld-Zuordnung: JSON exportieren/anwenden/kopieren, von URL laden (Gitea/GitHub raw).
- Fix: zwei Statustexte mit ASCII-Quote in " gehärtet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:01:37 +02:00

736 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 =
`<input type="checkbox" id="att-${i}" checked /> ` +
`<label for="att-${i}" style="margin:0;font-weight:400">${a.name} ` +
`<span class="muted">(${formatSize(a.size)})</span></label>`;
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 = '<option value="">— bitte wählen —</option>';
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 = '<div class="muted">Ziel wählen …</div>';
return;
}
// Briefkörbe haben keinen Store-Dialog -> keine Indexfelder.
$("dialogRow").style.display = "none";
if (STATE.isBasket) {
$("fields").innerHTML =
'<div class="muted">Briefkorb: keine Indexfelder. Die Dokumente werden ' +
"unindiziert im Briefkorb abgelegt und können dort später archiviert werden.</div>";
return;
}
$("fields").innerHTML = '<div class="muted">Lade Ablagedialog …</div>';
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 = `<div class="err">${e.message}</div>`;
}
}
// 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 = '<div class="muted">Lade Felder …</div>';
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 = `<div class="err">${e.message}</div>`;
}
}
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 = '<option value=""></option>';
} 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();