Thunderbird-MailExtension zum Ablegen von E-Mails in DocuWare (.eml + PDF + Anhänge), dynamische Indexfelder aus dem Store-Dialog, Auswahllisten, Identity-Service-Login. Self-distribution-Updates über updates.json auf eigenem Gitea.
582 lines
20 KiB
JavaScript
582 lines
20 KiB
JavaScript
// Ablage-Dialog: Schrankwahl, dynamische Felder, Upload.
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
|
||
let STATE = {
|
||
messageId: null,
|
||
messageIds: [], // alle ausgewählten Nachrichten
|
||
settings: null,
|
||
meta: null,
|
||
attachments: [],
|
||
cabinets: [], // alle Ziele (Archive + Briefkörbe)
|
||
cabinetId: "",
|
||
isBasket: false, // gewähltes Ziel ist ein Briefkorb?
|
||
dialogId: null,
|
||
fields: [], // Felddefinitionen des Store-Dialogs
|
||
};
|
||
|
||
// Mapping DocuWare-Feldname -> Funktion, die einen Vorbefüllwert aus meta liefert.
|
||
const PREFILL = {
|
||
EML_SENDER: (m) => m.senderEmail,
|
||
EML_SENDER_DISPLAYNAME: (m) => m.senderName,
|
||
EML_SENDER_NAME: (m) => m.senderName,
|
||
EML_RECEIVER: (m) => m.to,
|
||
EML_CC: (m) => m.cc,
|
||
EML_BCC: (m) => m.bcc,
|
||
EML_SUBJECT: (m) => m.subject,
|
||
DOC_SUBJECT: (m) => m.subject,
|
||
EML_BODY: (m) => m.bodyText,
|
||
EML_SENDINGDATE: (m) => m.date,
|
||
EML_DISPLAYDATE: (m) => m.date,
|
||
EML_RECEIVINGDATE: (m) => m.date,
|
||
EML_DIRECTION: (m) => m.direction,
|
||
EML_SIZE: (m) => m.sizeBytes,
|
||
EML_ACCOUNT: (m) => m.account,
|
||
DOC_DATE: (m) => m.date,
|
||
// Naheliegende generische Feldnamen (Schränke ohne EML_*-Schema)
|
||
SUBJECT: (m) => m.subject,
|
||
BETREFF: (m) => m.subject,
|
||
EMAIL: (m) => m.senderEmail,
|
||
E_MAIL: (m) => m.senderEmail,
|
||
DATE: (m) => m.date,
|
||
DATUM: (m) => m.date,
|
||
};
|
||
|
||
function status(msg, kind) {
|
||
const el = $("status");
|
||
el.textContent = msg || "";
|
||
el.className = kind || "muted";
|
||
}
|
||
|
||
function qs(name) {
|
||
return new URLSearchParams(location.search).get(name);
|
||
}
|
||
|
||
async function init() {
|
||
// messageIds (Mehrfachauswahl) bevorzugt, sonst einzelnes messageId.
|
||
const idsParam = qs("messageIds");
|
||
STATE.messageIds = (idsParam ? idsParam.split(",") : [qs("messageId")])
|
||
.map((x) => parseInt(x, 10))
|
||
.filter((n) => !isNaN(n));
|
||
STATE.messageId = STATE.messageIds[0];
|
||
STATE.settings = await Settings.get();
|
||
|
||
if (!STATE.settings.serverUrl) {
|
||
status("Bitte zuerst in den Einstellungen Server & Login hinterlegen.", "err");
|
||
$("submit").disabled = true;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Anzeige/Vorbefüllung anhand der ersten Nachricht.
|
||
STATE.meta = await Mail.getMeta(STATE.messageId);
|
||
STATE.attachments = await Mail.listAttachments(STATE.messageId);
|
||
} catch (e) {
|
||
status(`Mail konnte nicht gelesen werden: ${e.message}`, "err");
|
||
return;
|
||
}
|
||
|
||
if (STATE.messageIds.length > 1) {
|
||
$("multiNote").textContent =
|
||
`${STATE.messageIds.length} E-Mails werden abgelegt. ` +
|
||
"Eingetragene Werte gelten für alle; Absender/Betreff/Datum werden je E-Mail einzeln übernommen.";
|
||
}
|
||
|
||
renderAttachments();
|
||
applyDefaults();
|
||
|
||
try {
|
||
status("Verbinde mit DocuWare …");
|
||
await Auth.logon(STATE.settings);
|
||
await loadCabinets();
|
||
status("");
|
||
} catch (e) {
|
||
status(`Verbindung fehlgeschlagen: ${e.message}`, "err");
|
||
}
|
||
}
|
||
|
||
function applyDefaults() {
|
||
$("optPdf").checked = STATE.settings.storePdf;
|
||
const hasAtt = STATE.attachments.length > 0;
|
||
// Standard-Umfang aus den Einstellungen ableiten.
|
||
let scope = "both";
|
||
if (STATE.settings.storeEml && !STATE.settings.storeAttachments) scope = "eml";
|
||
else if (!STATE.settings.storeEml && STATE.settings.storeAttachments) scope = "att";
|
||
if (!hasAtt) scope = "eml"; // ohne Anhänge nur E-Mail sinnvoll
|
||
setScope(scope);
|
||
// Optionen, die Anhänge erfordern, deaktivieren wenn keine vorhanden.
|
||
document.querySelectorAll('input[name="scope"]').forEach((r) => {
|
||
if ((r.value === "att" || r.value === "both") && !hasAtt) r.disabled = true;
|
||
});
|
||
}
|
||
|
||
function setScope(value) {
|
||
const el = document.querySelector(`input[name="scope"][value="${value}"]`);
|
||
if (el) el.checked = true;
|
||
}
|
||
|
||
function getScope() {
|
||
const el = document.querySelector('input[name="scope"]:checked');
|
||
return el ? el.value : "both";
|
||
}
|
||
|
||
function renderAttachments() {
|
||
const ul = $("attList");
|
||
ul.innerHTML = "";
|
||
STATE.attachments.forEach((a, i) => {
|
||
const li = document.createElement("li");
|
||
li.innerHTML =
|
||
`<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();
|