Compare commits

..

8 Commits

Author SHA1 Message Date
e314a6af98 Release 0.12.3 2026-08-21 10:55:45 +02:00
03ff3795b2 Datum + Uhrzeit: Uhrzeit ging in Text- und DateTime-Feldern verloren
Das Token "Datum + Uhrzeit der E-Mail" lieferte nur das Datum. Drei Ursachen:

1. setFieldValue schrieb ein Date in ein TEXT-Feld ueber toDateInput, also nur
   "2026-08-04" - die Uhrzeit war weg, obwohl ein Textfeld sie problemlos traegt.
   Beim Upload wurde daraus String(dateObjekt), also die JS-Standardausgabe
   "Mon Aug 04 2026 16:28:51 GMT+0200 (...)". Neu: DocuWare.dateToText() liefert
   "04.08.2026 16:28:51", bei exakter Mitternacht nur "04.08.2026". Gilt fuer
   Text-, Memo- und Keyword-Felder und fuer Anzeige wie Upload gleichermassen.

2. TOKEN_FN.date war identisch mit dateTime (beide m.date). In einem Datums- oder
   DateTime-Feld fiel das nicht auf, in einem Textfeld schon: "Datum" haette dort
   ebenfalls eine Uhrzeit getragen. date liefert jetzt via dayOnly() den
   Kalendertag (lokale Mitternacht), wie archiveDate es schon tat.

3. Das datetime-local-Control kannte ohne step="1" nur Minuten und schnitt die
   Sekunden ab; fmtTime liefert sie jetzt mit.

Ausserdem geklaert: DateTime geht als LOKALE Wanduhrzeit ohne Zeitzonen-Suffix
raus ("2026-08-04T16:28:51") statt als toISOString() mit "Z". Ob der Server das
"Z" zurueckrechnet, haengt von dessen Zeitzone ab (Cloud meist UTC) - Zeiten
erschienen verschoben, und lokale Mitternacht kippte als UTC auf den VORTAG
22:00. Ohne Suffix ist der Wert eindeutig. Date-Felder bleiben unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:54:22 +02:00
d9ec2537e2 Release 0.12.2 2026-08-21 10:23:56 +02:00
f6c7761182 Feld-Zuordnung: Feldname wieder als Auswahl - und die Liste bleibt gefuellt
0.12.1 hatte den Feldnamen auf eine freie Texteingabe umgestellt. Das behob zwar
die Blockade, nahm aber die Auswahl weg - eigentlich war nur die Liste leer.

Jetzt wieder ein <select>, dessen Liste sich aus drei Quellen speist und damit
nie leer ist: Felder des gewaehlten Archivs, die Namen aus DEFAULT_FIELD_MAP und
alles im Formular bereits Vergebene. Ein Eintrag "Anderer Feldname" blendet eine
freie Eingabe ein, fuer Felder die in keiner Liste stehen. Ein gespeicherter Name,
den kein Archiv kennt, wird ins Dropdown aufgenommen statt verloren zu gehen.

Kern des Problems war die Herkunft der Liste: sie kam ausschliesslich vom Server
und setzte damit gecachte Schrankliste + Passwort dieser Sitzung + Archivwahl
voraus. Die Feldnamen werden nun je Archiv in storage.local unter
"fieldsByCabinet" gemerkt und beim Oeffnen VOR dem Aufbau der Zeilen gelesen -
die Auswahl steht also sofort und auch ohne Anmeldung. Ist ein Token da, wird im
Hintergrund aufgefrischt; schlaegt das fehl, bleibt die gemerkte Liste nutzbar.

