/** * mailpdf.js - E-Mail als lesbares PDF aufbereiten. * * Aufbau: Betreff als Überschrift, darunter ein grau hinterlegter * Kopfblock mit Konto, Absender, Empfängern, Datum und Anlagen, dann der * Nachrichtentext. * * Eingebettet werden die vollständige Originalnachricht als .eml sowie * auf Wunsch alle Anhänge im Original. * * BEWUSSTE EINSCHRÄNKUNG: HTML-Nachrichten werden als Text gesetzt, nicht * originalgetreu dargestellt. Eine Erweiterung hat keine * Darstellungsmaschine für HTML, und Thunderbird bietet Erweiterungen * keine Druckfunktion an - die tabs-Schnittstelle kennt in Manifest V3 * kein saveAsPDF. * * Deshalb steckt das Original mit im PDF. Wer die gestaltete Fassung * braucht, öffnet den Anhang. */ import { PdfDokument, textbreite, umbrechen, mm, FORMATE } from "./pdf.js"; const GRAU_TEXT = [0.35, 0.35, 0.35]; const GRAU_FLAECHE = 0.95; // etwa 5 Prozent const INNEN = 8; // Polster im Kopfblock /** * @param {object} daten * betreff, konto, von, an, kopie, datum Zeichenketten * text Nachrichtentext als Text * anhaenge [{name, groesse}] für die Aufstellung * dateien [{name, daten, mime, beschreibung}] wird eingebettet * @param {object} opt * schrift Grundschriftgröße in Punkten, Vorgabe 11 * format Papierformat: a4, a5, letter, legal * rand {links, rechts, oben, unten} in Millimetern * @returns {Uint8Array} */ export function baueMailPdf(daten, opt = {}) { const basis = begrenzen(Number(opt.schrift) || 11, 7, 16); // Die Überschrift wächst schwächer mit als der Fließtext. Sonst nimmt // sie bei größerer Grundschrift zu viel Raum und läuft über zwei // Zeilen, bevor der Inhalt überhaupt beginnt. const ueberschrift = basis + 2; const klein = Math.max(7, basis - 1.5); const papier = FORMATE[String(opt.format || "a4").toLowerCase()] || FORMATE.a4; const r = opt.rand || {}; // Die Ränder werden begrenzt, damit kein Dokument mit negativer // Textbreite entsteht - eine Zahl aus einem Eingabefeld kann alles // sein, und ein unbrauchbares PDF wäre schwer zu deuten. const grenze = (wert, vorgabe) => Math.min(papier.breite / 3, Math.max(0, Number(wert) ?? vorgabe)); const d = new PdfDokument({ titel: daten.betreff || "E-Mail", autor: daten.von || "", betreff: daten.betreff || "", 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(); // -- Überschrift d.absatz(daten.betreff || "(ohne Betreff)", { groesse: ueberschrift, fett: true, abstand: 1.25 }); d.abstand(6); // -- Kopfblock kopfblock(d, daten, klein); d.abstand(12); // -- Inhalt const text = String(daten.text ?? "").replace(/\r\n?/g, "\n").trimEnd(); if (text) { for (const zeile of text.split("\n")) { // Zitierte Abschnitte grau und eingerückt. Bei langen Verläufen // sieht man so auf einen Blick, was neu ist. const zitat = /^\s*>/.test(zeile); d.absatz(zeile || " ", { groesse: basis, abstand: 1.35, farbe: zitat ? GRAU_TEXT : null, einzug: zitat ? 10 : 0 }); } } else { d.absatz("(kein Textinhalt - siehe eingebettete Originalnachricht)", { groesse: basis, farbe: GRAU_TEXT }); } // -- Fussbereich d.abstand(10); d.linie(); d.absatz("Erzeugt aus der Originalnachricht, die diesem PDF als Datei " + "beiliegt. HTML-Nachrichten sind hier als Text gesetzt; die " + "gestaltete Fassung enthält der Anhang.", { groesse: Math.max(6.5, basis - 3), farbe: GRAU_TEXT, abstand: 1.3 }); for (const f of daten.dateien ?? []) { d.anhang(f.name, f.daten, { beschreibung: f.beschreibung || "", mime: f.mime || "application/octet-stream" }); } return d.bauen(); } /** * Kopfblock mit ausgerichteten Beschriftungen auf grauem Grund. * * Die Höhe muss VOR dem Zeichnen feststehen: Die Fläche wird zuerst * gesetzt, sonst läge sie über dem Text - im Inhaltsstrom gilt die * Reihenfolge. Deshalb wird der Umbruch hier vorweg berechnet und danach * nur noch ausgegeben. */ function kopfblock(d, daten, groesse) { const zeilenhoehe = groesse * 1.45; const felder = []; const nimm = (name, wert) => { if (wert && String(wert).trim()) { felder.push([name, [String(wert).trim()]]); } }; nimm("Konto", daten.konto); nimm("Von", daten.von); nimm("An", daten.an); nimm("Kopie", daten.kopie); nimm("Datum", daten.datum); if (daten.anhaenge?.length) { felder.push(["Anlagen", daten.anhaenge.map((a) => { const g = a.groesse ? ` (${lesbareGroesse(a.groesse)})` : ""; return `- ${a.name}${g}`; })]); } if (!felder.length) return; // Spaltenbreite aus der breitesten Beschriftung. So beginnen alle Werte // in derselben Spalte, ohne dass eine feste Breite geraten werden muss. let spalte = 0; for (const [name] of felder) { spalte = Math.max(spalte, textbreite(name, groesse, true)); } spalte += 14; const wertBreite = d.innenbreite - 2 * INNEN - spalte; // Umbruch vorweg berechnen const ausgabe = []; for (const [name, werte] of felder) { const zeilen = []; for (const w of werte) { for (const z of umbrechen(w, wertBreite, groesse)) zeilen.push(z); } ausgabe.push([name, zeilen]); } const anzahl = ausgabe.reduce((n, [, z]) => n + z.length, 0); const hoehe = anzahl * zeilenhoehe + 2 * INNEN; d.platzPruefen(hoehe); const oben = d.y; d.rechteck(d.rand, oben - hoehe, d.innenbreite, hoehe, GRAU_FLAECHE); let y = oben - INNEN; for (const [name, zeilen] of ausgabe) { for (const [i, zeile] of zeilen.entries()) { const grundlinie = y - groesse; if (i === 0) { d.textAn(d.rand + INNEN, grundlinie, name, { groesse, fett: true, farbe: GRAU_TEXT }); } d.textAn(d.rand + INNEN + spalte, grundlinie, zeile, { groesse }); y -= zeilenhoehe; } } d.y = oben - hoehe; } function begrenzen(wert, min, max) { return Math.min(max, Math.max(min, wert)); } export function lesbareGroesse(bytes) { const b = Number(bytes) || 0; if (b < 1024) return `${b} B`; if (b < 1024 * 1024) return `${(b / 1024).toFixed(0)} KB`; return `${(b / 1024 / 1024).toFixed(1)} MB`; } /** * Empfängerliste in eine Zeile überführen. * * Thunderbird liefert je nach Version ein Feld oder eine Zeichenkette. */ export function alsEmpfaenger(wert) { if (!wert) return ""; if (Array.isArray(wert)) return wert.filter(Boolean).join(", "); return String(wert); }