2026-06-03 11:15:59 +00:00
|
|
|
|
// 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 …");
|
2026-06-03 13:14:05 +00:00
|
|
|
|
await ensureAuth();
|
2026-06-03 11:15:59 +00:00
|
|
|
|
await loadCabinets();
|
|
|
|
|
|
status("");
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
status(`Verbindung fehlgeschlagen: ${e.message}`, "err");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 13:14:05 +00:00
|
|
|
|
// 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);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 11:15:59 +00:00
|
|
|
|
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 =
|
|
|
|
|
|
`<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 = [];
|
|
|
|
|
|
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);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
$("fields").innerHTML = `<div class="err">${e.message}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 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();
|