// 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)
uploadedIds: new Set(), // bereits erfolgreich abgelegte Nachrichten (für Retry-Schutz)
};
// 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) => {
// Bewusst per DOM-API (nicht innerHTML): der Anhang-Name stammt aus der Mail
// und ist angreiferkontrolliert – als Text einsetzen, nie als HTML.
const li = document.createElement("li");
const cb = document.createElement("input");
cb.type = "checkbox";
cb.id = `att-${i}`;
cb.checked = true;
const label = document.createElement("label");
label.setAttribute("for", `att-${i}`);
label.style.margin = "0";
label.style.fontWeight = "400";
label.textContent = `${a.name} `;
const size = document.createElement("span");
size.className = "muted";
size.textContent = `(${formatSize(a.size)})`;
label.appendChild(size);
li.append(cb, document.createTextNode(" "), 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 = '';
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");
// Feldnamen können Leer-/Sonderzeichen enthalten -> auf gültige id-Zeichen reduzieren.
dl.id = "dl-" + String(f.name).replace(/[^A-Za-z0-9_-]/g, "_");
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="${cssEsc(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="${cssEsc(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() !== "") {
// Date-Input als LOKALE Mitternacht parsen (raw+"T00:00:00"), sonst
// interpretiert new Date("YYYY-MM-DD") als UTC.
const value = el.type === "date" ? new Date(`${raw}T00:00:00`) : raw;
result[name] = { value, 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++;
// Retry-Schutz: nach einem Teilfehler nicht erneut hochladen (sonst Dubletten).
if (STATE.uploadedIds.has(mid)) continue;
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(() => {});
}
// Diese Nachricht ist vollständig abgelegt -> bei einem späteren Retry überspringen.
STATE.uploadedIds.add(mid);
}
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";
// Lokales Datum als YYYY-MM-DD (NICHT toISOString -> das rechnet auf UTC um und
// kippt eine Uhrzeit nahe Mitternacht auf den Vortag).
const toDateInput = (d) => {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
};
// Sicheres CSS-Escaping für Attribut-Selektoren (Feldnamen aus DocuWare).
const cssEsc = (s) =>
window.CSS && CSS.escape ? CSS.escape(String(s)) : String(s).replace(/["\\\]]/g, "\\$&");
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();