diff --git a/app/api/mail/route.ts b/app/api/mail/route.ts deleted file mode 100644 index 3f111623..00000000 --- a/app/api/mail/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextResponse } from 'next/server'; - -/** - * This route is deprecated. It redirects to the new courrier API endpoint. - * @deprecated Use the /api/courrier endpoint instead - */ -export async function GET(request: Request) { - console.warn('Deprecated: /api/mail route is being used. Update your code to use /api/courrier instead.'); - - // Extract query parameters - const url = new URL(request.url); - - // Redirect to the new API endpoint - const redirectUrl = new URL('/api/courrier', url.origin); - - // Copy all search parameters - url.searchParams.forEach((value, key) => { - redirectUrl.searchParams.set(key, value); - }); - - return NextResponse.redirect(redirectUrl.toString()); -} \ No newline at end of file diff --git a/app/api/mail/send/route.ts b/app/api/mail/send/route.ts deleted file mode 100644 index 2a6ec36d..00000000 --- a/app/api/mail/send/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextResponse } from 'next/server'; - -/** - * This route is deprecated. It redirects to the new courrier API endpoint. - * @deprecated Use the /api/courrier/send endpoint instead - */ -export async function POST(request: Request) { - console.warn('Deprecated: /api/mail/send route is being used. Update your code to use /api/courrier/send instead.'); - - try { - // Clone the request body - const body = await request.json(); - - // Make a new request to the courrier API - const newRequest = new Request(new URL('/api/courrier/send', request.url).toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(body) - }); - - // Forward the request - const response = await fetch(newRequest); - const data = await response.json(); - - return NextResponse.json(data, { status: response.status }); - } catch (error) { - console.error('Error forwarding to courrier/send:', error); - return NextResponse.json( - { error: 'Failed to send email' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/parse-email/route.ts b/app/api/parse-email/route.ts index de4f7b0a..74ba1059 100644 --- a/app/api/parse-email/route.ts +++ b/app/api/parse-email/route.ts @@ -1,38 +1,83 @@ -import { NextResponse } from 'next/server'; -import { simpleParser, AddressObject } from 'mailparser'; +import { NextRequest, NextResponse } from 'next/server'; +import { simpleParser } from 'mailparser'; +import * as DOMPurify from 'isomorphic-dompurify'; -function getEmailAddress(address: AddressObject | AddressObject[] | undefined): string | null { - if (!address) return null; - if (Array.isArray(address)) { - return address.map(a => a.text).join(', '); - } - return address.text; +interface EmailAddress { + name?: string; + address: string; } -// Clean up the HTML to make it safe but preserve styles -function processHtml(html: string | null): string | null { - if (!html) return null; +// 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 { - // Make the content display well in the email context - return html - // 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 />') - // Keep style tags but ensure they're closed properly - .replace(/]*)>([\s\S]*?)<\/style>/gi, (match) => { - // Just return the matched style tag as-is - return match; - }); - } catch (error) { - console.error('Error processing HTML:', error); - return html; + // 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(/]*)>([\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 `.${uniqueClass} {contain: content;} .${uniqueClass} ${css}`; + }); + } catch (e) { + console.error('Error processing HTML:', e); + return html; // Return original if processing fails } } -export async function POST(request: Request) { +export async function POST(req: NextRequest) { try { - const body = await request.json(); - const { email } = body; + const { email } = await req.json(); if (!email || typeof email !== 'string') { return NextResponse.json( @@ -40,33 +85,42 @@ export async function POST(request: Request) { { status: 400 } ); } - + const parsed = await simpleParser(email); - // Process the HTML to preserve styling but make it safe - // Handle the case where parsed.html could be a boolean - const processedHtml = typeof parsed.html === 'string' ? processHtml(parsed.html) : null; + // 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 || null, - from: getEmailAddress(parsed.from), - to: getEmailAddress(parsed.to), - cc: getEmailAddress(parsed.cc), - bcc: getEmailAddress(parsed.bcc), - date: parsed.date || null, - html: processedHtml, - text: parsed.textAsHtml || parsed.text || null, - attachments: parsed.attachments?.map(att => ({ - filename: att.filename, - contentType: att.contentType, - size: att.size - })) || [], - headers: parsed.headers || {} + 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' }, + { error: 'Failed to parse email content' }, { status: 500 } ); } diff --git a/app/courrier/loading-fix.tsx b/app/courrier/loading-fix.tsx index 0f7d3075..50141411 100644 --- a/app/courrier/loading-fix.tsx +++ b/app/courrier/loading-fix.tsx @@ -1,6 +1,9 @@ /** * This is a debugging component that provides troubleshooting tools * for the email loading process in the Courrier application. + * + * NOTE: This component should only be used during development for debugging purposes. + * It's kept in the codebase for future reference but won't render in production. */ 'use client'; @@ -26,6 +29,11 @@ export function LoadingFix({ loadEmails, emails }: LoadingFixProps) { + // Don't render anything in production mode + if (process.env.NODE_ENV === 'production') { + return null; + } + const forceResetLoadingStates = () => { console.log('[DEBUG] Force resetting loading states to false'); // Force both loading states to false diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index c45bacb1..6dd4c10f 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -95,6 +95,10 @@ interface ParsedEmailMetadata { }; } +/** + * @deprecated This function is deprecated and will be removed in future versions. + * Email parsing has been centralized in lib/mail-parser-wrapper.ts and the API endpoint. + */ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: string } { const [headers, ...bodyParts] = emailBody.split('\r\n\r\n'); return { @@ -322,6 +326,15 @@ function formatDate(date: Date | null): string { }).format(date); } +/** + * @deprecated This function is deprecated and will be removed in future versions. + * Use the ReplyContent component directly instead. + */ +function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') { + console.warn('getReplyBody is deprecated, use instead'); + return ; +} + function ReplyContent({ email, type }: { email: Email; type: 'reply' | 'reply-all' | 'forward' }) { const [content, setContent] = useState(''); const [error, setError] = useState(null); @@ -390,11 +403,6 @@ function ReplyContent({ email, type }: { email: Email; type: 'reply' | 'reply-al return
; } -// Update the getReplyBody function to use the new component -function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') { - return ; -} - function EmailPreview({ email }: { email: Email }) { const [preview, setPreview] = useState(''); const [error, setError] = useState(null); @@ -472,6 +480,9 @@ function EmailPreview({ email }: { email: Email }) { // Update the generateEmailPreview function to use the new component function generateEmailPreview(email: Email) { + // @deprecated - This function is deprecated and will be removed in future versions. + // Use the EmailPreview component directly instead. + console.warn('generateEmailPreview is deprecated, use instead'); return ; } diff --git a/components/ComposeEmail.tsx b/components/ComposeEmail.tsx deleted file mode 100644 index 88f16e2c..00000000 --- a/components/ComposeEmail.tsx +++ /dev/null @@ -1,587 +0,0 @@ -'use client'; - -import { useRef, useEffect, useState, useCallback } from 'react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Paperclip, X } from 'lucide-react'; -import { Textarea } from '@/components/ui/textarea'; -import { decodeComposeContent, encodeComposeContent } from '@/lib/compose-mime-decoder'; -import { Email } from '@/app/courrier/page'; -import mime from 'mime'; -import { simpleParser } from 'mailparser'; -import { decodeEmail } from '@/lib/mail-parser-wrapper'; -import DOMPurify from 'dompurify'; - -interface ComposeEmailProps { - showCompose: boolean; - setShowCompose: (show: boolean) => void; - composeTo: string; - setComposeTo: (to: string) => void; - composeCc: string; - setComposeCc: (cc: string) => void; - composeBcc: string; - setComposeBcc: (bcc: string) => void; - composeSubject: string; - setComposeSubject: (subject: string) => void; - composeBody: string; - setComposeBody: (body: string) => void; - showCc: boolean; - setShowCc: (show: boolean) => void; - showBcc: boolean; - setShowBcc: (show: boolean) => void; - attachments: any[]; - setAttachments: (attachments: any[]) => void; - handleSend: () => Promise; - originalEmail?: { - content: string; - type: 'reply' | 'reply-all' | 'forward'; - }; - onSend: (email: Email) => void; - onCancel: () => void; - onBodyChange?: (body: string) => void; - initialTo?: string; - initialSubject?: string; - initialBody?: string; - initialCc?: string; - initialBcc?: string; - replyTo?: Email | null; - forwardFrom?: Email | null; -} - -export default function ComposeEmail({ - showCompose, - setShowCompose, - composeTo, - setComposeTo, - composeCc, - setComposeCc, - composeBcc, - setComposeBcc, - composeSubject, - setComposeSubject, - composeBody, - setComposeBody, - showCc, - setShowCc, - showBcc, - setShowBcc, - attachments, - setAttachments, - handleSend, - originalEmail, - onSend, - onCancel, - onBodyChange, - initialTo, - initialSubject, - initialBody, - initialCc, - initialBcc, - replyTo, - forwardFrom -}: ComposeEmailProps) { - const composeBodyRef = useRef(null); - const [localContent, setLocalContent] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const contentRef = useRef(null); - - useEffect(() => { - if (replyTo || forwardFrom) { - const initializeContent = async () => { - if (!composeBodyRef.current) return; - - try { - const emailToProcess = replyTo || forwardFrom; - console.log('[DEBUG] Initializing compose content with email:', - emailToProcess ? { - id: emailToProcess.id, - subject: emailToProcess.subject, - hasContent: !!emailToProcess.content, - contentLength: emailToProcess.content ? emailToProcess.content.length : 0, - preview: emailToProcess.preview - } : 'null' - ); - - // Set initial loading state - composeBodyRef.current.innerHTML = ` -
-
-
Loading original message...
-
- `; - - setIsLoading(true); - - // Check if email object exists - if (!emailToProcess) { - console.error('[DEBUG] No email to process for reply/forward'); - composeBodyRef.current.innerHTML = ` -
-
-
No email selected for reply/forward.
-
- `; - setIsLoading(false); - return; - } - - // Check if we need to fetch full content first - if (!emailToProcess.content || emailToProcess.content.length === 0) { - console.log('[DEBUG] Need to fetch content before composing reply/forward'); - - try { - const response = await fetch(`/api/courrier/${emailToProcess.id}?folder=${encodeURIComponent(emailToProcess.folder || 'INBOX')}`); - - if (!response.ok) { - throw new Error(`Failed to fetch email content: ${response.status}`); - } - - const fullContent = await response.json(); - - // Update the email content with the fetched full content - emailToProcess.content = fullContent.content; - emailToProcess.contentFetched = true; - - console.log('[DEBUG] Successfully fetched content for reply/forward'); - } catch (error) { - console.error('[DEBUG] Error fetching content for reply:', error); - composeBodyRef.current.innerHTML = ` -
-
-
Failed to load email content. Please try again.
-
- `; - setIsLoading(false); - return; // Exit if we couldn't get the content - } - } - - // Use the exact same implementation as Panel 3's ReplyContent - try { - const decoded = await decodeEmail(emailToProcess.content); - - let formattedContent = ''; - - if (forwardFrom) { - // Create a clean header for the forwarded email - const headerHtml = ` -
-

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

