// Minimaler, abhängigkeitsfreier PDF-Generator. // Rendert die E-Mail (Kopf + Klartext-Body) als einfaches Text-PDF. // Bewusst schlicht (v1): kein HTML-Rendering, keine Bilder — dafür keine // externen Libraries. Reicht für eine lesbare Aktenkopie der Mail. const Pdf = { /** * Erzeugt ein PDF-File aus den Mail-Metadaten. * @param {object} meta Ergebnis von Mail.getMeta() * @returns {File} */ fromMail(meta) { const header = [ `Von: ${meta.senderName ? meta.senderName + " <" + meta.senderEmail + ">" : meta.senderEmail}`, `An: ${meta.to}`, meta.cc ? `CC: ${meta.cc}` : null, `Datum: ${this._fmtDate(meta.date)}`, `Betreff: ${meta.subject}`, "", "----------------------------------------------------------------------", "", ].filter((l) => l !== null); const lines = header.concat(this._wrap(meta.bodyText || "", 95)); const bytes = this._build(lines); const name = `${(meta.subject || "email").replace(/[\\/:*?"<>|]+/g, "_").slice(0, 80) || "email"}.pdf`; return new File([bytes], name, { type: "application/pdf" }); }, // --- PDF-Aufbau ----------------------------------------------------------- _build(allLines) { const PAGE_W = 595, PAGE_H = 842; // A4 in pt const MARGIN = 50, FONT = 10, LEADING = 14; const linesPerPage = Math.floor((PAGE_H - 2 * MARGIN) / LEADING); // Seiten aufteilen const pages = []; for (let i = 0; i < allLines.length; i += linesPerPage) { pages.push(allLines.slice(i, i + linesPerPage)); } if (pages.length === 0) pages.push([""]); const objects = []; // {id, body} body als latin1-String const addObj = (body) => { objects.push(body); return objects.length; }; // Platzhalter-IDs vorab festlegen // 1 Catalog, 2 Pages, 3 Font, dann je Seite: PageObj + ContentObj const catalogId = 1, pagesId = 2, fontId = 3; objects.length = 0; objects.push("", "", ""); // reserviere 1..3 const pageIds = []; const contentIds = []; pages.forEach((pageLines) => { const stream = this._contentStream(pageLines, MARGIN, PAGE_H - MARGIN, FONT, LEADING); const contentId = addObj( `<< /Length ${stream.length} >>\nstream\n${stream}\nendstream` ); const pageId = addObj( `<< /Type /Page /Parent ${pagesId} 0 R ` + `/MediaBox [0 0 ${PAGE_W} ${PAGE_H}] ` + `/Resources << /Font << /F1 ${fontId} 0 R >> >> ` + `/Contents ${contentId} 0 R >>` ); pageIds.push(pageId); contentIds.push(contentId); }); objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`; objects[pagesId - 1] = `<< /Type /Pages /Count ${pageIds.length} ` + `/Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] >>`; objects[fontId - 1] = `<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>`; // Serialisieren mit xref let pdf = "%PDF-1.4\n"; const offsets = []; objects.forEach((body, idx) => { offsets[idx] = pdf.length; pdf += `${idx + 1} 0 obj\n${body}\nendobj\n`; }); const xrefOffset = pdf.length; pdf += `xref\n0 ${objects.length + 1}\n`; pdf += "0000000000 65535 f \n"; offsets.forEach((off) => { pdf += `${String(off).padStart(10, "0")} 00000 n \n`; }); pdf += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\n` + `startxref\n${xrefOffset}\n%%EOF`; return this._latin1Bytes(pdf); }, _contentStream(lines, x, yTop, font, leading) { let s = `BT\n/F1 ${font} Tf\n${leading} TL\n${x} ${yTop} Td\n`; lines.forEach((line, i) => { if (i > 0) s += "T*\n"; s += `(${this._escape(line)}) Tj\n`; }); s += "ET"; return s; }, _wrap(text, width) { const out = []; (text || "").split(/\r?\n/).forEach((para) => { if (para.length <= width) { out.push(para); return; } let line = ""; para.split(/\s+/).forEach((word) => { if ((line + " " + word).trim().length > width) { if (line) out.push(line); // sehr lange Wörter hart umbrechen while (word.length > width) { out.push(word.slice(0, width)); word = word.slice(width); } line = word; } else { line = line ? `${line} ${word}` : word; } }); if (line) out.push(line); }); return out; }, _escape(s) { return this._toLatin1(s).replace(/[\\()]/g, "\\$&"); }, _toLatin1(s) { // Zeichen außerhalb Latin-1 durch '?' ersetzen. return (s || "").replace(/[^\x00-\xFF]/g, "?"); }, _latin1Bytes(str) { const bytes = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) bytes[i] = str.charCodeAt(i) & 0xff; return bytes; }, _fmtDate(d) { try { return new Intl.DateTimeFormat("de-DE", { dateStyle: "medium", timeStyle: "short", }).format(d); } catch (_) { return String(d); } }, }; if (typeof module !== "undefined") module.exports = { Pdf };