Übergabe von E-Mails als PDF mit eingebetteten Anlagen
This commit is contained in:
287
background.js
287
background.js
@@ -16,6 +16,7 @@ import {
|
||||
ladeEinstellungen, vollstaendig, sichererName, versionslabel,
|
||||
titelAusBetreff, zugriffsmuster, t
|
||||
} from "./lib/settings.js";
|
||||
import { baueMailPdf, alsEmpfaenger } from "./lib/mailpdf.js";
|
||||
|
||||
/**
|
||||
* Ergebnis einer Nachrichten-Abfrage vereinheitlichen.
|
||||
@@ -90,6 +91,16 @@ function alsDatum(wert) {
|
||||
return Number.isNaN(d.getTime()) ? undefined : d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** Datum mit Uhrzeit für die Anzeige im PDF. */
|
||||
function alsZeitpunkt(wert) {
|
||||
if (!wert) return "";
|
||||
const d = wert instanceof Date ? wert : new Date(wert);
|
||||
if (Number.isNaN(d.getTime())) return "";
|
||||
const z = (n) => String(n).padStart(2, "0");
|
||||
return `${z(d.getDate())}.${z(d.getMonth() + 1)}.${d.getFullYear()} `
|
||||
+ `${z(d.getHours())}:${z(d.getMinutes())} Uhr`;
|
||||
}
|
||||
|
||||
// Beim Laden eine Zeile ausgeben. Fehlt sie in der Konsole, ist das
|
||||
// Skript gar nicht erst gestartet - und dann kommt bei jedem Aufruf aus
|
||||
// einem Dialog nur "An unexpected error occurred".
|
||||
@@ -196,6 +207,7 @@ async function anhangAblegen(info, tab) {
|
||||
betreff: nachricht.subject,
|
||||
absender: nachricht.author,
|
||||
datum: alsDatum(nachricht.date),
|
||||
zeitpunkt: alsZeitpunkt(nachricht.date),
|
||||
dateien
|
||||
});
|
||||
}
|
||||
@@ -217,6 +229,7 @@ async function nachrichtAblegen(info, tab) {
|
||||
betreff: nachricht.subject,
|
||||
absender: nachricht.author,
|
||||
datum: alsDatum(nachricht.date),
|
||||
zeitpunkt: alsZeitpunkt(nachricht.date),
|
||||
dateien: anhaenge.map((a) => ({
|
||||
name: sichererName(a.name),
|
||||
size: a.size,
|
||||
@@ -400,14 +413,272 @@ async function berechtigungPruefen() {
|
||||
|
||||
// -------------------------------------------------------------- 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 }];
|
||||
/**
|
||||
* Nachrichtentext als reinen Text beschaffen.
|
||||
*
|
||||
* Bevorzugt wird der text/plain-Teil. Gibt es nur HTML, übernimmt
|
||||
* Thunderbird die Umwandlung - convertToPlainText ist dieselbe Funktion,
|
||||
* die beim Antworten in Nur-Text greift, und liefert deutlich bessere
|
||||
* Ergebnisse als eine selbstgebaute Entschlackung.
|
||||
*/
|
||||
async function nachrichtentext(messageId) {
|
||||
let teile = [];
|
||||
if (browser.messages.listInlineTextParts) {
|
||||
try {
|
||||
teile = await browser.messages.listInlineTextParts(messageId) || [];
|
||||
} catch { /* ältere Stände kennen die Funktion nicht */ }
|
||||
}
|
||||
|
||||
// Rückfall: die Baumstruktur aus getFull durchwandern
|
||||
if (!teile.length) {
|
||||
try {
|
||||
const voll = await browser.messages.getFull(messageId);
|
||||
const sammeln = (teil) => {
|
||||
if (teil?.body && /^text\/(plain|html)$/.test(teil.contentType || "")) {
|
||||
teile.push({ contentType: teil.contentType, content: teil.body });
|
||||
}
|
||||
for (const k of teil?.parts || []) sammeln(k);
|
||||
};
|
||||
sammeln(voll);
|
||||
} catch (err) {
|
||||
console.warn("[paperless] getFull fehlgeschlagen", err);
|
||||
}
|
||||
}
|
||||
|
||||
const klartext = teile.find((x) => (x.contentType || "") === "text/plain");
|
||||
if (klartext?.content?.trim()) return klartext.content;
|
||||
|
||||
const html = teile.find((x) => (x.contentType || "") === "text/html");
|
||||
if (html?.content) {
|
||||
if (browser.messengerUtilities?.convertToPlainText) {
|
||||
try {
|
||||
return await browser.messengerUtilities.convertToPlainText(
|
||||
html.content);
|
||||
} catch { /* dann der grobe Weg */ }
|
||||
}
|
||||
// Notbehelf ohne die Thunderbird-Funktion. Bewusst schlicht: Blöcke
|
||||
// werden zu Zeilenumbrüchen, Auszeichnung fällt weg.
|
||||
return html.content
|
||||
.replace(/<\s*(br|\/p|\/div|\/tr|\/li|\/h[1-6])[^>]*>/gi, "\n")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/ /gi, " ").replace(/&/gi, "&")
|
||||
.replace(/</gi, "<").replace(/>/gi, ">")
|
||||
.replace(/"/gi, '"').replace(/'/gi, "'")
|
||||
.replace(/\n{3,}/g, "\n\n");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Nachricht als PDF aufbereiten.
|
||||
*
|
||||
* Das Original und alle Anhänge werden im PDF eingebettet - es geht also
|
||||
* nichts verloren, auch wenn die Darstellung nicht originalgetreu ist.
|
||||
*/
|
||||
/**
|
||||
* Adresse des Kontos, in dem die Nachricht liegt.
|
||||
*
|
||||
* Bei Nachrichten an Blindkopie-Empfänger geht aus den Kopfzeilen nicht
|
||||
* hervor, welches Konto sie empfangen hat. Im Archiv ist das später die
|
||||
* entscheidende Angabe.
|
||||
*/
|
||||
/**
|
||||
* Adresse des Kontos, in dem die Nachricht liegt.
|
||||
*
|
||||
* Bei Nachrichten an Blindkopie-Empfänger geht aus An und Kopie nicht
|
||||
* hervor, welches Konto sie empfangen hat. Im Archiv ist das später die
|
||||
* entscheidende Angabe.
|
||||
*
|
||||
* Zwei Wege, weil der erste eine Berechtigung braucht:
|
||||
*
|
||||
* 1. accounts-Schnittstelle. Genau, aber accountsRead ist nötig - und
|
||||
* dieselbe Berechtigung entscheidet auch darüber, ob messages.get()
|
||||
* überhaupt eine Ordnerangabe liefert.
|
||||
* 2. Zustellkopfzeilen. Delivered-To und Verwandte tragen die Adresse,
|
||||
* an die tatsächlich zugestellt wurde. Sie sind nicht immer
|
||||
* vorhanden und stammen vom zustellenden Server, reichen aber ohne
|
||||
* zusätzliche Berechtigung aus.
|
||||
*/
|
||||
async function kontoAdresse(kopf, messageId) {
|
||||
const ueberAccounts = await kontoUeberAccounts(kopf);
|
||||
if (ueberAccounts) return ueberAccounts;
|
||||
return kontoUeberKopfzeilen(messageId);
|
||||
}
|
||||
|
||||
async function kontoUeberAccounts(kopf) {
|
||||
if (!browser.accounts?.get) {
|
||||
console.debug("[paperless] accounts-Schnittstelle nicht verfügbar - "
|
||||
+ "Berechtigung accountsRead fehlt. Ausweichweg über "
|
||||
+ "die Zustellkopfzeilen.");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Die Kontokennung steckt je nach Version an verschiedenen Stellen.
|
||||
const kandidaten = [
|
||||
kopf?.folder?.accountId,
|
||||
kopf?.folder?.id,
|
||||
typeof kopf?.folder === "string" ? kopf.folder : null,
|
||||
kopf?.accountId
|
||||
].filter(Boolean);
|
||||
|
||||
const kennungen = [];
|
||||
for (const k of kandidaten) {
|
||||
kennungen.push(String(k));
|
||||
const m = /^([^:/]+)/.exec(String(k));
|
||||
if (m && m[1] !== String(k)) kennungen.push(m[1]);
|
||||
}
|
||||
|
||||
for (const kennung of kennungen) {
|
||||
try {
|
||||
const konto = await browser.accounts.get(kennung);
|
||||
const adresse = adresseAusKonto(konto);
|
||||
if (adresse) return adresse;
|
||||
} catch { /* nächster Kandidat */ }
|
||||
}
|
||||
|
||||
// Gibt es nur ein Konto, ist die Zuordnung ohnehin eindeutig.
|
||||
try {
|
||||
const alle = await browser.accounts.list(false);
|
||||
if (alle?.length === 1) return adresseAusKonto(alle[0]);
|
||||
} catch { /* dann eben nicht */ }
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function adresseAusKonto(konto) {
|
||||
if (!konto) return "";
|
||||
const ident = konto.identities || [];
|
||||
const erste = ident.find((i) => i.email) || ident[0];
|
||||
if (erste?.email) {
|
||||
return erste.name ? `${erste.name} <${erste.email}>` : erste.email;
|
||||
}
|
||||
return konto.name || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Empfangsadresse aus den Zustellkopfzeilen.
|
||||
*
|
||||
* Diese Kopfzeilen setzt der zustellende Server. Delivered-To ist die
|
||||
* verbreitetste; X-Original-To und Envelope-To kommen bei anderen
|
||||
* Servern vor.
|
||||
*/
|
||||
async function kontoUeberKopfzeilen(messageId) {
|
||||
if (!messageId || !browser.messages?.getFull) return "";
|
||||
try {
|
||||
const voll = await browser.messages.getFull(messageId);
|
||||
const h = voll?.headers || {};
|
||||
for (const name of ["delivered-to", "x-original-to", "envelope-to",
|
||||
"x-envelope-to", "x-rcpt-to"]) {
|
||||
const wert = h[name];
|
||||
if (Array.isArray(wert) && wert.length && String(wert[0]).trim()) {
|
||||
return String(wert[0]).trim();
|
||||
}
|
||||
if (typeof wert === "string" && wert.trim()) return wert.trim();
|
||||
}
|
||||
console.debug("[paperless] keine Zustellkopfzeile gefunden. Vorhanden:",
|
||||
Object.keys(h).join(", "));
|
||||
} catch (err) {
|
||||
console.debug("[paperless] Kopfzeilen nicht lesbar:", err?.message);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function nachrichtAlsPdf(auftrag, s) {
|
||||
const id = auftrag.messageId;
|
||||
const kopf = await browser.messages.get(id).catch(() => ({}));
|
||||
const text = await nachrichtentext(id);
|
||||
const konto = await kontoAdresse(kopf, id);
|
||||
|
||||
const dateien = [];
|
||||
const aufstellung = [];
|
||||
|
||||
// Das Original zuerst - es ist der wichtigste Anhang.
|
||||
try {
|
||||
const roh = await browser.messages.getRaw(id);
|
||||
dateien.push({
|
||||
name: sichererName(`${titelAusBetreff(auftrag.betreff)}.eml`),
|
||||
daten: new TextEncoder().encode(roh),
|
||||
mime: "message/rfc822",
|
||||
beschreibung: "Originalnachricht"
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("[paperless] getRaw fehlgeschlagen", err);
|
||||
}
|
||||
|
||||
// Dann die Anhänge, sofern gewünscht.
|
||||
if (s.pdfAnhaengeEinbetten !== false) {
|
||||
for (const a of auftrag.dateien || []) {
|
||||
aufstellung.push({ name: a.name, groesse: a.size });
|
||||
try {
|
||||
const datei = await browser.messages.getAttachmentFile(
|
||||
a.messageId, a.partName);
|
||||
dateien.push({
|
||||
name: sichererName(a.name),
|
||||
daten: new Uint8Array(await datei.arrayBuffer()),
|
||||
mime: a.contentType || "application/octet-stream",
|
||||
beschreibung: "Anhang der Nachricht"
|
||||
});
|
||||
} catch (err) {
|
||||
console.warn("[paperless] Anhang nicht lesbar:", a.name, err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const a of auftrag.dateien || []) {
|
||||
aufstellung.push({ name: a.name, groesse: a.size });
|
||||
}
|
||||
}
|
||||
|
||||
const pdf = baueMailPdf({
|
||||
betreff: auftrag.betreff || kopf.subject || "",
|
||||
konto,
|
||||
von: kopf.author || auftrag.absender || "",
|
||||
an: alsEmpfaenger(kopf.recipients),
|
||||
kopie: alsEmpfaenger(kopf.ccList),
|
||||
datum: auftrag.zeitpunkt || auftrag.datum || "",
|
||||
text,
|
||||
anhaenge: aufstellung,
|
||||
dateien
|
||||
}, {
|
||||
schrift: Number(s.pdfSchriftgroesse) || 11,
|
||||
format: s.pdfFormat || "a4",
|
||||
rand: {
|
||||
links: Number(s.pdfRandLinks ?? 20),
|
||||
rechts: Number(s.pdfRandRechts ?? 10),
|
||||
oben: Number(s.pdfRandOben ?? 10),
|
||||
unten: Number(s.pdfRandUnten ?? 10)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
blob: new Blob([pdf], { type: "application/pdf" }),
|
||||
name: sichererName(`${titelAusBetreff(auftrag.betreff)}.pdf`)
|
||||
};
|
||||
}
|
||||
|
||||
async function datenHolen(auftrag, s) {
|
||||
if (auftrag.quelle === "nachricht") {
|
||||
// Als PDF aufbereiten oder im Rohformat ablegen - beides hat seine
|
||||
// Berechtigung. Das Rohformat ist verlustfrei und wieder in ein
|
||||
// Mailprogramm einlesbar, das PDF dafür lesbar und durchsuchbar.
|
||||
// Die Auswahl aus dem Dialog hat Vorrang vor der Voreinstellung.
|
||||
const alsPdf = auftrag.alsPdf !== undefined
|
||||
? Boolean(auftrag.alsPdf)
|
||||
: (s.mailFormat || "pdf") === "pdf";
|
||||
if (alsPdf) {
|
||||
try {
|
||||
return [await nachrichtAlsPdf(auftrag, s)];
|
||||
} catch (err) {
|
||||
console.error("[paperless] PDF-Aufbereitung gescheitert", err);
|
||||
// Nicht abbrechen: lieber die Rohfassung ablegen als gar nichts.
|
||||
}
|
||||
}
|
||||
const roh = await browser.messages.getRaw(auftrag.messageId);
|
||||
return [{
|
||||
blob: new Blob([roh], { type: "message/rfc822" }),
|
||||
name: sichererName(`${titelAusBetreff(auftrag.betreff)}.eml`)
|
||||
}];
|
||||
}
|
||||
|
||||
const out = [];
|
||||
for (const d of auftrag.dateien || []) {
|
||||
if (!d.ausgewaehlt) continue;
|
||||
@@ -421,7 +692,7 @@ async function datenHolen(auftrag) {
|
||||
async function ablegen(nachricht) {
|
||||
const c = await client();
|
||||
const s = await ladeEinstellungen();
|
||||
const dateien = await datenHolen(nachricht.auftrag);
|
||||
const dateien = await datenHolen(nachricht.auftrag, s);
|
||||
if (!dateien.length) throw new PaperlessError(t("errNothingSelected"));
|
||||
|
||||
const ergebnisse = [];
|
||||
|
||||
Reference in New Issue
Block a user