diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index da2919f7..c544235a 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -11,6 +11,13 @@ import { Input } from '@/components/ui/input'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import DOMPurify from 'isomorphic-dompurify'; +// Import the new email formatter utilities +import { + formatForwardedEmail, + formatReplyEmail, + EmailMessage as FormatterEmailMessage +} from '@/lib/utils/email-formatter'; + // Define EmailMessage interface locally instead of importing from server-only file interface EmailAddress { name: string; @@ -44,124 +51,6 @@ interface EmailMessage { contentFetched?: boolean; } -// Simplified formatEmailForReplyOrForward that doesn't rely on server code -function formatEmailForReplyOrForward( - email: EmailMessage, - type: 'reply' | 'reply-all' | 'forward' -): { - to: string; - cc?: string; - subject: string; - body: string; -} { - // Format subject - let subject = email.subject || ''; - if (type === 'reply' || type === 'reply-all') { - if (!subject.startsWith('Re:')) { - subject = `Re: ${subject}`; - } - } else if (type === 'forward') { - if (!subject.startsWith('Fwd:')) { - subject = `Fwd: ${subject}`; - } - } - - // Create quote header - const date = typeof email.date === 'string' - ? new Date(email.date) - : email.date; - - const formattedDate = date.toLocaleString('en-US', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); - - const sender = email.from[0]; - const fromText = sender?.name - ? `${sender.name} <${sender.address}>` - : sender?.address || 'Unknown sender'; - - const quoteHeader = `
On ${formattedDate}, ${fromText} wrote:
`; - - // Format content - const quotedContent = email.html || email.content || email.text || ''; - - // Format recipients - let to = ''; - let cc = ''; - - if (type === 'reply') { - // Reply to sender only - to = email.from.map(addr => `${addr.name} <${addr.address}>`).join(', '); - } else if (type === 'reply-all') { - // Reply to sender and all recipients - to = email.from.map(addr => `${addr.name} <${addr.address}>`).join(', '); - - // Add all original recipients to CC - const allRecipients = [ - ...(email.to || []), - ...(email.cc || []) - ]; - - cc = allRecipients - .map(addr => `${addr.name} <${addr.address}>`) - .join(', '); - } else if (type === 'forward') { - // Forward doesn't set recipients - to = ''; - - // Format forward differently - const formattedDate = typeof email.date === 'string' - ? new Date(email.date).toLocaleString() - : email.date.toLocaleString(); - - const fromText = email.from.map(f => f.name ? `${f.name} <${f.address}>` : f.address).join(', '); - const toText = email.to.map(t => t.name ? `${t.name} <${t.address}>` : t.address).join(', '); - - return { - to: '', - subject, - body: ` -
-
-

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

-

From: ${fromText}

-

Date: ${formattedDate}

-

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

-

To: ${toText}

