diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index e65cc5e5..230cd6f5 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -7,6 +7,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import { decodeEmail } from '@/lib/mail-parser-wrapper'; +import DOMPurify from 'isomorphic-dompurify'; interface ComposeEmailProps { initialEmail?: EmailMessage | null; @@ -94,9 +95,9 @@ export default function ComposeEmail({ // New function to initialize forwarded email using same approach as Panel 3 const initializeForwardedEmail = async () => { - if (!initialEmail || !initialEmail.content) { - console.error('No email content available for forwarding'); - setBody('
No content available
'); + if (!initialEmail) { + console.error('No email available for forwarding'); + setBody('
No email available for forwarding
'); return; } @@ -128,25 +129,65 @@ export default function ComposeEmail({ } }; - // Create a simple forwarded message header with proper formatting + // Create a forwarded message header with proper formatting const headerContent = ` -
-
----------- Forwarded message ---------
-From: ${initialEmail.from || ''}
-Date: ${formatDate(initialEmail.date)}
-Subject: ${initialEmail.subject || ''}
-To: ${initialEmail.to || ''}
-
-
- `; +
+

---------- Forwarded message ---------

+

From: ${initialEmail.from || ''}

+

Date: ${formatDate(initialEmail.date)}

+

Subject: ${initialEmail.subject || ''}

+

To: ${initialEmail.to || ''}

+
`; - // Instead of trying to parse and clean the HTML, directly use the raw content - // This preserves all original formatting, CSS, and HTML structure - setBody(headerContent + initialEmail.content); + // Process content based on its type + let contentBody = ''; + + // Check if email content exists + if (!initialEmail.content || initialEmail.content.trim() === '') { + contentBody = '
No content available
'; + } else if (initialEmail.content.trim().startsWith('<') && initialEmail.content.includes('Error processing original content'; + } + } else { + // It's plain text, convert newlines to
tags and wrap in a div + contentBody = `
${initialEmail.content.replace(/\n/g, '
')}
`; + } + + // If content seems empty or invalid after processing, provide a fallback + if (!contentBody.trim() || contentBody.trim() === '
') { + contentBody = '
No content available
'; + } + + // Set the complete forwarded email + setBody(headerContent + contentBody); } catch (error) { console.error('Error initializing forwarded email:', error); - setBody('
Error loading forwarded content
'); + // Still provide the headers even if there's an error with the content + const errorHeaderContent = ` +
+

---------- Forwarded message ---------

+

From: ${initialEmail.from || ''}

+

Date: ${formatDate(initialEmail.date)}

+

Subject: ${initialEmail.subject || ''}

+

To: ${initialEmail.to || ''}

+
+
Error loading forwarded content
`; + setBody(errorHeaderContent); } finally { setSending(false); } diff --git a/components/email/EmailPreview.tsx b/components/email/EmailPreview.tsx index e194ad99..b230583b 100644 --- a/components/email/EmailPreview.tsx +++ b/components/email/EmailPreview.tsx @@ -6,6 +6,7 @@ import { EmailMessage } from '@/lib/services/email-service'; import { Loader2, Paperclip, Download } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; +import { cleanHtml } from '@/lib/mail-parser-wrapper'; interface EmailPreviewProps { email: EmailMessage | null; @@ -20,18 +21,31 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP const renderContent = () => { if (!email?.content) return

No content available

; - // Sanitize HTML content - const sanitizedContent = DOMPurify.sanitize(email.content, { - ADD_TAGS: ['style', 'table', 'thead', 'tbody', 'tr', 'td', 'th'], - ADD_ATTR: ['colspan', 'rowspan', 'style', 'width', 'height'] - }); - - return ( -
- ); + try { + // Use DOMPurify directly with enhanced sanitization options + const sanitizedContent = DOMPurify.sanitize(email.content, { + ADD_TAGS: ['style', 'meta', 'link', 'table', 'thead', 'tbody', 'tr', 'td', 'th', 'hr', 'font', 'div', 'span', 'a', 'img', 'b', 'strong', 'i', 'em', 'u', 'br', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code', 'center', 'section', 'header', 'footer', 'article', 'nav', 'keyframes'], + ADD_ATTR: ['*', 'colspan', 'rowspan', 'cellpadding', 'cellspacing', 'border', 'bgcolor', 'width', 'height', 'align', 'valign', 'class', 'id', 'style', 'color', 'face', 'size', 'background', 'src', 'href', 'target', 'rel', 'alt', 'title', 'name', 'animation', 'animation-name', 'animation-duration', 'animation-fill-mode'], + ALLOW_UNKNOWN_PROTOCOLS: true, + WHOLE_DOCUMENT: true, + KEEP_CONTENT: true, + RETURN_DOM: false, + FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'select', 'option', 'textarea', 'canvas', 'video', 'audio'], + FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onchange', 'onsubmit'], + USE_PROFILES: { html: true, svg: false, svgFilters: false, mathMl: false }, + FORCE_BODY: true + }); + + return ( +
+ ); + } catch (error) { + console.error('Error rendering email content:', error); + return

Error displaying email content

; + } }; // Format the date diff --git a/lib/mail-parser-wrapper.ts b/lib/mail-parser-wrapper.ts index 1216719b..6f23bd95 100644 --- a/lib/mail-parser-wrapper.ts +++ b/lib/mail-parser-wrapper.ts @@ -105,14 +105,16 @@ export function cleanHtml(html: string): string { try { // Enhanced configuration to preserve more HTML elements for complex emails return DOMPurify.sanitize(html, { - ADD_TAGS: ['style', 'meta', 'link', 'table', 'thead', 'tbody', 'tr', 'td', 'th', 'hr', 'font', 'div', 'span', 'a', 'img', 'b', 'strong', 'i', 'em', 'u', 'br', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code', 'center', 'section', 'header', 'footer', 'article', 'nav'], - ADD_ATTR: ['*', 'colspan', 'rowspan', 'cellpadding', 'cellspacing', 'border', 'bgcolor', 'width', 'height', 'align', 'valign', 'class', 'id', 'style', 'color', 'face', 'size', 'background', 'src', 'href', 'target', 'rel', 'alt', 'title', 'name'], + ADD_TAGS: ['style', 'meta', 'link', 'table', 'thead', 'tbody', 'tr', 'td', 'th', 'hr', 'font', 'div', 'span', 'a', 'img', 'b', 'strong', 'i', 'em', 'u', 'br', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code', 'center', 'section', 'header', 'footer', 'article', 'nav', 'keyframes'], + ADD_ATTR: ['*', 'colspan', 'rowspan', 'cellpadding', 'cellspacing', 'border', 'bgcolor', 'width', 'height', 'align', 'valign', 'class', 'id', 'style', 'color', 'face', 'size', 'background', 'src', 'href', 'target', 'rel', 'alt', 'title', 'name', 'animation', 'animation-name', 'animation-duration', 'animation-fill-mode'], ALLOW_UNKNOWN_PROTOCOLS: true, WHOLE_DOCUMENT: true, KEEP_CONTENT: true, RETURN_DOM: false, FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'select', 'option', 'textarea', 'canvas', 'video', 'audio'], - FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onchange', 'onsubmit'] + FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onchange', 'onsubmit'], + USE_PROFILES: { html: true, svg: false, svgFilters: false, mathMl: false }, + FORCE_BODY: true }); } catch (error) { console.error('Error cleaning HTML:', error); diff --git a/lib/server/email-parser.ts b/lib/server/email-parser.ts index 587644da..740633ef 100644 --- a/lib/server/email-parser.ts +++ b/lib/server/email-parser.ts @@ -1,20 +1,16 @@ import { simpleParser } from 'mailparser'; -function cleanHtml(html: string): string { +export function cleanHtml(html: string): string { try { - // Basic HTML cleaning without DOMPurify + // More permissive cleaning that preserves styling but removes potentially harmful elements return html - .replace(/)<[^<]*)*<\/script>/gi, '') // Remove script tags - .replace(/)<[^<]*)*<\/style>/gi, '') // Remove style tags - .replace(/]*>/gi, '') // Remove meta tags - .replace(/]*>[\s\S]*?<\/head>/gi, '') // Remove head - .replace(/]*>[\s\S]*?<\/title>/gi, '') // Remove title - .replace(/]*>/gi, '') // Remove body opening tag - .replace(/<\/body>/gi, '') // Remove body closing tag - .replace(/]*>/gi, '') // Remove html opening tag - .replace(/<\/html>/gi, '') // Remove html closing tag - .replace(/\s+/g, ' ') // Clean up whitespace - .trim(); + .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, ''); } catch (error) { console.error('Error cleaning HTML:', error); return html;