'use client'; import DOMPurify from 'dompurify'; export interface ParsedEmail { subject: string | null; from: string | null; to: string | null; cc: string | null; bcc: string | null; date: Date | null; html: string | null; text: string | null; attachments: Array<{ filename: string; contentType: string; size: number; }>; headers: Record; } export async function decodeEmail(emailContent: string): Promise { try { // Ensure the email content is properly formatted const formattedContent = emailContent?.trim(); if (!formattedContent) { return { subject: null, from: null, to: null, cc: null, bcc: null, date: null, html: null, text: 'No content available', attachments: [], headers: {} }; } const response = await fetch('/api/parse-email', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: formattedContent }), }); const data = await response.json(); if (!response.ok) { console.error('API Error:', data); return { subject: null, from: null, to: null, cc: null, bcc: null, date: null, html: null, text: data.error || 'Failed to parse email', attachments: [], headers: {} }; } // If we have a successful response but no content if (!data.html && !data.text) { return { ...data, date: data.date ? new Date(data.date) : null, html: null, text: 'No content available', attachments: data.attachments || [], headers: data.headers || {} }; } return { ...data, date: data.date ? new Date(data.date) : null, text: data.text || null, html: data.html || null, attachments: data.attachments || [], headers: data.headers || {} }; } catch (error) { console.error('Error parsing email:', error); return { subject: null, from: null, to: null, cc: null, bcc: null, date: null, html: null, text: 'Error parsing email content', attachments: [], headers: {} }; } } /** * Cleans HTML content by removing potentially harmful elements while preserving styling. * This is the centralized HTML sanitization function to be used across the application. * @param html HTML content to sanitize * @param options Optional configuration * @returns Sanitized HTML */ export function cleanHtml(html: string, options: { preserveStyles?: boolean; scopeStyles?: boolean; addWrapper?: boolean; } = {}): string { if (!html) return ''; try { const defaultOptions = { preserveStyles: true, scopeStyles: true, addWrapper: true, ...options }; // Extract style tags if we're preserving them const styleTagsContent: string[] = []; let processedHtml = html; if (defaultOptions.preserveStyles) { processedHtml = html.replace(/]*>([\s\S]*?)<\/style>/gi, (match, styleContent) => { styleTagsContent.push(styleContent); return ''; // Remove style tags temporarily }); } // Process the HTML content processedHtml = processedHtml // Remove potentially harmful elements and attributes .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/)<[^<]*)*<\/iframe>/gi, '') .replace(/)<[^<]*)*<\/object>/gi, '') .replace(/)<[^<]*)*<\/embed>/gi, '') .replace(/)<[^<]*)*<\/form>/gi, '') .replace(/on\w+="[^"]*"/gi, '') // Remove inline event handlers (onclick, onload, etc.) .replace(/on\w+='[^']*'/gi, '') // Fix self-closing tags that might break React .replace(/<(br|hr|img|input|link|meta|area|base|col|embed|keygen|param|source|track|wbr)([^>]*)>/gi, '<$1$2 />'); // If we're scoping styles, prefix classes if (defaultOptions.scopeStyles) { processedHtml = processedHtml.replace(/class=["']([^"']*)["']/gi, (match, classContent) => { const classes = classContent.split(/\s+/).map((cls: string) => `email-forwarded-${cls}`).join(' '); return `class="${classes}"`; }); } // Add scoped styles if needed if (defaultOptions.preserveStyles && styleTagsContent.length > 0 && defaultOptions.scopeStyles) { // Create a modified version of the styles that scope them to our container const scopedStyles = styleTagsContent.map(style => { // Replace CSS selectors to be scoped to our container return style // Add scope to class selectors .replace(/(\.[a-zA-Z0-9_-]+)/g, '.email-forwarded-$1') // Add scope to ID selectors .replace(/(#[a-zA-Z0-9_-]+)/g, '#email-forwarded-$1') // Fix any CSS that might break out .replace(/@import/g, '/* @import */') .replace(/@media/g, '/* @media */'); }).join('\n'); // Add the styles back in a scoped way if (defaultOptions.addWrapper) { return ` `; } else { return `${processedHtml}`; } } // Just wrap the content if needed if (defaultOptions.addWrapper) { return ``; } return processedHtml; } catch (error) { console.error('Error cleaning HTML:', error); // Return something safe in case of error return `
Error processing HTML content
`; } }