Ein Ablagefenster für Mehrfachauswahl + Sitzungs-/Retry-Fixes
Mehrfachauswahl öffnet jetzt EIN Ablagefenster (?messageIds=1,2,3) statt eines Fensters je Nachricht. Der dafür schon vorhandene, bisher unerreichbare Multi-Pfad in dialog.js hatte drei Fehler, die dabei aufgeschlagen wären: - Der Anhang-Haken wurde aus der ERSTEN Mail abgeleitet: hatte die keine Anhänge, fielen die Anhänge aller übrigen Mails still weg. - Die Anhangsliste zeigte die Anhänge von Mail 1 mit wirkungslosen Haken. - Der „bereits abgelegt"-Banner galt nur für Mail 1, sah aber nach einer Aussage über die ganze Auswahl aus. Weiter: - Server-/Zugangsdaten-Wechsel verwirft jetzt auch die serverspezifischen Caches. auth:logout leert zusätzlich ARCHIVED_CACHE; mit scope:"server" fliegen die gecachte Schrankliste sowie defaultCabinetId/dialogByCabinet raus, weil deren GUIDs nur auf dem alten Server existieren. - Retry nach einem Teilfehler lud bereits übertragene Teile erneut hoch und erzeugte Dubletten. Jeder Teil (eml/pdf/Anhang) wird nun je Ziel einmal hochgeladen; dieselbe Mail lässt sich dadurch auch bewusst in ein zweites Archiv ablegen. - ARCHIVED_CACHE wächst nicht mehr unbegrenzt (500 Einträge, ältester fliegt). - HTTP 401/403 liefern statt des nackten Statuscodes eine verständliche Meldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
d02228e4c9
commit
e201e6c654
@ -30,8 +30,19 @@ browser.runtime.onMessage.addListener((msg) => {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sitzung verwerfen. Immer: Token + Badge-Cache (der Cache ist serverspezifisch
|
||||||
|
// und würde sonst ein ✓ aus der alten Verbindung weiterzeigen).
|
||||||
|
// scope:"server" = die Server-URL hat sich geändert -> zusätzlich die gecachte
|
||||||
|
// Schrankliste wegwerfen, denn deren GUIDs gehören zum alten Server.
|
||||||
if (msg.type === "auth:logout") {
|
if (msg.type === "auth:logout") {
|
||||||
Auth.reset();
|
Auth.reset();
|
||||||
|
ARCHIVED_CACHE.clear();
|
||||||
|
if (msg.scope === "server") {
|
||||||
|
return browser.storage.local
|
||||||
|
.remove("cabinets")
|
||||||
|
.catch(() => {})
|
||||||
|
.then(() => ({ ok: true }));
|
||||||
|
}
|
||||||
return Promise.resolve({ ok: true });
|
return Promise.resolve({ ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +51,7 @@ browser.runtime.onMessage.addListener((msg) => {
|
|||||||
if (msg.type === "archive:done") {
|
if (msg.type === "archive:done") {
|
||||||
const mid = DocuWare.normalizeMessageId(msg.rfcMessageId);
|
const mid = DocuWare.normalizeMessageId(msg.rfcMessageId);
|
||||||
if (mid && msg.cabinetId) {
|
if (mid && msg.cabinetId) {
|
||||||
ARCHIVED_CACHE.set(`${mid}|${msg.cabinetId}`, { found: true, docId: msg.docId || null });
|
cacheArchived(`${mid}|${msg.cabinetId}`, { found: true, docId: msg.docId || null });
|
||||||
}
|
}
|
||||||
return Promise.resolve({ ok: true });
|
return Promise.resolve({ ok: true });
|
||||||
}
|
}
|
||||||
@ -54,6 +65,16 @@ browser.runtime.onMessage.addListener((msg) => {
|
|||||||
// Schrank wird das Ergebnis im Speicher gecacht (eine Suche pro Mail).
|
// Schrank wird das Ergebnis im Speicher gecacht (eine Suche pro Mail).
|
||||||
|
|
||||||
const ARCHIVED_CACHE = new Map(); // "messageId|cabinetId" -> { found, docId }
|
const ARCHIVED_CACHE = new Map(); // "messageId|cabinetId" -> { found, docId }
|
||||||
|
const ARCHIVED_CACHE_MAX = 500; // Obergrenze: Thunderbird-Sitzungen laufen wochenlang
|
||||||
|
|
||||||
|
// Schreibt in den Badge-Cache und hält ihn klein (ältester Eintrag fliegt raus –
|
||||||
|
// Map-Iteration ist Einfügereihenfolge).
|
||||||
|
function cacheArchived(key, info) {
|
||||||
|
if (ARCHIVED_CACHE.size >= ARCHIVED_CACHE_MAX && !ARCHIVED_CACHE.has(key)) {
|
||||||
|
ARCHIVED_CACHE.delete(ARCHIVED_CACHE.keys().next().value);
|
||||||
|
}
|
||||||
|
ARCHIVED_CACHE.set(key, info);
|
||||||
|
}
|
||||||
|
|
||||||
async function rfcMessageIdOf(messageId) {
|
async function rfcMessageIdOf(messageId) {
|
||||||
try {
|
try {
|
||||||
@ -95,7 +116,7 @@ async function updateArchivedBadge(tab, message) {
|
|||||||
() => null
|
() => null
|
||||||
);
|
);
|
||||||
info = { found: !!hit, docId: hit ? hit.id : null };
|
info = { found: !!hit, docId: hit ? hit.id : null };
|
||||||
ARCHIVED_CACHE.set(key, info);
|
cacheArchived(key, info);
|
||||||
}
|
}
|
||||||
setBadge(tabId, info.found);
|
setBadge(tabId, info.found);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@ -110,19 +131,15 @@ browser.messageDisplay.onMessagesDisplayed.addListener((tab, displayed) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function openDialog(messageIds) {
|
function openDialog(messageIds) {
|
||||||
const ids = Array.isArray(messageIds) ? messageIds : [messageIds];
|
const ids = (Array.isArray(messageIds) ? messageIds : [messageIds])
|
||||||
// Pro Nachricht ein eigenes Ablagefenster (leicht versetzt gestapelt).
|
.map((id) => parseInt(id, 10))
|
||||||
ids.forEach((id, i) => {
|
.filter((n) => !isNaN(n));
|
||||||
const url = browser.runtime.getURL(`${DIALOG_URL}?messageId=${id}`);
|
if (!ids.length) return;
|
||||||
browser.windows.create({
|
// EIN Ablagefenster für die gesamte Auswahl – dialog.js arbeitet die Liste
|
||||||
url,
|
// nacheinander ab (Fortschritt „Mail 3/20"). Früher wurde pro Nachricht ein
|
||||||
type: "popup",
|
// eigenes Fenster geöffnet, was bei größerer Auswahl unbrauchbar war.
|
||||||
width: 720,
|
const url = browser.runtime.getURL(`${DIALOG_URL}?messageIds=${ids.join(",")}`);
|
||||||
height: 820,
|
browser.windows.create({ url, type: "popup", width: 720, height: 820, left: 100, top: 80 });
|
||||||
left: 100 + i * 28,
|
|
||||||
top: 80 + i * 28,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Button in der geöffneten Nachricht
|
// Button in der geöffneten Nachricht
|
||||||
|
|||||||
@ -15,7 +15,14 @@ let STATE = {
|
|||||||
fields: [], // Felddefinitionen des Store-Dialogs
|
fields: [], // Felddefinitionen des Store-Dialogs
|
||||||
existingDoc: null, // Treffer der Dedup-Suche im gewählten Schrank (oder null)
|
existingDoc: null, // Treffer der Dedup-Suche im gewählten Schrank (oder null)
|
||||||
cabinetFieldsByCab: {}, // Cache: cabinetId -> [Feldnamen] (für EML_ID-Erkennung)
|
cabinetFieldsByCab: {}, // Cache: cabinetId -> [Feldnamen] (für EML_ID-Erkennung)
|
||||||
uploadedIds: new Set(), // bereits erfolgreich abgelegte Nachrichten (für Retry-Schutz)
|
// Vollständig abgelegte Nachrichten je Ziel ("<cab>|<mid>") – Tag/Badge nur einmal.
|
||||||
|
// Ziel im Schlüssel, damit dieselbe Mail bewusst in ein zweites Archiv abgelegt
|
||||||
|
// werden kann, ohne den Dialog neu zu öffnen.
|
||||||
|
uploadedIds: new Set(),
|
||||||
|
// Bereits hochgeladene EINZELTEILE, damit ein Retry nach einem Teilfehler nichts
|
||||||
|
// doppelt hochlädt. Schlüssel inkl. Ziel-Schrank ("<cab>|<mid>|eml"), damit ein
|
||||||
|
// Wechsel des Ziels nach einem Fehlversuch wieder alles hochlädt.
|
||||||
|
uploadedParts: new Set(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
|
// Mail-Token -> Funktion, die den Wert aus den Mail-Metadaten liefert.
|
||||||
@ -88,10 +95,12 @@ async function init() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (STATE.messageIds.length > 1) {
|
if (isMulti()) {
|
||||||
$("multiNote").textContent =
|
$("multiNote").textContent =
|
||||||
`${STATE.messageIds.length} E-Mails werden abgelegt. ` +
|
`${STATE.messageIds.length} E-Mails werden abgelegt. ` +
|
||||||
"Eingetragene Werte gelten für alle; Absender/Betreff/Datum werden je E-Mail einzeln übernommen.";
|
"Eingetragene Werte gelten für alle; zugeordnete Felder (Absender/Betreff/Datum …) " +
|
||||||
|
"werden je E-Mail einzeln übernommen. Anhänge werden – falls aktiviert – je E-Mail " +
|
||||||
|
"vollständig mit abgelegt. Angezeigt sind die Werte der ersten E-Mail.";
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAttachments();
|
renderAttachments();
|
||||||
@ -165,10 +174,21 @@ function askPassword(errMsg) {
|
|||||||
function applyDefaults() {
|
function applyDefaults() {
|
||||||
// Mail-Format (.eml ODER PDF) aus den Einstellungen.
|
// Mail-Format (.eml ODER PDF) aus den Einstellungen.
|
||||||
setMailFormat(STATE.settings.mailFormat === "pdf" ? "pdf" : "eml");
|
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;
|
|
||||||
const att = $("optAtt");
|
const att = $("optAtt");
|
||||||
att.checked = hasAtt && STATE.settings.storeAttachments !== false;
|
const want = STATE.settings.storeAttachments !== false;
|
||||||
|
if (isMulti()) {
|
||||||
|
// Mehrfachauswahl: STATE.attachments gehört nur zur ERSTEN Mail. Daraus darf
|
||||||
|
// nicht geschlossen werden, ob die übrigen Anhänge haben – sonst bliebe der
|
||||||
|
// Haken deaktiviert und die Anhänge aller anderen Mails fielen still weg.
|
||||||
|
// Ob es etwas hochzuladen gibt, entscheidet submit() je Mail einzeln.
|
||||||
|
att.checked = want;
|
||||||
|
att.disabled = false;
|
||||||
|
$("attList").style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Einzelmail: Haken nur sinnvoll, wenn die Mail überhaupt Anhänge hat.
|
||||||
|
const hasAtt = STATE.attachments.length > 0;
|
||||||
|
att.checked = hasAtt && want;
|
||||||
att.disabled = !hasAtt;
|
att.disabled = !hasAtt;
|
||||||
$("attList").style.display = hasAtt && att.checked ? "" : "none";
|
$("attList").style.display = hasAtt && att.checked ? "" : "none";
|
||||||
}
|
}
|
||||||
@ -186,6 +206,10 @@ function getMailFormat() {
|
|||||||
function renderAttachments() {
|
function renderAttachments() {
|
||||||
const ul = $("attList");
|
const ul = $("attList");
|
||||||
ul.innerHTML = "";
|
ul.innerHTML = "";
|
||||||
|
// Die Einzelauswahl der Anhänge gilt nur für EINE Mail. Bei Mehrfachauswahl
|
||||||
|
// würden hier die Anhänge der ersten Mail stehen, die Haken aber nichts tun
|
||||||
|
// (submit() legt dort je Mail alle Anhänge ab) -> Liste bleibt leer/verborgen.
|
||||||
|
if (isMulti()) return;
|
||||||
STATE.attachments.forEach((a, i) => {
|
STATE.attachments.forEach((a, i) => {
|
||||||
// Bewusst per DOM-API (nicht innerHTML): der Anhang-Name stammt aus der Mail
|
// Bewusst per DOM-API (nicht innerHTML): der Anhang-Name stammt aus der Mail
|
||||||
// und ist angreiferkontrolliert – als Text einsetzen, nie als HTML.
|
// und ist angreiferkontrolliert – als Text einsetzen, nie als HTML.
|
||||||
@ -304,6 +328,9 @@ async function onCabinetChange() {
|
|||||||
async function checkExisting() {
|
async function checkExisting() {
|
||||||
STATE.existingDoc = null;
|
STATE.existingDoc = null;
|
||||||
hideArchivedBanner();
|
hideArchivedBanner();
|
||||||
|
// Bei Mehrfachauswahl nicht prüfen: der Banner bezöge sich nur auf die erste
|
||||||
|
// Mail, würde aber wie eine Aussage über die ganze Auswahl wirken.
|
||||||
|
if (isMulti()) return;
|
||||||
if (STATE.isBasket || !STATE.cabinetId || !STATE.meta || !STATE.meta.rfcMessageId) return;
|
if (STATE.isBasket || !STATE.cabinetId || !STATE.meta || !STATE.meta.rfcMessageId) return;
|
||||||
try {
|
try {
|
||||||
const names = await cabinetFieldNames(STATE.cabinetId);
|
const names = await cabinetFieldNames(STATE.cabinetId);
|
||||||
@ -577,7 +604,7 @@ async function submit() {
|
|||||||
|
|
||||||
// Dubletten-Warnung (nur Einzelmail): liegt die Mail im Ziel schon, warnen –
|
// Dubletten-Warnung (nur Einzelmail): liegt die Mail im Ziel schon, warnen –
|
||||||
// „Trotzdem ablegen" bleibt aber möglich.
|
// „Trotzdem ablegen" bleibt aber möglich.
|
||||||
if (STATE.messageIds.length === 1 && !STATE.isBasket && STATE.meta.rfcMessageId) {
|
if (!isMulti() && !STATE.isBasket && STATE.meta.rfcMessageId) {
|
||||||
let hit = STATE.existingDoc;
|
let hit = STATE.existingDoc;
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
const names = await cabinetFieldNames(STATE.cabinetId);
|
const names = await cabinetFieldNames(STATE.cabinetId);
|
||||||
@ -598,16 +625,18 @@ async function submit() {
|
|||||||
await saveOptionPrefs(fmt, wantAttScope); // Sticky: Format + Anhänge-Wahl merken
|
await saveOptionPrefs(fmt, wantAttScope); // Sticky: Format + Anhänge-Wahl merken
|
||||||
|
|
||||||
$("submit").disabled = true;
|
$("submit").disabled = true;
|
||||||
const multi = STATE.messageIds.length > 1;
|
const multi = isMulti();
|
||||||
let tagFailed = false;
|
let tagFailed = false;
|
||||||
|
let current = ""; // zuletzt bearbeitete Mail – für eine brauchbare Fehlermeldung
|
||||||
try {
|
try {
|
||||||
const fieldNames = STATE.isBasket ? [] : await cabinetFieldNames(STATE.cabinetId);
|
const fieldNames = STATE.isBasket ? [] : await cabinetFieldNames(STATE.cabinetId);
|
||||||
let mi = 0;
|
let mi = 0;
|
||||||
for (const mid of STATE.messageIds) {
|
for (const mid of STATE.messageIds) {
|
||||||
mi++;
|
mi++;
|
||||||
// Retry-Schutz: nach einem Teilfehler nicht erneut hochladen (sonst Dubletten).
|
// Retry-Schutz: in DIESES Ziel bereits vollständig abgelegt -> überspringen.
|
||||||
if (STATE.uploadedIds.has(mid)) continue;
|
if (STATE.uploadedIds.has(`${STATE.cabinetId}|${mid}`)) continue;
|
||||||
const prefix = multi ? `Mail ${mi}/${STATE.messageIds.length}: ` : "";
|
const prefix = multi ? `Mail ${mi}/${STATE.messageIds.length}: ` : "";
|
||||||
|
current = multi ? `Mail ${mi}/${STATE.messageIds.length}` : "";
|
||||||
// Meta/Anhänge der jeweiligen Nachricht.
|
// Meta/Anhänge der jeweiligen Nachricht.
|
||||||
const meta = mid === STATE.messageId ? STATE.meta : await Mail.getMeta(mid);
|
const meta = mid === STATE.messageId ? STATE.meta : await Mail.getMeta(mid);
|
||||||
const atts =
|
const atts =
|
||||||
@ -622,14 +651,18 @@ async function submit() {
|
|||||||
const wantAtt = wantAttScope && atts.length > 0;
|
const wantAtt = wantAttScope && atts.length > 0;
|
||||||
|
|
||||||
if (wantEml) {
|
if (wantEml) {
|
||||||
|
await uploadOnce(`${mid}|eml`, async () => {
|
||||||
status(`${prefix}E-Mail …`);
|
status(`${prefix}E-Mail …`);
|
||||||
const eml = await Mail.getEmlFile(mid, meta.subject);
|
const eml = await Mail.getEmlFile(mid, meta.subject);
|
||||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, fields);
|
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, eml, eml.name, fields);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (wantPdf) {
|
if (wantPdf) {
|
||||||
|
await uploadOnce(`${mid}|pdf`, async () => {
|
||||||
status(`${prefix}PDF …`);
|
status(`${prefix}PDF …`);
|
||||||
const pdf = Pdf.fromMail(meta);
|
const pdf = Pdf.fromMail(meta);
|
||||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, fields);
|
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, pdf, pdf.name, fields);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (wantAtt) {
|
if (wantAtt) {
|
||||||
// Bei Einzelmail: angehakte Anhänge. Bei Mehrfach: alle Anhänge je Mail.
|
// Bei Einzelmail: angehakte Anhänge. Bei Mehrfach: alle Anhänge je Mail.
|
||||||
@ -637,10 +670,12 @@ async function submit() {
|
|||||||
for (const i of indices) {
|
for (const i of indices) {
|
||||||
const a = atts[i];
|
const a = atts[i];
|
||||||
if (!a) continue;
|
if (!a) continue;
|
||||||
|
await uploadOnce(`${mid}|att:${a.partName}`, async () => {
|
||||||
status(`${prefix}Anhang ${a.name} …`);
|
status(`${prefix}Anhang ${a.name} …`);
|
||||||
const file = await Mail.getAttachmentFile(mid, a.partName);
|
const file = await Mail.getAttachmentFile(mid, a.partName);
|
||||||
const af = STATE.isBasket ? {} : { ...fields, DOC_FILE_NAME: { value: a.name, type: "Text" } };
|
const af = STATE.isBasket ? {} : { ...fields, DOC_FILE_NAME: { value: a.name, type: "Text" } };
|
||||||
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, file, a.name, af);
|
await DocuWare.uploadDocument(STATE.settings, STATE.cabinetId, file, a.name, af);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,19 +691,33 @@ async function submit() {
|
|||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diese Nachricht ist vollständig abgelegt -> bei einem späteren Retry überspringen.
|
// In diesem Ziel vollständig abgelegt -> bei einem späteren Retry überspringen.
|
||||||
STATE.uploadedIds.add(mid);
|
STATE.uploadedIds.add(`${STATE.cabinetId}|${mid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = multi ? `${STATE.messageIds.length} E-Mails abgelegt.` : "Erfolgreich abgelegt.";
|
const base = multi ? `${STATE.messageIds.length} E-Mails abgelegt.` : "Erfolgreich abgelegt.";
|
||||||
status(tagFailed ? `${base} (Markierung nicht möglich)` : base, "ok");
|
status(tagFailed ? `${base} (Markierung nicht möglich)` : base, "ok");
|
||||||
setTimeout(() => window.close(), tagFailed ? 2500 : 1200);
|
setTimeout(() => window.close(), tagFailed ? 2500 : 1200);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
status(`Fehler beim Ablegen: ${e.message}`, "err");
|
// Bei Mehrfachauswahl sagen, WO es hakte – und dass ein erneuter Klick die
|
||||||
|
// bereits übertragenen E-Mails/Teile überspringt (kein Dublettenrisiko).
|
||||||
|
const where = current ? ` (${current})` : "";
|
||||||
|
const hint = multi ? " Bereits abgelegtes wird bei erneutem Klick übersprungen." : "";
|
||||||
|
status(`Fehler beim Ablegen${where}: ${e.message}${hint}`, "err");
|
||||||
$("submit").disabled = false;
|
$("submit").disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lädt einen Teil (eml/pdf/Anhang) genau EINMAL hoch. Schlägt ein späterer Teil
|
||||||
|
// fehl und der Nutzer klickt erneut auf „Ablegen", werden die bereits erfolgreich
|
||||||
|
// übertragenen Teile übersprungen – sonst entstünden Dubletten in DocuWare.
|
||||||
|
async function uploadOnce(part, doUpload) {
|
||||||
|
const key = `${STATE.cabinetId}|${part}`;
|
||||||
|
if (STATE.uploadedParts.has(key)) return;
|
||||||
|
await doUpload();
|
||||||
|
STATE.uploadedParts.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
// Indexfelder für eine konkrete Nachricht: gemappte Felder (EML_*/generisch)
|
// Indexfelder für eine konkrete Nachricht: gemappte Felder (EML_*/generisch)
|
||||||
// werden je Mail aus deren Metadaten neu berechnet, manuelle Werte bleiben.
|
// werden je Mail aus deren Metadaten neu berechnet, manuelle Werte bleiben.
|
||||||
function fieldsForMessage(baseResult, meta, multi) {
|
function fieldsForMessage(baseResult, meta, multi) {
|
||||||
@ -720,6 +769,8 @@ async function tagMessage(messageId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Helfer ---
|
// --- Helfer ---
|
||||||
|
// Mehrfachauswahl? Steuert Anhang-Liste, Dubletten-Banner und Feldberechnung.
|
||||||
|
const isMulti = () => STATE.messageIds.length > 1;
|
||||||
const isMemo = (f) => /memo/i.test(f.type) || f.name === "EML_BODY";
|
const isMemo = (f) => /memo/i.test(f.type) || f.name === "EML_BODY";
|
||||||
// Lokales Datum als YYYY-MM-DD (NICHT toISOString -> das rechnet auf UTC um und
|
// Lokales Datum als YYYY-MM-DD (NICHT toISOString -> das rechnet auf UTC um und
|
||||||
// kippt eine Uhrzeit nahe Mitternacht auf den Vortag).
|
// kippt eine Uhrzeit nahe Mitternacht auf den Vortag).
|
||||||
@ -763,7 +814,7 @@ document.querySelectorAll('input[name="mailFormat"]').forEach((r) =>
|
|||||||
r.addEventListener("change", () => saveOptionPrefs(getMailFormat(), $("optAtt").checked))
|
r.addEventListener("change", () => saveOptionPrefs(getMailFormat(), $("optAtt").checked))
|
||||||
);
|
);
|
||||||
$("optAtt").addEventListener("change", () => {
|
$("optAtt").addEventListener("change", () => {
|
||||||
const show = $("optAtt").checked && STATE.attachments.length > 0;
|
const show = !isMulti() && $("optAtt").checked && STATE.attachments.length > 0;
|
||||||
$("attList").style.display = show ? "" : "none";
|
$("attList").style.display = show ? "" : "none";
|
||||||
saveOptionPrefs(getMailFormat(), $("optAtt").checked);
|
saveOptionPrefs(getMailFormat(), $("optAtt").checked);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
// DocuWare Platform REST-Client.
|
// DocuWare Platform REST-Client.
|
||||||
// Alle Pfade relativ zu {serverUrl}/DocuWare/Platform.
|
// Alle Pfade relativ zu {serverUrl}/DocuWare/Platform.
|
||||||
// Auth läuft per Session-Cookie (siehe auth.js); daher überall credentials:"include".
|
// Auth primär per Bearer-Token (Identity Service) über Auth.authHeaders(); das
|
||||||
|
// credentials:"include" überall ist für den Cookie-Fallback alter On-Prem-Server
|
||||||
|
// nötig (siehe auth.js) und stört im Token-Modus nicht.
|
||||||
|
|
||||||
const DocuWare = {
|
const DocuWare = {
|
||||||
async _get(settings, path) {
|
async _get(settings, path) {
|
||||||
@ -10,13 +12,32 @@ const DocuWare = {
|
|||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: { Accept: "application/json", ...Auth.authHeaders() },
|
headers: { Accept: "application/json", ...Auth.authHeaders() },
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) throw await this._httpError(res, `GET ${path}`);
|
||||||
const t = await res.text().catch(() => "");
|
|
||||||
throw new Error(`GET ${path} -> HTTP ${res.status}. ${t}`.trim());
|
|
||||||
}
|
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut aus einer fehlgeschlagenen Antwort einen sprechenden Fehler.
|
||||||
|
* 401/403 sind der häufigste Fall (Token abgelaufen oder Zugangsdaten/Server
|
||||||
|
* gewechselt) – der nackte Statuscode hilft dem Anwender dabei nicht weiter.
|
||||||
|
*/
|
||||||
|
async _httpError(res, what) {
|
||||||
|
const body = (await res.text().catch(() => "")).slice(0, 300);
|
||||||
|
if (res.status === 401) {
|
||||||
|
return new Error(
|
||||||
|
"Nicht angemeldet (HTTP 401). Die DocuWare-Sitzung ist abgelaufen oder die " +
|
||||||
|
"Zugangsdaten stimmen nicht. Bitte das Ablagefenster neu öffnen bzw. in den " +
|
||||||
|
"Einstellungen „Verbindung testen“."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (res.status === 403) {
|
||||||
|
return new Error(
|
||||||
|
"Keine Berechtigung (HTTP 403). Der Benutzer darf diese Aktion in DocuWare nicht ausführen."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Error(`${what} -> HTTP ${res.status}. ${body}`.trim());
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liste der Ablageziele: Archive UND Briefkörbe.
|
* Liste der Ablageziele: Archive UND Briefkörbe.
|
||||||
* @returns {Promise<Array<{id, name, color, isBasket}>>}
|
* @returns {Promise<Array<{id, name, color, isBasket}>>}
|
||||||
@ -304,10 +325,7 @@ const DocuWare = {
|
|||||||
headers: { Accept: "application/json", ...Auth.authHeaders() },
|
headers: { Accept: "application/json", ...Auth.authHeaders() },
|
||||||
body: form,
|
body: form,
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) throw await this._httpError(res, "Upload fehlgeschlagen");
|
||||||
const t = await res.text().catch(() => "");
|
|
||||||
throw new Error(`Upload fehlgeschlagen (HTTP ${res.status}). ${t}`.trim());
|
|
||||||
}
|
|
||||||
const doc = await res.json().catch(() => ({}));
|
const doc = await res.json().catch(() => ({}));
|
||||||
return { id: doc.Id, raw: doc };
|
return { id: doc.Id, raw: doc };
|
||||||
},
|
},
|
||||||
|
|||||||
@ -20,9 +20,54 @@ function insecureWarning(url) {
|
|||||||
: "";
|
: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schnappschuss der auth-relevanten Felder (Server/Org/Benutzer) zum Zeitpunkt
|
||||||
|
// des letzten Speicherns/Ladens. Ändert sich davon etwas, ist ein evtl. noch im
|
||||||
|
// Broker liegender Token ungültig (er gehört zu den alten Daten) -> verwerfen.
|
||||||
|
let authSnapshot = { serverUrl: "", organization: "", username: "" };
|
||||||
|
const AUTH_FIELDS = ["serverUrl", "organization", "username"];
|
||||||
|
|
||||||
|
// Server-URLs nur für den Vergleich vereinheitlichen, damit ein rein kosmetischer
|
||||||
|
// Edit (Schrägstrich am Ende, Groß-/Kleinschreibung) nicht als Wechsel zählt.
|
||||||
|
const normUrl = (u) => String(u || "").trim().replace(/\/+$/, "").toLowerCase();
|
||||||
|
|
||||||
|
function snapshotAuth(s) {
|
||||||
|
authSnapshot = { serverUrl: s.serverUrl || "", organization: s.organization || "", username: s.username || "" };
|
||||||
|
}
|
||||||
|
function authChanged(s) {
|
||||||
|
return AUTH_FIELDS.some((k) => (s[k] || "") !== (authSnapshot[k] || ""));
|
||||||
|
}
|
||||||
|
// Nur ein echter Server-Wechsel entwertet die Schrank-GUIDs (Schrankliste,
|
||||||
|
// Standardziel, gemerkte Ablagedialoge). Org-/Benutzer-Wechsel tut das nicht.
|
||||||
|
function serverChanged(s) {
|
||||||
|
return normUrl(s.serverUrl) !== normUrl(authSnapshot.serverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nach dem Speichern aufräumen, wenn sich Zugangsdaten geändert haben:
|
||||||
|
* Token immer verwerfen; bei Server-Wechsel zusätzlich alles wegwerfen, was
|
||||||
|
* Schrank-GUIDs des alten Servers enthält (sonst zeigt der Ablage-Dialog fremde
|
||||||
|
* Archive bzw. legt gegen eine nicht mehr existierende GUID ab).
|
||||||
|
* @returns {Promise<object>} die ggf. bereinigten Einstellungen
|
||||||
|
*/
|
||||||
|
async function invalidateSession(s) {
|
||||||
|
if (!authChanged(s)) { snapshotAuth(s); return s; }
|
||||||
|
const server = serverChanged(s);
|
||||||
|
await browser.runtime
|
||||||
|
.sendMessage({ type: "auth:logout", scope: server ? "server" : "auth" })
|
||||||
|
.catch(() => {});
|
||||||
|
let next = s;
|
||||||
|
if (server) {
|
||||||
|
next = await Settings.set({ defaultCabinetId: "", dialogByCabinet: {} });
|
||||||
|
fillCabinets([], ""); // Dropdowns leeren – die alte Liste gilt nicht mehr
|
||||||
|
}
|
||||||
|
snapshotAuth(next);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const s = await Settings.get();
|
const s = await Settings.get();
|
||||||
FIELDS.forEach((f) => ($(f).value = s[f] || ""));
|
FIELDS.forEach((f) => ($(f).value = s[f] || ""));
|
||||||
|
snapshotAuth(s);
|
||||||
CHECKS.forEach((c) => ($(c).checked = !!s[c]));
|
CHECKS.forEach((c) => ($(c).checked = !!s[c]));
|
||||||
setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml");
|
setMailFormat(s.mailFormat === "pdf" ? "pdf" : "eml");
|
||||||
$("mappingUrl").value = s.mappingUrl || "";
|
$("mappingUrl").value = s.mappingUrl || "";
|
||||||
@ -93,16 +138,24 @@ function fillCabinets(cabinets, selectedId) {
|
|||||||
// Besorgt einen Sitzungs-Token: vorhandenen vom Broker nutzen, sonst mit dem
|
// Besorgt einen Sitzungs-Token: vorhandenen vom Broker nutzen, sonst mit dem
|
||||||
// (transienten) Passwortfeld anmelden. Liefert false, wenn kein Passwort da ist.
|
// (transienten) Passwortfeld anmelden. Liefert false, wenn kein Passwort da ist.
|
||||||
async function ensureToken() {
|
async function ensureToken() {
|
||||||
const st = await browser.runtime.sendMessage({ type: "auth:status" });
|
|
||||||
if (st && st.hasToken && st.token) { Auth.setToken(st.token); return true; }
|
|
||||||
const pw = $("password").value;
|
const pw = $("password").value;
|
||||||
if (!pw) return false;
|
// Wurde ein Passwort eingegeben, IMMER frisch anmelden: ein evtl. noch im Broker
|
||||||
|
// liegender Token könnte zu früheren Zugangsdaten/Server gehören und würde sonst
|
||||||
|
// zu „GET /FileCabinets -> HTTP 401" führen (bis zum Thunderbird-Neustart).
|
||||||
|
// Der Broker (background.js) ruft bei auth:logon zuerst Auth.reset() und liest die
|
||||||
|
// gerade gespeicherten Einstellungen neu.
|
||||||
|
if (pw) {
|
||||||
const res = await browser.runtime.sendMessage({ type: "auth:logon", password: pw });
|
const res = await browser.runtime.sendMessage({ type: "auth:logon", password: pw });
|
||||||
if (!res || !res.ok) throw new Error(res && res.error ? res.error : "Anmeldung fehlgeschlagen.");
|
if (!res || !res.ok) throw new Error(res && res.error ? res.error : "Anmeldung fehlgeschlagen.");
|
||||||
Auth.setToken(res.token);
|
Auth.setToken(res.token);
|
||||||
$("password").value = ""; // Passwort nicht behalten
|
$("password").value = ""; // Passwort nicht behalten
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Kein Passwort getippt: vorhandenen Sitzungs-Token vom Broker übernehmen.
|
||||||
|
const st = await browser.runtime.sendMessage({ type: "auth:status" });
|
||||||
|
if (st && st.hasToken && st.token) { Auth.setToken(st.token); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Feld-Mapping-Editor ---------------------------------------------------
|
// --- Feld-Mapping-Editor ---------------------------------------------------
|
||||||
|
|
||||||
@ -222,15 +275,22 @@ function collect() {
|
|||||||
|
|
||||||
$("save").addEventListener("click", async () => {
|
$("save").addEventListener("click", async () => {
|
||||||
const data = collect();
|
const data = collect();
|
||||||
await Settings.set(data);
|
const saved = await Settings.set(data);
|
||||||
|
// Server/Org/Benutzer geändert? Dann gehört ein evtl. noch im Broker liegender
|
||||||
|
// Token zu den ALTEN Daten -> verwerfen, damit Ablage/Test sich neu anmelden.
|
||||||
|
const wasServerChange = serverChanged(saved); // vor dem Neu-Schnappschuss prüfen
|
||||||
|
await invalidateSession(saved);
|
||||||
const warn = insecureWarning(data.serverUrl);
|
const warn = insecureWarning(data.serverUrl);
|
||||||
setStatus("Gespeichert." + warn, warn ? "err" : "ok");
|
const note = wasServerChange ? " Neuer Server – bitte Verbindung testen und Standardziel neu wählen." : "";
|
||||||
|
setStatus("Gespeichert." + note + warn, warn ? "err" : "ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
$("test").addEventListener("click", async () => {
|
$("test").addEventListener("click", async () => {
|
||||||
setStatus("Verbinde …");
|
setStatus("Verbinde …");
|
||||||
try {
|
try {
|
||||||
const s = await Settings.set(collect()); // erst speichern (ohne Passwort), dann testen
|
// Erst speichern (ohne Passwort), dann testen. Bei geänderten Zugangsdaten
|
||||||
|
// vorher den alten Broker-Token verwerfen (sonst HTTP 401).
|
||||||
|
const s = await invalidateSession(await Settings.set(collect()));
|
||||||
const ok = await ensureToken();
|
const ok = await ensureToken();
|
||||||
if (!ok) { setStatus("Bitte Passwort eingeben.", "err"); return; }
|
if (!ok) { setStatus("Bitte Passwort eingeben.", "err"); return; }
|
||||||
const cabinets = await DocuWare.listCabinets(s);
|
const cabinets = await DocuWare.listCabinets(s);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user