214 lines
6.7 KiB
JavaScript
214 lines
6.7 KiB
JavaScript
/**
|
|
* 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);
|
|
}
|