Initial commit
This commit is contained in:
552
background.js
Normal file
552
background.js
Normal file
@@ -0,0 +1,552 @@
|
||||
/**
|
||||
* background.js - Ereignisseite der Erweiterung.
|
||||
*
|
||||
* Hier liegen die Menüeinträge, die Schaltflächen und die Vermittlung
|
||||
* zwischen den Dialogfenstern und der Schnittstelle. Die Dialoge selbst
|
||||
* sind gewöhnliche HTML-Seiten; sie rufen über runtime.sendMessage hier
|
||||
* an, statt selbst mit Paperless zu sprechen.
|
||||
*
|
||||
* Der Grund für diese Aufteilung: Die Berechtigung für die Zieladresse
|
||||
* wird zur Laufzeit angefordert und hängt am Hintergrundkontext. Ein
|
||||
* Dialogfenster müsste sie sonst erneut erbitten.
|
||||
*/
|
||||
|
||||
import { PaperlessClient, PaperlessError } from "./lib/client.js";
|
||||
import {
|
||||
ladeEinstellungen, vollstaendig, sichererName, versionslabel,
|
||||
titelAusBetreff, t
|
||||
} from "./lib/settings.js";
|
||||
|
||||
/**
|
||||
* Ergebnis einer Nachrichten-Abfrage vereinheitlichen.
|
||||
*
|
||||
* Je nach Version liefert Thunderbird ein Feld, ein MessageList-Objekt mit
|
||||
* der Eigenschaft "messages" oder eine einzelne Nachricht. Ohne diese
|
||||
* Vereinheitlichung bricht die Erweiterung bei jedem Versionswechsel.
|
||||
*/
|
||||
function alsListe(wert) {
|
||||
if (!wert) return [];
|
||||
if (Array.isArray(wert)) return wert;
|
||||
if (Array.isArray(wert.messages)) return wert.messages;
|
||||
if (wert.id !== undefined) return [wert];
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Angezeigte Nachricht(en) eines Fensters holen.
|
||||
*
|
||||
* getDisplayedMessage (Einzahl) ist in neueren Ständen entfallen; dort
|
||||
* gibt es nur noch getDisplayedMessages (Mehrzahl). Beide werden
|
||||
* probiert, dazu als letzter Ausweg die Auswahl im Nachrichtenbereich.
|
||||
*/
|
||||
async function angezeigteNachrichten(tabId) {
|
||||
const md = browser.messageDisplay;
|
||||
const versuche = [];
|
||||
|
||||
// OHNE Reiterkennung zuerst. Beim Kontextmenü eines Anhangs übergibt
|
||||
// Thunderbird einen Reiter mit type null - das ist nicht der
|
||||
// Nachrichtenbereich, und jeder Aufruf mit dieser Kennung geht ins
|
||||
// Leere. Ohne Argument nimmt Thunderbird den aktiven Reiter, und das
|
||||
// ist der richtige.
|
||||
if (md?.getDisplayedMessages) {
|
||||
versuche.push(["getDisplayedMessages()",
|
||||
() => md.getDisplayedMessages()]);
|
||||
if (tabId !== undefined && tabId !== null) {
|
||||
versuche.push(["getDisplayedMessages(tabId)",
|
||||
() => md.getDisplayedMessages(tabId)]);
|
||||
}
|
||||
}
|
||||
// Einzahl-Fassung für ältere Stände.
|
||||
if (md?.getDisplayedMessage) {
|
||||
versuche.push(["getDisplayedMessage()",
|
||||
() => md.getDisplayedMessage()]);
|
||||
}
|
||||
// Auswahl in der Nachrichtenliste, wenn kein Nachrichtenbereich offen ist.
|
||||
if (browser.mailTabs?.getSelectedMessages) {
|
||||
versuche.push(["mailTabs.getSelectedMessages()",
|
||||
() => browser.mailTabs.getSelectedMessages()]);
|
||||
}
|
||||
|
||||
for (const [name, fn] of versuche) {
|
||||
try {
|
||||
const r = alsListe(await fn());
|
||||
if (r.length) return r;
|
||||
} catch (err) {
|
||||
console.debug("[paperless]", name, "ohne Erfolg:", err?.message);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Datum einer Nachricht als JJJJ-MM-TT.
|
||||
*
|
||||
* Je nach Version ist date ein Date-Objekt oder eine Zeichenkette.
|
||||
*/
|
||||
function alsDatum(wert) {
|
||||
if (!wert) return undefined;
|
||||
const d = wert instanceof Date ? wert : new Date(wert);
|
||||
return Number.isNaN(d.getTime()) ? undefined : d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
// Offene Dialoge, damit sie ihre Aufgabe abholen können.
|
||||
const auftraege = new Map();
|
||||
let naechsteAuftragsnummer = 1;
|
||||
|
||||
// ---------------------------------------------------------------- Menüs
|
||||
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
browser.menus.create({
|
||||
id: "paperless-anhang",
|
||||
title: t("menuStoreAttachment"),
|
||||
contexts: ["message_attachments"]
|
||||
});
|
||||
browser.menus.create({
|
||||
id: "paperless-nachricht",
|
||||
title: t("menuStoreMessage"),
|
||||
contexts: ["message_list"]
|
||||
});
|
||||
});
|
||||
|
||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
try {
|
||||
console.debug("[paperless] Menü", info.menuItemId,
|
||||
"Anhänge:", alsListe(info.attachments).length);
|
||||
if (info.menuItemId === "paperless-anhang") {
|
||||
await anhangAblegen(info, tab);
|
||||
} else if (info.menuItemId === "paperless-nachricht") {
|
||||
await nachrichtAblegen(info, tab);
|
||||
}
|
||||
} catch (err) {
|
||||
await melde(err);
|
||||
}
|
||||
});
|
||||
|
||||
browser.messageDisplayAction.onClicked.addListener(async (tab) => {
|
||||
try {
|
||||
await nachrichtAblegen({}, tab);
|
||||
} catch (err) {
|
||||
await melde(err);
|
||||
}
|
||||
});
|
||||
|
||||
browser.composeAction.onClicked.addListener(async (tab) => {
|
||||
try {
|
||||
await dialogOeffnen("einfuegen", { tabId: tab.id });
|
||||
} catch (err) {
|
||||
await melde(err);
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------- Ablegen
|
||||
|
||||
async function anhangAblegen(info, tab) {
|
||||
const nachricht = alsListe(info.displayedMessages)[0]
|
||||
|| alsListe(info.selectedMessages)[0]
|
||||
|| (await angezeigteNachrichten(tab?.id))[0];
|
||||
if (!nachricht) throw new PaperlessError(t("errNoMessage"));
|
||||
|
||||
const alle = await browser.messages.listAttachments(nachricht.id);
|
||||
// Beim Klick auf einen einzelnen Anhang liefert das Menü nur diesen.
|
||||
// Über die Sammelfunktion "Alle Anhänge" kommen mehrere.
|
||||
const gewaehlt = alsListe(info.attachments).length
|
||||
? alsListe(info.attachments).map((a) => a.partName)
|
||||
: alle.map((a) => a.partName);
|
||||
|
||||
// Nur die Beschreibung sammeln. Die eigentlichen Daten werden erst beim
|
||||
// Hochladen geholt - sonst läge ein grosser Anhang unnötig im Speicher,
|
||||
// während der Anwender noch im Dialog überlegt.
|
||||
const dateien = alle
|
||||
.filter((teil) => gewaehlt.includes(teil.partName))
|
||||
.map((teil) => ({
|
||||
name: sichererName(teil.name),
|
||||
size: teil.size,
|
||||
contentType: teil.contentType,
|
||||
partName: teil.partName,
|
||||
messageId: nachricht.id
|
||||
}));
|
||||
|
||||
await dialogOeffnen("ablegen", {
|
||||
quelle: "anhaenge",
|
||||
messageId: nachricht.id,
|
||||
betreff: nachricht.subject,
|
||||
absender: nachricht.author,
|
||||
datum: alsDatum(nachricht.date),
|
||||
dateien
|
||||
});
|
||||
}
|
||||
|
||||
async function nachrichtAblegen(info, tab) {
|
||||
let nachrichten = alsListe(info.selectedMessages);
|
||||
if (!nachrichten.length) nachrichten = alsListe(info.displayedMessages);
|
||||
if (!nachrichten.length) {
|
||||
nachrichten = await angezeigteNachrichten(tab?.id);
|
||||
}
|
||||
if (!nachrichten.length) throw new PaperlessError(t("errNoMessage"));
|
||||
|
||||
const nachricht = nachrichten[0];
|
||||
const anhaenge = await browser.messages.listAttachments(nachricht.id);
|
||||
|
||||
await dialogOeffnen("ablegen", {
|
||||
quelle: "nachricht",
|
||||
messageId: nachricht.id,
|
||||
betreff: nachricht.subject,
|
||||
absender: nachricht.author,
|
||||
datum: alsDatum(nachricht.date),
|
||||
dateien: anhaenge.map((a) => ({
|
||||
name: sichererName(a.name),
|
||||
size: a.size,
|
||||
contentType: a.contentType,
|
||||
partName: a.partName,
|
||||
messageId: nachricht.id
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- Dialoge
|
||||
|
||||
async function dialogOeffnen(art, daten) {
|
||||
const einstellungen = await ladeEinstellungen();
|
||||
if (!vollstaendig(einstellungen)) {
|
||||
await browser.runtime.openOptionsPage();
|
||||
return;
|
||||
}
|
||||
const nummer = naechsteAuftragsnummer++;
|
||||
auftraege.set(nummer, daten);
|
||||
|
||||
// Nach einer halben Stunde verwerfen. Ein Auftrag enthält Betreff,
|
||||
// Absender und Dateinamen; das muss nicht bis zum Neustart im Speicher
|
||||
// liegen, nur weil jemand den Dialog offen stehen liess.
|
||||
setTimeout(() => auftraege.delete(nummer), 30 * 60 * 1000);
|
||||
const seite = art === "einfuegen" ? "ui/insert.html" : "ui/store.html";
|
||||
|
||||
// Die Adresse MUSS über runtime.getURL aufgelöst werden. Ein relativer
|
||||
// Pfad wird in Manifest V3 nicht gegen das Erweiterungsverzeichnis
|
||||
// aufgelöst - das Fenster öffnet dann stillschweigend nicht.
|
||||
const url = browser.runtime.getURL(`${seite}?auftrag=${nummer}`);
|
||||
|
||||
try {
|
||||
await browser.windows.create({
|
||||
url,
|
||||
type: "popup",
|
||||
width: art === "einfuegen" ? 760 : 820,
|
||||
height: art === "einfuegen" ? 640 : 720
|
||||
});
|
||||
} catch (err) {
|
||||
// Manche Stände lehnen type "popup" ab. Dann ein gewöhnliches Fenster.
|
||||
console.warn("[paperless] popup abgelehnt, versuche normales Fenster",
|
||||
err);
|
||||
await browser.windows.create({ url });
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------- Vermittlung für die Dialoge
|
||||
|
||||
browser.runtime.onMessage.addListener(async (nachricht, absender) => {
|
||||
// Nur Nachrichten aus dieser Erweiterung annehmen. Thunderbird lässt
|
||||
// fremde Absender hier zwar ohnehin nicht durch, solange kein
|
||||
// externally_connectable gesetzt ist - die Prüfung kostet nichts und
|
||||
// hält die Zusicherung explizit.
|
||||
if (absender?.id && absender.id !== browser.runtime.id) {
|
||||
return { ok: false, fehler: "Fremder Absender" };
|
||||
}
|
||||
try {
|
||||
switch (nachricht?.befehl) {
|
||||
case "auftrag":
|
||||
return { ok: true, daten: auftraege.get(nachricht.nummer) || null };
|
||||
|
||||
case "einstellungen":
|
||||
return { ok: true, daten: await ladeEinstellungen() };
|
||||
|
||||
case "berechtigungPruefen":
|
||||
return { ok: true, daten: await berechtigungPruefen() };
|
||||
|
||||
case "suchen": {
|
||||
const c = await client();
|
||||
return { ok: true, daten: await c.search(nachricht.query) };
|
||||
}
|
||||
|
||||
case "dokument": {
|
||||
const c = await client();
|
||||
return { ok: true, daten: await c.document(nachricht.id) };
|
||||
}
|
||||
|
||||
case "versionen": {
|
||||
const c = await client();
|
||||
const doc = await c.document(nachricht.id);
|
||||
return { ok: true, daten: await c.versions(doc) };
|
||||
}
|
||||
|
||||
case "stammdaten": {
|
||||
const c = await client();
|
||||
const [tags, korr, typen] = await Promise.all(
|
||||
[c.tags(), c.correspondents(), c.documentTypes()]);
|
||||
return { ok: true, daten: { tags, korr, typen } };
|
||||
}
|
||||
|
||||
case "ablegen":
|
||||
return { ok: true, daten: await ablegen(nachricht) };
|
||||
|
||||
case "klassifizieren": {
|
||||
const c = await client();
|
||||
const payload = { ...nachricht.payload };
|
||||
payload.correspondent = await aufloesen(
|
||||
c, "correspondents/", nachricht.korrName);
|
||||
payload.document_type = await aufloesen(
|
||||
c, "document_types/", nachricht.typName);
|
||||
await c.patchDocument(nachricht.id, payload);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
case "anhaengen":
|
||||
return { ok: true, daten: await anhaengenAnEntwurf(nachricht) };
|
||||
|
||||
case "freigabelink":
|
||||
return { ok: true, daten: await freigabelink(nachricht) };
|
||||
|
||||
default:
|
||||
return { ok: false, fehler: `Unbekannter Befehl: ${nachricht?.befehl}` };
|
||||
}
|
||||
} catch (err) {
|
||||
return { ok: false, fehler: String(err?.message || err) };
|
||||
}
|
||||
});
|
||||
|
||||
async function client() {
|
||||
const s = await ladeEinstellungen();
|
||||
if (!vollstaendig(s)) throw new PaperlessError(t("errNoSettings"));
|
||||
|
||||
// Ohne Zugriffsrecht für den Ursprung schlägt fetch mit einem nackten
|
||||
// "NetworkError" fehl. Die Prüfung vorweg erspart eine irreführende
|
||||
// Fehlersuche beim Netzwerk.
|
||||
let origin;
|
||||
try {
|
||||
origin = new URL(s.baseUrl).origin + "/*";
|
||||
} catch {
|
||||
throw new PaperlessError(t("errBadUrl"));
|
||||
}
|
||||
const erteilt = await browser.permissions.contains({ origins: [origin] });
|
||||
if (!erteilt) {
|
||||
throw new PaperlessError(
|
||||
`${t("errNoPermission")}\n\n${origin}\n\n`
|
||||
+ `In den Einstellungen erneut auf Speichern klicken - die Abfrage `
|
||||
+ `kann nur aus einer Benutzeraktion heraus erscheinen.`);
|
||||
}
|
||||
return new PaperlessClient(s.baseUrl, s.token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zugriffsrecht für die Zieladresse.
|
||||
*
|
||||
* Es wird bewusst nicht pauschal im Manifest verlangt, sondern erst zur
|
||||
* Laufzeit für genau die eingetragene Adresse angefordert. Eine
|
||||
* Erweiterung, die von Haus aus jede Adresse erreichen darf, ist eine
|
||||
* unnötig große Angriffsfläche.
|
||||
*/
|
||||
async function berechtigungPruefen() {
|
||||
const s = await ladeEinstellungen();
|
||||
if (!vollstaendig(s)) return { noetig: false, erteilt: false };
|
||||
const muster = new URL(s.baseUrl).origin + "/*";
|
||||
const erteilt = await browser.permissions.contains({ origins: [muster] });
|
||||
return { noetig: true, erteilt, muster };
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- Vorgänge
|
||||
|
||||
async function datenHolen(auftrag) {
|
||||
if (auftrag.quelle === "nachricht") {
|
||||
const roh = await browser.messages.getRaw(auftrag.messageId);
|
||||
const blob = new Blob([roh], { type: "message/rfc822" });
|
||||
const name = sichererName(
|
||||
`${titelAusBetreff(auftrag.betreff)}.eml`);
|
||||
return [{ blob, name }];
|
||||
}
|
||||
const out = [];
|
||||
for (const d of auftrag.dateien || []) {
|
||||
if (!d.ausgewaehlt) continue;
|
||||
const datei = await browser.messages.getAttachmentFile(
|
||||
d.messageId, d.partName);
|
||||
out.push({ blob: datei, name: sichererName(d.name) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function ablegen(nachricht) {
|
||||
const c = await client();
|
||||
const s = await ladeEinstellungen();
|
||||
const dateien = await datenHolen(nachricht.auftrag);
|
||||
if (!dateien.length) throw new PaperlessError(t("errNothingSelected"));
|
||||
|
||||
const ergebnisse = [];
|
||||
for (const datei of dateien) {
|
||||
if (nachricht.ziel === "vorhanden") {
|
||||
const antwort = await c.updateVersion(
|
||||
nachricht.zielId, datei.blob, datei.name,
|
||||
versionslabel(s.versionLabel));
|
||||
const id = await c.waitForTask(antwort);
|
||||
ergebnisse.push({ id, name: datei.name, art: "version",
|
||||
zielId: nachricht.zielId });
|
||||
} else {
|
||||
const felder = { title: nachricht.titel || datei.name };
|
||||
if (nachricht.erstellt) felder.created = nachricht.erstellt;
|
||||
const antwort = await c.postDocument(datei.blob, datei.name, felder);
|
||||
const id = await c.waitForTask(antwort);
|
||||
ergebnisse.push({ id, name: datei.name, art: "neu" });
|
||||
}
|
||||
}
|
||||
return ergebnisse;
|
||||
}
|
||||
|
||||
async function aufloesen(c, pfad, name) {
|
||||
const gesucht = String(name || "").trim();
|
||||
if (!gesucht) return null;
|
||||
const liste = pfad.startsWith("correspondents")
|
||||
? await c.correspondents() : await c.documentTypes();
|
||||
const treffer = liste.find(
|
||||
(x) => x.name.trim().toLowerCase() === gesucht.toLowerCase());
|
||||
if (treffer) return treffer.id;
|
||||
const neu = await c.create(pfad, gesucht);
|
||||
return neu.id;
|
||||
}
|
||||
|
||||
async function anhaengenAnEntwurf(nachricht) {
|
||||
const c = await client();
|
||||
const { blob, name } = await c.download(nachricht.versionId, true);
|
||||
const datei = new File([blob], sichererName(name || `paperless-${nachricht.versionId}`));
|
||||
await browser.compose.addAttachment(nachricht.tabId, { file: datei });
|
||||
return { name: datei.name };
|
||||
}
|
||||
|
||||
async function freigabelink(nachricht) {
|
||||
const c = await client();
|
||||
const url = await c.shareLink(nachricht.id, nachricht.tage,
|
||||
nachricht.variante);
|
||||
if (!url) throw new PaperlessError(t("errNoLink"));
|
||||
|
||||
// Kennzeichnung setzen. Fehler werden NICHT verschluckt: Der Tag ist der
|
||||
// Grund, warum sich geteilte Dokumente später wiederfinden lassen - ein
|
||||
// stiller Fehlschlag wäre schlimmer als eine Meldung.
|
||||
let tagStatus = null;
|
||||
if (nachricht.tagName) {
|
||||
try {
|
||||
const name = String(nachricht.tagName).trim();
|
||||
const tags = await c.tags();
|
||||
let treffer = tags.find(
|
||||
(x) => String(x.name).trim().toLowerCase() === name.toLowerCase());
|
||||
if (!treffer) treffer = await c.create("tags/", name);
|
||||
if (!treffer || treffer.id === undefined) {
|
||||
throw new PaperlessError(
|
||||
`Tag "${name}" ohne Kennung: ${JSON.stringify(treffer)}`);
|
||||
}
|
||||
const doc = await c.document(nachricht.id);
|
||||
const vorhanden = (doc.tags || []).map(Number);
|
||||
if (!vorhanden.includes(Number(treffer.id))) {
|
||||
await c.patchDocument(
|
||||
nachricht.id, { tags: [...vorhanden, Number(treffer.id)] });
|
||||
}
|
||||
tagStatus = { ok: true, name };
|
||||
} catch (err) {
|
||||
tagStatus = { ok: false, fehler: String(err?.message || err) };
|
||||
console.error("[paperless] Tag konnte nicht gesetzt werden", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Ablaufdatum selbst rechnen. Es steht auch in der Antwort, aber nicht
|
||||
// in jeder Version unter demselben Namen - die eigene Rechnung stimmt
|
||||
// mit dem überein, was im Dialog eingestellt wurde.
|
||||
const gueltigBis = nachricht.tage
|
||||
? new Date(Date.now() + Number(nachricht.tage) * 86400000)
|
||||
: null;
|
||||
|
||||
if (nachricht.tabId !== undefined && nachricht.einfuegen) {
|
||||
await linkEinfuegen(nachricht.tabId, url, nachricht.linktext,
|
||||
gueltigBis);
|
||||
}
|
||||
return {
|
||||
url,
|
||||
tagStatus,
|
||||
gueltigBis: gueltigBis ? gueltigBis.toISOString() : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Datum als TT.MM.JJJJ.
|
||||
*
|
||||
* Bewusst kein toLocaleDateString: Die Nachricht geht an einen Empfänger
|
||||
* mit unbekannter Spracheinstellung. 03.09. und 09.03. sehen gleich aus
|
||||
* und bedeuten Verschiedenes - bei einem Ablaufdatum ist das der falsche
|
||||
* Ort für Mehrdeutigkeit.
|
||||
*/
|
||||
function alsDeutschesDatum(d) {
|
||||
const z = (n) => String(n).padStart(2, "0");
|
||||
return `${z(d.getDate())}.${z(d.getMonth() + 1)}.${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Freigabelink in einen Entwurf einfügen.
|
||||
*
|
||||
* ACHTUNG: Die Compose-Schnittstelle kennt kein Einfügen an der
|
||||
* Schreibmarke. Der Körper muss vollständig gelesen und zurückgeschrieben
|
||||
* werden. Der Eintrag landet deshalb am Ende, bei HTML vor dem
|
||||
* schliessenden </body>.
|
||||
*
|
||||
* Name UND Adresse stehen sichtbar. Ein Link, dessen Ziel man nicht sieht,
|
||||
* ist beim Empfänger schwer einzuschätzen - erst recht bei einer Adresse,
|
||||
* die ohne Anmeldung Zugriff gewährt.
|
||||
*/
|
||||
async function linkEinfuegen(tabId, url, linktext, gueltigBis) {
|
||||
const details = await browser.compose.getComposeDetails(tabId);
|
||||
const name = String(linktext || "").trim();
|
||||
const hinweis = gueltigBis
|
||||
? t("linkValidUntil", [alsDeutschesDatum(gueltigBis)])
|
||||
: t("linkValidUnlimited");
|
||||
|
||||
if (details.isPlainText) {
|
||||
const koerper = details.plainTextBody || "";
|
||||
const zeilen = [name, url, hinweis].filter(Boolean);
|
||||
await browser.compose.setComposeDetails(tabId, {
|
||||
plainTextBody: `${koerper}\n\n${zeilen.join("\n")}\n`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const koerper = details.body || "";
|
||||
const sicherUrl = escapeHtml(url);
|
||||
const kopf = name ? `<strong>${escapeHtml(name)}</strong><br>` : "";
|
||||
const block =
|
||||
`<p style="margin:0 0 .8em 0">${kopf}`
|
||||
+ `<a href="${sicherUrl}">${sicherUrl}</a><br>`
|
||||
+ `<span style="font-size:90%;color:#666">${escapeHtml(hinweis)}</span>`
|
||||
+ `</p>`;
|
||||
|
||||
const neuerKoerper = koerper.includes("</body>")
|
||||
? koerper.replace("</body>", `${block}</body>`)
|
||||
: koerper + block;
|
||||
await browser.compose.setComposeDetails(tabId, { body: neuerKoerper });
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s).replace(/[&<>"']/g, (c) => ({
|
||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
||||
}[c]));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- Meldungen
|
||||
|
||||
async function melde(err) {
|
||||
const text = String(err?.message || err);
|
||||
// Immer auch auf die Konsole. Benachrichtigungen können abgeschaltet
|
||||
// sein, und dann bliebe ein Fehler völlig unsichtbar.
|
||||
console.error("[paperless]", err);
|
||||
try {
|
||||
await browser.notifications.create({
|
||||
type: "basic",
|
||||
iconUrl: browser.runtime.getURL("icons/paperless-logo_64.png"),
|
||||
title: "Paperless",
|
||||
message: text.slice(0, 300)
|
||||
});
|
||||
} catch {
|
||||
console.error("[paperless]", err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user