Bei Server-Wechsel wird fieldsByCabinet mitgeloescht - die Feldlisten haengen an
Schrank-GUIDs des alten Servers (gleiche Logik wie fuer cabinets/defaultCabinetId).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:23:36 +02:00
ecbf1fcf33 Release 0.12.1 2026-08-21 10:08:13 +02:00
2e6a1a58e5 Feld-Zuordnung: Feldname wieder frei eingebbar
Die Spalte "DocuWare-Feldname" war ein starres <select>, dessen Optionen
ausschliesslich aus availableFields kamen - der Feldliste eines Archivs. Die ist
aber erst gefuellt, wenn Schrankliste gecacht, Passwort dieser Sitzung eingegeben
und ein Archiv gewaehlt ist. Bis dahin enthielt jedes Dropdown nur
"- Feld waehlen -": eine neue Zeile hatte null auswaehlbare Felder, und weil
collectMapping() Zeilen ohne Feldnamen verwirft, verschwand sie beim Speichern
kommentarlos wieder.

Jetzt wieder freie Texteingabe mit Vorschlagsliste (<input list>), wie der
veraltete Kommentar an mapCabinet es schon beschrieb. Die Vorschlaege kommen aus
drei Quellen statt einer: Felder des gewaehlten Archivs, die Namen aus
DEFAULT_FIELD_MAP und alles bereits Eingetragene - damit stehen sofort nach dem
Oeffnen Vorschlaege bereit, auch ohne Anmeldung.

fillFieldSelect entfaellt; refreshFieldSelects baut nur noch die gemeinsame
<datalist> auf und wird zusaetzlich am Ende von loadMapping aufgerufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:07:41 +02:00
cbaee51b4e Release 0.12.0 2026-08-20 16:56:38 +02:00
7e351a818a 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>
2026-08-20 16:55:32 +02:00
8 changed files with 359 additions and 47 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,
@ -35,12 +37,41 @@ const TOKEN_FN = {
bcc: (m) => m.bcc,
subject: (m) => m.subject,
body: (m) => m.bodyText,
date: (m) => m.date,
// Nur der Kalendertag: auf lokale Mitternacht gesetzt, damit „Datum" auch in
// einem DateTime- oder Textfeld keine Uhrzeit trägt (gleiche Konvention wie
// archiveDate). Den echten Zeitpunkt liefert dateTime.
date: (m) => dayOnly(m.date),
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: () => dayOnly(new Date()),
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 +80,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 +499,12 @@ 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";
// Ohne step="1" kennt das Control nur Minuten und schneidet die Sekunden ab.
el.step = "1";
} else if (t.includes("date")) {
el = document.createElement("input");
el.type = "date";
@ -493,10 +537,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 +557,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) {
@ -517,7 +569,10 @@ function setFieldValue(name, f, val) {
}
el.value = String(val);
} else {
el.value = val instanceof Date ? toDateInput(val) : String(val);
// Textfeld: Datum lesbar ausschreiben ("04.08.2026 16:28:51"). Früher lief
// hier toDateInput – das warf die Uhrzeit weg, obwohl ein Textfeld sie
// problemlos trägt (genau der Fall „Datum + Uhrzeit" ohne Uhrzeit).
el.value = val instanceof Date ? DocuWare.dateToText(val) : String(val);
}
wrap.classList.add("prefilled");
}
@ -576,8 +631,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 +702,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 +714,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 +735,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 +789,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 +864,38 @@ 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).
// Kalendertag ohne Uhrzeit (lokale Mitternacht). Funktionsdeklaration, damit
// TOKEN_FN sie oben im Modul schon verwenden kann.
function dayOnly(d) {
const dt = d instanceof Date ? d : new Date(d);
if (isNaN(dt)) return dt;
return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate());
}
// Uhrzeit inkl. Sekunden ("16:28:51"). Die Sekunden braucht auch
// toDateTimeInput – ein datetime-local mit step="1" nimmt sie entgegen.
function fmtTime(d) {
const dt = d instanceof Date ? d : new Date(d);
if (isNaN(dt)) return "";
const p = (n) => String(n).padStart(2, "0");
return `${p(dt.getHours())}:${p(dt.getMinutes())}:${p(dt.getSeconds())}`;
}
// 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 +927,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

