DocuWare Ablage – Thunderbird-Extension v0.8.0
Thunderbird-MailExtension zum Ablegen von E-Mails in DocuWare (.eml + PDF + Anhänge), dynamische Indexfelder aus dem Store-Dialog, Auswahllisten, Identity-Service-Login. Self-distribution-Updates über updates.json auf eigenem Gitea.
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Lokale Einstellungs-Exporte enthalten das DocuWare-Passwort im Klartext – NIE committen!
|
||||
docuware-ablage-einstellungen.json
|
||||
*einstellung*.json
|
||||
settings*.json
|
||||
|
||||
# Build-Artefakte (das signierte XPI wird als Gitea-Release verteilt, nicht im Repo)
|
||||
*.xpi
|
||||
108
README.md
Normal file
@ -0,0 +1,108 @@
|
||||
# DocuWare Ablage – Thunderbird-Extension
|
||||
|
||||
Legt eine markierte E-Mail aus Thunderbird in DocuWare ab. Nach Klick auf den Button
|
||||
erscheint ein **Ablage-Dialog**: Aktenschrank wählen, Indexfelder ausfüllen (aus der
|
||||
Mail vorbefüllt), ablegen. Hochgeladen werden – je nach Auswahl – die Mail als `.eml`,
|
||||
die Mail als PDF und die Anhänge als separate Dokumente.
|
||||
|
||||
## Funktionsweise
|
||||
|
||||
- **Direkte Anbindung** an die DocuWare **Platform REST-API** (kein eigener Server).
|
||||
Auth v1 per **Cookie-Logon** (`/Account/Logon`).
|
||||
- **Dynamische Felder**: Die Eingabemaske wird aus dem *Store-Dialog* des gewählten
|
||||
Schranks erzeugt (Felder, Pflichtfelder, Auswahllisten).
|
||||
- **Vorbefüllung** der `EML_*`-Felder aus der Mail (Absender, Empfänger, Betreff,
|
||||
Datum, Richtung, Größe, Body …).
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
manifest.json MailExtension-Manifest (Manifest v2)
|
||||
background/background.js Button + Kontextmenü, öffnet den Dialog
|
||||
dialog/ Ablage-Dialog (HTML/CSS/JS)
|
||||
options/ Einstellungen (Server, Login, Defaults)
|
||||
lib/
|
||||
store.js Einstellungen (browser.storage.local)
|
||||
auth.js Cookie-Logon (Auth-Abstraktion)
|
||||
docuware.js Platform-REST-Client (Schränke, Dialoge, Felder, Upload)
|
||||
mail.js Mail-Extraktion (eml, Metadaten, Anhänge)
|
||||
pdf.js abhängigkeitsfreier Text-PDF-Generator
|
||||
icons/
|
||||
```
|
||||
|
||||
## Installation (Entwicklung)
|
||||
|
||||
1. Thunderbird → Menü → **Add-ons und Themes** → Zahnrad → **Add-on aus Datei
|
||||
installieren …** *oder* zum Testen:
|
||||
2. Adresszeile: `about:debugging` → **Dieses Thunderbird** → **Temporäres Add-on
|
||||
laden …** → `manifest.json` in diesem Ordner wählen.
|
||||
|
||||
> Hinweis: Bei einem Self-Signed-Zertifikat des DocuWare-Servers muss dieses einmalig
|
||||
> in Thunderbird akzeptiert werden, sonst schlagen die `fetch`-Aufrufe fehl.
|
||||
|
||||
## Einrichtung
|
||||
|
||||
Add-on-Einstellungen öffnen und ausfüllen:
|
||||
|
||||
- **Server-URL** – Basis ohne `/DocuWare/Platform`, z.B. `https://docuware.firma.de`
|
||||
- **Organisation**, **Benutzername**, **Passwort**
|
||||
- **Verbindung testen** → lädt die Aktenschränke (Bestätigung, dass Login & URL passen)
|
||||
- optional **Standard-Aktenschrank** und Default-Optionen (eml/pdf/Anhänge)
|
||||
|
||||
> ⚠️ Das Passwort wird unverschlüsselt in `storage.local` gespeichert (v1). Für den
|
||||
> Produktiveinsatz sollte auf den DocuWare Identity Service / OAuth umgestellt werden
|
||||
> (`lib/auth.js` ist dafür gekapselt).
|
||||
|
||||
## Benutzung
|
||||
|
||||
1. E-Mail öffnen → Button **„In DocuWare ablegen"** (oder Rechtsklick in der
|
||||
Nachrichtenliste → *In DocuWare ablegen*).
|
||||
2. Aktenschrank wählen → Felder prüfen/ergänzen (Pflichtfelder mit `*`).
|
||||
3. Auswählen, was abgelegt wird (eml / PDF / Anhänge).
|
||||
4. **Ablegen**. Bei Erfolg wird die Mail optional mit dem Tag *DocuWare* markiert.
|
||||
|
||||
## Verifikation (End-to-End)
|
||||
|
||||
Gegen den **Test-Schrank `LL_TEST_BELEGE`** ablegen, dann per DocuWare-MCP gegenprüfen:
|
||||
|
||||
- `docuware_search` (file_cabinet_name `LL_TEST_BELEGE`, query = Betreff) → Treffer?
|
||||
- `docuware_get_document` → Indexfelder (`EML_SENDER`, `EML_SUBJECT`, …) korrekt?
|
||||
- `docuware_download_document` → `.eml`/PDF herunterladen & prüfen
|
||||
- Anhänge erscheinen als eigene Dokumente
|
||||
- Aufräumen: `docuware_delete_document` (nur im Test-Schrank!)
|
||||
|
||||
## Verteilung & automatische Updates (eigenes Gitea, nicht öffentlich)
|
||||
|
||||
Das Add-on wird **nicht** öffentlich auf addons.thunderbird.net gelistet, sondern
|
||||
über AMO als **„Selbstständig" (self-distribution)** signiert und über dein eigenes
|
||||
Gitea verteilt + aktualisiert.
|
||||
|
||||
> Im XPI und in `updates.json` stehen **keine** Zugangsdaten. Das DocuWare-Passwort
|
||||
> liegt ausschließlich lokal in `storage.local` und wird nie mitverteilt.
|
||||
|
||||
**Einmalige Einrichtung:**
|
||||
|
||||
1. In `manifest.json` → `applications.gecko.update_url` die Domain `GITEA.EXAMPLE.DE`
|
||||
durch deine (HTTPS-)Gitea-Adresse ersetzen. **HTTPS ist Pflicht** (TB lehnt HTTP ab).
|
||||
2. `updates.json` ebenfalls auf deine Gitea-URLs anpassen und in dein Repo committen
|
||||
(erreichbar unter der `update_url`, z.B. via *raw*-Link auf `main`).
|
||||
|
||||
**Bei jedem neuen Release:**
|
||||
|
||||
1. `version` in `manifest.json` erhöhen (z.B. `0.8.0` → `0.8.1`).
|
||||
2. XPI bauen (`manifest.json` muss im Archiv-Wurzelverzeichnis liegen).
|
||||
3. XPI bei AMO hochladen → **„Selbstständig"** wählen → **signiertes** XPI herunterladen.
|
||||
4. Signiertes XPI als Release-Anhang in Gitea hochladen (passend zum `update_link`).
|
||||
5. In `updates.json` einen neuen Eintrag mit `version` + `update_link` ergänzen,
|
||||
committen/pushen.
|
||||
|
||||
Thunderbird prüft die `update_url` periodisch und aktualisiert automatisch auf die
|
||||
neueste dort gelistete Version.
|
||||
|
||||
## Bekannte Grenzen / nächste Schritte
|
||||
|
||||
- **PDF** ist v1 ein einfaches Text-PDF (Kopf + Klartext-Body) ohne HTML-Layout/Bilder.
|
||||
- **Auth** nur Cookie-Logon; OAuth/Identity Service noch nicht implementiert.
|
||||
- **Upload-Multipart-Format** ggf. je nach DocuWare-Version anpassen
|
||||
(`docuware.js → uploadDocument`).
|
||||
- Mehrfachauswahl von Mails: aktuell wird die erste markierte Mail abgelegt.
|
||||
62
background/background.js
Normal file
@ -0,0 +1,62 @@
|
||||
// Hintergrund-Skript: Button + Kontextmenü, öffnet den Ablage-Dialog.
|
||||
|
||||
const DIALOG_URL = "dialog/dialog.html";
|
||||
|
||||
function openDialog(messageIds) {
|
||||
const ids = Array.isArray(messageIds) ? messageIds : [messageIds];
|
||||
// Pro Nachricht ein eigenes Ablagefenster (leicht versetzt gestapelt).
|
||||
ids.forEach((id, i) => {
|
||||
const url = browser.runtime.getURL(`${DIALOG_URL}?messageId=${id}`);
|
||||
browser.windows.create({
|
||||
url,
|
||||
type: "popup",
|
||||
width: 720,
|
||||
height: 820,
|
||||
left: 100 + i * 28,
|
||||
top: 80 + i * 28,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Button in der geöffneten Nachricht
|
||||
browser.messageDisplayAction.onClicked.addListener(async (tab) => {
|
||||
const msg = await browser.messageDisplay.getDisplayedMessage(tab.id);
|
||||
if (msg) openDialog([msg.id]);
|
||||
});
|
||||
|
||||
// Kontextmenü-Eintrag in der Nachrichtenliste
|
||||
browser.menus.create({
|
||||
id: "docuware-archive",
|
||||
title: "In DocuWare ablegen",
|
||||
contexts: ["message_list"],
|
||||
});
|
||||
|
||||
browser.menus.onClicked.addListener((info) => {
|
||||
if (info.menuItemId !== "docuware-archive") return;
|
||||
const msgs = info.selectedMessages && info.selectedMessages.messages;
|
||||
if (msgs && msgs.length > 0) openDialog(msgs.map((m) => m.id));
|
||||
});
|
||||
|
||||
// Tastenkürzel (Strg+Alt+I): angezeigte bzw. alle markierten Nachrichten ablegen.
|
||||
browser.commands.onCommand.addListener(async (command) => {
|
||||
if (command !== "open-archive-dialog") return;
|
||||
const ids = await currentMessageIds();
|
||||
if (ids.length) openDialog(ids);
|
||||
});
|
||||
|
||||
// Ermittelt die "aktiven" Nachrichten: erst die im Reader angezeigte, sonst
|
||||
// alle im Nachrichtenlisten-Tab markierten.
|
||||
async function currentMessageIds() {
|
||||
try {
|
||||
const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
if (tab) {
|
||||
const shown = await browser.messageDisplay
|
||||
.getDisplayedMessage(tab.id)
|
||||
.catch(() => null);
|
||||
if (shown) return [shown.id];
|
||||
}
|
||||
const sel = await browser.mailTabs.getSelectedMessages().catch(() => null);
|
||||
if (sel && sel.messages && sel.messages.length) return sel.messages.map((m) => m.id);
|
||||
} catch (_) {}
|
||||
return [];
|
||||
}
|
||||
64
dialog/dialog.css
Normal file
@ -0,0 +1,64 @@
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font: 13px/1.45 "Segoe UI", system-ui, sans-serif;
|
||||
margin: 0; color: #1a1a1a; background: #fff;
|
||||
display: flex; flex-direction: column; height: 100vh;
|
||||
}
|
||||
|
||||
/* Titel-Leiste wie im Connect-to-Outlook-Dialog */
|
||||
.titlebar {
|
||||
background: #2b3a44; color: #fff;
|
||||
padding: 8px 14px; font-weight: 600; font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
header { padding: 12px 18px 6px; border-bottom: 1px solid #e5e7eb; flex-shrink: 0; }
|
||||
header h1 { font-size: 16px; font-weight: 600; margin: 0 0 10px; }
|
||||
|
||||
.toolbar { display: flex; align-items: center; gap: 14px; }
|
||||
.toolbar .spacer { flex: 1; }
|
||||
button.link {
|
||||
background: none; border: 0; color: #1f2937; cursor: pointer;
|
||||
font: inherit; padding: 4px 2px; display: inline-flex; align-items: center; gap: 5px;
|
||||
}
|
||||
button.link:hover { color: #2563eb; }
|
||||
button.primary {
|
||||
background: #f4b400; color: #1a1a1a; border: 0; border-radius: 3px;
|
||||
padding: 7px 22px; font-weight: 600; cursor: pointer;
|
||||
}
|
||||
button.primary:hover { background: #e0a500; }
|
||||
button:disabled { opacity: .5; cursor: default; }
|
||||
|
||||
main { flex: 1; overflow-y: auto; padding: 14px 18px; }
|
||||
|
||||
.target { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||
.target > label { text-align: right; color: #374151; font-weight: 500; }
|
||||
|
||||
/* Indexfelder: rechtsbündiges Label links, Eingabe rechts */
|
||||
.fields { display: flex; flex-direction: column; gap: 8px; }
|
||||
.field { display: grid; grid-template-columns: 150px 1fr; align-items: center; gap: 10px; }
|
||||
.field > label { text-align: right; color: #374151; font-weight: 500; padding-top: 2px; }
|
||||
.field.full { grid-template-columns: 150px 1fr; align-items: start; }
|
||||
.field .req { color: #b91c1c; }
|
||||
|
||||
select, input[type="text"], input[type="number"], input[type="date"], textarea {
|
||||
width: 100%; padding: 6px 9px; border: 1px solid #b9c0c8; border-radius: 3px; font: inherit; background: #fff;
|
||||
}
|
||||
select:focus, input:focus, textarea:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 2px #2563eb33; }
|
||||
textarea { resize: vertical; min-height: 56px; }
|
||||
|
||||
/* Vorbefüllte (aus der Mail übernommene) Felder dezent kennzeichnen */
|
||||
.field.prefilled input, .field.prefilled textarea { background: #f3f4f6; color: #4b5563; }
|
||||
|
||||
.options { margin: 0; }
|
||||
.radio, .check { display: flex; align-items: center; gap: 8px; margin: 5px 0; font-weight: 400; }
|
||||
.radio input, .check input { accent-color: #2563eb; }
|
||||
|
||||
.att-list { list-style: none; margin: 6px 0 0 26px; padding: 0; max-height: 110px; overflow-y: auto; }
|
||||
.att-list li { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 2px 0; color: #374151; }
|
||||
|
||||
/* Footer bleibt fix sichtbar, nur der Felderbereich (main) scrollt. */
|
||||
footer { border-top: 1px solid #e5e7eb; padding: 10px 18px; background: #fafafa; flex-shrink: 0; }
|
||||
.muted { color: #6b7280; font-size: 12px; }
|
||||
.err { color: #b91c1c; } .ok { color: #15803d; }
|
||||
.field.invalid input, .field.invalid textarea, .field.invalid select { border-color: #b91c1c; }
|
||||
54
dialog/dialog.html
Normal file
@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>In DocuWare ablegen</title>
|
||||
<link rel="stylesheet" href="dialog.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="titlebar">In DocuWare ablegen</div>
|
||||
|
||||
<header>
|
||||
<h1 id="cabHeading">Ablage</h1>
|
||||
<div class="toolbar">
|
||||
<button id="cancel" class="link">‹ Abbrechen</button>
|
||||
<button id="reset" class="link">↺ Zurücksetzen</button>
|
||||
<span class="spacer"></span>
|
||||
<button id="submit" class="primary">Ablegen</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="multiNote" class="muted" style="margin-bottom:8px;"></div>
|
||||
<div class="target">
|
||||
<label for="cabinet">Ziel</label>
|
||||
<select id="cabinet"><option value="">Lade …</option></select>
|
||||
</div>
|
||||
|
||||
<div class="target" id="dialogRow" style="display:none;">
|
||||
<label for="storeDialog">Ablagedialog</label>
|
||||
<select id="storeDialog"></select>
|
||||
</div>
|
||||
|
||||
<div id="fields" class="fields"><div class="muted">Ziel wählen …</div></div>
|
||||
</main>
|
||||
|
||||
<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>
|
||||
<ul id="attList" class="att-list"></ul>
|
||||
</section>
|
||||
<div id="status" class="muted"></div>
|
||||
</footer>
|
||||
|
||||
<script src="../lib/store.js"></script>
|
||||
<script src="../lib/auth.js"></script>
|
||||
<script src="../lib/docuware.js"></script>
|
||||
<script src="../lib/mail.js"></script>
|
||||
<script src="../lib/pdf.js"></script>
|
||||
<script src="dialog.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
581
dialog/dialog.js
Normal file
@ -0,0 +1,581 @@
|
||||
// Ablage-Dialog: Schrankwahl, dynamische Felder, Upload.
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
let STATE = {
|
||||
messageId: null,
|
||||
messageIds: [], // alle ausgewählten Nachrichten
|
||||
settings: null,
|
||||
meta: null,
|
||||
attachments: [],
|
||||
cabinets: [], // alle Ziele (Archive + Briefkörbe)
|
||||
cabinetId: "",
|
||||
isBasket: false, // gewähltes Ziel ist ein Briefkorb?
|
||||
dialogId: null,
|
||||
fields: [], // Felddefinitionen des Store-Dialogs
|
||||
};
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
function status(msg, kind) {
|
||||
const el = $("status");
|
||||
el.textContent = msg || "";
|
||||
el.className = kind || "muted";
|
||||
}
|
||||
|
||||
function qs(name) {
|
||||
return new URLSearchParams(location.search).get(name);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
// messageIds (Mehrfachauswahl) bevorzugt, sonst einzelnes messageId.
|
||||
const idsParam = qs("messageIds");
|
||||
STATE.messageIds = (idsParam ? idsParam.split(",") : [qs("messageId")])
|
||||
.map((x) => parseInt(x, 10))
|
||||
.filter((n) => !isNaN(n));
|
||||
STATE.messageId = STATE.messageIds[0];
|
||||
STATE.settings = await Settings.get();
|
||||
|
||||
if (!STATE.settings.serverUrl) {
|
||||
status("Bitte zuerst in den Einstellungen Server & Login hinterlegen.", "err");
|
||||
$("submit").disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Anzeige/Vorbefüllung anhand der ersten Nachricht.
|
||||
STATE.meta = await Mail.getMeta(STATE.messageId);
|
||||
STATE.attachments = await Mail.listAttachments(STATE.messageId);
|
||||
} catch (e) {
|
||||
status(`Mail konnte nicht gelesen werden: ${e.message}`, "err");
|
||||
return;
|
||||
}
|
||||
|
||||
if (STATE.messageIds.length > 1) {
|
||||
$("multiNote").textContent =
|
||||
`${STATE.messageIds.length} E-Mails werden abgelegt. ` +
|
||||
"Eingetragene Werte gelten für alle; Absender/Betreff/Datum werden je E-Mail einzeln übernommen.";
|
||||
}
|
||||
|
||||
renderAttachments();
|
||||
applyDefaults();
|
||||
|
||||
try {
|
||||
status("Verbinde mit DocuWare …");
|
||||
await Auth.logon(STATE.settings);
|
||||
await loadCabinets();
|
||||
status("");
|
||||
} catch (e) {
|
||||
status(`Verbindung fehlgeschlagen: ${e.message}`, "err");
|
||||
}
|
||||
}
|
||||
|
||||
function applyDefaults() {
|
||||
$("optPdf").checked = STATE.settings.storePdf;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
function setScope(value) {
|
||||
const el = document.querySelector(`input[name="scope"][value="${value}"]`);
|
||||
if (el) el.checked = true;
|
||||
}
|
||||
|
||||
function getScope() {
|
||||
const el = document.querySelector('input[name="scope"]:checked');
|
||||
return el ? el.value : "both";
|
||||
}
|
||||
|
||||
function renderAttachments() {
|
||||
const ul = $("attList");
|
||||
ul.innerHTML = "";
|
||||
STATE.attachments.forEach((a, i) => {
|
||||
const li = document.createElement("li");
|
||||
li.innerHTML =
|
||||
`<input type="checkbox" id="att-${i}" checked /> ` +
|
||||
`<label for="att-${i}" style="margin:0;font-weight:400">${a.name} ` +
|
||||
`<span class="muted">(${formatSize(a.size)})</span></label>`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCabinets() {
|
||||
let cabinets = (await browser.storage.local.get("cabinets")).cabinets;
|
||||
if (!cabinets || cabinets.length === 0) {
|
||||
cabinets = await DocuWare.listCabinets(STATE.settings);
|
||||
await browser.storage.local.set({ cabinets });
|
||||
}
|
||||
STATE.cabinets = cabinets;
|
||||
|
||||
const sel = $("cabinet");
|
||||
sel.innerHTML = '<option value="">— bitte wählen —</option>';
|
||||
|
||||
const addGroup = (label, list) => {
|
||||
if (!list.length) return;
|
||||
const g = document.createElement("optgroup");
|
||||
g.label = label;
|
||||
list.forEach((c) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = c.id;
|
||||
o.textContent = c.name;
|
||||
g.appendChild(o);
|
||||
});
|
||||
sel.appendChild(g);
|
||||
};
|
||||
addGroup("Archive", cabinets.filter((c) => !c.isBasket));
|
||||
addGroup("Briefkörbe", cabinets.filter((c) => c.isBasket));
|
||||
|
||||
if (STATE.settings.defaultCabinetId) {
|
||||
sel.value = STATE.settings.defaultCabinetId;
|
||||
if (sel.value) await onCabinetChange();
|
||||
}
|
||||
}
|
||||
|
||||
async function onCabinetChange() {
|
||||
STATE.cabinetId = $("cabinet").value;
|
||||
STATE.dialogId = null;
|
||||
STATE.fields = [];
|
||||
const target = STATE.cabinets.find((c) => String(c.id) === String(STATE.cabinetId));
|
||||
STATE.isBasket = !!(target && target.isBasket);
|
||||
$("cabHeading").textContent = target ? `Ablegen in „${target.name}"` : "Ablage";
|
||||
|
||||
// Sticky: zuletzt gewählten Schrank als Standard merken.
|
||||
if (STATE.cabinetId) {
|
||||
Settings.set({ defaultCabinetId: STATE.cabinetId }).then((s) => (STATE.settings = s));
|
||||
}
|
||||
|
||||
if (!STATE.cabinetId) {
|
||||
$("fields").innerHTML = '<div class="muted">Ziel wählen …</div>';
|
||||
return;
|
||||
}
|
||||
// Briefkörbe haben keinen Store-Dialog -> keine Indexfelder.
|
||||
$("dialogRow").style.display = "none";
|
||||
if (STATE.isBasket) {
|
||||
$("fields").innerHTML =
|
||||
'<div class="muted">Briefkorb: keine Indexfelder. Die Dokumente werden ' +
|
||||
"unindiziert im Briefkorb abgelegt und können dort später archiviert werden.</div>";
|
||||
return;
|
||||
}
|
||||
$("fields").innerHTML = '<div class="muted">Lade Ablagedialog …</div>';
|
||||
try {
|
||||
const cands = await DocuWare.listFieldsDialogs(STATE.settings, STATE.cabinetId);
|
||||
STATE.dialogCands = cands;
|
||||
if (!cands.length) throw new Error("Kein Store- oder Suchdialog für diesen Schrank gefunden.");
|
||||
|
||||
// Auswahl-Dropdown nur zeigen, wenn es mehrere Dialoge gibt.
|
||||
const ds = $("storeDialog");
|
||||
ds.innerHTML = "";
|
||||
cands.forEach((c) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = c.id;
|
||||
o.textContent = c.type === "search" ? `${c.name} (Suchdialog)` : c.name;
|
||||
ds.appendChild(o);
|
||||
});
|
||||
$("dialogRow").style.display = cands.length > 1 ? "grid" : "none";
|
||||
|
||||
// Gemerkte Wahl pro Schrank, sonst erster Dialog.
|
||||
const remembered = (STATE.settings.dialogByCabinet || {})[STATE.cabinetId];
|
||||
const pick = cands.find((c) => c.id === remembered) || cands[0];
|
||||
ds.value = pick.id;
|
||||
await loadDialogFields(pick);
|
||||
} catch (e) {
|
||||
$("fields").innerHTML = `<div class="err">${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Lädt und rendert die Felder für einen konkreten Dialog.
|
||||
async function loadDialogFields(pick) {
|
||||
STATE.dialogId = pick.id;
|
||||
STATE.dialogType = pick.type;
|
||||
STATE.fields = [];
|
||||
$("fields").innerHTML = '<div class="muted">Lade Felder …</div>';
|
||||
STATE.fields = await DocuWare.getStoreFields(STATE.settings, STATE.cabinetId, STATE.dialogId);
|
||||
renderFields();
|
||||
if (pick.type === "search") {
|
||||
const note = document.createElement("div");
|
||||
note.className = "muted";
|
||||
note.style.marginTop = "6px";
|
||||
note.textContent =
|
||||
"Hinweis: Kein Store-Dialog verfügbar – Felder stammen aus dem Suchdialog.";
|
||||
$("fields").appendChild(note);
|
||||
}
|
||||
}
|
||||
|
||||
// Nutzer wählt einen anderen Ablagedialog -> merken (pro Schrank) und neu laden.
|
||||
async function onStoreDialogChange() {
|
||||
const id = $("storeDialog").value;
|
||||
const pick = (STATE.dialogCands || []).find((c) => c.id === id);
|
||||
if (!pick) return;
|
||||
const map = { ...(STATE.settings.dialogByCabinet || {}), [STATE.cabinetId]: id };
|
||||
STATE.settings = await Settings.set({ dialogByCabinet: map });
|
||||
try {
|
||||
await loadDialogFields(pick);
|
||||
} catch (e) {
|
||||
$("fields").innerHTML = `<div class="err">${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderFields() {
|
||||
const container = $("fields");
|
||||
container.innerHTML = "";
|
||||
for (const f of STATE.fields) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "field" + (isMemo(f) ? " full" : "");
|
||||
wrap.dataset.name = f.name;
|
||||
wrap.dataset.type = f.type;
|
||||
wrap.dataset.required = f.required ? "1" : "";
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = f.label;
|
||||
if (f.required) {
|
||||
const r = document.createElement("span");
|
||||
r.className = "req"; r.textContent = " *";
|
||||
label.appendChild(r);
|
||||
}
|
||||
wrap.appendChild(label);
|
||||
|
||||
const input = buildInput(f);
|
||||
wrap.appendChild(input);
|
||||
if (input._datalist) wrap.appendChild(input._datalist);
|
||||
container.appendChild(wrap);
|
||||
}
|
||||
applyFieldDefaults(); // vordefinierte Werte (PrefillValue) zuerst
|
||||
prefillFields(); // Mail-Vorbefüllung überschreibt gemappte Felder
|
||||
populateSelectLists(); // Auswahllisten/Vorschläge asynchron nachladen
|
||||
}
|
||||
|
||||
// Baut das Eingabe-Element (synchron). Auswahllisten werden später befüllt.
|
||||
function buildInput(f) {
|
||||
const t = (f.type || "").toLowerCase();
|
||||
let el;
|
||||
if (isMemo(f)) {
|
||||
el = document.createElement("textarea");
|
||||
} else if (f.isSelectList && f.selectListOnly) {
|
||||
// Nur Listenwerte erlaubt -> starres Dropdown.
|
||||
el = document.createElement("select");
|
||||
el.innerHTML = '<option value=""></option>';
|
||||
} else if (t.includes("date")) {
|
||||
el = document.createElement("input");
|
||||
el.type = "date";
|
||||
} else if (t.includes("decimal") || t.includes("numeric") || t.includes("int")) {
|
||||
el = document.createElement("input");
|
||||
el.type = "number"; el.step = "any";
|
||||
} else {
|
||||
el = document.createElement("input");
|
||||
el.type = "text";
|
||||
if (f.isSelectList) {
|
||||
// Combobox: frei tippbar mit Vorschlagsliste (wie in Connect-to-Outlook).
|
||||
const dl = document.createElement("datalist");
|
||||
dl.id = "dl-" + f.name;
|
||||
el.setAttribute("list", dl.id);
|
||||
el._datalist = dl;
|
||||
}
|
||||
}
|
||||
el.dataset.role = "value";
|
||||
if (f.readOnly && el.tagName !== "SELECT") el.readOnly = true;
|
||||
return el;
|
||||
}
|
||||
|
||||
// Vordefinierte Werte (PrefillValue aus dem Dialog) eintragen.
|
||||
function applyFieldDefaults() {
|
||||
STATE.fields.forEach((f) => {
|
||||
if (f.prefill === undefined || f.prefill === null || f.prefill === "") return;
|
||||
setFieldValue(f.name, f, f.prefill);
|
||||
});
|
||||
}
|
||||
|
||||
function prefillFields() {
|
||||
STATE.fields.forEach((f) => {
|
||||
const fn = PREFILL[f.name];
|
||||
if (!fn) return;
|
||||
const val = fn(STATE.meta);
|
||||
if (val === undefined || val === null || val === "") return;
|
||||
setFieldValue(f.name, f, val);
|
||||
});
|
||||
}
|
||||
|
||||
// Setzt einen Wert auf das Control eines Feldes (kümmert sich um Datum/Select).
|
||||
function setFieldValue(name, f, val) {
|
||||
const wrap = document.querySelector(`.field[data-name="${name}"]`);
|
||||
const el = wrap && wrap.querySelector("[data-role='value']");
|
||||
if (!el) return;
|
||||
if (el.type === "date") {
|
||||
const d = val instanceof Date ? val : new Date(val);
|
||||
if (!isNaN(d)) el.value = toDateInput(d);
|
||||
} else if (el.tagName === "SELECT") {
|
||||
let opt = Array.from(el.options).find((o) => o.value === String(val));
|
||||
if (!opt) {
|
||||
opt = document.createElement("option");
|
||||
opt.value = String(val); opt.textContent = String(val);
|
||||
el.appendChild(opt);
|
||||
}
|
||||
el.value = String(val);
|
||||
} else {
|
||||
el.value = val instanceof Date ? toDateInput(val) : String(val);
|
||||
}
|
||||
wrap.classList.add("prefilled");
|
||||
}
|
||||
|
||||
// Lädt die Auswahllisten-Werte und füllt Dropdowns/Vorschlagslisten.
|
||||
async function populateSelectLists() {
|
||||
for (const f of STATE.fields) {
|
||||
if (!f.isSelectList) continue;
|
||||
let vals = [];
|
||||
try {
|
||||
vals = await DocuWare.getSelectList(
|
||||
STATE.settings, STATE.cabinetId, STATE.dialogId, f.name
|
||||
);
|
||||
} catch (_) { /* leere Liste ist ok */ }
|
||||
if (!vals.length) continue;
|
||||
const wrap = document.querySelector(`.field[data-name="${f.name}"]`);
|
||||
const el = wrap && wrap.querySelector("[data-role='value']");
|
||||
if (!el) continue;
|
||||
if (el.tagName === "SELECT") {
|
||||
const current = el.value;
|
||||
vals.forEach((v) => {
|
||||
if (!Array.from(el.options).some((o) => o.value === v)) {
|
||||
const o = document.createElement("option");
|
||||
o.value = v; o.textContent = v;
|
||||
el.appendChild(o);
|
||||
}
|
||||
});
|
||||
if (current) el.value = current;
|
||||
} else {
|
||||
const dl = el._datalist || document.getElementById(el.getAttribute("list"));
|
||||
if (dl) {
|
||||
dl.innerHTML = "";
|
||||
vals.forEach((v) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = v;
|
||||
dl.appendChild(o);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectFieldValues() {
|
||||
const result = {};
|
||||
let firstInvalid = null;
|
||||
document.querySelectorAll(".field").forEach((wrap) => {
|
||||
wrap.classList.remove("invalid");
|
||||
const el = wrap.querySelector("[data-role='value']");
|
||||
const name = wrap.dataset.name;
|
||||
const type = wrap.dataset.type;
|
||||
const required = wrap.dataset.required === "1";
|
||||
const raw = el ? el.value : "";
|
||||
if (required && !String(raw).trim()) {
|
||||
wrap.classList.add("invalid");
|
||||
if (!firstInvalid) firstInvalid = name;
|
||||
}
|
||||
if (String(raw).trim() !== "") {
|
||||
result[name] = { value: el.type === "date" ? new Date(raw) : raw, type };
|
||||
}
|
||||
});
|
||||
return { result, firstInvalid };
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!STATE.cabinetId) { status("Bitte ein Ziel wählen.", "err"); return; }
|
||||
let result = {};
|
||||
if (!STATE.isBasket) {
|
||||
const collected = collectFieldValues();
|
||||
if (collected.firstInvalid) {
|
||||
status(`Pflichtfeld fehlt: ${collected.firstInvalid}`, "err");
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
await saveOptionPrefs(scope, wantPdf); // Sticky: Umfang merken
|
||||
|
||||
$("submit").disabled = true;
|
||||
const multi = STATE.messageIds.length > 1;
|
||||
let tagFailed = false;
|
||||
try {
|
||||
let mi = 0;
|
||||
for (const mid of STATE.messageIds) {
|
||||
mi++;
|
||||
const prefix = multi ? `Mail ${mi}/${STATE.messageIds.length}: ` : "";
|
||||
// Meta/Anhänge der jeweiligen Nachricht.
|
||||
const meta = mid === STATE.messageId ? STATE.meta : await Mail.getMeta(mid);
|
||||
const atts =
|
||||
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 wantAtt = wantAttScope && atts.length > 0;
|
||||
|
||||
if (wantEml) {
|
||||
status(`${prefix}E-Mail …`);
|
||||
const eml = await Mail.getEmlFile(mid, meta.subject);
|
||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, fields);
|
||||
}
|
||||
if (wantPdf) {
|
||||
status(`${prefix}PDF …`);
|
||||
const pdf = Pdf.fromMail(meta);
|
||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, fields);
|
||||
}
|
||||
if (wantAtt) {
|
||||
// Bei Einzelmail: angehakte Anhänge. Bei Mehrfach: alle Anhänge je Mail.
|
||||
const indices = multi ? atts.map((_, i) => i) : selectedAttachmentIndices();
|
||||
for (const i of indices) {
|
||||
const a = atts[i];
|
||||
if (!a) continue;
|
||||
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" } };
|
||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, file, a.name, af);
|
||||
}
|
||||
}
|
||||
|
||||
if (STATE.settings.tagOnSuccess) {
|
||||
try { await tagMessage(mid); }
|
||||
catch (te) { console.warn("DocuWare-Markierung fehlgeschlagen:", te); tagFailed = true; }
|
||||
}
|
||||
}
|
||||
|
||||
const base = multi ? `${STATE.messageIds.length} E-Mails abgelegt.` : "Erfolgreich abgelegt.";
|
||||
status(tagFailed ? `${base} (Markierung nicht möglich)` : base, "ok");
|
||||
setTimeout(() => window.close(), tagFailed ? 2500 : 1200);
|
||||
} catch (e) {
|
||||
status(`Fehler beim Ablegen: ${e.message}`, "err");
|
||||
$("submit").disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
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";
|
||||
try {
|
||||
STATE.settings = await Settings.set({ storeEml, storeAttachments, storePdf: wantPdf });
|
||||
} catch (_) { /* nicht kritisch */ }
|
||||
}
|
||||
|
||||
function selectedAttachmentIndices() {
|
||||
return STATE.attachments
|
||||
.map((_, i) => i)
|
||||
.filter((i) => { const c = $(`att-${i}`); return c && c.checked; });
|
||||
}
|
||||
|
||||
// Markiert die Mail mit dem Tag "DocuWare". Wirft bei Fehler (Aufrufer fängt).
|
||||
async function tagMessage(messageId) {
|
||||
const KEY = "docuware";
|
||||
const NAME = "DocuWare";
|
||||
const COLOR = "#0e8a8a";
|
||||
// Tags-API unterscheidet sich je nach Thunderbird-Version.
|
||||
const api = browser.messages.tags;
|
||||
let list = [];
|
||||
if (api && api.list) list = await api.list();
|
||||
else if (browser.messages.listTags) list = await browser.messages.listTags();
|
||||
|
||||
let tag = list.find((t) => t.key === KEY || t.tag === NAME);
|
||||
if (!tag) {
|
||||
if (api && api.create) await api.create(KEY, NAME, COLOR);
|
||||
else if (browser.messages.createTag) await browser.messages.createTag(KEY, NAME, COLOR);
|
||||
tag = { key: KEY };
|
||||
}
|
||||
const msg = await browser.messages.get(messageId);
|
||||
const current = msg.tags || [];
|
||||
if (!current.includes(tag.key)) {
|
||||
await browser.messages.update(messageId, { tags: [...current, tag.key] });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Helfer ---
|
||||
const isMemo = (f) => /memo/i.test(f.type) || f.name === "EML_BODY";
|
||||
const toDateInput = (d) => d.toISOString().slice(0, 10);
|
||||
function formatSize(b) {
|
||||
if (!b) return "–";
|
||||
if (b < 1024) return `${b} B`;
|
||||
if (b < 1024 * 1024) return `${(b / 1024).toFixed(0)} KB`;
|
||||
return `${(b / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
document.querySelectorAll(".field").forEach((wrap) => {
|
||||
const el = wrap.querySelector("[data-role='value']");
|
||||
if (el) el.value = "";
|
||||
wrap.classList.remove("prefilled", "invalid");
|
||||
});
|
||||
applyFieldDefaults();
|
||||
prefillFields();
|
||||
applyDefaults();
|
||||
status("");
|
||||
}
|
||||
|
||||
$("cabinet").addEventListener("change", onCabinetChange);
|
||||
$("storeDialog").addEventListener("change", onStoreDialogChange);
|
||||
$("submit").addEventListener("click", submit);
|
||||
$("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)
|
||||
);
|
||||
|
||||
init();
|
||||
|
After Width: | Height: | Size: 41 KiB |
4892
docs/DocuWare.postman_collection.json
Normal file
284
docs/DocuWare.postman_environment.json
Normal file
@ -0,0 +1,284 @@
|
||||
{
|
||||
"id": "0dcdfed3-9e3d-490d-819d-e7c811d33d71",
|
||||
"name": "DocuWare",
|
||||
"values": [
|
||||
{
|
||||
"key": "ServerUrl",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "OrgId",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "FileCabinetId",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "SearchDialogId",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "StoreDialogId",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DocumentId",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "Page",
|
||||
"value": "0",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "Layer",
|
||||
"value": "1",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "StampId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "Platform",
|
||||
"value": "DocuWare/Platform",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "Organization",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "Username",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "Password",
|
||||
"value": "",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "TrustedUserName",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "TrustedUserPassword",
|
||||
"value": "",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DestinationFileCabinetId",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "ClientID",
|
||||
"value": "",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "ClientSecret",
|
||||
"value": "",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "IdentityServiceUrl",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "TokenEndpoint",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "SectionId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "UserId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "CreatedUserId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "CreatedUserNetworkId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "FileCabinetName",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DataRecordId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "CurrentDate",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "IndexDialogId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "Filename",
|
||||
"value": "TestDocument.pdf",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "CheckoutName",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DocumentTrayName",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DocumentTrayId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "MergedDocumentId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "SectionNumber",
|
||||
"value": "0",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "GroupId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "RoleId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DocumentTypeField",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DocumentTypeValue",
|
||||
"value": "API Test Document",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DocumentTypeValueUpdated",
|
||||
"value": "API Test Document Updated",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DocumentDateField",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "DeletedDocumentId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "AccessToken",
|
||||
"value": "",
|
||||
"type": "any",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "LoginToken",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "WorkflowId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "WorkflowInstanceId",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "WindowsUsername",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "WindowsUserPassword",
|
||||
"value": "",
|
||||
"type": "secret",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"key": "WindowsAuthEndpoint",
|
||||
"value": "",
|
||||
"type": "default",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"_postman_variable_scope": "environment",
|
||||
"_postman_exported_at": "2024-09-06T17:56:02.482Z",
|
||||
"_postman_exported_using": "Postman/11.9.2"
|
||||
}
|
||||
BIN
docs/DocuWareMark.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
docs/Sticky.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/ThunderBirdOhneMark.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
icons/icon-16.png
Normal file
|
After Width: | Height: | Size: 371 B |
BIN
icons/icon-32.png
Normal file
|
After Width: | Height: | Size: 730 B |
BIN
icons/icon-64.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
166
lib/auth.js
Normal file
@ -0,0 +1,166 @@
|
||||
// DocuWare-Authentifizierung.
|
||||
//
|
||||
// Primär: DocuWare Identity Service (OpenID Connect).
|
||||
// Neuere DocuWare-Versionen / DocuWare Cloud haben das alte Cookie-Logon
|
||||
// (/Account/Logon) abgeschaltet (HTTP 410) und verlangen ein OAuth-Token.
|
||||
// Wir nutzen den Resource-Owner-Password-Grant (ROPC) mit dem von DocuWare
|
||||
// vorinstallierten ÖFFENTLICHEN Client "docuware.platform.net.client" --
|
||||
// d.h. KEINE eigene App-Registrierung und KEIN Client-Secret nötig.
|
||||
//
|
||||
// Ablauf:
|
||||
// 1. GET {platform}/Home/IdentityServiceInfo -> Identity-Service-URL
|
||||
// 2. GET {identity}/.well-known/openid-configuration -> token_endpoint
|
||||
// 3. POST {token_endpoint} grant_type=password ... -> access_token
|
||||
// 4. Folgeaufrufe der Platform-API mit Authorization: Bearer <token>
|
||||
//
|
||||
// Fallback: Findet sich kein Identity Service (alte On-Prem-Server), wird das
|
||||
// klassische Cookie-Logon versucht.
|
||||
//
|
||||
// Token wird im Speicher gehalten (TOKEN) und von authHeaders() ausgegeben.
|
||||
|
||||
const DW_PUBLIC_CLIENT_ID = "docuware.platform.net.client";
|
||||
const DW_SCOPE = "docuware.platform";
|
||||
|
||||
let TOKEN = null; // { accessToken, refreshToken, expiresAt, tokenEndpoint }
|
||||
|
||||
const Auth = {
|
||||
/**
|
||||
* Meldet sich an DocuWare an. Wirft bei Fehlschlag.
|
||||
* @returns {Promise<{mode:string}>}
|
||||
*/
|
||||
async logon(settings) {
|
||||
const identityUrl = await this._discoverIdentityService(settings);
|
||||
if (identityUrl) {
|
||||
await this._logonIdentity(settings, identityUrl);
|
||||
return { mode: "identity" };
|
||||
}
|
||||
// Fallback: altes Cookie-Logon (nur ältere On-Prem-Server).
|
||||
await this._logonCookie(settings);
|
||||
return { mode: "cookie" };
|
||||
},
|
||||
|
||||
/** Zusätzliche Header für Requests. Bearer-Token, falls vorhanden. */
|
||||
authHeaders() {
|
||||
if (TOKEN && TOKEN.accessToken) {
|
||||
return { Authorization: `Bearer ${TOKEN.accessToken}` };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
/** Aktuell ein Token vorhanden und (noch) gültig? */
|
||||
hasValidToken() {
|
||||
return !!(TOKEN && TOKEN.accessToken && TOKEN.expiresAt > Date.now() + 5000);
|
||||
},
|
||||
|
||||
/** Token verwerfen (z.B. beim Wechsel der Zugangsdaten). */
|
||||
reset() {
|
||||
TOKEN = null;
|
||||
},
|
||||
|
||||
// --- Identity Service -----------------------------------------------------
|
||||
|
||||
/**
|
||||
* Ermittelt die Identity-Service-URL. Liefert null, wenn der Server keinen
|
||||
* Identity Service meldet (dann Cookie-Fallback).
|
||||
*/
|
||||
async _discoverIdentityService(settings) {
|
||||
const platform = Settings.platformUrl(settings);
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`${platform}/Home/IdentityServiceInfo`, {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) return null;
|
||||
const info = await res.json().catch(() => ({}));
|
||||
const url =
|
||||
info.IdentityServiceUrl ||
|
||||
info.identityServiceUrl ||
|
||||
info.Url ||
|
||||
info.url ||
|
||||
null;
|
||||
return url ? url.replace(/\/+$/, "") : null;
|
||||
},
|
||||
|
||||
async _logonIdentity(settings, identityUrl) {
|
||||
// OpenID-Discovery -> token_endpoint.
|
||||
const discoRes = await fetch(`${identityUrl}/.well-known/openid-configuration`, {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!discoRes.ok) {
|
||||
throw new Error(
|
||||
`Identity Service nicht erreichbar (HTTP ${discoRes.status} bei ${identityUrl}).`
|
||||
);
|
||||
}
|
||||
const disco = await discoRes.json();
|
||||
const tokenEndpoint = disco.token_endpoint;
|
||||
if (!tokenEndpoint) throw new Error("token_endpoint im Identity Service nicht gefunden.");
|
||||
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "password",
|
||||
scope: DW_SCOPE,
|
||||
client_id: DW_PUBLIC_CLIENT_ID,
|
||||
username: settings.username,
|
||||
password: settings.password,
|
||||
});
|
||||
|
||||
const res = await fetch(tokenEndpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.access_token) {
|
||||
const detail =
|
||||
data.error_description || data.error || (await this._safeText(res));
|
||||
throw new Error(`Identity-Logon fehlgeschlagen (HTTP ${res.status}). ${detail || ""}`.trim());
|
||||
}
|
||||
|
||||
TOKEN = {
|
||||
accessToken: data.access_token,
|
||||
refreshToken: data.refresh_token || null,
|
||||
expiresAt: Date.now() + (Number(data.expires_in) || 3600) * 1000,
|
||||
tokenEndpoint,
|
||||
};
|
||||
},
|
||||
|
||||
// --- Cookie-Fallback (alte Server) ----------------------------------------
|
||||
|
||||
async _logonCookie(settings) {
|
||||
const platform = Settings.platformUrl(settings);
|
||||
const body = new URLSearchParams({
|
||||
UserName: settings.username,
|
||||
Password: settings.password,
|
||||
Organization: settings.organization,
|
||||
RedirectToMyselfInCaseOfError: "false",
|
||||
RememberMe: "false",
|
||||
});
|
||||
const res = await fetch(`${platform}/Account/Logon`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: body.toString(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await this._safeText(res);
|
||||
throw new Error(`Logon fehlgeschlagen (HTTP ${res.status}). ${text}`.trim());
|
||||
}
|
||||
},
|
||||
|
||||
async _safeText(res) {
|
||||
return res.text().catch(() => "");
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") module.exports = { Auth };
|
||||
359
lib/docuware.js
Normal file
@ -0,0 +1,359 @@
|
||||
// DocuWare Platform REST-Client.
|
||||
// Alle Pfade relativ zu {serverUrl}/DocuWare/Platform.
|
||||
// Auth läuft per Session-Cookie (siehe auth.js); daher überall credentials:"include".
|
||||
|
||||
const DocuWare = {
|
||||
async _get(settings, path) {
|
||||
const platform = Settings.platformUrl(settings);
|
||||
const res = await fetch(`${platform}${path}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: { Accept: "application/json", ...Auth.authHeaders() },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const t = await res.text().catch(() => "");
|
||||
throw new Error(`GET ${path} -> HTTP ${res.status}. ${t}`.trim());
|
||||
}
|
||||
return res.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* Liste der Ablageziele: Archive UND Briefkörbe.
|
||||
* @returns {Promise<Array<{id, name, color, isBasket}>>}
|
||||
*/
|
||||
async listCabinets(settings) {
|
||||
const data = await this._get(settings, "/FileCabinets");
|
||||
const items = data.FileCabinet || [];
|
||||
return items.map((c) => ({
|
||||
id: c.Id,
|
||||
name: c.Name,
|
||||
color: c.Color,
|
||||
isBasket: !!c.IsBasket,
|
||||
}));
|
||||
},
|
||||
|
||||
/** Dialoge eines Schranks. Liefert den ersten Store-Dialog (oder null). */
|
||||
async getStoreDialogId(settings, cabinetId) {
|
||||
const dialogs = await this._dialogList(settings, cabinetId);
|
||||
const store = dialogs.find((d) => this._isStore(d));
|
||||
return store ? store.Id : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Wählt den Dialog, aus dem die Indexfelder gelesen werden:
|
||||
* bevorzugt der Store-Dialog, sonst ein Suchdialog (mit echter GUID).
|
||||
* Gespeichert wird per API auch ohne Store-Dialog (Fields-only Upload).
|
||||
* @returns {Promise<{id:string, type:"store"|"search"}|null>}
|
||||
*/
|
||||
async getFieldsDialog(settings, cabinetId) {
|
||||
const dialogs = await this._dialogList(settings, cabinetId);
|
||||
const store = dialogs.find((d) => this._isStore(d));
|
||||
if (store) return { id: store.Id, type: "store" };
|
||||
const searches = dialogs.filter(
|
||||
(d) => (d.Type || d.DialogType || "").toLowerCase() === "search"
|
||||
);
|
||||
const real = searches.find(
|
||||
(d) => d.Id && d.Id !== "00000000-0000-0000-0000-000000000000"
|
||||
);
|
||||
const pick = real || searches[0];
|
||||
return pick ? { id: pick.Id, type: "search" } : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Alle Dialoge, aus denen Indexfelder gelesen werden können:
|
||||
* bevorzugt ALLE Store-Dialoge; gibt es keinen, die Suchdialoge.
|
||||
* @returns {Promise<Array<{id, name, type:"store"|"search"}>>}
|
||||
*/
|
||||
async listFieldsDialogs(settings, cabinetId) {
|
||||
const dialogs = await this._dialogList(settings, cabinetId);
|
||||
const stores = dialogs
|
||||
.filter((d) => this._isStore(d))
|
||||
.map((d) => ({ id: d.Id, name: d.DisplayName || d.Name || "Ablagedialog", type: "store" }));
|
||||
if (stores.length) return stores;
|
||||
|
||||
const searches = dialogs
|
||||
.filter((d) => (d.Type || d.DialogType || "").toLowerCase() === "search")
|
||||
.map((d) => ({ id: d.Id, name: d.DisplayName || d.Name || "Suchdialog", type: "search" }));
|
||||
const real = searches.filter((d) => d.id && d.id !== "00000000-0000-0000-0000-000000000000");
|
||||
return real.length ? real : searches;
|
||||
},
|
||||
|
||||
async _dialogList(settings, cabinetId) {
|
||||
const data = await this._get(settings, `/FileCabinets/${cabinetId}/Dialogs`);
|
||||
return this._asArray(data.Dialog).length
|
||||
? this._asArray(data.Dialog)
|
||||
: this._asArray(data.Dialogs);
|
||||
},
|
||||
|
||||
_isStore(d) {
|
||||
return (d.Type || d.DialogType || "").toLowerCase() === "store" || d.IsForStoring;
|
||||
},
|
||||
|
||||
/**
|
||||
* Felddefinitionen eines Store-Dialogs.
|
||||
* Robust gegen die verschiedenen DocuWare-JSON-Formen:
|
||||
* Fields (flaches Array) | Fields.Field | Field | Items.
|
||||
* Feldtypen werden zusätzlich aus den Schrank-Stammdaten ergänzt, falls der
|
||||
* Dialog sie nicht mitliefert (wichtig für korrekte Datums-/Zahlenformate).
|
||||
* @returns {Promise<Array<{name, label, type, required, isSelectList}>>}
|
||||
*/
|
||||
async getStoreFields(settings, cabinetId, dialogId) {
|
||||
const data = await this._get(
|
||||
settings,
|
||||
`/FileCabinets/${cabinetId}/Dialogs/${dialogId}`
|
||||
);
|
||||
let raw = this._fieldArray(data);
|
||||
if (!raw.length) {
|
||||
// Diagnose: zeigt die tatsächliche Struktur, damit wir das Format sehen.
|
||||
throw new Error(
|
||||
`Dialog lieferte keine Felder. Top-Level-Keys: [${Object.keys(data).join(", ")}].`
|
||||
);
|
||||
}
|
||||
|
||||
return raw
|
||||
.map((f) => {
|
||||
const name = f.DBFieldName || f.DBName || f.FieldName || f.Name;
|
||||
const hasList =
|
||||
f.SelectListsAssigned === true ||
|
||||
f.AssignedInternalSelectList === true ||
|
||||
(Array.isArray(f.Links) && f.Links.some((l) => l.rel === "simpleSelectList"));
|
||||
return {
|
||||
name,
|
||||
label: f.DlgLabel || f.FieldLabel || f.Label || f.DisplayName || name,
|
||||
type: f.DWFieldType || f.FieldType || f.Type || "Text",
|
||||
required: f.NotEmpty === true || f.Required === true || f.Mandatory === true,
|
||||
readOnly: f.ReadOnly === true || f.Locked === true,
|
||||
visible: f.Visible !== false,
|
||||
isSelectList: hasList,
|
||||
selectListOnly: f.SelectListOnly === true,
|
||||
prefill: this._prefillValue(f),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(f) =>
|
||||
f.name &&
|
||||
f.visible &&
|
||||
!this._isSystemField(f.name) &&
|
||||
!/table/i.test(f.type) // Tabellenfelder unterstützt v1 nicht
|
||||
);
|
||||
},
|
||||
|
||||
/** Liest den vordefinierten Default-Wert (PrefillValue) eines Feldes. */
|
||||
_prefillValue(f) {
|
||||
const pv = f.PrefillValue || f.DefaultItem || f.Default;
|
||||
if (pv === undefined || pv === null) return "";
|
||||
if (Array.isArray(pv)) {
|
||||
const items = pv
|
||||
.map((p) => (p && typeof p === "object" ? p.Item : p))
|
||||
.filter((x) => x !== null && x !== undefined && x !== "");
|
||||
return items.join("; ");
|
||||
}
|
||||
if (typeof pv === "object") return pv.Item != null ? String(pv.Item) : "";
|
||||
return String(pv);
|
||||
},
|
||||
|
||||
/** Map DBFieldName -> {type, label, isSelectList} aus /FileCabinets/{id}. */
|
||||
async _cabinetFieldMap(settings, cabinetId) {
|
||||
const data = await this._get(settings, `/FileCabinets/${cabinetId}`);
|
||||
const raw = this._fieldArray(data);
|
||||
const map = {};
|
||||
raw.forEach((f) => {
|
||||
const name = f.DBFieldName || f.DBName || f.Name;
|
||||
if (!name) return;
|
||||
map[name] = {
|
||||
type: f.DWFieldType || f.FieldType || f.Type || "Text",
|
||||
label: f.FieldLabel || f.DlgLabel || f.Label || f.DisplayName || name,
|
||||
isSelectList: !!f.Keywords || !!f.HasSelectList || !!f.SelectList,
|
||||
};
|
||||
});
|
||||
return map;
|
||||
},
|
||||
|
||||
/** Holt die Feldliste aus den gängigen Container-Formen. */
|
||||
_fieldArray(data) {
|
||||
if (!data) return [];
|
||||
if (this._asArray(data.Fields).length) return this._asArray(data.Fields);
|
||||
if (data.Fields && this._asArray(data.Fields.Field).length)
|
||||
return this._asArray(data.Fields.Field);
|
||||
if (this._asArray(data.Field).length) return this._asArray(data.Field);
|
||||
if (this._asArray(data.Items).length) return this._asArray(data.Items);
|
||||
return [];
|
||||
},
|
||||
|
||||
_asArray(x) {
|
||||
return Array.isArray(x) ? x : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* DIAGNOSE: liefert die echten Rohstrukturen, damit Auswahllisten- und
|
||||
* Default-Parsing exakt (statt geraten) implementiert werden kann.
|
||||
*/
|
||||
async diagnoseStoreDialog(settings, cabinetId) {
|
||||
const out = {};
|
||||
const dialogs = await this._dialogList(settings, cabinetId);
|
||||
out.dialogs = dialogs.map((d) => ({
|
||||
Id: d.Id,
|
||||
Type: d.Type || d.DialogType,
|
||||
Name: d.DisplayName || d.Name,
|
||||
}));
|
||||
|
||||
const pick = await this.getFieldsDialog(settings, cabinetId);
|
||||
out.usedDialog = pick; // welcher Dialog für die Felder genutzt wird
|
||||
if (!pick) {
|
||||
out.note = "Weder Store- noch Suchdialog gefunden.";
|
||||
return out;
|
||||
}
|
||||
|
||||
const detail = await this._get(
|
||||
settings,
|
||||
`/FileCabinets/${cabinetId}/Dialogs/${pick.id}`
|
||||
);
|
||||
const arr = this._fieldArray(detail);
|
||||
out.detailTopLevelKeys = Object.keys(detail);
|
||||
out.fieldCount = arr.length;
|
||||
out.sampleFields = arr.slice(0, 10); // genug, um das Schema zu erkennen
|
||||
|
||||
// Roh-Antwort einer Auswahlliste am ersten Feld, um deren Form zu sehen.
|
||||
if (arr.length) {
|
||||
const fname = arr[0].DBFieldName || arr[0].DBName || arr[0].FieldName || arr[0].Name;
|
||||
out.sampleSelectListField = fname;
|
||||
try {
|
||||
const platform = Settings.platformUrl(settings);
|
||||
const url =
|
||||
`${platform}/FileCabinets/${cabinetId}/Query/SelectListExpression` +
|
||||
`?DialogId=${encodeURIComponent(pick.id)}&FieldName=${encodeURIComponent(fname)}`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
...Auth.authHeaders(),
|
||||
},
|
||||
body: JSON.stringify({ ValuePrefix: "", Limit: 25, Typed: false, ExcludeExternal: true }),
|
||||
});
|
||||
out.sampleSelectListStatus = res.status;
|
||||
out.sampleSelectListRaw = await res.json().catch(() => "(kein JSON)");
|
||||
} catch (e) {
|
||||
out.sampleSelectListError = String(e);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
|
||||
/**
|
||||
* Werte einer Auswahlliste für ein Feld.
|
||||
* POST /FileCabinets/{id}/Query/SelectListExpression?DialogId=..&FieldName=..
|
||||
*/
|
||||
async getSelectList(settings, cabinetId, dialogId, fieldName, prefix = "") {
|
||||
const platform = Settings.platformUrl(settings);
|
||||
const url =
|
||||
`${platform}/FileCabinets/${cabinetId}/Query/SelectListExpression` +
|
||||
`?DialogId=${encodeURIComponent(dialogId)}&FieldName=${encodeURIComponent(fieldName)}`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
...Auth.authHeaders(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ValuePrefix: prefix,
|
||||
Limit: 1000,
|
||||
Typed: false,
|
||||
ExcludeExternal: false, // auch fest hinterlegte Listen liefern
|
||||
}),
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json().catch(() => ({}));
|
||||
// Antwort-Form variiert: Value/Values/List(.Value) mit Strings oder Objekten.
|
||||
const raw =
|
||||
data.Value || data.Values ||
|
||||
(data.List && (data.List.Value || data.List.Values)) || [];
|
||||
return raw
|
||||
.map((v) => (v && typeof v === "object" ? v.Item ?? v.Value ?? v.Name ?? "" : v))
|
||||
.filter((v) => v !== "" && v !== null && v !== undefined)
|
||||
.map(String);
|
||||
},
|
||||
|
||||
/**
|
||||
* Lädt eine Datei mit Indexfeldern in einen Schrank hoch.
|
||||
* @param {File|Blob} file Inhalt (eml/pdf/Anhang)
|
||||
* @param {string} fileName Dateiname inkl. Endung
|
||||
* @param {Object<string,{value:any,type:string}>} fields Indexfelder
|
||||
* @returns {Promise<{id:number}>}
|
||||
*/
|
||||
async uploadDocument(settings, cabinetId, file, fileName, fields) {
|
||||
const platform = Settings.platformUrl(settings);
|
||||
const indexJson = JSON.stringify({ Fields: this._buildFields(fields) });
|
||||
|
||||
const form = new FormData();
|
||||
// Reihenfolge wichtig: erst Index-Teil ("Document"), dann Datei ("File[]").
|
||||
// Part-Namen exakt wie in der DocuWare-API-Collection.
|
||||
form.append(
|
||||
"Document",
|
||||
new Blob([indexJson], { type: "application/json" }),
|
||||
"Index.json"
|
||||
);
|
||||
form.append("File[]", file, fileName);
|
||||
|
||||
const res = await fetch(`${platform}/FileCabinets/${cabinetId}/Documents`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { Accept: "application/json", ...Auth.authHeaders() },
|
||||
body: form,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const t = await res.text().catch(() => "");
|
||||
throw new Error(`Upload fehlgeschlagen (HTTP ${res.status}). ${t}`.trim());
|
||||
}
|
||||
const doc = await res.json().catch(() => ({}));
|
||||
return { id: doc.Id, raw: doc };
|
||||
},
|
||||
|
||||
// --- Helfer ---------------------------------------------------------------
|
||||
|
||||
/** Wandelt {name:{value,type}} in das DocuWare-Field-Array. */
|
||||
_buildFields(fields) {
|
||||
const out = [];
|
||||
for (const [name, def] of Object.entries(fields)) {
|
||||
const value = def && typeof def === "object" ? def.value : def;
|
||||
const type = def && typeof def === "object" ? def.type : "Text";
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
out.push(this._field(name, value, type));
|
||||
}
|
||||
return out;
|
||||
},
|
||||
|
||||
_field(name, value, type) {
|
||||
const t = (type || "Text").toLowerCase();
|
||||
if (t.includes("date")) {
|
||||
// DocuWare erwartet /Date(ms)/ oder ISO; ISO funktioniert bei Platform.
|
||||
const d = value instanceof Date ? value : new Date(value);
|
||||
return { FieldName: name, Item: d.toISOString(), ItemElementName: "Date" };
|
||||
}
|
||||
if (t.includes("decimal") || t.includes("currency")) {
|
||||
return { FieldName: name, Item: Number(value), ItemElementName: "Decimal" };
|
||||
}
|
||||
if (t.includes("numeric") || t.includes("int")) {
|
||||
return { FieldName: name, Item: Number(value), ItemElementName: "Int" };
|
||||
}
|
||||
if (t.includes("memo")) {
|
||||
return { FieldName: name, Item: String(value), ItemElementName: "Memo" };
|
||||
}
|
||||
if (t.includes("keyword")) {
|
||||
const arr = Array.isArray(value) ? value : [String(value)];
|
||||
return { FieldName: name, Item: arr, ItemElementName: "Keywords" };
|
||||
}
|
||||
return { FieldName: name, Item: String(value), ItemElementName: "String" };
|
||||
},
|
||||
|
||||
_isSystemField(dbName) {
|
||||
if (!dbName) return true;
|
||||
// DW-interne Systemfelder (DWxxx) und Volltext nicht im Dialog anzeigen.
|
||||
return /^DW[A-Z]/.test(dbName) || dbName === "DocuWareFulltext";
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") module.exports = { DocuWare };
|
||||
104
lib/mail.js
Normal file
@ -0,0 +1,104 @@
|
||||
// Extraktion von Mail-Inhalten über die Thunderbird messages.* API.
|
||||
|
||||
const Mail = {
|
||||
/** Rohe RFC822-Nachricht als .eml-File. */
|
||||
async getEmlFile(messageId, subject) {
|
||||
const raw = await browser.messages.getRaw(messageId); // String (RFC822)
|
||||
const blob = new Blob([raw], { type: "message/rfc822" });
|
||||
const name = `${this._safeName(subject) || "email"}.eml`;
|
||||
return new File([blob], name, { type: "message/rfc822" });
|
||||
},
|
||||
|
||||
/** Kopf + Body als strukturierte Metadaten zum Vorbefüllen. */
|
||||
async getMeta(messageId) {
|
||||
const header = await browser.messages.get(messageId);
|
||||
const full = await browser.messages.getFull(messageId);
|
||||
const bodyText = this._extractText(full);
|
||||
|
||||
const sender = this._parseAddress(header.author);
|
||||
const account = await this._accountForMessage(messageId);
|
||||
|
||||
return {
|
||||
subject: header.subject || "",
|
||||
senderEmail: sender.email,
|
||||
senderName: sender.name,
|
||||
to: (header.recipients || []).join(", "),
|
||||
cc: (header.ccList || []).join(", "),
|
||||
bcc: (header.bccList || []).join(", "),
|
||||
date: header.date instanceof Date ? header.date : new Date(header.date),
|
||||
bodyText,
|
||||
account,
|
||||
direction: this._direction(header, account),
|
||||
sizeBytes: header.size || 0,
|
||||
};
|
||||
},
|
||||
|
||||
/** Liste der Anhänge: [{partName, name, contentType, size}]. */
|
||||
async listAttachments(messageId) {
|
||||
const atts = await browser.messages.listAttachments(messageId);
|
||||
return (atts || [])
|
||||
.filter((a) => a.partName && a.name) // echte Datei-Anhänge
|
||||
.map((a) => ({
|
||||
partName: a.partName,
|
||||
name: a.name,
|
||||
contentType: a.contentType,
|
||||
size: a.size || 0,
|
||||
}));
|
||||
},
|
||||
|
||||
/** Anhang als File holen. */
|
||||
async getAttachmentFile(messageId, partName) {
|
||||
return browser.messages.getAttachmentFile(messageId, partName); // File
|
||||
},
|
||||
|
||||
// --- Helfer ---------------------------------------------------------------
|
||||
|
||||
_extractText(fullPart) {
|
||||
// Rekursiv den ersten text/plain-Teil suchen, sonst text/html entkernen.
|
||||
let plain = "";
|
||||
let html = "";
|
||||
const walk = (part) => {
|
||||
if (!part) return;
|
||||
if (part.contentType === "text/plain" && part.body) plain += part.body;
|
||||
else if (part.contentType === "text/html" && part.body) html += part.body;
|
||||
(part.parts || []).forEach(walk);
|
||||
};
|
||||
walk(fullPart);
|
||||
if (plain.trim()) return plain;
|
||||
if (html.trim()) return html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
||||
return "";
|
||||
},
|
||||
|
||||
_parseAddress(str) {
|
||||
if (!str) return { name: "", email: "" };
|
||||
const m = str.match(/^\s*"?([^"<]*)"?\s*<([^>]+)>\s*$/);
|
||||
if (m) return { name: m[1].trim(), email: m[2].trim() };
|
||||
return { name: "", email: str.trim() };
|
||||
},
|
||||
|
||||
async _accountForMessage(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 : "";
|
||||
}
|
||||
} catch (_) {}
|
||||
return "";
|
||||
},
|
||||
|
||||
_direction(header, accountName) {
|
||||
// Heuristik: ist der Account-Name/eine Identität Absender -> Ausgang.
|
||||
// Robustere Erkennung erfolgt später über Identitäten; v1 simpel:
|
||||
const folderType = header.folder && header.folder.type;
|
||||
if (folderType === "sent" || folderType === "outbox") return "Ausgang";
|
||||
return "Eingang";
|
||||
},
|
||||
|
||||
_safeName(s) {
|
||||
return (s || "").replace(/[\\/:*?"<>|]+/g, "_").slice(0, 80).trim();
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") module.exports = { Mail };
|
||||
154
lib/pdf.js
Normal file
@ -0,0 +1,154 @@
|
||||
// Minimaler, abhängigkeitsfreier PDF-Generator.
|
||||
// Rendert die E-Mail (Kopf + Klartext-Body) als einfaches Text-PDF.
|
||||
// Bewusst schlicht (v1): kein HTML-Rendering, keine Bilder — dafür keine
|
||||
// externen Libraries. Reicht für eine lesbare Aktenkopie der Mail.
|
||||
|
||||
const Pdf = {
|
||||
/**
|
||||
* Erzeugt ein PDF-File aus den Mail-Metadaten.
|
||||
* @param {object} meta Ergebnis von Mail.getMeta()
|
||||
* @returns {File}
|
||||
*/
|
||||
fromMail(meta) {
|
||||
const header = [
|
||||
`Von: ${meta.senderName ? meta.senderName + " <" + meta.senderEmail + ">" : meta.senderEmail}`,
|
||||
`An: ${meta.to}`,
|
||||
meta.cc ? `CC: ${meta.cc}` : null,
|
||||
`Datum: ${this._fmtDate(meta.date)}`,
|
||||
`Betreff: ${meta.subject}`,
|
||||
"",
|
||||
"----------------------------------------------------------------------",
|
||||
"",
|
||||
].filter((l) => l !== null);
|
||||
|
||||
const lines = header.concat(this._wrap(meta.bodyText || "", 95));
|
||||
const bytes = this._build(lines);
|
||||
const name = `${(meta.subject || "email").replace(/[\\/:*?"<>|]+/g, "_").slice(0, 80) || "email"}.pdf`;
|
||||
return new File([bytes], name, { type: "application/pdf" });
|
||||
},
|
||||
|
||||
// --- PDF-Aufbau -----------------------------------------------------------
|
||||
|
||||
_build(allLines) {
|
||||
const PAGE_W = 595, PAGE_H = 842; // A4 in pt
|
||||
const MARGIN = 50, FONT = 10, LEADING = 14;
|
||||
const linesPerPage = Math.floor((PAGE_H - 2 * MARGIN) / LEADING);
|
||||
|
||||
// Seiten aufteilen
|
||||
const pages = [];
|
||||
for (let i = 0; i < allLines.length; i += linesPerPage) {
|
||||
pages.push(allLines.slice(i, i + linesPerPage));
|
||||
}
|
||||
if (pages.length === 0) pages.push([""]);
|
||||
|
||||
const objects = []; // {id, body} body als latin1-String
|
||||
const addObj = (body) => { objects.push(body); return objects.length; };
|
||||
|
||||
// Platzhalter-IDs vorab festlegen
|
||||
// 1 Catalog, 2 Pages, 3 Font, dann je Seite: PageObj + ContentObj
|
||||
const catalogId = 1, pagesId = 2, fontId = 3;
|
||||
objects.length = 0;
|
||||
objects.push("", "", ""); // reserviere 1..3
|
||||
|
||||
const pageIds = [];
|
||||
const contentIds = [];
|
||||
pages.forEach((pageLines) => {
|
||||
const stream = this._contentStream(pageLines, MARGIN, PAGE_H - MARGIN, FONT, LEADING);
|
||||
const contentId = addObj(
|
||||
`<< /Length ${stream.length} >>\nstream\n${stream}\nendstream`
|
||||
);
|
||||
const pageId = addObj(
|
||||
`<< /Type /Page /Parent ${pagesId} 0 R ` +
|
||||
`/MediaBox [0 0 ${PAGE_W} ${PAGE_H}] ` +
|
||||
`/Resources << /Font << /F1 ${fontId} 0 R >> >> ` +
|
||||
`/Contents ${contentId} 0 R >>`
|
||||
);
|
||||
pageIds.push(pageId);
|
||||
contentIds.push(contentId);
|
||||
});
|
||||
|
||||
objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`;
|
||||
objects[pagesId - 1] =
|
||||
`<< /Type /Pages /Count ${pageIds.length} ` +
|
||||
`/Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] >>`;
|
||||
objects[fontId - 1] =
|
||||
`<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>`;
|
||||
|
||||
// Serialisieren mit xref
|
||||
let pdf = "%PDF-1.4\n";
|
||||
const offsets = [];
|
||||
objects.forEach((body, idx) => {
|
||||
offsets[idx] = pdf.length;
|
||||
pdf += `${idx + 1} 0 obj\n${body}\nendobj\n`;
|
||||
});
|
||||
|
||||
const xrefOffset = pdf.length;
|
||||
pdf += `xref\n0 ${objects.length + 1}\n`;
|
||||
pdf += "0000000000 65535 f \n";
|
||||
offsets.forEach((off) => {
|
||||
pdf += `${String(off).padStart(10, "0")} 00000 n \n`;
|
||||
});
|
||||
pdf +=
|
||||
`trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\n` +
|
||||
`startxref\n${xrefOffset}\n%%EOF`;
|
||||
|
||||
return this._latin1Bytes(pdf);
|
||||
},
|
||||
|
||||
_contentStream(lines, x, yTop, font, leading) {
|
||||
let s = `BT\n/F1 ${font} Tf\n${leading} TL\n${x} ${yTop} Td\n`;
|
||||
lines.forEach((line, i) => {
|
||||
if (i > 0) s += "T*\n";
|
||||
s += `(${this._escape(line)}) Tj\n`;
|
||||
});
|
||||
s += "ET";
|
||||
return s;
|
||||
},
|
||||
|
||||
_wrap(text, width) {
|
||||
const out = [];
|
||||
(text || "").split(/\r?\n/).forEach((para) => {
|
||||
if (para.length <= width) { out.push(para); return; }
|
||||
let line = "";
|
||||
para.split(/\s+/).forEach((word) => {
|
||||
if ((line + " " + word).trim().length > width) {
|
||||
if (line) out.push(line);
|
||||
// sehr lange Wörter hart umbrechen
|
||||
while (word.length > width) { out.push(word.slice(0, width)); word = word.slice(width); }
|
||||
line = word;
|
||||
} else {
|
||||
line = line ? `${line} ${word}` : word;
|
||||
}
|
||||
});
|
||||
if (line) out.push(line);
|
||||
});
|
||||
return out;
|
||||
},
|
||||
|
||||
_escape(s) {
|
||||
return this._toLatin1(s).replace(/[\\()]/g, "\\$&");
|
||||
},
|
||||
|
||||
_toLatin1(s) {
|
||||
// Zeichen außerhalb Latin-1 durch '?' ersetzen.
|
||||
return (s || "").replace(/[^\x00-\xFF]/g, "?");
|
||||
},
|
||||
|
||||
_latin1Bytes(str) {
|
||||
const bytes = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) bytes[i] = str.charCodeAt(i) & 0xff;
|
||||
return bytes;
|
||||
},
|
||||
|
||||
_fmtDate(d) {
|
||||
try {
|
||||
return new Intl.DateTimeFormat("de-DE", {
|
||||
dateStyle: "medium", timeStyle: "short",
|
||||
}).format(d);
|
||||
} catch (_) {
|
||||
return String(d);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") module.exports = { Pdf };
|
||||
38
lib/store.js
Normal file
@ -0,0 +1,38 @@
|
||||
// Einstellungen lesen/schreiben (browser.storage.local).
|
||||
// Hinweis: Credentials werden v1 im Klartext gespeichert (siehe README / Risiken).
|
||||
|
||||
const DEFAULTS = {
|
||||
serverUrl: "", // z.B. https://docuware.example.com (ohne /DocuWare/Platform)
|
||||
organization: "",
|
||||
username: "",
|
||||
password: "",
|
||||
defaultCabinetId: "",
|
||||
dialogByCabinet: {}, // gemerkter Ablagedialog je Schrank: { [cabinetId]: dialogId }
|
||||
storeEml: true,
|
||||
storePdf: true,
|
||||
storeAttachments: true,
|
||||
tagOnSuccess: true,
|
||||
};
|
||||
|
||||
const Settings = {
|
||||
async get() {
|
||||
const stored = await browser.storage.local.get("settings");
|
||||
return { ...DEFAULTS, ...(stored.settings || {}) };
|
||||
},
|
||||
|
||||
async set(partial) {
|
||||
const current = await this.get();
|
||||
const next = { ...current, ...partial };
|
||||
await browser.storage.local.set({ settings: next });
|
||||
return next;
|
||||
},
|
||||
|
||||
/** Basis-URL der Platform-API ohne abschließenden Slash. */
|
||||
platformUrl(settings) {
|
||||
const base = (settings.serverUrl || "").replace(/\/+$/, "");
|
||||
if (!base) throw new Error("Keine DocuWare-Server-URL konfiguriert.");
|
||||
return `${base}/DocuWare/Platform`;
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined") module.exports = { Settings, DEFAULTS };
|
||||
49
manifest.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "DocuWare Ablage",
|
||||
"description": "Legt markierte E-Mails (als .eml, PDF und mit Anhängen) in DocuWare ab.",
|
||||
"version": "0.8.0",
|
||||
"author": "l.lingler",
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"id": "docuware-archive@thunderbird",
|
||||
"strict_min_version": "115.0",
|
||||
"update_url": "https://sylyx.xyz/sylyx/thunderbird2docuware/raw/branch/main/updates.json"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"32": "icons/icon-32.png",
|
||||
"64": "icons/icon-64.png"
|
||||
},
|
||||
"permissions": [
|
||||
"messagesRead",
|
||||
"messagesUpdate",
|
||||
"messagesTags",
|
||||
"accountsRead",
|
||||
"menus",
|
||||
"storage",
|
||||
"tabs",
|
||||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["background/background.js"]
|
||||
},
|
||||
"message_display_action": {
|
||||
"default_title": "In DocuWare ablegen",
|
||||
"default_icon": {
|
||||
"16": "icons/icon-16.png",
|
||||
"32": "icons/icon-32.png"
|
||||
}
|
||||
},
|
||||
"commands": {
|
||||
"open-archive-dialog": {
|
||||
"suggested_key": { "default": "Ctrl+Alt+I" },
|
||||
"description": "Markierte E-Mail in DocuWare ablegen"
|
||||
}
|
||||
},
|
||||
"options_ui": {
|
||||
"page": "options/options.html",
|
||||
"open_in_tab": true
|
||||
}
|
||||
}
|
||||
89
options/options.html
Normal file
@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>DocuWare Ablage – Einstellungen</title>
|
||||
<style>
|
||||
body { font: 14px/1.5 system-ui, sans-serif; max-width: 560px; margin: 24px auto; padding: 0 16px; color: #1a1a1a; }
|
||||
h1 { font-size: 20px; }
|
||||
fieldset { border: 1px solid #ddd; border-radius: 8px; margin: 16px 0; padding: 12px 16px; }
|
||||
legend { font-weight: 600; padding: 0 6px; }
|
||||
label { display: block; margin: 10px 0 4px; font-weight: 500; }
|
||||
input[type="text"], input[type="password"], input[type="url"] { width: 100%; padding: 7px 9px; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; }
|
||||
.check { display: flex; align-items: center; gap: 8px; font-weight: 400; margin: 8px 0; }
|
||||
.row { display: flex; gap: 12px; align-items: center; margin-top: 16px; }
|
||||
button { padding: 8px 16px; border: 0; border-radius: 6px; background: #2563eb; color: #fff; font-weight: 600; cursor: pointer; }
|
||||
button.secondary { background: #e5e7eb; color: #111; }
|
||||
#status { font-size: 13px; min-height: 18px; }
|
||||
.ok { color: #15803d; } .err { color: #b91c1c; }
|
||||
.hint { font-size: 12px; color: #666; margin-top: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DocuWare Ablage – Einstellungen</h1>
|
||||
|
||||
<fieldset>
|
||||
<legend>Verbindung</legend>
|
||||
<label for="serverUrl">Server-URL</label>
|
||||
<input type="url" id="serverUrl" placeholder="https://docuware.example.com" />
|
||||
<div class="hint">Basis-URL ohne <code>/DocuWare/Platform</code>.</div>
|
||||
|
||||
<label for="organization">Organisation</label>
|
||||
<input type="text" id="organization" />
|
||||
|
||||
<label for="username">Benutzername</label>
|
||||
<input type="text" id="username" autocomplete="username" />
|
||||
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" autocomplete="current-password" />
|
||||
<div class="hint">⚠️ Wird unverschlüsselt lokal gespeichert.</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Standardwerte</legend>
|
||||
<label for="defaultCabinet">Standard-Ziel (Archiv oder Briefkorb)</label>
|
||||
<select id="defaultCabinet" style="width:100%;padding:7px;border:1px solid #ccc;border-radius:6px;">
|
||||
<option value="">— keiner —</option>
|
||||
</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>
|
||||
<div class="check"><input type="checkbox" id="tagOnSuccess" /> <label for="tagOnSuccess" style="margin:0">Mail nach Ablage als „DocuWare" markieren</label></div>
|
||||
</fieldset>
|
||||
|
||||
<div class="row">
|
||||
<button id="save">Speichern</button>
|
||||
<button id="test" class="secondary">Verbindung testen</button>
|
||||
<span id="status"></span>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend>Einstellungen sichern</legend>
|
||||
<div class="hint">Exportiert/importiert alle Einstellungen (inkl. Login) als Datei –
|
||||
so musst du nach einer Neuinstallation nichts neu eintippen.</div>
|
||||
<div class="row">
|
||||
<button id="exportBtn" class="secondary">Exportieren</button>
|
||||
<button id="importBtn" class="secondary">Importieren</button>
|
||||
<input type="file" id="importFile" accept="application/json,.json" style="display:none" />
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Diagnose (für Entwicklung)</legend>
|
||||
<div class="hint">Analysiert den Store-Dialog des oben gewählten Standard-Ziels und
|
||||
zeigt die Rohstruktur (Felder, Auswahllisten, Defaults). Inhalt bitte kopieren und
|
||||
an den Entwickler geben.</div>
|
||||
<div class="row">
|
||||
<button id="diagBtn" class="secondary">Store-Dialog analysieren</button>
|
||||
</div>
|
||||
<textarea id="diag" readonly style="width:100%;height:240px;margin-top:10px;font:12px/1.4 monospace;border:1px solid #ccc;border-radius:6px;padding:8px;display:none;"></textarea>
|
||||
</fieldset>
|
||||
|
||||
<script src="../lib/store.js"></script>
|
||||
<script src="../lib/auth.js"></script>
|
||||
<script src="../lib/docuware.js"></script>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
122
options/options.js
Normal file
@ -0,0 +1,122 @@
|
||||
// Logik der Einstellungsseite.
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const FIELDS = ["serverUrl", "organization", "username", "password"];
|
||||
const CHECKS = ["storeEml", "storePdf", "storeAttachments", "tagOnSuccess"];
|
||||
|
||||
function setStatus(msg, kind) {
|
||||
const el = $("status");
|
||||
el.textContent = msg;
|
||||
el.className = kind || "";
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const s = await Settings.get();
|
||||
FIELDS.forEach((f) => ($(f).value = s[f] || ""));
|
||||
CHECKS.forEach((c) => ($(c).checked = !!s[c]));
|
||||
// Schrankliste, falls vorher schon getestet (im Storage gecached)
|
||||
const cached = (await browser.storage.local.get("cabinets")).cabinets || [];
|
||||
fillCabinets(cached, s.defaultCabinetId);
|
||||
}
|
||||
|
||||
function fillCabinets(cabinets, selectedId) {
|
||||
const sel = $("defaultCabinet");
|
||||
sel.innerHTML = '<option value="">— keiner —</option>';
|
||||
const addGroup = (label, list) => {
|
||||
if (!list.length) return;
|
||||
const g = document.createElement("optgroup");
|
||||
g.label = label;
|
||||
list.forEach((c) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = c.id;
|
||||
o.textContent = c.name;
|
||||
if (String(c.id) === String(selectedId)) o.selected = true;
|
||||
g.appendChild(o);
|
||||
});
|
||||
sel.appendChild(g);
|
||||
};
|
||||
addGroup("Archive", cabinets.filter((c) => !c.isBasket));
|
||||
addGroup("Briefkörbe", cabinets.filter((c) => c.isBasket));
|
||||
}
|
||||
|
||||
function collect() {
|
||||
const partial = {};
|
||||
FIELDS.forEach((f) => (partial[f] = $(f).value.trim()));
|
||||
CHECKS.forEach((c) => (partial[c] = $(c).checked));
|
||||
partial.defaultCabinetId = $("defaultCabinet").value;
|
||||
return partial;
|
||||
}
|
||||
|
||||
$("save").addEventListener("click", async () => {
|
||||
await Settings.set(collect());
|
||||
setStatus("Gespeichert.", "ok");
|
||||
});
|
||||
|
||||
$("test").addEventListener("click", async () => {
|
||||
setStatus("Verbinde …");
|
||||
try {
|
||||
const s = await Settings.set(collect()); // erst speichern, dann testen
|
||||
await Auth.logon(s);
|
||||
const cabinets = await DocuWare.listCabinets(s);
|
||||
await browser.storage.local.set({ cabinets });
|
||||
fillCabinets(cabinets, s.defaultCabinetId);
|
||||
const arch = cabinets.filter((c) => !c.isBasket).length;
|
||||
const bk = cabinets.filter((c) => c.isBasket).length;
|
||||
setStatus(`Verbindung ok – ${arch} Archive, ${bk} Briefkörbe gefunden.`, "ok");
|
||||
} catch (e) {
|
||||
setStatus(`Fehler: ${e.message}`, "err");
|
||||
}
|
||||
});
|
||||
|
||||
$("exportBtn").addEventListener("click", async () => {
|
||||
const s = await Settings.get();
|
||||
const blob = new Blob([JSON.stringify(s, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "docuware-ablage-einstellungen.json";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
setStatus("Einstellungen exportiert.", "ok");
|
||||
});
|
||||
|
||||
$("importBtn").addEventListener("click", () => $("importFile").click());
|
||||
|
||||
$("importFile").addEventListener("change", async (e) => {
|
||||
const file = e.target.files && e.target.files[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
await Settings.set(data);
|
||||
await load();
|
||||
setStatus("Einstellungen importiert.", "ok");
|
||||
} catch (err) {
|
||||
setStatus(`Import fehlgeschlagen: ${err.message}`, "err");
|
||||
} finally {
|
||||
e.target.value = "";
|
||||
}
|
||||
});
|
||||
|
||||
$("diagBtn").addEventListener("click", async () => {
|
||||
const cabinetId = $("defaultCabinet").value;
|
||||
const out = $("diag");
|
||||
out.style.display = "block";
|
||||
if (!cabinetId) {
|
||||
out.value = "Bitte oben zuerst ein Standard-Ziel (Archiv) wählen.";
|
||||
return;
|
||||
}
|
||||
out.value = "Analysiere …";
|
||||
try {
|
||||
const s = await Settings.set(collect());
|
||||
await Auth.logon(s);
|
||||
const dump = await DocuWare.diagnoseStoreDialog(s, cabinetId);
|
||||
out.value = JSON.stringify(dump, null, 2);
|
||||
} catch (e) {
|
||||
out.value = `Fehler: ${e.message}`;
|
||||
}
|
||||
});
|
||||
|
||||
load();
|
||||
12
updates.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"addons": {
|
||||
"docuware-archive@thunderbird": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "0.8.0",
|
||||
"update_link": "https://sylyx.xyz/sylyx/thunderbird2docuware/releases/download/v0.8.0/docuware-ablage-0.8.0.xpi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||