Feld-Zuordnung: Dateiname, Datum+Uhrzeit, Benutzer und weitere Werte

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 <noreply@anthropic.com>
This commit is contained in:
sylyx 2026-08-20 16:55:32 +02:00
parent 6ca1e01b46
commit 7e351a818a
4 changed files with 176 additions and 20 deletions

View File

@ -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 = '<option value=""></option>';
} 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 <input type="datetime-local">.
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";

View File

@ -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);

View File

@ -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) {

View File

@ -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",