diff --git a/dialog/dialog.js b/dialog/dialog.js index 2c1470e..eeffc65 100644 --- a/dialog/dialog.js +++ b/dialog/dialog.js @@ -15,6 +15,7 @@ let STATE = { 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. @@ -186,11 +187,27 @@ 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"); - li.innerHTML = - ` ` + - ``; + + 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); }); } @@ -429,7 +446,8 @@ function buildInput(f) { if (f.isSelectList) { // Combobox: frei tippbar mit Vorschlagsliste (wie in Connect-to-Outlook). const dl = document.createElement("datalist"); - dl.id = "dl-" + f.name; + // 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; } @@ -457,7 +475,7 @@ function prefillFields() { // 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 wrap = document.querySelector(`.field[data-name="${cssEsc(name)}"]`); const el = wrap && wrap.querySelector("[data-role='value']"); if (!el) return; if (el.type === "date") { @@ -488,7 +506,7 @@ async function populateSelectLists() { ); } catch (_) { /* leere Liste ist ok */ } if (!vals.length) continue; - const wrap = document.querySelector(`.field[data-name="${f.name}"]`); + 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") { @@ -530,7 +548,10 @@ function collectFieldValues() { if (!firstInvalid) firstInvalid = name; } if (String(raw).trim() !== "") { - result[name] = { value: el.type === "date" ? new Date(raw) : raw, type }; + // 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 }; @@ -584,6 +605,8 @@ async function submit() { 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); @@ -632,6 +655,9 @@ async function submit() { .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."; @@ -695,7 +721,17 @@ async function tagMessage(messageId) { // --- Helfer --- const isMemo = (f) => /memo/i.test(f.type) || f.name === "EML_BODY"; -const toDateInput = (d) => d.toISOString().slice(0, 10); +// 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`; diff --git a/lib/docuware.js b/lib/docuware.js index d3e6db0..ccb822b 100644 --- a/lib/docuware.js +++ b/lib/docuware.js @@ -414,7 +414,12 @@ const DocuWare = { if (t.includes("date")) { // DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform. const d = value instanceof Date ? value : new Date(value); - return { FieldName: name, Item: d.toISOString(), ItemElementName: "Date" }; + if (isNaN(d)) return { FieldName: name, Item: String(value), ItemElementName: "Date" }; + // Auf den LOKALEN Kalendertag als UTC-Mitternacht normalisieren. Sonst würde + // toISOString eine Uhrzeit nahe Mitternacht in eine andere Zeitzone und damit + // auf den Vor-/Folgetag kippen (Datumsfelder tragen keine Uhrzeit). + const utcDay = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); + return { FieldName: name, Item: utcDay.toISOString(), ItemElementName: "Date" }; } if (t.includes("decimal") || t.includes("currency")) { return { FieldName: name, Item: Number(value), ItemElementName: "Decimal" }; diff --git a/lib/mail.js b/lib/mail.js index 7629c4d..29a4833 100644 --- a/lib/mail.js +++ b/lib/mail.js @@ -29,7 +29,7 @@ const Mail = { date: header.date instanceof Date ? header.date : new Date(header.date), bodyText, account, - direction: this._direction(header, account), + direction: this._direction(header), sizeBytes: header.size || 0, }; }, @@ -140,9 +140,8 @@ const Mail = { return ""; }, - _direction(header, accountName) { - // Heuristik: ist der Account-Name/eine Identität Absender -> Ausgang. - // Robustere Erkennung erfolgt später über Identitäten; v1 simpel: + _direction(header) { + // Heuristik über den Ordnertyp; robustere Erkennung via Identitäten käme später. const folderType = header.folder && header.folder.type; if (folderType === "sent" || folderType === "outbox") return "Ausgang"; return "Eingang"; diff --git a/lib/store.js b/lib/store.js index b175436..27f1532 100644 --- a/lib/store.js +++ b/lib/store.js @@ -73,13 +73,23 @@ const Settings = { return s; }, + // Laufende Schreibkette: serialisiert parallele set()-Aufrufe, damit zwei fast + // gleichzeitige Writes (z.B. Schrankwahl + Format-Umschalten) sich nicht beim + // read-modify-write gegenseitig überschreiben. + _writeQueue: Promise.resolve(), + async set(partial) { - const current = await this.get(); - const next = { ...current, ...partial }; - // Sicherheitsnetz: Passwort darf NIE persistiert werden (z.B. aus Alt-Importen). - delete next.password; - await browser.storage.local.set({ settings: next }); - return next; + const run = async () => { + const current = await this.get(); + const next = { ...current, ...partial }; + // Sicherheitsnetz: Passwort darf NIE persistiert werden (z.B. aus Alt-Importen). + delete next.password; + await browser.storage.local.set({ settings: next }); + return next; + }; + const p = this._writeQueue.then(run, run); + this._writeQueue = p.catch(() => {}); // Fehler bricht die Kette nicht ab + return p; }, /** Basis-URL der Platform-API ohne abschließenden Slash. */ diff --git a/options/options.js b/options/options.js index 6b2c0aa..5afd1d9 100644 --- a/options/options.js +++ b/options/options.js @@ -12,6 +12,14 @@ function setStatus(msg, kind) { el.className = kind || ""; } +// Warnt, wenn der Server über unverschlüsseltes http:// läuft – dann ginge das +// Passwort im Klartext über die Leitung. Liefert einen anzuhängenden Hinweis. +function insecureWarning(url) { + return /^http:\/\//i.test(String(url || "").trim()) + ? " ⚠ Achtung: http:// ist unverschlüsselt – bitte https:// verwenden." + : ""; +} + async function load() { const s = await Settings.get(); FIELDS.forEach((f) => ($(f).value = s[f] || "")); @@ -213,8 +221,10 @@ function collect() { // --- Events ---------------------------------------------------------------- $("save").addEventListener("click", async () => { - await Settings.set(collect()); - setStatus("Gespeichert.", "ok"); + const data = collect(); + await Settings.set(data); + const warn = insecureWarning(data.serverUrl); + setStatus("Gespeichert." + warn, warn ? "err" : "ok"); }); $("test").addEventListener("click", async () => { @@ -228,7 +238,8 @@ $("test").addEventListener("click", async () => { fillCabinets(cabinets, s.defaultCabinetId); const arch = cabinets.filter((c) => !c.isBasket).length; const bk = cabinets.filter((c) => c.isBasket).length; - setStatus(`Verbindung ok – ${arch} Archive, ${bk} Briefkörbe gefunden.`, "ok"); + const warn = insecureWarning(s.serverUrl); + setStatus(`Verbindung ok – ${arch} Archive, ${bk} Briefkörbe gefunden.` + warn, warn ? "err" : "ok"); } catch (e) { setStatus(`Fehler: ${e.message}`, "err"); }