diff --git a/_locales/de/messages.json b/_locales/de/messages.json index c7b8c9f..edfbea2 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -308,5 +308,17 @@ }, "marginBottom": { "message": "unten" + }, + "artOriginal": { + "message": "Original ablegen" + }, + "artPdf": { + "message": "PDF aufbereiten" + }, + "artMessagePdf": { + "message": "als PDF aufbereiten" + }, + "artPdfUnavailable": { + "message": "Für dieses Format nicht möglich - nur Textdateien lassen sich aufbereiten." } } diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7f63074..4e90796 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -308,5 +308,17 @@ }, "marginBottom": { "message": "bottom" + }, + "artOriginal": { + "message": "store original" + }, + "artPdf": { + "message": "prepare as PDF" + }, + "artMessagePdf": { + "message": "prepare as PDF" + }, + "artPdfUnavailable": { + "message": "Not possible for this format - only text files can be prepared." } } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 011b29c..97d1515 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -308,5 +308,17 @@ }, "marginBottom": { "message": "bas" + }, + "artOriginal": { + "message": "enregistrer l'original" + }, + "artPdf": { + "message": "préparer en PDF" + }, + "artMessagePdf": { + "message": "préparer en PDF" + }, + "artPdfUnavailable": { + "message": "Impossible pour ce format - seuls les fichiers texte peuvent être préparés." } } diff --git a/background.js b/background.js index 3d87b39..324ba2c 100644 --- a/background.js +++ b/background.js @@ -16,7 +16,8 @@ import { ladeEinstellungen, vollstaendig, sichererName, versionslabel, titelAusBetreff, zugriffsmuster, t } from "./lib/settings.js"; -import { baueMailPdf, alsEmpfaenger } from "./lib/mailpdf.js"; +import { baueMailPdf, baueTextPdf, alsEmpfaenger, umwandelbar } + from "./lib/mailpdf.js"; /** * Ergebnis einer Nachrichten-Abfrage vereinheitlichen. @@ -684,11 +685,75 @@ async function datenHolen(auftrag, s) { if (!d.ausgewaehlt) continue; const datei = await browser.messages.getAttachmentFile( d.messageId, d.partName); + + // Aufbereiten nur, wenn der Anwender es gewählt hat UND das Format es + // hergibt. Eine Rechnung als PDF ist bereits das Dokument; sie durch + // eine Textfassung zu ersetzen wäre Unfug. + if (d.alsPdf && umwandelbar(d.contentType, d.name)) { + try { + out.push(await anhangAlsPdf(datei, d, s)); + continue; + } catch (err) { + console.error("[paperless] Anhang konnte nicht aufbereitet werden", + d.name, err); + // Nicht abbrechen: lieber das Original ablegen als gar nichts. + } + } out.push({ blob: datei, name: sichererName(d.name) }); } return out; } +/** + * Einen Textanhang als PDF aufbereiten, mit der Ursprungsdatei darin. + * + * Sinnvoll etwa bei einem HTML-Anhang, den im Archiv sonst niemand + * ansieht. Für Formate ohne Textinhalt greift diese Funktion nicht - + * das prüft umwandelbar() vorher. + */ +async function anhangAlsPdf(datei, beschreibung, s) { + const roh = new Uint8Array(await datei.arrayBuffer()); + let text = new TextDecoder("utf-8", { fatal: false }).decode(roh); + + const istHtml = /^text\/html$/i.test(beschreibung.contentType || "") + || /\.(html?|xhtml)$/i.test(beschreibung.name || "") + || /<\s*html[\s>]/i.test(text.slice(0, 2000)); + + if (istHtml && browser.messengerUtilities?.convertToPlainText) { + try { + text = await browser.messengerUtilities.convertToPlainText(text); + } catch { /* dann bleibt der Rohtext */ } + } + + const basis = String(beschreibung.name || "Dokument") + .replace(/\.[^.]{1,8}$/, ""); + + const pdf = baueTextPdf({ + titel: basis, + text, + dateien: [{ + name: sichererName(beschreibung.name || "original"), + daten: roh, + mime: beschreibung.contentType || "application/octet-stream", + beschreibung: "Originaldatei" + }] + }, { + 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(`${basis}.pdf`) + }; +} + async function ablegen(nachricht) { const c = await client(); const s = await ladeEinstellungen(); diff --git a/lib/mailpdf.js b/lib/mailpdf.js index 005225b..9865942 100644 --- a/lib/mailpdf.js +++ b/lib/mailpdf.js @@ -190,6 +190,79 @@ function kopfblock(d, daten, groesse) { d.y = oben - hoehe; } +/** + * Beliebigen Text als PDF setzen, mit der Ursprungsdatei als Anhang. + * + * Für Anhänge, die Text enthalten - HTML, Nur-Text, eingebettete + * Nachrichten. Formate wie DOCX oder XLSX lassen sich hier NICHT + * umwandeln; dafür bräuchte es eine Darstellungsmaschine. + */ +export function baueTextPdf(daten, opt = {}) { + const basis = begrenzen(Number(opt.schrift) || 11, 7, 16); + const papier = FORMATE[String(opt.format || "a4").toLowerCase()] + || FORMATE.a4; + const r = opt.rand || {}; + const grenze = (wert, vorgabe) => + Math.min(papier.breite / 3, Math.max(0, Number(wert) ?? vorgabe)); + + const d = new PdfDokument({ + titel: daten.titel || "Dokument", + breite: mm(papier.breite), + hoehe: mm(papier.hoehe), + randLinks: mm(grenze(r.links ?? 20, 20)), + randRechts: mm(grenze(r.rechts ?? 10, 10)), + randOben: mm(grenze(r.oben ?? 10, 10)), + randUnten: mm(grenze(r.unten ?? 10, 10)) + }); + d.neueSeite(); + + d.absatz(daten.titel || "Dokument", + { groesse: basis + 2, fett: true, abstand: 1.25 }); + d.abstand(6); + d.linie(); + d.abstand(8); + + const text = String(daten.text ?? "").replace(/\r\n?/g, "\n").trimEnd(); + if (text) { + for (const zeile of text.split("\n")) { + d.absatz(zeile || " ", { groesse: basis, abstand: 1.35 }); + } + } else { + d.absatz("(kein Textinhalt - siehe eingebettete Originaldatei)", + { groesse: basis, farbe: GRAU_TEXT }); + } + + d.abstand(10); + d.linie(); + d.absatz("Aus der Originaldatei erzeugt, die diesem PDF beiliegt.", + { groesse: Math.max(6.5, basis - 3), farbe: GRAU_TEXT }); + + for (const f of daten.dateien ?? []) { + d.anhang(f.name, f.daten, { + beschreibung: f.beschreibung || "", + mime: f.mime || "application/octet-stream" + }); + } + return d.bauen(); +} + +/** + * Lässt sich diese Datei als PDF aufbereiten? + * + * Nur was Text enthält. Bei DOCX oder XLSX müsste die Erweiterung das + * Format auswerten und setzen - dafür gibt es keine Grundlage. + */ +export function umwandelbar(mime, name = "") { + const m = String(mime || "").toLowerCase(); + if (/^text\//.test(m)) return true; + if (m === "message/rfc822") return true; + if (["application/json", "application/xml", "application/xhtml+xml", + "application/rtf"].includes(m)) return true; + // Manche Server melden alles als octet-stream. Dann entscheidet die + // Endung. + return /\.(txt|htm|html|xml|json|csv|md|log|eml)$/i.test(String(name)); +} + function begrenzen(wert, min, max) { return Math.min(max, Math.max(min, wert)); } diff --git a/manifest.json b/manifest.json index 006c631..e510475 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "description": "__MSG_extDescription__", - "version": "1.11.3", + "version": "1.12.0", "author": "Name des Herausgebers", "homepage_url": "https://example.org/paperless-thunderbird", "default_locale": "de", diff --git a/ui/gemeinsam.css b/ui/gemeinsam.css index 9ae33ab..8c29060 100644 --- a/ui/gemeinsam.css +++ b/ui/gemeinsam.css @@ -7,6 +7,10 @@ color-scheme: light dark; } * { box-sizing: border-box; } +/* Muss VOR den Anzeigearten stehen und !important tragen: Eine Regel wie + ".wahl { display: flex }" überstimmt sonst das eingebaute + "[hidden] { display: none }", und ausgeblendete Elemente erscheinen. */ +[hidden] { display: none !important; } body { font: message-box; font-size: 13px; @@ -43,6 +47,17 @@ input[type=text], input[type=password], input[type=number], select { .mass input { width: 68px; } .zeile > input[type=text] { flex: 1; } .wahl { display: flex; gap: 6px; align-items: center; margin: 3px 0; } + +/* Dateiliste mit Wahl der Ablageart je Datei. */ +.dateizeile { + display: flex; align-items: center; gap: 10px; + padding: 3px 0; flex-wrap: wrap; +} +.dateizeile .name { flex: 1; min-width: 200px; } +.dateizeile .art { display: flex; gap: 12px; align-items: center; } +.dateizeile .art label { margin: 0; white-space: nowrap; font-size: 12px; } +.dateizeile .art input { width: auto; margin: 0; } +.dateizeile .art label.aus { color: GrayText; } .wahl input { width: auto; margin: 0; } button { padding: 5px 14px; diff --git a/ui/store.html b/ui/store.html index 462ce12..0976896 100644 --- a/ui/store.html +++ b/ui/store.html @@ -13,12 +13,6 @@