-

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

-

Date: ${formatDate(decoded.date)}

-

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

-

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

-
- `; - - // Use the original HTML as-is without DOMPurify or any modification - formattedContent = ` - ${headerHtml} - ${decoded.html || decoded.text || 'No content available'} - `; - } else { - formattedContent = ` -
-

On ${formatDate(decoded.date)}, ${decoded.from || ''} wrote:

-
- -
-
- `; - } - - // Set the content in the compose area with proper structure - const wrappedContent = ` -
-

- ${formattedContent} -
- `; - - if (composeBodyRef.current) { - composeBodyRef.current.innerHTML = wrappedContent; - - // Place cursor at the beginning before the quoted content - const selection = window.getSelection(); - const range = document.createRange(); - const firstDiv = composeBodyRef.current.querySelector('div[style*="min-height: 20px;"]'); - if (firstDiv) { - range.setStart(firstDiv, 0); - range.collapse(true); - selection?.removeAllRanges(); - selection?.addRange(range); - (firstDiv as HTMLElement).focus(); - } - - // Update compose state - setComposeBody(wrappedContent); - setLocalContent(wrappedContent); - console.log('[DEBUG] Successfully set compose content'); - } - } catch (error) { - console.error('[DEBUG] Error parsing email for compose:', error); - - // Fallback to basic content display - const errorContent = ` -
-
-
- ---------- Original Message ---------
- ${emailToProcess.subject ? `Subject: ${emailToProcess.subject}
` : ''} - ${emailToProcess.from ? `From: ${emailToProcess.from}
` : ''} - ${emailToProcess.date ? `Date: ${new Date(emailToProcess.date).toLocaleString()}
` : ''} -
-
- ${emailToProcess.preview || 'No content available'} -
-
- `; - - if (composeBodyRef.current) { - composeBodyRef.current.innerHTML = errorContent; - setComposeBody(errorContent); - setLocalContent(errorContent); - } - } - } catch (error) { - console.error('[DEBUG] Error initializing compose content:', error); - if (composeBodyRef.current) { - const errorContent = ` -
-
-
Error loading original message.
-
- Technical details: ${error instanceof Error ? error.message : 'Unknown error'} -
-
- `; - composeBodyRef.current.innerHTML = errorContent; - setComposeBody(errorContent); - setLocalContent(errorContent); - } - } finally { - setIsLoading(false); - } - }; - - initializeContent(); - } - }, [replyTo, forwardFrom, setComposeBody]); - - const handleInput = (e: React.FormEvent) => { - if (!e.currentTarget) return; - const content = e.currentTarget.innerHTML; - if (!content.trim()) { - setLocalContent(''); - setComposeBody(''); - } else { - setLocalContent(content); - setComposeBody(content); - } - - if (onBodyChange) { - onBodyChange(content); - } - - // Ensure scrolling and cursor behavior works after edits - const messageContentDivs = e.currentTarget.querySelectorAll('.message-content'); - messageContentDivs.forEach(div => { - // Make sure the div remains scrollable after input events - (div as HTMLElement).style.maxHeight = '300px'; - (div as HTMLElement).style.overflowY = 'auto'; - (div as HTMLElement).style.border = '1px solid #e5e7eb'; - (div as HTMLElement).style.borderRadius = '4px'; - (div as HTMLElement).style.padding = '10px'; - - // Ensure wheel events are properly handled - if (!(div as HTMLElement).hasAttribute('data-scroll-handler-attached')) { - div.addEventListener('wheel', function(this: HTMLElement, ev: Event) { - const e = ev as WheelEvent; - const target = this; - - // Check if we're at the boundary of the scrollable area - const isAtBottom = target.scrollHeight - target.scrollTop <= target.clientHeight + 1; - const isAtTop = target.scrollTop <= 0; - - // Only prevent default if we're not at the boundaries in the direction of scrolling - if ((e.deltaY > 0 && !isAtBottom) || (e.deltaY < 0 && !isAtTop)) { - e.stopPropagation(); - e.preventDefault(); // Prevent the parent container from scrolling - } - }, { passive: false }); - - // Mark this element as having a scroll handler attached - (div as HTMLElement).setAttribute('data-scroll-handler-attached', 'true'); - } - }); - }; - - const handleSendEmail = async () => { - if (!composeBodyRef.current) return; - - const composeArea = composeBodyRef.current.querySelector('.compose-area'); - if (!composeArea) return; - - const content = composeArea.innerHTML; - if (!content.trim()) { - console.error('Email content is empty'); - return; - } - - try { - const encodedContent = await encodeComposeContent(content); - setComposeBody(encodedContent); - await handleSend(); - setShowCompose(false); - } catch (error) { - console.error('Error sending email:', error); - } - }; - - const handleFileAttachment = async (e: React.ChangeEvent) => { - if (!e.target.files) return; - - const newAttachments: any[] = []; - const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB in bytes - const oversizedFiles: string[] = []; - - for (const file of e.target.files) { - if (file.size > MAX_FILE_SIZE) { - oversizedFiles.push(file.name); - continue; - } - - try { - // Read file as base64 - const base64Content = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => { - const base64 = reader.result as string; - resolve(base64.split(',')[1]); // Remove data URL prefix - }; - reader.readAsDataURL(file); - }); - - newAttachments.push({ - name: file.name, - type: file.type, - content: base64Content, - encoding: 'base64' - }); - } catch (error) { - console.error('Error processing attachment:', error); - } - } - - if (oversizedFiles.length > 0) { - alert(`The following files exceed the 10MB size limit and were not attached:\n${oversizedFiles.join('\n')}`); - } - - if (newAttachments.length > 0) { - setAttachments([...attachments, ...newAttachments]); - } - }; - - // Add focus handling for better UX - const handleComposeAreaClick = (e: React.MouseEvent) => { - // If the click is directly on the compose area and not on any child element - if (e.target === e.currentTarget) { - // Find the cursor position element - const cursorPosition = e.currentTarget.querySelector('.cursor-position'); - if (cursorPosition) { - // Focus the cursor position element - (cursorPosition as HTMLElement).focus(); - - // Set cursor at the beginning - const selection = window.getSelection(); - const range = document.createRange(); - range.setStart(cursorPosition, 0); - range.collapse(true); - selection?.removeAllRanges(); - selection?.addRange(range); - } - } - }; - - // Add formatDate function to match Panel 3 implementation - function formatDate(date: Date | null): string { - if (!date) return ''; - return new Intl.DateTimeFormat('fr-FR', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit' - }).format(date); - } - - if (!showCompose) return null; - - return ( -
-
- {/* Modal Header */} -
-

- {replyTo ? 'Reply' : forwardFrom ? 'Forward' : 'New Message'} -

- -
- - {/* Modal Body */} -
-
- {/* To Field */} -
- - setComposeTo(e.target.value)} - placeholder="recipient@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- - {/* CC/BCC Toggle Buttons */} -
- - -
- - {/* CC Field */} - {showCc && ( -
- - setComposeCc(e.target.value)} - placeholder="cc@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- )} - - {/* BCC Field */} - {showBcc && ( -
- - setComposeBcc(e.target.value)} - placeholder="bcc@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- )} - - {/* Subject Field */} -
- - setComposeSubject(e.target.value)} - placeholder="Enter subject" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- - {/* Message Body */} -
- -
-
-
-
- - {/* Modal Footer */} -
-
- {/* File Input for Attachments */} - - -
-
- - -
-
-
-
- ); -} \ No newline at end of file diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index dbc4bdd0..5bfb89fb 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -179,8 +179,7 @@ export default function ComposeEmail({ : initialEmail.date.toLocaleString() : new Date().toLocaleString(); - // Create a clean wrapper that won't interfere with the original email's styling - // Use inline styles for the header to avoid CSS conflicts + // Create a clean header with inline styles only - no external CSS const headerHtml = `
@@ -193,91 +192,83 @@ export default function ComposeEmail({
`; - // Process the original content - let originalContent = ''; + // Default content is a clear "no content" message + let contentHtml = '
No content available in original email
'; - // First try to use the API to parse and sanitize the email content - try { - // Use server-side parsing via fetch API to properly handle complex emails - const response = await fetch('/api/parse-email', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: initialEmail.content || initialEmail.html || initialEmail.text || '' - }), - }); - - if (response.ok) { - const parsedEmail = await response.json(); - - if (parsedEmail.html && parsedEmail.html.trim()) { - console.log('Using parsed HTML content for forward'); - - // Create an iframe-like containment for the email content - // This prevents CSS from the original email leaking into our compose view - originalContent = ` - - `; - } else if (parsedEmail.text && parsedEmail.text.trim()) { - console.log('Using parsed text content for forward'); - originalContent = `
${parsedEmail.text}
`; - } else { - console.log('No content available from parser'); - originalContent = '
No content available
'; - } - } else { - throw new Error('Failed to parse email content'); - } - } catch (parseError) { - console.error('Error parsing email content:', parseError); - - // Fall back to direct content handling if API parsing fails - if (initialEmail.html && initialEmail.html.trim()) { - console.log('Falling back to HTML content for forward'); - // Use DOMPurify to sanitize HTML and remove dangerous elements - originalContent = DOMPurify.sanitize(initialEmail.html, { - ADD_TAGS: ['style', 'div', 'span', 'p', 'br', 'hr', 'h1', 'h2', 'h3', 'img', 'table', 'tr', 'td', 'th'], - ADD_ATTR: ['style', 'class', 'id', 'src', 'alt', 'href', 'target'], - FORBID_TAGS: ['script', 'iframe', 'object', 'embed'], - FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'] + // Check if we have content to forward + if (initialEmail.content || initialEmail.html || initialEmail.text) { + try { + // Use the parse-email API endpoint which centralizes our email parsing logic + const response = await fetch('/api/parse-email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: initialEmail.content || initialEmail.html || initialEmail.text || '' + }), }); - } else if (initialEmail.content && initialEmail.content.trim()) { - console.log('Falling back to content field for forward'); - originalContent = DOMPurify.sanitize(initialEmail.content); - } else if (initialEmail.text && initialEmail.text.trim()) { - console.log('Falling back to text content for forward'); - originalContent = `
${initialEmail.text}
`; - } else { - console.log('No content available for forward'); - originalContent = '
No content available
'; + + if (response.ok) { + const parsedEmail = await response.json(); + + // Use the parsed HTML content if available + if (parsedEmail.html) { + contentHtml = parsedEmail.html; + console.log('Successfully parsed HTML content'); + } else if (parsedEmail.text) { + // Text-only content is wrapped in pre-formatted styling + contentHtml = `
${parsedEmail.text}
`; + console.log('Using text content'); + } else { + console.warn('API returned success but no content'); + } + } else { + console.error('API returned error:', await response.text()); + throw new Error('API call failed'); + } + } catch (error) { + console.error('Error parsing email:', error); + + // Fallback processing - using our cleanHtml utility directly + if (initialEmail.html) { + // Import the cleanHtml function dynamically if needed + const { cleanHtml } = await import('@/lib/mail-parser-wrapper'); + contentHtml = cleanHtml(initialEmail.html, { + preserveStyles: true, + scopeStyles: true, + addWrapper: true + }); + console.log('Using direct HTML cleaning fallback'); + } else if (initialEmail.content) { + contentHtml = DOMPurify.sanitize(initialEmail.content, { + ADD_TAGS: ['style'], + FORBID_TAGS: ['script', 'iframe'] + }); + console.log('Using DOMPurify sanitized content'); + } else if (initialEmail.text) { + contentHtml = `
${initialEmail.text}
`; + console.log('Using plain text fallback'); + } } + } else { + console.warn('No email content available for forwarding'); } - // Preserve all original structure by wrapping, not modifying the original content - // Important: We add a style scope to prevent CSS leakage + // Combine the header and content - using containment const forwardedContent = ` ${headerHtml} - -