save email as pdf likae attachment version 2

This commit is contained in:
2026-02-04 19:49:09 +01:00
parent d98c97a81f
commit 2d052c76d9
21 changed files with 1143 additions and 151 deletions
+161 -140
View File
@@ -1,153 +1,174 @@
import puppeteer from 'puppeteer';
// ==================== PDF SERVICE ====================
import PDFDocument from 'pdfkit';
interface EmailData {
from: string;
to: string;
cc?: string;
subject: string;
date: Date;
bodyText?: string;
bodyHtml?: string;
}
/**
* Konvertiert HTML zu PDF mit Puppeteer
* Generiert ein PDF aus einer E-Mail
*/
export async function htmlToPdf(html: string): Promise<Buffer> {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
export async function generateEmailPdf(email: EmailData): Promise<Buffer> {
return new Promise((resolve, reject) => {
try {
const doc = new PDFDocument({
size: 'A4',
margins: { top: 50, bottom: 50, left: 50, right: 50 },
});
const chunks: Buffer[] = [];
doc.on('data', (chunk) => chunks.push(chunk));
doc.on('end', () => resolve(Buffer.concat(chunks)));
doc.on('error', reject);
// Header
doc
.fontSize(18)
.font('Helvetica-Bold')
.text('E-Mail', { align: 'center' });
doc.moveDown(1.5);
// Metadaten-Tabelle
doc.fontSize(10).font('Helvetica');
// Von
doc
.font('Helvetica-Bold')
.text('Von: ', { continued: true })
.font('Helvetica')
.text(email.from);
// An
doc
.font('Helvetica-Bold')
.text('An: ', { continued: true })
.font('Helvetica')
.text(email.to);
// CC (falls vorhanden)
if (email.cc) {
doc
.font('Helvetica-Bold')
.text('CC: ', { continued: true })
.font('Helvetica')
.text(email.cc);
}
// Datum
const formattedDate = email.date.toLocaleString('de-DE', {
dateStyle: 'full',
timeStyle: 'short',
});
doc
.font('Helvetica-Bold')
.text('Datum: ', { continued: true })
.font('Helvetica')
.text(formattedDate);
// Betreff
doc
.font('Helvetica-Bold')
.text('Betreff: ', { continued: true })
.font('Helvetica')
.text(email.subject || '(Kein Betreff)');
doc.moveDown(1);
// Trennlinie
doc
.moveTo(50, doc.y)
.lineTo(doc.page.width - 50, doc.y)
.stroke();
doc.moveDown(1);
// Inhalt
doc.fontSize(11);
// HTML in Text konvertieren (vereinfacht)
let content = email.bodyText || '';
if (!content && email.bodyHtml) {
// Einfache HTML-zu-Text Konvertierung
content = htmlToPlainText(email.bodyHtml);
}
if (!content) {
content = '(Kein Inhalt)';
}
// Text mit Zeilenumbrüchen ausgeben
doc.text(content, {
align: 'left',
lineGap: 2,
});
// Footer mit Erstellungsdatum
const footerY = doc.page.height - 40;
doc
.fontSize(8)
.fillColor('#666666')
.text(
`Exportiert am ${new Date().toLocaleString('de-DE')}`,
50,
footerY,
{ align: 'center', width: doc.page.width - 100 }
);
doc.end();
} catch (error) {
reject(error);
}
});
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdfBuffer = await page.pdf({
format: 'A4',
margin: {
top: '20mm',
right: '15mm',
bottom: '20mm',
left: '15mm',
},
printBackground: true,
});
return Buffer.from(pdfBuffer);
} finally {
await browser.close();
}
}
/**
* Baut ein HTML-Dokument für eine E-Mail mit Header
* Konvertiert HTML in Plain-Text (vereinfacht)
*/
export function buildEmailHtml(email: {
subject?: string | null;
fromAddress: string;
fromName?: string | null;
toAddresses: string;
receivedAt: Date;
htmlBody?: string | null;
textBody?: string | null;
}): string {
const formatDate = (date: Date) => {
return new Date(date).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
function htmlToPlainText(html: string): string {
let text = html;
// To-Adressen parsen (JSON Array)
let toList: string[] = [];
try {
toList = JSON.parse(email.toAddresses);
} catch {
toList = [email.toAddresses];
}
// Zeilenumbrüche vor Block-Elementen
text = text.replace(/<(br|p|div|h[1-6]|li|tr)[^>]*>/gi, '\n');
const fromDisplay = email.fromName
? `${email.fromName} <${email.fromAddress}>`
: email.fromAddress;
// Listen-Elemente
text = text.replace(/<li[^>]*>/gi, '\n• ');
const body = email.htmlBody || `<pre style="white-space: pre-wrap; font-family: inherit;">${email.textBody || ''}</pre>`;
// Links mit URL
text = text.replace(/<a[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi, '$2 ($1)');
return `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<style>
* {
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
font-size: 12pt;
line-height: 1.5;
color: #333;
margin: 0;
padding: 0;
}
.email-header {
background: #f5f5f5;
border-bottom: 2px solid #ddd;
padding: 15px;
margin-bottom: 20px;
}
.email-header h1 {
margin: 0 0 15px 0;
font-size: 16pt;
color: #222;
}
.email-header table {
width: 100%;
border-collapse: collapse;
}
.email-header th {
text-align: left;
width: 60px;
padding: 3px 10px 3px 0;
color: #666;
font-weight: normal;
vertical-align: top;
}
.email-header td {
padding: 3px 0;
}
.email-body {
padding: 0 5px;
}
.email-body img {
max-width: 100%;
height: auto;
}
</style>
</head>
<body>
<div class="email-header">
<h1>${escapeHtml(email.subject || '(Kein Betreff)')}</h1>
<table>
<tr>
<th>Von:</th>
<td>${escapeHtml(fromDisplay)}</td>
</tr>
<tr>
<th>An:</th>
<td>${escapeHtml(toList.join(', '))}</td>
</tr>
<tr>
<th>Datum:</th>
<td>${formatDate(email.receivedAt)}</td>
</tr>
</table>
</div>
<div class="email-body">
${body}
</div>
</body>
</html>
`.trim();
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
// Alle anderen Tags entfernen
text = text.replace(/<[^>]+>/g, '');
// HTML-Entities dekodieren
text = text.replace(/&nbsp;/g, ' ');
text = text.replace(/&amp;/g, '&');
text = text.replace(/&lt;/g, '<');
text = text.replace(/&gt;/g, '>');
text = text.replace(/&quot;/g, '"');
text = text.replace(/&#39;/g, "'");
text = text.replace(/&auml;/g, 'ä');
text = text.replace(/&ouml;/g, 'ö');
text = text.replace(/&uuml;/g, 'ü');
text = text.replace(/&Auml;/g, 'Ä');
text = text.replace(/&Ouml;/g, 'Ö');
text = text.replace(/&Uuml;/g, 'Ü');
text = text.replace(/&szlig;/g, 'ß');
// Mehrfache Leerzeilen reduzieren
text = text.replace(/\n{3,}/g, '\n\n');
// Führende/folgende Leerzeichen entfernen
text = text.trim();
return text;
}