Compare commits

...

2 Commits

Author SHA1 Message Date
d02228e4c9 Release 0.10.8 2026-06-17 13:44:46 +02:00
589c0018a9 Fixes: XSS-Schutz, Datums-Zeitzone, Multi-Retry, Settings-Race, http-Warnung
- dialog.js: Anhang-Liste per DOM-API statt innerHTML (kein HTML-Inject über
  angreiferkontrollierte Anhang-Namen); Feldname-Selektoren via CSS.escape,
  datalist-id bereinigt; Datums-Eingabe lokal statt UTC (kein Off-by-one);
  bereits abgelegte Mails bei Retry überspringen (keine Dubletten)
- docuware.js: Datumsfelder auf lokalen Kalendertag normalisieren (UTC-Mitternacht)
- store.js: Settings.set serialisiert (kein read-modify-write Race)
- options.js: Warnung bei unverschlüsseltem http://-Server
- mail.js: ungenutzten Parameter aus _direction entfernt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:43:59 +02:00
7 changed files with 89 additions and 24 deletions

View File

@ -15,6 +15,7 @@ let STATE = {
fields: [], // Felddefinitionen des Store-Dialogs fields: [], // Felddefinitionen des Store-Dialogs
existingDoc: null, // Treffer der Dedup-Suche im gewählten Schrank (oder null) existingDoc: null, // Treffer der Dedup-Suche im gewählten Schrank (oder null)
cabinetFieldsByCab: {}, // Cache: cabinetId -> [Feldnamen] (für EML_ID-Erkennung) 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. // Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
@ -186,11 +187,27 @@ function renderAttachments() {
const ul = $("attList"); const ul = $("attList");
ul.innerHTML = ""; ul.innerHTML = "";
STATE.attachments.forEach((a, i) => { 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 li = document.createElement("li");
li.innerHTML =
`<input type="checkbox" id="att-${i}" checked /> ` + const cb = document.createElement("input");
`<label for="att-${i}" style="margin:0;font-weight:400">${a.name} ` + cb.type = "checkbox";
`<span class="muted">(${formatSize(a.size)})</span></label>`; 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); ul.appendChild(li);
}); });
} }
@ -429,7 +446,8 @@ function buildInput(f) {
if (f.isSelectList) { if (f.isSelectList) {
// Combobox: frei tippbar mit Vorschlagsliste (wie in Connect-to-Outlook). // Combobox: frei tippbar mit Vorschlagsliste (wie in Connect-to-Outlook).
const dl = document.createElement("datalist"); 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.setAttribute("list", dl.id);
el._datalist = dl; el._datalist = dl;
} }
@ -457,7 +475,7 @@ function prefillFields() {
// Setzt einen Wert auf das Control eines Feldes (kümmert sich um Datum/Select). // Setzt einen Wert auf das Control eines Feldes (kümmert sich um Datum/Select).
function setFieldValue(name, f, val) { 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']"); const el = wrap && wrap.querySelector("[data-role='value']");
if (!el) return; if (!el) return;
if (el.type === "date") { if (el.type === "date") {
@ -488,7 +506,7 @@ async function populateSelectLists() {
); );
} catch (_) { /* leere Liste ist ok */ } } catch (_) { /* leere Liste ist ok */ }
if (!vals.length) continue; 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']"); const el = wrap && wrap.querySelector("[data-role='value']");
if (!el) continue; if (!el) continue;
if (el.tagName === "SELECT") { if (el.tagName === "SELECT") {
@ -530,7 +548,10 @@ function collectFieldValues() {
if (!firstInvalid) firstInvalid = name; if (!firstInvalid) firstInvalid = name;
} }
if (String(raw).trim() !== "") { 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 }; return { result, firstInvalid };
@ -584,6 +605,8 @@ async function submit() {
let mi = 0; let mi = 0;
for (const mid of STATE.messageIds) { for (const mid of STATE.messageIds) {
mi++; 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}: ` : ""; const prefix = multi ? `Mail ${mi}/${STATE.messageIds.length}: ` : "";
// Meta/Anhänge der jeweiligen Nachricht. // Meta/Anhänge der jeweiligen Nachricht.
const meta = mid === STATE.messageId ? STATE.meta : await Mail.getMeta(mid); 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 }) .sendMessage({ type: "archive:done", rfcMessageId: meta.rfcMessageId, cabinetId: STATE.cabinetId })
.catch(() => {}); .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."; const base = multi ? `${STATE.messageIds.length} E-Mails abgelegt.` : "Erfolgreich abgelegt.";
@ -695,7 +721,17 @@ async function tagMessage(messageId) {
// --- Helfer --- // --- Helfer ---
const isMemo = (f) => /memo/i.test(f.type) || f.name === "EML_BODY"; 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) { function formatSize(b) {
if (!b) return ""; if (!b) return "";
if (b < 1024) return `${b} B`; if (b < 1024) return `${b} B`;

View File

@ -414,7 +414,12 @@ const DocuWare = {
if (t.includes("date")) { if (t.includes("date")) {
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform. // DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
const d = value instanceof Date ? value : new Date(value); 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")) { if (t.includes("decimal") || t.includes("currency")) {
return { FieldName: name, Item: Number(value), ItemElementName: "Decimal" }; return { FieldName: name, Item: Number(value), ItemElementName: "Decimal" };

View File

@ -29,7 +29,7 @@ const Mail = {
date: header.date instanceof Date ? header.date : new Date(header.date), date: header.date instanceof Date ? header.date : new Date(header.date),
bodyText, bodyText,
account, account,
direction: this._direction(header, account), direction: this._direction(header),
sizeBytes: header.size || 0, sizeBytes: header.size || 0,
}; };
}, },
@ -140,9 +140,8 @@ const Mail = {
return ""; return "";
}, },
_direction(header, accountName) { _direction(header) {
// Heuristik: ist der Account-Name/eine Identität Absender -> Ausgang. // Heuristik über den Ordnertyp; robustere Erkennung via Identitäten käme später.
// Robustere Erkennung erfolgt später über Identitäten; v1 simpel:
const folderType = header.folder && header.folder.type; const folderType = header.folder && header.folder.type;
if (folderType === "sent" || folderType === "outbox") return "Ausgang"; if (folderType === "sent" || folderType === "outbox") return "Ausgang";
return "Eingang"; return "Eingang";

View File

@ -73,13 +73,23 @@ const Settings = {
return s; 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) { async set(partial) {
const run = async () => {
const current = await this.get(); const current = await this.get();
const next = { ...current, ...partial }; const next = { ...current, ...partial };
// Sicherheitsnetz: Passwort darf NIE persistiert werden (z.B. aus Alt-Importen). // Sicherheitsnetz: Passwort darf NIE persistiert werden (z.B. aus Alt-Importen).
delete next.password; delete next.password;
await browser.storage.local.set({ settings: next }); await browser.storage.local.set({ settings: next });
return 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. */ /** Basis-URL der Platform-API ohne abschließenden Slash. */

View File

@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "DocuWare Ablage", "name": "DocuWare Ablage",
"description": "Legt markierte E-Mails (als .eml, PDF und mit Anhängen) in DocuWare ab.", "description": "Legt markierte E-Mails (als .eml, PDF und mit Anhängen) in DocuWare ab.",
"version": "0.10.7", "version": "0.10.8",
"author": "l.lingler", "author": "l.lingler",
"applications": { "applications": {
"gecko": { "gecko": {

View File

@ -12,6 +12,14 @@ function setStatus(msg, kind) {
el.className = 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() { async function load() {
const s = await Settings.get(); const s = await Settings.get();
FIELDS.forEach((f) => ($(f).value = s[f] || "")); FIELDS.forEach((f) => ($(f).value = s[f] || ""));
@ -213,8 +221,10 @@ function collect() {
// --- Events ---------------------------------------------------------------- // --- Events ----------------------------------------------------------------
$("save").addEventListener("click", async () => { $("save").addEventListener("click", async () => {
await Settings.set(collect()); const data = collect();
setStatus("Gespeichert.", "ok"); await Settings.set(data);
const warn = insecureWarning(data.serverUrl);
setStatus("Gespeichert." + warn, warn ? "err" : "ok");
}); });
$("test").addEventListener("click", async () => { $("test").addEventListener("click", async () => {
@ -228,7 +238,8 @@ $("test").addEventListener("click", async () => {
fillCabinets(cabinets, s.defaultCabinetId); fillCabinets(cabinets, s.defaultCabinetId);
const arch = cabinets.filter((c) => !c.isBasket).length; const arch = cabinets.filter((c) => !c.isBasket).length;
const bk = 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) { } catch (e) {
setStatus(`Fehler: ${e.message}`, "err"); setStatus(`Fehler: ${e.message}`, "err");
} }

View File

@ -33,6 +33,10 @@
{ {
"version": "0.10.7", "version": "0.10.7",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.7/docuware-ablage-0.10.7.xpi" "update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.7/docuware-ablage-0.10.7.xpi"
},
{
"version": "0.10.8",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.8/docuware-ablage-0.10.8.xpi"
} }
] ]
} }