save email as pdf likae attachment version 2
This commit is contained in:
+161
-140
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
// Alle anderen Tags entfernen
|
||||
text = text.replace(/<[^>]+>/g, '');
|
||||
|
||||
// HTML-Entities dekodieren
|
||||
text = text.replace(/ /g, ' ');
|
||||
text = text.replace(/&/g, '&');
|
||||
text = text.replace(/</g, '<');
|
||||
text = text.replace(/>/g, '>');
|
||||
text = text.replace(/"/g, '"');
|
||||
text = text.replace(/'/g, "'");
|
||||
text = text.replace(/ä/g, 'ä');
|
||||
text = text.replace(/ö/g, 'ö');
|
||||
text = text.replace(/ü/g, 'ü');
|
||||
text = text.replace(/Ä/g, 'Ä');
|
||||
text = text.replace(/Ö/g, 'Ö');
|
||||
text = text.replace(/Ü/g, 'Ü');
|
||||
text = text.replace(/ß/g, 'ß');
|
||||
|
||||
// Mehrfache Leerzeilen reduzieren
|
||||
text = text.replace(/\n{3,}/g, '\n\n');
|
||||
|
||||
// Führende/folgende Leerzeichen entfernen
|
||||
text = text.trim();
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user