Compare commits

...

2 Commits

Author SHA1 Message Date
c7f205d9bc Release 0.10.3 2026-06-16 14:16:34 +02:00
53d3675dc1 Feature: Mail-Format .eml ODER PDF (nicht beides) + konfigurierbares Feld-Mapping
- Dialog/Optionen: mailFormat-Radio (.eml/PDF) + eigener Anhänge-Haken statt
  scope/optPdf; nie mehr eml UND pdf gleichzeitig. Migration storeEml/storePdf.
- Feld-Zuordnung in den Einstellungen editierbar (MAIL_TOKENS/DEFAULT_FIELD_MAP
  in store.js); PREFILL -> TOKEN_FN + effectiveFieldMap/prefillFor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:23:39 +02:00
8 changed files with 215 additions and 80 deletions

View File

@ -51,6 +51,9 @@ textarea { resize: vertical; min-height: 56px; }
.field.prefilled input, .field.prefilled textarea { background: #f3f4f6; color: #4b5563; }
.options { margin: 0; }
.format { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 5px 0; }
.format-label { font-weight: 500; }
.format .radio { margin: 0; }
.radio, .check { display: flex; align-items: center; gap: 8px; margin: 5px 0; font-weight: 400; }
.radio input, .check input { accent-color: #2563eb; }

View File

@ -68,10 +68,12 @@
<footer>
<section class="options">
<label class="radio"><input type="radio" name="scope" value="both" checked /> E-Mail und Anhänge</label>
<label class="radio"><input type="radio" name="scope" value="att" /> Nur Anhänge</label>
<label class="radio"><input type="radio" name="scope" value="eml" /> Nur E-Mail</label>
<label class="check"><input type="checkbox" id="optPdf" /> Zusätzlich als PDF ablegen</label>
<div class="format">
<span class="format-label">E-Mail ablegen als:</span>
<label class="radio"><input type="radio" name="mailFormat" value="eml" checked /> Original (.eml)</label>
<label class="radio"><input type="radio" name="mailFormat" value="pdf" /> PDF</label>
</div>
<label class="check"><input type="checkbox" id="optAtt" checked /> Anhänge mit ablegen</label>
<ul id="attList" class="att-list"></ul>
</section>
<div id="status" class="muted"></div>

View File

@ -17,33 +17,35 @@ let STATE = {
cabinetFieldsByCab: {}, // Cache: cabinetId -> [Feldnamen] (für EML_ID-Erkennung)
};
// Mapping DocuWare-Feldname -> Funktion, die einen Vorbefüllwert aus meta liefert.
const PREFILL = {
EML_SENDER: (m) => m.senderEmail,
EML_SENDER_DISPLAYNAME: (m) => m.senderName,
EML_SENDER_NAME: (m) => m.senderName,
EML_RECEIVER: (m) => m.to,
EML_CC: (m) => m.cc,
EML_BCC: (m) => m.bcc,
EML_SUBJECT: (m) => m.subject,
DOC_SUBJECT: (m) => m.subject,
EML_BODY: (m) => m.bodyText,
EML_SENDINGDATE: (m) => m.date,
EML_DISPLAYDATE: (m) => m.date,
EML_RECEIVINGDATE: (m) => m.date,
EML_DIRECTION: (m) => m.direction,
EML_SIZE: (m) => m.sizeBytes,
EML_ACCOUNT: (m) => m.account,
DOC_DATE: (m) => m.date,
// Naheliegende generische Feldnamen (Schränke ohne EML_*-Schema)
SUBJECT: (m) => m.subject,
BETREFF: (m) => m.subject,
EMAIL: (m) => m.senderEmail,
E_MAIL: (m) => m.senderEmail,
DATE: (m) => m.date,
DATUM: (m) => m.date,
// Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
// Welches DocuWare-Feld welchen Token bekommt, steht im Mapping (s. effectiveFieldMap).
const TOKEN_FN = {
senderEmail: (m) => m.senderEmail,
senderName: (m) => m.senderName,
to: (m) => m.to,
cc: (m) => m.cc,
bcc: (m) => m.bcc,
subject: (m) => m.subject,
body: (m) => m.bodyText,
date: (m) => m.date,
direction: (m) => m.direction,
size: (m) => m.sizeBytes,
account: (m) => m.account,
};
// Aktives Feld-Mapping: eigenes (aus den Einstellungen) wenn gesetzt, sonst Default.
function effectiveFieldMap() {
const m = STATE.settings && STATE.settings.fieldMapping;
return m && Object.keys(m).length ? m : DEFAULT_FIELD_MAP;
}
// Vorbefüllwert für ein DocuWare-Feld aus den Mail-Metadaten (oder undefined).
function prefillFor(fieldName, meta) {
const token = effectiveFieldMap()[fieldName];
const fn = token && TOKEN_FN[token];
return fn ? fn(meta) : undefined;
}
function status(msg, kind) {
const el = $("status");
el.textContent = msg || "";
@ -153,28 +155,24 @@ function askPassword(errMsg) {
}
function applyDefaults() {
$("optPdf").checked = STATE.settings.storePdf;
// Mail-Format (.eml ODER PDF) aus den Einstellungen.
setMailFormat(STATE.settings.mailFormat === "pdf" ? "pdf" : "eml");
// Anhänge-Haken nur sinnvoll, wenn die Mail überhaupt Anhänge hat.
const hasAtt = STATE.attachments.length > 0;
// Standard-Umfang aus den Einstellungen ableiten.
let scope = "both";
if (STATE.settings.storeEml && !STATE.settings.storeAttachments) scope = "eml";
else if (!STATE.settings.storeEml && STATE.settings.storeAttachments) scope = "att";
if (!hasAtt) scope = "eml"; // ohne Anhänge nur E-Mail sinnvoll
setScope(scope);
// Optionen, die Anhänge erfordern, deaktivieren wenn keine vorhanden.
document.querySelectorAll('input[name="scope"]').forEach((r) => {
if ((r.value === "att" || r.value === "both") && !hasAtt) r.disabled = true;
});
const att = $("optAtt");
att.checked = hasAtt && STATE.settings.storeAttachments !== false;
att.disabled = !hasAtt;
$("attList").style.display = hasAtt && att.checked ? "" : "none";
}
function setScope(value) {
const el = document.querySelector(`input[name="scope"][value="${value}"]`);
function setMailFormat(value) {
const el = document.querySelector(`input[name="mailFormat"][value="${value}"]`);
if (el) el.checked = true;
}
function getScope() {
const el = document.querySelector('input[name="scope"]:checked');
return el ? el.value : "both";
function getMailFormat() {
const el = document.querySelector('input[name="mailFormat"]:checked');
return el ? el.value : "eml";
}
function renderAttachments() {
@ -444,9 +442,7 @@ function applyFieldDefaults() {
function prefillFields() {
STATE.fields.forEach((f) => {
const fn = PREFILL[f.name];
if (!fn) return;
const val = fn(STATE.meta);
const val = prefillFor(f.name, STATE.meta);
if (val === undefined || val === null || val === "") return;
setFieldValue(f.name, f, val);
});
@ -545,15 +541,11 @@ async function submit() {
result = collected.result;
}
const scope = getScope();
const wantEml = scope !== "att";
const wantPdf = $("optPdf").checked;
const wantAttScope = scope !== "eml";
if (!wantEml && !wantAttScope) {
status("Nichts ausgewählt zum Ablegen.", "err");
return;
}
const fmt = getMailFormat(); // "eml" ODER "pdf" nie beides
const wantEml = fmt === "eml";
const wantPdf = fmt === "pdf";
const att = $("optAtt");
const wantAttScope = att.checked && !att.disabled;
// Dubletten-Warnung (nur Einzelmail): liegt die Mail im Ziel schon, warnen
// „Trotzdem ablegen" bleibt aber möglich.
@ -575,7 +567,7 @@ async function submit() {
}
}
await saveOptionPrefs(scope, wantPdf); // Sticky: Umfang merken
await saveOptionPrefs(fmt, wantAttScope); // Sticky: Format + Anhänge-Wahl merken
$("submit").disabled = true;
const multi = STATE.messageIds.length > 1;
@ -650,21 +642,17 @@ function fieldsForMessage(baseResult, meta, multi) {
if (!multi) return baseResult; // Einzelmail: manuelle Edits respektieren
const result = { ...baseResult };
STATE.fields.forEach((f) => {
const fn = PREFILL[f.name];
if (!fn) return;
const val = fn(meta);
const val = prefillFor(f.name, meta);
if (val === undefined || val === null || val === "") return;
result[f.name] = { value: val, type: f.type };
});
return result;
}
// Sticky: zuletzt genutzten Ablage-Umfang in den Einstellungen merken.
async function saveOptionPrefs(scope, wantPdf) {
const storeEml = scope !== "att";
const storeAttachments = scope !== "eml";
// Sticky: zuletzt genutztes Format + Anhänge-Wahl in den Einstellungen merken.
async function saveOptionPrefs(mailFormat, storeAttachments) {
try {
STATE.settings = await Settings.set({ storeEml, storeAttachments, storePdf: wantPdf });
STATE.settings = await Settings.set({ mailFormat, storeAttachments });
} catch (_) { /* nicht kritisch */ }
}
@ -728,11 +716,13 @@ $("reset").addEventListener("click", resetForm);
$("cancel").addEventListener("click", () => window.close());
// Sticky: Optionen sofort bei Änderung merken (nicht erst beim Ablegen).
document.querySelectorAll('input[name="scope"]').forEach((r) =>
r.addEventListener("change", () => saveOptionPrefs(getScope(), $("optPdf").checked))
);
$("optPdf").addEventListener("change", () =>
saveOptionPrefs(getScope(), $("optPdf").checked)
document.querySelectorAll('input[name="mailFormat"]').forEach((r) =>
r.addEventListener("change", () => saveOptionPrefs(getMailFormat(), $("optAtt").checked))
);
$("optAtt").addEventListener("change", () => {
const show = $("optAtt").checked && STATE.attachments.length > 0;
$("attList").style.display = show ? "" : "none";
saveOptionPrefs(getMailFormat(), $("optAtt").checked);
});
init();

View File

@ -9,16 +9,65 @@ const DEFAULTS = {
username: "",
defaultCabinetId: "",
dialogByCabinet: {}, // gemerkter Ablagedialog je Schrank: { [cabinetId]: dialogId }
storeEml: true,
storePdf: true,
mailFormat: "eml", // E-Mail wird als "eml" ODER "pdf" abgelegt (nie beides)
storeAttachments: true,
tagOnSuccess: true,
fieldMapping: {}, // DocuWare-Feldname -> Mail-Attribut (Token). Leer = DEFAULT_FIELD_MAP.
};
// Mögliche Mail-Attribute für die Vorbefüllung (Token + Anzeigename).
// Wird von dialog.js (Werte aus meta) und options.js (Mapping-Editor) genutzt.
const MAIL_TOKENS = [
{ key: "senderEmail", label: "Absender E-Mail" },
{ key: "senderName", label: "Absender Name" },
{ key: "to", label: "Empfänger (An)" },
{ key: "cc", label: "CC" },
{ key: "bcc", label: "BCC" },
{ key: "subject", label: "Betreff" },
{ key: "body", label: "Text (Body)" },
{ key: "date", label: "Datum" },
{ key: "direction", label: "Richtung (Ein-/Ausgang)" },
{ key: "size", label: "Größe (Bytes)" },
{ key: "account", label: "Konto" },
];
// Standard-Zuordnung DocuWare-Feldname -> Mail-Token. Greift, solange der Nutzer
// in den Einstellungen kein eigenes fieldMapping gespeichert hat.
const DEFAULT_FIELD_MAP = {
EML_SENDER: "senderEmail",
EML_SENDER_DISPLAYNAME: "senderName",
EML_SENDER_NAME: "senderName",
EML_RECEIVER: "to",
EML_CC: "cc",
EML_BCC: "bcc",
EML_SUBJECT: "subject",
DOC_SUBJECT: "subject",
EML_BODY: "body",
EML_SENDINGDATE: "date",
EML_DISPLAYDATE: "date",
EML_RECEIVINGDATE: "date",
EML_DIRECTION: "direction",
EML_SIZE: "size",
EML_ACCOUNT: "account",
DOC_DATE: "date",
SUBJECT: "subject",
BETREFF: "subject",
EMAIL: "senderEmail",
E_MAIL: "senderEmail",
DATE: "date",
DATUM: "date",
};
const Settings = {
async get() {
const stored = await browser.storage.local.get("settings");
return { ...DEFAULTS, ...(stored.settings || {}) };
const raw = stored.settings || {};
const s = { ...DEFAULTS, ...raw };
// Migration: altes storeEml/storePdf -> mailFormat (einmalig, nicht persistiert).
if (raw.mailFormat === undefined && raw.storePdf && !raw.storeEml) {
s.mailFormat = "pdf";
}
return s;
},
async set(partial) {
@ -38,4 +87,5 @@ const Settings = {
},
};
if (typeof module !== "undefined") module.exports = { Settings, DEFAULTS };
if (typeof module !== "undefined")
module.exports = { Settings, DEFAULTS, MAIL_TOKENS, DEFAULT_FIELD_MAP };

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.10.2",
"version": "0.10.3",
"author": "l.lingler",
"applications": {
"gecko": {

View File

@ -17,6 +17,13 @@
#status { font-size: 13px; min-height: 18px; }
.ok { color: #15803d; } .err { color: #b91c1c; }
.hint { font-size: 12px; color: #666; margin-top: 2px; }
table.map { width: 100%; border-collapse: collapse; margin-top: 8px; }
table.map th { text-align: left; font-size: 12px; color: #666; font-weight: 600; padding: 2px 4px; }
table.map td { padding: 3px 4px; vertical-align: middle; }
table.map input, table.map select { width: 100%; padding: 5px 7px; border: 1px solid #ccc; border-radius: 5px; box-sizing: border-box; }
table.map td.del { width: 36px; }
table.map button { padding: 5px 9px; }
button.secondary { background: #e5e7eb; color: #111; }
</style>
</head>
<body>
@ -48,9 +55,12 @@
</select>
<div class="hint">Liste wird nach erfolgreichem Verbindungstest gefüllt.</div>
<div class="check"><input type="checkbox" id="storeEml" /> <label for="storeEml" style="margin:0">E-Mail als .eml ablegen</label></div>
<div class="check"><input type="checkbox" id="storePdf" /> <label for="storePdf" style="margin:0">E-Mail als PDF ablegen</label></div>
<div class="check"><input type="checkbox" id="storeAttachments" /> <label for="storeAttachments" style="margin:0">Anhänge separat ablegen</label></div>
<label>Standard-Format der E-Mail</label>
<div class="check"><input type="radio" name="mailFormat" id="fmtEml" value="eml" /> <label for="fmtEml" style="margin:0">Original (.eml)</label></div>
<div class="check"><input type="radio" name="mailFormat" id="fmtPdf" value="pdf" /> <label for="fmtPdf" style="margin:0">PDF</label></div>
<div class="hint">Die E-Mail wird immer in genau einem Format abgelegt (nicht beides). Im Ablage-Fenster umstellbar.</div>
<div class="check" style="margin-top:10px"><input type="checkbox" id="storeAttachments" /> <label for="storeAttachments" style="margin:0">Anhänge mit ablegen</label></div>
<div class="check"><input type="checkbox" id="tagOnSuccess" /> <label for="tagOnSuccess" style="margin:0">Mail nach Ablage als „DocuWare" markieren</label></div>
</fieldset>
@ -60,6 +70,21 @@
<span id="status"></span>
</div>
<fieldset>
<legend>Feld-Zuordnung (Vorbefüllung)</legend>
<div class="hint">Legt fest, welches DocuWare-Indexfeld aus welchem Mail-Attribut vorbefüllt
wird. <strong>Feldname</strong> = DocuWare-Datenbankname (z. B. <code>EML_SUBJECT</code>).
Gilt für alle Aktenschränke. Leere/entfernte Zeilen befüllen nichts vor.</div>
<table class="map">
<thead><tr><th>DocuWare-Feldname</th><th>Mail-Attribut</th><th></th></tr></thead>
<tbody id="mapBody"></tbody>
</table>
<div class="row">
<button id="mapAdd" class="secondary" type="button">+ Zeile</button>
<button id="mapReset" class="secondary" type="button">Auf Standard zurücksetzen</button>
</div>
</fieldset>
<fieldset>
<legend>Einstellungen sichern</legend>
<div class="hint">Exportiert/importiert die Einstellungen (Server, Organisation,

View File

@ -4,7 +4,7 @@ const $ = (id) => document.getElementById(id);
// Passwort ist bewusst NICHT dabei es wird nie gespeichert, nur zum Testen/Login
// transient verwendet (siehe "Verbindung testen").
const FIELDS = ["serverUrl", "organization", "username"];
const CHECKS = ["storeEml", "storePdf", "storeAttachments", "tagOnSuccess"];
const CHECKS = ["storeAttachments", "tagOnSuccess"];
function setStatus(msg, kind) {
const el = $("status");
@ -16,11 +16,67 @@ async function load() {
const s = await Settings.get();
FIELDS.forEach((f) => ($(f).value = s[f] || ""));
CHECKS.forEach((c) => ($(c).checked = !!s[c]));
setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml");
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);
}
function setMailFormat(v) {
const el = document.querySelector(`input[name="mailFormat"][value="${v}"]`);
if (el) el.checked = true;
}
function getMailFormat() {
const el = document.querySelector('input[name="mailFormat"]:checked');
return el ? el.value : "eml";
}
// --- Feld-Mapping-Editor ---------------------------------------------------
function addMapRow(field = "", token = "") {
const tr = document.createElement("tr");
const inp = document.createElement("input");
inp.type = "text"; inp.className = "map-field"; inp.value = field;
inp.placeholder = "EML_SUBJECT";
const tdF = document.createElement("td"); tdF.appendChild(inp);
const sel = document.createElement("select"); sel.className = "map-token";
const none = document.createElement("option"); none.value = ""; none.textContent = "— nichts —";
sel.appendChild(none);
MAIL_TOKENS.forEach((t) => {
const o = document.createElement("option");
o.value = t.key; o.textContent = t.label;
if (t.key === token) o.selected = true;
sel.appendChild(o);
});
const tdT = document.createElement("td"); tdT.appendChild(sel);
const del = document.createElement("button");
del.type = "button"; del.className = "secondary"; del.textContent = "✕";
del.addEventListener("click", () => tr.remove());
const tdX = document.createElement("td"); tdX.className = "del"; tdX.appendChild(del);
tr.append(tdF, tdT, tdX);
$("mapBody").appendChild(tr);
}
function loadMapping(map) {
$("mapBody").innerHTML = "";
Object.entries(map || {}).forEach(([f, t]) => addMapRow(f, t));
}
function collectMapping() {
const map = {};
document.querySelectorAll("#mapBody tr").forEach((tr) => {
const f = tr.querySelector(".map-field").value.trim();
const t = tr.querySelector(".map-token").value;
if (f && t) map[f] = t; // leere Feldnamen/Token ignorieren
});
return map;
}
function fillCabinets(cabinets, selectedId) {
const sel = $("defaultCabinet");
sel.innerHTML = '<option value="">— keiner —</option>';
@ -46,9 +102,14 @@ function collect() {
FIELDS.forEach((f) => (partial[f] = $(f).value.trim()));
CHECKS.forEach((c) => (partial[c] = $(c).checked));
partial.defaultCabinetId = $("defaultCabinet").value;
partial.mailFormat = getMailFormat();
partial.fieldMapping = collectMapping();
return partial;
}
$("mapAdd").addEventListener("click", () => addMapRow());
$("mapReset").addEventListener("click", () => loadMapping(DEFAULT_FIELD_MAP));
$("save").addEventListener("click", async () => {
await Settings.set(collect());
setStatus("Gespeichert.", "ok");

View File

@ -13,6 +13,10 @@
{
"version": "0.10.2",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.2/docuware-ablage-0.10.2.xpi"
},
{
"version": "0.10.3",
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.10.3/docuware-ablage-0.10.3.xpi"
}
]
}