Neah/app/api/parse-email/route.ts
2025-04-26 11:27:01 +02:00

127 lines
3.7 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { simpleParser } from 'mailparser';
import * as DOMPurify from 'isomorphic-dompurify';
interface EmailAddress {
name?: string;
address: string;
}
// Helper to extract email addresses from mailparser Address objects
function getEmailAddresses(addresses: any): EmailAddress[] {
if (!addresses) return [];
// Handle various address formats
if (Array.isArray(addresses)) {
return addresses.map(addr => ({
name: addr.name || undefined,
address: addr.address
}));
}
if (typeof addresses === 'object') {
const result: EmailAddress[] = [];
// Handle mailparser format with text, html, value properties
if (addresses.value) {
addresses.value.forEach((addr: any) => {
result.push({
name: addr.name || undefined,
address: addr.address
});
});
return result;
}
// Handle direct object with address property
if (addresses.address) {
return [{
name: addresses.name || undefined,
address: addresses.address
}];
}
}
return [];
}
// Process HTML to ensure it displays well in our email context
function processHtml(html: string): string {
if (!html) return '';
try {
// Fix self-closing tags that might break in contentEditable
html = html.replace(/<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([^>]*)>/gi,
(match, tag, attrs) => `<${tag}${attrs}${attrs.endsWith('/') ? '' : '/'}>`)
// Clean up HTML with DOMPurify - CRITICAL for security
// Allow style tags but remove script tags
const cleaned = DOMPurify.sanitize(html, {
ADD_TAGS: ['style'],
FORBID_TAGS: ['script', 'iframe', 'object', 'embed'],
WHOLE_DOCUMENT: false
});
// Scope CSS to prevent leakage
return cleaned.replace(/<style([^>]*)>([\s\S]*?)<\/style>/gi, (match, attrs, css) => {
// Generate a unique class for this email content
const uniqueClass = `email-content-${Date.now()}`;
// Add the unique class to outer container that will be added
return `<style${attrs}>.${uniqueClass} {contain: content;} .${uniqueClass} ${css}</style>`;
});
} catch (e) {
console.error('Error processing HTML:', e);
return html; // Return original if processing fails
}
}
export async function POST(req: NextRequest) {
try {
const { email } = await req.json();
if (!email || typeof email !== 'string') {
return NextResponse.json(
{ error: 'Invalid email content' },
{ status: 400 }
);
}
const parsed = await simpleParser(email);
// Process the HTML content to make it safe and displayable
const html = parsed.html
? processHtml(parsed.html.toString())
: undefined;
const text = parsed.text
? parsed.text.toString()
: undefined;
// Extract attachments info if available
const attachments = parsed.attachments?.map(attachment => ({
filename: attachment.filename,
contentType: attachment.contentType,
contentDisposition: attachment.contentDisposition,
size: attachment.size
})) || [];
// Return all parsed email details
return NextResponse.json({
subject: parsed.subject,
from: getEmailAddresses(parsed.from),
to: getEmailAddresses(parsed.to),
cc: getEmailAddresses(parsed.cc),
bcc: getEmailAddresses(parsed.bcc),
date: parsed.date,
html,
text,
attachments
});
} catch (error) {
console.error('Error parsing email:', error);
return NextResponse.json(
{ error: 'Failed to parse email content' },
{ status: 500 }
);
}
}