-
-
- ${quotedContent ? quotedContent : '

No content available

'} -
-
- ` - }; - } - - // Format body with improved styling for replies - const body = ` -
-
-
${quoteHeader}
-
-
- ${quotedContent} -
-
-
`; - - return { - to, - cc: cc || undefined, - subject, - body - }; -} - // Legacy interface for backward compatibility with old ComposeEmail component interface LegacyComposeEmailProps { showCompose: boolean; @@ -270,99 +159,70 @@ export default function ComposeEmail(props: ComposeEmailAllProps) { // Initialize the form when replying to or forwarding an email useEffect(() => { if (initialEmail && type !== 'new') { - // For all types of emails, format with a consistent approach - let formattedContent = ''; - - if (type === 'forward') { - // Format subject with Fwd: prefix if needed - const subjectBase = initialEmail.subject || '(No subject)'; - const subjectRegex = /^(Fwd|FW|Forward):\s*/i; - const subject = subjectRegex.test(subjectBase) - ? subjectBase - : `Fwd: ${subjectBase}`; - - setSubject(subject); - - // Format forwarded content - const fromString = initialEmail.from?.map(addr => - addr.name ? `${addr.name} <${addr.address}>` : addr.address - ).join(', ') || ''; - - const toString = initialEmail.to?.map(addr => - addr.name ? `${addr.name} <${addr.address}>` : addr.address - ).join(', ') || ''; - - const dateString = typeof initialEmail.date === 'string' - ? new Date(initialEmail.date).toLocaleString() - : initialEmail.date.toLocaleString(); - - // Preprocess original content to avoid direction issues - const originalContent = preprocessEmailContent( - initialEmail.content || initialEmail.html || initialEmail.text || '' - ); - - formattedContent = ` -

-
-
-
-
---------- Forwarded message ---------
-
From: ${fromString}
-
Date: ${dateString}
-
Subject: ${initialEmail.subject || ''}
-
To: ${toString}
-
-
${originalContent}
-
-
- `; - } else { - // For reply/reply-all - const formattedEmail = formatEmailForReplyOrForward(initialEmail, type as 'reply' | 'reply-all'); - - setTo(formattedEmail.to); - - if (formattedEmail.cc) { - setCc(formattedEmail.cc); - setShowCc(true); + try { + const formatterEmail: FormatterEmailMessage = { + id: initialEmail.id, + messageId: initialEmail.messageId, + subject: initialEmail.subject, + from: initialEmail.from || [], + to: initialEmail.to || [], + cc: initialEmail.cc || [], + bcc: initialEmail.bcc || [], + date: initialEmail.date, + content: initialEmail.content, + html: initialEmail.html, + text: initialEmail.text, + hasAttachments: initialEmail.hasAttachments || false + }; + + if (type === 'forward') { + // For forwarding, use the dedicated formatter + const { subject, content } = formatForwardedEmail(formatterEmail); + setSubject(subject); + setBody(content); + setUserMessage(content); + } else { + // For reply/reply-all, use the reply formatter + const { to, cc, subject, content } = formatReplyEmail(formatterEmail, type as 'reply' | 'reply-all'); + setTo(to); + if (cc) { + setCc(cc); + setShowCc(true); + } + setSubject(subject); + setBody(content); + setUserMessage(content); } - setSubject(formattedEmail.subject); - - // Process content to fix direction issues - formattedContent = preprocessEmailContent(formattedEmail.body); - } - - // Set the entire content as one editable area - // Force LTR direction for quoted content - setBody(formattedContent); - setUserMessage(formattedContent); - - // Focus editor after initializing - setTimeout(() => { - if (editorRef.current) { - editorRef.current.focus(); - editorRef.current.dir = isRTL ? 'rtl' : 'ltr'; - - // Place cursor at the beginning of the content - const selection = window.getSelection(); - if (selection) { - const range = document.createRange(); + // Focus editor after initializing + setTimeout(() => { + if (editorRef.current) { + editorRef.current.focus(); - // Find the first text node or element node - let firstNode = editorRef.current.firstChild; - if (firstNode) { - range.setStart(firstNode, 0); - range.collapse(true); - - selection.removeAllRanges(); - selection.addRange(range); + // Place cursor at the beginning + const selection = window.getSelection(); + if (selection) { + try { + const range = document.createRange(); + + if (editorRef.current.firstChild) { + range.setStart(editorRef.current.firstChild, 0); + range.collapse(true); + + selection.removeAllRanges(); + selection.addRange(range); + } + } catch (e) { + console.error('Error positioning cursor:', e); + } } } - } - }, 100); + }, 100); + } catch (error) { + console.error('Error formatting email:', error); + } } - }, [initialEmail, type, isRTL]); + }, [initialEmail, type]); // Format date for the forwarded message header const formatDate = (date: Date | null): string => { @@ -401,43 +261,6 @@ export default function ComposeEmail(props: ComposeEmailAllProps) { ).join(', '); }; - // Initialize forwarded email with clear structure and style preservation - const initializeForwardedEmail = async () => { - console.log('Starting initializeForwardedEmail'); - if (!initialEmail) { - console.error('No email available for forwarding'); - setBody('
No email available for forwarding
'); - return; - } - - try { - // Format subject with Fwd: prefix if needed - const subjectBase = initialEmail.subject || '(No subject)'; - const subjectRegex = /^(Fwd|FW|Forward):\s*/i; - const subject = subjectRegex.test(subjectBase) - ? subjectBase - : `Fwd: ${subjectBase}`; - - setSubject(subject); - - // For forwarded emails, we'll use a completely different approach - // Just save the original HTML content and use it directly - // This preserves all formatting without trying to parse it - - // Set message parts for the editor - const content = initialEmail.content || initialEmail.html || initialEmail.text || ''; - setOriginalContent(content); - setUserMessage(''); // Start with empty user message - setBody(''); // Will be constructed when sending - - // Log for debugging - console.log('Set originalContent:', content.substring(0, 100) + '...'); - } catch (error) { - console.error('Error formatting forwarded email:', error); - setBody('
Error formatting forwarded email content
'); - } - }; - // Handle attachment selection const handleAttachmentClick = () => { attachmentInputRef.current?.click(); diff --git a/lib/utils/email-formatter.ts b/lib/utils/email-formatter.ts new file mode 100644 index 00000000..8c894190 --- /dev/null +++ b/lib/utils/email-formatter.ts @@ -0,0 +1,208 @@ +/** + * Utilities for email formatting with proper text direction handling + */ + +import DOMPurify from 'isomorphic-dompurify'; + +// Interface definitions +export interface EmailAddress { + name: string; + address: string; +} + +export interface EmailMessage { + id: string; + messageId?: string; + subject: string; + from: EmailAddress[]; + to: EmailAddress[]; + cc?: EmailAddress[]; + bcc?: EmailAddress[]; + date: Date | string; + flags?: { + seen: boolean; + flagged: boolean; + answered: boolean; + deleted: boolean; + draft: boolean; + }; + preview?: string; + content?: string; + html?: string; + text?: string; + hasAttachments?: boolean; + attachments?: any[]; + folder?: string; + size?: number; + contentFetched?: boolean; +} + +/** + * Format email addresses for display + */ +export function formatEmailAddresses(addresses: EmailAddress[]): string { + if (!addresses || addresses.length === 0) return ''; + + return addresses.map(addr => + addr.name && addr.name !== addr.address + ? `${addr.name} <${addr.address}>` + : addr.address + ).join(', '); +} + +/** + * Format date for display + */ +export function formatEmailDate(date: Date | string | undefined): string { + if (!date) return ''; + + try { + const dateObj = typeof date === 'string' ? new Date(date) : date; + return dateObj.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch (e) { + return typeof date === 'string' ? date : date.toString(); + } +} + +/** + * Clean HTML content to prevent RTL/LTR issues + */ +export function cleanHtmlContent(content: string): string { + if (!content) return ''; + + // First sanitize the HTML + const sanitized = DOMPurify.sanitize(content); + + // Process content to ensure consistent direction + let processed = sanitized; + + // Replace RTL attributes with LTR + processed = processed.replace(/dir\s*=\s*["']rtl["']/gi, 'dir="ltr"'); + processed = processed.replace(/style\s*=\s*["']([^"']*)direction\s*:\s*rtl;?([^"']*)["']/gi, + (match, before, after) => `style="${before}direction: ltr;${after}"`); + + return processed; +} + +/** + * Format an email for forwarding + */ +export function formatForwardedEmail(email: EmailMessage): { + subject: string; + content: string; +} { + // Format subject with Fwd: prefix if needed + const subjectBase = email.subject || '(No subject)'; + const subject = subjectBase.match(/^(Fwd|FW|Forward):/i) + ? subjectBase + : `Fwd: ${subjectBase}`; + + // Get sender and recipient information + const fromString = formatEmailAddresses(email.from || []); + const toString = formatEmailAddresses(email.to || []); + const dateString = formatEmailDate(email.date); + + // Get and clean original content + const originalContent = cleanHtmlContent(email.content || email.html || email.text || ''); + + // Create formatted content with explicit LTR formatting + const content = ` +
+
+
+
+
---------- Forwarded message ---------
+
From: ${fromString}
+
Date: ${dateString}
+
Subject: ${email.subject || ''}
+
To: ${toString}
+
+ +
+
+ `; + + return { subject, content }; +} + +/** + * Format an email for reply or reply-all + */ +export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all'): { + to: string; + cc?: string; + subject: string; + content: string; +} { + // Format subject with Re: prefix if needed + const subjectBase = email.subject || '(No subject)'; + const subject = subjectBase.match(/^Re:/i) + ? subjectBase + : `Re: ${subjectBase}`; + + // Get sender information for quote header + const sender = email.from[0]; + const fromText = sender?.name + ? `${sender.name} <${sender.address}>` + : sender?.address || 'Unknown sender'; + + // Format date for quote header + const date = typeof email.date === 'string' ? new Date(email.date) : email.date; + const formattedDate = date.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + + // Create quote header + const quoteHeader = `
On ${formattedDate}, ${fromText} wrote:
`; + + // Get and clean original content + const quotedContent = cleanHtmlContent(email.html || email.content || email.text || ''); + + // Format recipients + let to = formatEmailAddresses(email.from || []); + let cc = ''; + + if (type === 'reply-all') { + // For reply-all, add all original recipients to CC + const allRecipients = [ + ...(email.to || []), + ...(email.cc || []) + ]; + + cc = formatEmailAddresses(allRecipients); + } + + // Format content with explicit LTR for quoted parts + const content = ` +
+
+
${quoteHeader}
+
+
+ ${quotedContent} +
+
+
+ `; + + return { + to, + cc: cc || undefined, + subject, + content + }; +} \ No newline at end of file