thunderbird2docuware/dialog/dialog.js
sylyx e201e6c654 Ein Ablagefenster für Mehrfachauswahl + Sitzungs-/Retry-Fixes
Mehrfachauswahl öffnet jetzt EIN Ablagefenster (?messageIds=1,2,3) statt eines
Fensters je Nachricht. Der dafür schon vorhandene, bisher unerreichbare Multi-Pfad
in dialog.js hatte drei Fehler, die dabei aufgeschlagen wären:
- Der Anhang-Haken wurde aus der ERSTEN Mail abgeleitet: hatte die keine Anhänge,
  fielen die Anhänge aller übrigen Mails still weg.
- Die Anhangsliste zeigte die Anhänge von Mail 1 mit wirkungslosen Haken.
- Der „bereits abgelegt"-Banner galt nur für Mail 1, sah aber nach einer Aussage
  über die ganze Auswahl aus.

Weiter:
- Server-/Zugangsdaten-Wechsel verwirft jetzt auch die serverspezifischen Caches.
  auth:logout leert zusätzlich ARCHIVED_CACHE; mit scope:"server" fliegen die
  gecachte Schrankliste sowie defaultCabinetId/dialogByCabinet raus, weil deren
  GUIDs nur auf dem alten Server existieren.
- Retry nach einem Teilfehler lud bereits übertragene Teile erneut hoch und
  erzeugte Dubletten. Jeder Teil (eml/pdf/Anhang) wird nun je Ziel einmal
  hochgeladen; dieselbe Mail lässt sich dadurch auch bewusst in ein zweites
  Archiv ablegen.
- ARCHIVED_CACHE wächst nicht mehr unbegrenzt (500 Einträge, ältester fliegt).
- HTTP 401/403 liefern statt des nackten Statuscodes eine verständliche Meldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:16:35 +02:00

823 lines
30 KiB
JavaScript
Raw 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)
// Vollständig abgelegte Nachrichten je Ziel ("<cab>|<mid>") – Tag/Badge nur einmal.
// Ziel im Schlüssel, damit dieselbe Mail bewusst in ein zweites Archiv abgelegt
// werden kann, ohne den Dialog neu zu öffnen.
uploadedIds: new Set(),
// Bereits hochgeladene EINZELTEILE, damit ein Retry nach einem Teilfehler nichts
// doppelt hochlädt. Schlüssel inkl. Ziel-Schrank ("<cab>|<mid>|eml"), damit ein
// Wechsel des Ziels nach einem Fehlversuch wieder alles hochlädt.
uploadedParts: new Set(),
};
// 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 (isMulti()) {
$("multiNote").textContent =
`${STATE.messageIds.length} E-Mails werden abgelegt. ` +
"Eingetragene Werte gelten für alle; zugeordnete Felder (Absender/Betreff/Datum …) " +
"werden je E-Mail einzeln übernommen. Anhänge werden – falls aktiviert – je E-Mail " +
"vollständig mit abgelegt. Angezeigt sind die Werte der ersten E-Mail.";
}
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");
const att = $("optAtt");
const want = STATE.settings.storeAttachments !== false;
if (isMulti()) {
// Mehrfachauswahl: STATE.attachments gehört nur zur ERSTEN Mail. Daraus darf
// nicht geschlossen werden, ob die übrigen Anhänge haben – sonst bliebe der
// Haken deaktiviert und die Anhänge aller anderen Mails fielen still weg.
// Ob es etwas hochzuladen gibt, entscheidet submit() je Mail einzeln.
att.checked = want;
att.disabled = false;
$("attList").style.display = "none";
return;
}
// Einzelmail: Haken nur sinnvoll, wenn die Mail überhaupt Anhänge hat.
const hasAtt = STATE.attachments.length > 0;
att.checked = hasAtt && want;
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 = "";
// Die Einzelauswahl der Anhänge gilt nur für EINE Mail. Bei Mehrfachauswahl
// würden hier die Anhänge der ersten Mail stehen, die Haken aber nichts tun
// (submit() legt dort je Mail alle Anhänge ab) -> Liste bleibt leer/verborgen.
if (isMulti()) return;
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 = '<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();
// Bei Mehrfachauswahl nicht prüfen: der Banner bezöge sich nur auf die erste
// Mail, würde aber wie eine Aussage über die ganze Auswahl wirken.
if (isMulti()) return;
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");
// 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 (!isMulti() && !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 = isMulti();
let tagFailed = false;
let current = ""; // zuletzt bearbeitete Mail – für eine brauchbare Fehlermeldung
try {
const fieldNames = STATE.isBasket ? [] : await cabinetFieldNames(STATE.cabinetId);
let mi = 0;
for (const mid of STATE.messageIds) {
mi++;
// Retry-Schutz: in DIESES Ziel bereits vollständig abgelegt -> überspringen.
if (STATE.uploadedIds.has(`${STATE.cabinetId}|${mid}`)) continue;
const prefix = multi ? `Mail ${mi}/${STATE.messageIds.length}: ` : "";
current = 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) {
await uploadOnce(`${mid}|eml`, async () => {
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) {
await uploadOnce(`${mid}|pdf`, async () => {
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;
await uploadOnce(`${mid}|att:${a.partName}`, async () => {
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(() => {});
}
// In diesem Ziel vollständig abgelegt -> bei einem späteren Retry überspringen.
STATE.uploadedIds.add(`${STATE.cabinetId}|${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) {
// Bei Mehrfachauswahl sagen, WO es hakte – und dass ein erneuter Klick die
// bereits übertragenen E-Mails/Teile überspringt (kein Dublettenrisiko).
const where = current ? ` (${current})` : "";
const hint = multi ? " Bereits abgelegtes wird bei erneutem Klick übersprungen." : "";
status(`Fehler beim Ablegen${where}: ${e.message}${hint}`, "err");
$("submit").disabled = false;
}
}
// Lädt einen Teil (eml/pdf/Anhang) genau EINMAL hoch. Schlägt ein späterer Teil
// fehl und der Nutzer klickt erneut auf „Ablegen", werden die bereits erfolgreich
// übertragenen Teile übersprungen – sonst entstünden Dubletten in DocuWare.
async function uploadOnce(part, doUpload) {
const key = `${STATE.cabinetId}|${part}`;
if (STATE.uploadedParts.has(key)) return;
await doUpload();
STATE.uploadedParts.add(key);
}
// 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 ---
// Mehrfachauswahl? Steuert Anhang-Liste, Dubletten-Banner und Feldberechnung.
const isMulti = () => STATE.messageIds.length > 1;
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 = !isMulti() && $("optAtt").checked && STATE.attachments.length > 0;
$("attList").style.display = show ? "" : "none";
saveOptionPrefs(getMailFormat(), $("optAtt").checked);
});
init();