@ -427,8 +427,48 @@ const DocuWare = {
return out;
},
/**
* Date-Objekt als deutscher Text: "04.08.2026 16:28:51". Ohne Uhrzeit (exakt
* Mitternacht) nur "04.08.2026" – so tragen reine Datums-Tokens kein
* künstliches " 00:00:00". Gebraucht, sobald ein Datum in einem TEXT-Feld
* landet: String(dateObjekt) lieferte sonst die JS-Standardausgabe
* ("Mon Aug 04 2026 16:28:51 GMT+0200 (Mitteleuropäische Sommerzeit)").
* Bewusst lokale Zeit ohne UTC-Umrechnung – der Text soll zeigen, was der
* Nutzer in Thunderbird sieht.
*/
dateToText(value) {
const d = value instanceof Date ? value : new Date(value);
if (isNaN(d)) return String(value);
const p = (n) => String(n).padStart(2, "0");
const day = `${p(d.getDate())}.${p(d.getMonth() + 1)}.${d.getFullYear()}`;
if (!d.getHours() && !d.getMinutes() && !d.getSeconds()) return day;
return `${day} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
},
_field(name, value, type) {
const t = (type || "Text").toLowerCase();
// Datum in einem Text-/Memo-Feld lesbar machen (s. dateToText).
const asText = (v) => (v instanceof Date ? this.dateToText(v) : String(v));
// 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" };
// LOKALE Wanduhrzeit ohne Zeitzonen-Suffix ("2026-08-04T16:28:51").
//
// Vorher ging hier d.toISOString() raus, also UTC mit "Z". Ob DocuWare das
// "Z" in Serverzeit zurückrechnet, hängt vom Server ab (Cloud-Instanzen
// laufen meist auf UTC) – die Uhrzeit erschien dann um den Versatz
// verschoben, in Wien -2 h. Schlimmer beim Kalendertag: lokale Mitternacht
// wird als UTC zum VORTAG 22:00.
// Ohne Suffix ist der Wert eindeutig: der Server übernimmt ihn so, wie der
// Nutzer die Zeit in Thunderbird sieht. Reine Date-Felder bleiben unberührt.
const p = (n) => String(n).padStart(2, "0");
const iso =
`${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}` +
`T${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
return { FieldName: name, Item: iso, 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);
@ -446,13 +486,13 @@ const DocuWare = {
return { FieldName: name, Item: Number(value), ItemElementName: "Int" };
}
if (t.includes("memo")) {
return { FieldName: name, Item: String(value), ItemElementName: "Memo" };
return { FieldName: name, Item: asText(value), ItemElementName: "Memo" };
}
if (t.includes("keyword")) {
const arr = Array.isArray(value) ? value : [String(value)];
const arr = Array.isArray(value) ? value : [asText(value)];
return { FieldName: name, Item: arr, ItemElementName: "Keywords" };
}
return { FieldName: name, Item: String(value), ItemElementName: "String" };
return { FieldName: name, Item: asText(value), ItemElementName: "String" };
},
_isSystemField(dbName) {

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

View File

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

View File

@ -108,7 +108,8 @@
.map-row.has-const .map-left { flex: 0 0 250px; }
.map-right { display: none; flex: 1 1 auto; min-width: 0; }
.map-row.has-const .map-right { display: flex; align-items: center; }
.map-left select, .map-right input { width: 100%; padding: 7px 9px; }
.map-left select, .map-left input, .map-right input { width: 100%; padding: 7px 9px; }
.map-left input[hidden] { display: none; }
.map-hint { font-size: 11px; letter-spacing: .4px; text-transform: uppercase;
color: var(--muted); font-weight: 600; margin: 2px 2px 8px; }
.icon-btn {
@ -227,6 +228,8 @@
<button id="mapAdd" class="ghost" type="button">+ Zeile</button>
<button id="mapReset" class="ghost" type="button">Auf Standard zurücksetzen</button>
</div>
<div class="hint" style="margin-top:8px">Steht ein Feld nicht in der Liste,
„<strong>✎ Anderer Feldname</strong>" wählen und den Namen eintippen.</div>
<div class="hint" style="margin-top:8px">Als Mail-Attribut „<strong>Fester Wert</strong>"
wählen, um einen festen Text (z. B. <code>NEU</code>) ins Feld zu schreiben.</div>

View File

@ -58,7 +58,12 @@ async function invalidateSession(s) {
let next = s;
if (server) {
next = await Settings.set({ defaultCabinetId: "", dialogByCabinet: {} });
// Gemerkte Feldlisten hängen an Schrank-GUIDs des alten Servers -> weg damit.
fieldsByCabinet = {};
await browser.storage.local.remove("fieldsByCabinet").catch(() => {});
availableFields = [];
fillCabinets([], ""); // Dropdowns leeren – die alte Liste gilt nicht mehr
refreshFieldSelects();
}
snapshotAuth(next);
return next;
@ -71,13 +76,23 @@ async function load() {
CHECKS.forEach((c) => ($(c).checked = !!s[c]));
setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml");
$("mappingUrl").value = s.mappingUrl || "";
loadMapping(s.fieldMapping && Object.keys(s.fieldMapping).length ? s.fieldMapping : DEFAULT_FIELD_MAP);
// Schrankliste, falls vorher schon getestet (im Storage gecached)
const cached = (await browser.storage.local.get("cabinets")).cabinets || [];
fillCabinets(cached, s.defaultCabinetId);
// Best-effort: Felder des Standard-Archivs gleich laden – nur wenn schon ein
// Sitzungs-Token da ist (sonst wählt der Nutzer das Archiv manuell).
// Gemerkte Schrank- und Feldlisten VOR dem Aufbau der Zeilen lesen, damit die
// Feldname-Dropdowns schon beim ersten Rendern gefüllt sind.
const local = await browser.storage.local.get(["cabinets", "fieldsByCabinet"]);
fieldsByCabinet = local.fieldsByCabinet || {};
if (s.defaultCabinetId && Array.isArray(fieldsByCabinet[s.defaultCabinetId])) {
availableFields = fieldsByCabinet[s.defaultCabinetId];
}
loadMapping(s.fieldMapping && Object.keys(s.fieldMapping).length ? s.fieldMapping : DEFAULT_FIELD_MAP);
fillCabinets(local.cabinets || [], s.defaultCabinetId);
if (availableFields.length) {
$("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl (gemerkt).`;
}
// Best-effort auffrischen – nur wenn schon ein Sitzungs-Token da ist. Ohne
// Token bleibt es bei der gemerkten Liste, die Auswahl funktioniert trotzdem.
if (s.defaultCabinetId && [...$("mapCabinet").options].some((o) => o.value === s.defaultCabinetId)) {
$("mapCabinet").value = s.defaultCabinetId;
try {
@ -86,10 +101,11 @@ async function load() {
Auth.setToken(stt.token);
availableFields = (await DocuWare.cabinetFieldNames(s, s.defaultCabinetId))
.filter((n) => n && !/^DW[A-Z]/.test(n)).sort();
await rememberFields(s.defaultCabinetId, availableFields);
refreshFieldSelects();
$("mapFieldStatus").textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
}
} catch (_) { /* egal – Archiv kann manuell gewählt werden */ }
} catch (_) { /* egal – gemerkte Liste bleibt nutzbar */ }
}
}
@ -159,8 +175,19 @@ async function ensureToken() {
// --- Feld-Mapping-Editor ---------------------------------------------------
const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert"
let availableFields = []; // Feldnamen aus dem zuletzt gewählten Archiv
const CONST_OPT = "__const__"; // Sentinel im Quelle-Dropdown für „Fester Wert"
const CUSTOM_OPT = "__custom__"; // Sentinel im Feldname-Dropdown für „eigener Name"
let availableFields = []; // Feldnamen des zuletzt gewählten Archivs
// Gemerkte Feldnamen je Archiv (storage.local, Schlüssel "fieldsByCabinet").
// Ohne diesen Cache war die Auswahlliste bei jedem Thunderbird-Start wieder leer:
// sie kam nur vom Server und setzte damit Passworteingabe + Archivwahl voraus.
let fieldsByCabinet = {};
async function rememberFields(cabinetId, names) {
fieldsByCabinet = { ...fieldsByCabinet, [cabinetId]: names };
await browser.storage.local.set({ fieldsByCabinet }).catch(() => {});
}
// Zerlegt einen Mapping-Wert (Token-String ODER { const } / { token }) in den UI-Zustand.
function mapValueState(value) {
@ -172,23 +199,53 @@ function mapValueState(value) {
return { sel: "", konst: "" };
}
// Befüllt ein Feldname-Dropdown mit den Archiv-Feldern + dem aktuellen Wert (bleibt erhalten).
// Feldname einer Zeile – aus dem Dropdown oder, bei „Anderer Feldname", aus der
// daneben eingeblendeten Eingabe.
function rowFieldName(row) {
const sel = row.querySelector(".map-field-sel");
if (!sel) return "";
if (sel.value === CUSTOM_OPT) {
const inp = row.querySelector(".map-field-custom");
return inp ? inp.value.trim() : "";
}
return sel.value.trim();
}
// Auswahlliste für die Feldname-Dropdowns. Drei Quellen, damit sie NIE leer ist:
// Felder des gewählten Archivs (auch aus dem Cache, also ohne Anmeldung), die
// Standard-Feldnamen und alles im Formular bereits Vergebene.
function fieldSuggestions() {
const used = [...document.querySelectorAll("#mapBody .map-row")].map(rowFieldName);
const all = [...(availableFields || []), ...Object.keys(DEFAULT_FIELD_MAP), ...used];
return [...new Set(all.filter(Boolean))].sort();
}
// Ein Feldname-Dropdown neu aufbauen. Der aktuelle Wert steht immer mit drin,
// auch wenn das Archiv ihn nicht (mehr) kennt – sonst ginge er beim Neuaufbau
// still verloren.
function fillFieldSelect(sel, current) {
sel.innerHTML = "";
const empty = document.createElement("option");
empty.value = ""; empty.textContent = "— Feld wählen —";
sel.appendChild(empty);
[...new Set([...(availableFields || []), current].filter(Boolean))].sort().forEach((n) => {
[...new Set([...fieldSuggestions(), current].filter(Boolean))].sort().forEach((n) => {
const o = document.createElement("option");
o.value = n; o.textContent = n;
sel.appendChild(o);
});
const own = document.createElement("option");
own.value = CUSTOM_OPT; own.textContent = "✎ Anderer Feldname …";
sel.appendChild(own);
sel.value = current || "";
}
// Aktualisiert alle Feldname-Dropdowns auf die aktuelle Archiv-Feldliste (Auswahl bleibt).
// Alle Dropdowns auf die aktuelle Liste bringen (Auswahl bleibt). Zeilen mit
// freier Eingabe bleiben unangetastet.
function refreshFieldSelects() {
document.querySelectorAll("#mapBody .map-field").forEach((sel) => fillFieldSelect(sel, sel.value));
document.querySelectorAll("#mapBody .map-field-sel").forEach((sel) => {
if (sel.value === CUSTOM_OPT) return;
fillFieldSelect(sel, sel.value);
});
}
function addMapRow(field = "", value = "") {
@ -196,11 +253,33 @@ function addMapRow(field = "", value = "") {
const row = document.createElement("div");
row.className = "map-row" + (st.sel === CONST_OPT ? " has-const" : "");
// Links gestapelt: Feldname (Dropdown) über Mail-Attribut.
// Links gestapelt: Feldname (Dropdown, bei Bedarf freie Eingabe) über Mail-Attribut.
const left = document.createElement("div"); left.className = "map-left";
const fieldSel = document.createElement("select"); fieldSel.className = "map-field";
// Feldname wird ausgewählt. Die Liste ist immer gefüllt (Archiv-Cache +
// Standardnamen), und „✎ Anderer Feldname" blendet eine freie Eingabe ein –
// für Felder, die in keiner Liste stehen.
const fieldSel = document.createElement("select");
fieldSel.className = "map-field-sel";
const own = document.createElement("input");
own.type = "text"; own.className = "map-field-custom";
own.placeholder = "Eigener Feldname, z. B. EML_SUBJECT";
own.hidden = true;
fillFieldSelect(fieldSel, field);
if (field && fieldSel.value !== field) {
// Sicherheitsnetz: Name nicht im Dropdown gelandet -> als freie Eingabe zeigen,
// statt ihn stillschweigend zu verlieren.
fieldSel.value = CUSTOM_OPT; own.value = field; own.hidden = false;
}
fieldSel.addEventListener("change", () => {
const isOwn = fieldSel.value === CUSTOM_OPT;
own.hidden = !isOwn;
if (isOwn) own.focus();
});
own.addEventListener("change", refreshFieldSelects);
const src = document.createElement("select"); src.className = "map-source";
const none = document.createElement("option"); none.value = ""; none.textContent = "— nichts —";
@ -216,7 +295,7 @@ function addMapRow(field = "", value = "") {
if (st.sel === CONST_OPT) co.selected = true;
src.appendChild(co);
left.append(fieldSel, src);
left.append(fieldSel, own, src);
// Rechts (breit): fester Wert – nur sichtbar bei „Fester Wert".
const right = document.createElement("div"); right.className = "map-right";
@ -242,12 +321,13 @@ function addMapRow(field = "", value = "") {
function loadMapping(map) {
$("mapBody").innerHTML = "";
Object.entries(map || {}).forEach(([f, v]) => addMapRow(f, v));
refreshFieldSelects();
}
function collectMapping() {
const map = {};
document.querySelectorAll("#mapBody .map-row").forEach((row) => {
const f = row.querySelector(".map-field").value.trim();
const f = rowFieldName(row);
if (!f) return; // ohne Feldname ignorieren
const sel = row.querySelector(".map-source").value;
if (sel === CONST_OPT) {
@ -355,19 +435,32 @@ $("mappingPull").addEventListener("click", async () => {
}
});
// Archiv wählen -> dessen Indexfelder als Vorschläge (datalist) für alle Feldnamen.
// Archiv wählen -> dessen Indexfelder als Vorschläge für alle Feldname-Eingaben.
$("mapCabinet").addEventListener("change", async () => {
const id = $("mapCabinet").value;
const st = $("mapFieldStatus");
if (!id) { availableFields = []; refreshFieldSelects(); st.textContent = ""; return; }
st.textContent = "Lade Felder …";
// Gemerkte Liste sofort anzeigen – dann ist die Auswahl auch ohne Anmeldung da.
if (Array.isArray(fieldsByCabinet[id])) {
availableFields = fieldsByCabinet[id];
refreshFieldSelects();
st.textContent = `${availableFields.length} Felder zur Auswahl (gemerkt).`;
} else {
st.textContent = "Lade Felder …";
}
try {
const s = await Settings.get();
const ok = await ensureToken();
if (!ok) { st.textContent = "Erst „Verbindung testen“ (Passwort nötig)."; return; }
if (!ok) {
if (!availableFields.length) st.textContent = "Erst „Verbindung testen“ (Passwort nötig).";
return; // gemerkte Liste bleibt stehen
}
availableFields = (await DocuWare.cabinetFieldNames(s, id))
.filter((n) => n && !/^DW[A-Z]/.test(n)) // DW*-Systemfelder ausblenden
.sort();
await rememberFields(id, availableFields);
refreshFieldSelects();
st.textContent = `${availableFields.length} Felder zur Auswahl geladen.`;
} catch (e) {

View File

@ -41,6 +41,22 @@
{
"version": "0.11.0",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.11.0/docuware-ablage-0.11.0.xpi"
},
{
"version": "0.12.0",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.0/docuware-ablage-0.12.0.xpi"
},
{
"version": "0.12.1",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.1/docuware-ablage-0.12.1.xpi"
},
{
"version": "0.12.2",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.2/docuware-ablage-0.12.2.xpi"
},
{
"version": "0.12.3",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.12.3/docuware-ablage-0.12.3.xpi"
}
]
}