From 7e351a818a4b038fc1495161baf57bb7385fd80b Mon Sep 17 00:00:00 2001 From: sylyx Date: Thu, 20 Aug 2026 16:55:32 +0200 Subject: [PATCH] Feld-Zuordnung: Dateiname, Datum+Uhrzeit, Benutzer und weitere Werte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MAIL_TOKENS wächst von 11 auf 22 auswählbare Werte: - Dokument: fileName (je Dokument), attachmentCount, rfcMessageId - Datum/Zeit: dateTime, time, archiveDate, archiveDateTime - Wer/Woher: dwUser, dwOrganization, accountEmail, folder fileName kann nicht wie die übrigen Tokens einmal je Mail aufgelöst werden: die .eml/PDF trägt den Mailnamen, jeder Anhang seinen eigenen. Darum tragen TOKEN_FN jetzt ein zweites Argument ctx {fileName, attachmentCount}, und Felder, die auf ein Token aus PER_FILE_TOKENS gemappt sind, werden über applyPerFileFields() vor JEDEM Upload neu gesetzt. Im Formular steht nur eine Vorschau (Tooltip erklärt das), die beim Umschalten .eml <-> PDF nachzieht. Fix DateTime-Felder: _field prüfte t.includes("date") zuerst, und "DateTime" enthält "date" — jedes DocuWare-DateTime-Feld wurde damit auf UTC-Mitternacht normalisiert und verlor die Uhrzeit. Das betraf auch bestehende Zuordnungen. Jetzt wird datetime zuerst geprüft; der Dialog rendert solche Felder als datetime-local und liest sie lokal zurück. Date-Felder bleiben unverändert auf dem lokalen Kalendertag als UTC-Mitternacht. Offen und im Code vermerkt: ob DocuWare bei DateTime das "Z" auswertet oder die ISO-Zeichenkette als lokale Wanduhrzeit liest, ist nicht gegen einen Server verifiziert. Mail.getMeta liefert zusätzlich folder und accountEmail; _accountForMessage wurde zu _accountInfo (Name + Identitäts-Adresse). DEFAULT_FIELD_MAP: DOC_FILE_NAME -> fileName. Co-Authored-By: Claude Opus 5 --- dialog/dialog.js | 140 ++++++++++++++++++++++++++++++++++++++++++----- lib/docuware.js | 15 +++++ lib/mail.js | 19 +++++-- lib/store.js | 22 ++++++++ 4 files changed, 176 insertions(+), 20 deletions(-) diff --git a/dialog/dialog.js b/dialog/dialog.js index 391b37e..f95ec0d 100644 --- a/dialog/dialog.js +++ b/dialog/dialog.js @@ -27,6 +27,8 @@ let STATE = { // Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert. // Welches DocuWare-Feld welchen Token bekommt, steht im Mapping (s. effectiveFieldMap). +// Zweites Argument ctx trägt Werte, die nicht an der Mail hängen, sondern am +// konkreten Upload: { fileName, attachmentCount }. const TOKEN_FN = { senderEmail: (m) => m.senderEmail, senderName: (m) => m.senderName, @@ -39,8 +41,34 @@ const TOKEN_FN = { direction: (m) => m.direction, size: (m) => m.sizeBytes, account: (m) => m.account, + + // Datum/Zeit. date liefert den Kalendertag, dateTime den echten Zeitpunkt – + // in DocuWare braucht letzteres ein Feld vom Typ DateTime, sonst schneidet + // der Server die Uhrzeit ab. + dateTime: (m) => m.date, + time: (m) => fmtTime(m.date), + // archiveDate ist bewusst auf Mitternacht gesetzt: so bleibt es „nur Datum", + // auch wenn es auf ein DateTime-Feld gemappt wird. archiveDateTime trägt die Uhrzeit. + archiveDate: () => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), n.getDate()); }, + archiveDateTime: () => new Date(), + + // Dokumentbezogen + fileName: (m, ctx) => (ctx && ctx.fileName) || "", + attachmentCount: (m, ctx) => (ctx && ctx.attachmentCount) || 0, + rfcMessageId: (m) => DocuWare.normalizeMessageId(m.rfcMessageId), + + // Wer/Woher – aus den Einstellungen bzw. dem Thunderbird-Konto. + dwUser: () => (STATE.settings && STATE.settings.username) || "", + dwOrganization: () => (STATE.settings && STATE.settings.organization) || "", + accountEmail: (m) => m.accountEmail || "", + folder: (m) => m.folder || "", }; +// Tokens, deren Wert vom einzelnen hochgeladenen Dokument abhängt. Felder, die +// darauf gemappt sind, werden je Upload neu gesetzt – sonst trüge jeder Anhang +// den Dateinamen der E-Mail. +const PER_FILE_TOKENS = new Set(["fileName"]); + // Aktives Feld-Mapping: eigenes (aus den Einstellungen) wenn gesetzt, sonst Default. function effectiveFieldMap() { const m = STATE.settings && STATE.settings.fieldMapping; @@ -49,16 +77,23 @@ function effectiveFieldMap() { // 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) { +function prefillFor(fieldName, meta, ctx) { 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); + if (m.token && TOKEN_FN[m.token]) return TOKEN_FN[m.token](meta, ctx); return undefined; } const fn = TOKEN_FN[m]; - return fn ? fn(meta) : undefined; + return fn ? fn(meta, ctx) : undefined; +} + +// Auf welchen Token ist ein Feld gemappt? (String-Form oder { token }.) +function tokenOf(fieldName) { + const m = effectiveFieldMap()[fieldName]; + if (typeof m === "string") return m; + return m && typeof m === "object" ? m.token : undefined; } function status(msg, kind) { @@ -461,6 +496,10 @@ function buildInput(f) { // Nur Listenwerte erlaubt -> starres Dropdown. el = document.createElement("select"); el.innerHTML = ''; + } else if (t.includes("datetime")) { + // DateTime-Feld: Tag UND Uhrzeit erfassbar (vor dem reinen date-Zweig prüfen). + el = document.createElement("input"); + el.type = "datetime-local"; } else if (t.includes("date")) { el = document.createElement("input"); el.type = "date"; @@ -493,10 +532,18 @@ function applyFieldDefaults() { } function prefillFields() { + const ctx = formCtx(); STATE.fields.forEach((f) => { - const val = prefillFor(f.name, STATE.meta); + const val = prefillFor(f.name, STATE.meta, ctx); if (val === undefined || val === null || val === "") return; setFieldValue(f.name, f, val); + // Dateiname-Felder sind nur eine Vorschau: beim Ablegen trägt jeder Anhang + // seinen eigenen Namen. Im Tooltip erklären, statt still zu überschreiben. + if (PER_FILE_TOKENS.has(tokenOf(f.name))) { + const wrap = document.querySelector(`.field[data-name="${cssEsc(f.name)}"]`); + const el = wrap && wrap.querySelector("[data-role='value']"); + if (el) el.title = "Wird beim Ablegen je Dokument gesetzt (Anhänge tragen ihren eigenen Dateinamen)."; + } }); } @@ -505,9 +552,9 @@ 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") { + if (el.type === "date" || el.type === "datetime-local") { const d = val instanceof Date ? val : new Date(val); - if (!isNaN(d)) el.value = toDateInput(d); + if (!isNaN(d)) el.value = el.type === "date" ? toDateInput(d) : toDateTimeInput(d); } else if (el.tagName === "SELECT") { let opt = Array.from(el.options).find((o) => o.value === String(val)); if (!opt) { @@ -576,8 +623,12 @@ function collectFieldValues() { } 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; + // interpretiert new Date("YYYY-MM-DD") als UTC. datetime-local ist bereits + // lokal und wird direkt geparst. + const value = + el.type === "date" ? new Date(`${raw}T00:00:00`) + : el.type === "datetime-local" ? new Date(raw) + : raw; result[name] = { value, type }; } }); @@ -643,7 +694,8 @@ async function submit() { 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 msgCtx = { attachmentCount: atts.length }; + const fields = STATE.isBasket ? {} : fieldsForMessage(result, meta, multi, msgCtx); // 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" }; @@ -654,14 +706,16 @@ async function submit() { 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); + const ef = applyPerFileFields(fields, meta, { ...msgCtx, fileName: eml.name }); + await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, ef); }); } 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); + const pf = applyPerFileFields(fields, meta, { ...msgCtx, fileName: pdf.name }); + await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, pf); }); } if (wantAtt) { @@ -673,7 +727,14 @@ async function submit() { 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" } }; + // Anhang trägt seinen EIGENEN Dateinamen – sowohl in gemappten + // fileName-Feldern als auch im festen DOC_FILE_NAME. + const af = STATE.isBasket + ? {} + : { + ...applyPerFileFields(fields, meta, { ...msgCtx, fileName: a.name }), + DOC_FILE_NAME: { value: a.name, type: "Text" }, + }; await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, file, a.name, af); }); } @@ -720,17 +781,32 @@ async function uploadOnce(part, doUpload) { // 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) { +function fieldsForMessage(baseResult, meta, multi, ctx) { if (!multi) return baseResult; // Einzelmail: manuelle Edits respektieren const result = { ...baseResult }; STATE.fields.forEach((f) => { - const val = prefillFor(f.name, meta); + const val = prefillFor(f.name, meta, ctx); if (val === undefined || val === null || val === "") return; result[f.name] = { value: val, type: f.type }; }); return result; } +// Felder, die auf ein dokumentabhängiges Token (fileName) gemappt sind, für +// GENAU dieses Dokument setzen – auch bei Einzelmail, weil ein Formularwert +// nicht gleichzeitig für .eml und jeden Anhang stimmen kann. +function applyPerFileFields(fields, meta, ctx) { + if (STATE.isBasket) return fields; // Briefkorb: keine Indexfelder + const out = { ...fields }; + STATE.fields.forEach((f) => { + if (!PER_FILE_TOKENS.has(tokenOf(f.name))) return; + const val = prefillFor(f.name, meta, ctx); + if (val === undefined || val === null || val === "") return; + out[f.name] = { value: val, type: f.type }; + }); + return out; +} + // Sticky: zuletzt genutztes Format + Anhänge-Wahl in den Einstellungen merken. async function saveOptionPrefs(mailFormat, storeAttachments) { try { @@ -780,6 +856,27 @@ const toDateInput = (d) => { const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; }; +// Lokal als YYYY-MM-DDTHH:MM für . +const toDateTimeInput = (d) => `${toDateInput(d)}T${fmtTime(d)}`; +// Uhrzeit HH:MM lokal (auch als eigenständiger Text-Token nutzbar). +function fmtTime(d) { + const dt = d instanceof Date ? d : new Date(d); + if (isNaN(dt)) return ""; + return `${String(dt.getHours()).padStart(2, "0")}:${String(dt.getMinutes()).padStart(2, "0")}`; +} + +// Dateiname, den die E-Mail selbst beim Upload bekäme – für die Vorschau im +// Formular. Beim tatsächlichen Upload gilt der echte Name (siehe submit()). +function previewFileName() { + const subj = (STATE.meta && STATE.meta.subject) || ""; + const base = subj.replace(/[\\/:*?"<>|]+/g, "_").slice(0, 80).trim() || "email"; + return `${base}.${getMailFormat() === "pdf" ? "pdf" : "eml"}`; +} + +// Kontext für die Token-Auflösung im Formular (Vorschauwerte). +function formCtx() { + return { fileName: previewFileName(), attachmentCount: STATE.attachments.length }; +} // 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, "\\$&"); @@ -811,8 +908,21 @@ $("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)) + r.addEventListener("change", () => { + refreshPerFilePreview(); // .eml ↔ .pdf: Dateiname-Vorschau nachziehen + saveOptionPrefs(getMailFormat(), $("optAtt").checked); + }) ); + +// Aktualisiert die Vorschauwerte dokumentabhängiger Felder (Dateiname). +function refreshPerFilePreview() { + const ctx = formCtx(); + STATE.fields.forEach((f) => { + if (!PER_FILE_TOKENS.has(tokenOf(f.name))) return; + const val = prefillFor(f.name, STATE.meta, ctx); + if (val !== undefined && val !== null && val !== "") setFieldValue(f.name, f, val); + }); +} $("optAtt").addEventListener("change", () => { const show = !isMulti() && $("optAtt").checked && STATE.attachments.length > 0; $("attList").style.display = show ? "" : "none"; diff --git a/lib/docuware.js b/lib/docuware.js index 53aff50..da87194 100644 --- a/lib/docuware.js +++ b/lib/docuware.js @@ -429,6 +429,21 @@ const DocuWare = { _field(name, value, type) { const t = (type || "Text").toLowerCase(); + // WICHTIG: "datetime" zuerst prüfen – "DateTime".includes("date") ist true und + // würde sonst die Uhrzeit auf Mitternacht normalisieren (Datenverlust). + if (t.includes("datetime")) { + const d = value instanceof Date ? value : new Date(value); + if (isNaN(d)) return { FieldName: name, Item: String(value), ItemElementName: "DateTime" }; + // Mit Uhrzeit: echter Zeitpunkt, daher unverändert als ISO (UTC) senden. + // + // UNGEPRÜFT gegen den Server: ob DocuWare das "Z" auswertet (dann stimmt die + // Uhrzeit) oder die ISO-Zeichenkette als lokale Wanduhrzeit übernimmt (dann + // erscheint sie um den UTC-Versatz verschoben, in Wien -2 h). Reine Date-Felder + // sind davon nicht betroffen, die werden auf UTC-Mitternacht normalisiert. + // Falls im Testarchiv verschoben: hier stattdessen die lokale Wanduhrzeit ohne + // Zeitzone senden, also `${toLocalIso(d)}` statt d.toISOString(). + return { FieldName: name, Item: d.toISOString(), ItemElementName: "DateTime" }; + } if (t.includes("date")) { // DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform. const d = value instanceof Date ? value : new Date(value); diff --git a/lib/mail.js b/lib/mail.js index 29a4833..15d5b61 100644 --- a/lib/mail.js +++ b/lib/mail.js @@ -16,9 +16,11 @@ const Mail = { const bodyText = this._extractText(full); const sender = this._parseAddress(header.author); - const account = await this._accountForMessage(messageId); + const acc = await this._accountInfo(messageId); return { + folder: (header.folder && (header.folder.name || header.folder.path)) || "", + accountEmail: acc.email, rfcMessageId: this._header(full, "message-id"), // RFC822 Message-ID (Dedup-Schlüssel) subject: header.subject || "", senderEmail: sender.email, @@ -28,7 +30,7 @@ const Mail = { bcc: (header.bccList || []).join(", "), date: header.date instanceof Date ? header.date : new Date(header.date), bodyText, - account, + account: acc.name, direction: this._direction(header), sizeBytes: header.size || 0, }; @@ -128,16 +130,23 @@ const Mail = { return { name: "", email: str.trim() }; }, - async _accountForMessage(messageId) { + /** + * Konto-Name und eigene E-Mail-Adresse (erste Identität) zur Nachricht. + * @returns {Promise<{name:string, email:string}>} + */ + async _accountInfo(messageId) { try { const msg = await browser.messages.get(messageId); const folder = msg.folder; if (folder && folder.accountId) { const acc = await browser.accounts.get(folder.accountId); - return acc ? acc.name : ""; + if (acc) { + const ident = (acc.identities || [])[0]; + return { name: acc.name || "", email: (ident && ident.email) || "" }; + } } } catch (_) {} - return ""; + return { name: "", email: "" }; }, _direction(header) { diff --git a/lib/store.js b/lib/store.js index 27f1532..ca81792 100644 --- a/lib/store.js +++ b/lib/store.js @@ -32,6 +32,27 @@ const MAIL_TOKENS = [ { key: "direction", label: "Richtung (Ein-/Ausgang)" }, { key: "size", label: "Größe (Bytes)" }, { key: "account", label: "Konto" }, + + // --- Datum/Zeit --------------------------------------------------------- + // "date" liefert nur den Kalendertag. Für DocuWare-Felder vom Typ DateTime + // gibt es "dateTime" (Tag + Uhrzeit); "time" nur die Uhrzeit als Text. + { key: "dateTime", label: "Datum + Uhrzeit der E-Mail" }, + { key: "time", label: "Uhrzeit der E-Mail" }, + { key: "archiveDate", label: "Ablagedatum (heute)" }, + { key: "archiveDateTime", label: "Ablagezeitpunkt (jetzt)" }, + + // --- Dokument ----------------------------------------------------------- + // fileName wird JE HOCHGELADENEM DOKUMENT gesetzt: .eml/PDF tragen den + // Mailnamen, jeder Anhang seinen eigenen Dateinamen. + { key: "fileName", label: "Dateiname (je Dokument)" }, + { key: "attachmentCount", label: "Anzahl Anhänge" }, + { key: "rfcMessageId", label: "Message-ID (RFC822)" }, + + // --- Wer/Woher ---------------------------------------------------------- + { key: "dwUser", label: "DocuWare-Benutzer (Mitarbeiter)" }, + { key: "dwOrganization", label: "DocuWare-Organisation" }, + { key: "accountEmail", label: "Eigene E-Mail-Adresse" }, + { key: "folder", label: "Ordner" }, ]; // Standard-Zuordnung DocuWare-Feldname -> Mail-Token. Greift, solange der Nutzer @@ -53,6 +74,7 @@ const DEFAULT_FIELD_MAP = { EML_SIZE: "size", EML_ACCOUNT: "account", DOC_DATE: "date", + DOC_FILE_NAME: "fileName", // .eml/PDF: Mailname, Anhang: eigener Dateiname SUBJECT: "subject", BETREFF: "subject", EMAIL: "senderEmail",