'use client'; import { useState, useRef, useEffect } from 'react'; import { X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2, ChevronRight } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import DOMPurify from 'isomorphic-dompurify'; import { Label } from '@/components/ui/label'; import { sanitizeHtml, formatEmailAddresses } from "@/lib/utils/email-formatter"; // Import sub-components import ComposeEmailHeader from './ComposeEmailHeader'; import ComposeEmailForm from './ComposeEmailForm'; import ComposeEmailFooter from './ComposeEmailFooter'; import RichEmailEditor from './RichEmailEditor'; import QuotedEmailContent from './QuotedEmailContent'; // Import ONLY from the centralized formatter import { formatReplyEmail, formatForwardedEmail, type EmailMessage, type EmailAddress } from '@/lib/utils/email-formatter'; /** * CENTRAL EMAIL COMPOSER COMPONENT * * This is the unified, centralized email composer component used throughout the application. * It handles new emails, replies, and forwards with proper text direction. * * All code that needs to compose emails should import this component from: * @/components/email/ComposeEmail * * It uses the centralized email formatter from @/lib/utils/email-formatter.ts * for consistent handling of email content and text direction. */ // Define interface for the legacy props interface LegacyComposeEmailProps { 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: any) => Promise; onCancel: () => void; replyTo?: any | null; forwardFrom?: any | null; } // Define interface for the modern props interface ComposeEmailProps { initialEmail?: EmailMessage | null; type?: 'new' | 'reply' | 'reply-all' | 'forward'; onClose: () => void; onSend: (emailData: { to: string; cc?: string; bcc?: string; subject: string; body: string; attachments?: Array<{ name: string; content: string; type: string; }>; }) => Promise; } // Union type for handling both types of props type ComposeEmailAllProps = ComposeEmailProps | LegacyComposeEmailProps; // Type guard to check if props are legacy function isLegacyProps( props: ComposeEmailAllProps ): props is LegacyComposeEmailProps { return 'showCompose' in props; } // Helper function to adapt EmailMessage to QuotedEmailContent props format function EmailMessageToQuotedContentAdapter({ email, type }: { email: EmailMessage, type: 'reply' | 'reply-all' | 'forward' }) { // Get the email content const content = email.content || email.html || email.text || ''; // Get the sender const sender = email.from && email.from.length > 0 ? { name: email.from[0].name, email: email.from[0].address } : { email: 'unknown@example.com' }; // Map the type to what QuotedEmailContent expects const mappedType = type === 'reply-all' ? 'reply' : type; return ( ); } export default function ComposeEmail(props: ComposeEmailAllProps) { // Handle legacy props by adapting them to new component if (isLegacyProps(props)) { return ; } // Continue with modern implementation for new props const { initialEmail, type = 'new', onClose, onSend } = props; // Email form state const [to, setTo] = useState([]); const [cc, setCc] = useState([]); const [bcc, setBcc] = useState([]); const [subject, setSubject] = useState(''); const [emailContent, setEmailContent] = useState(''); const [showCc, setShowCc] = useState(false); const [showBcc, setShowBcc] = useState(false); const [sending, setSending] = useState(false); const [attachments, setAttachments] = useState>([]); const fileInputRef = useRef(null); // Initialize the form when replying to or forwarding an email useEffect(() => { if (initialEmail && type !== 'new') { try { // Set recipients based on email type if (type === 'reply') { // Reply only to sender setTo(initialEmail.from || []); setCc([]); setBcc([]); } else if (type === 'reply-all') { // Reply to sender and all recipients, excluding current user setTo(initialEmail.from || []); // Set CC to all original recipients setCc(initialEmail.cc || []); // Enable CC field if there are recipients if ((initialEmail.cc && initialEmail.cc.length > 0) || (initialEmail.to && initialEmail.to.length > 0)) { setShowCc(true); } } else if (type === 'forward') { // Forward doesn't preset recipients setTo([]); setCc([]); setBcc([]); } // Set subject based on email type if (type === 'reply' || type === 'reply-all') { setSubject(initialEmail.subject ? `Re: ${initialEmail.subject.replace(/^Re: /i, '')}` : ''); } else if (type === 'forward') { setSubject(initialEmail.subject ? `Fwd: ${initialEmail.subject.replace(/^Fwd: /i, '')}` : ''); } // Set initial content - just an empty div, QuotedEmailContent will be added in the render setEmailContent('
'); } catch (error) { console.error('Error initializing compose form:', error); // Set safe defaults setTo([]); setCc([]); setBcc([]); setSubject(''); setEmailContent('
'); } } }, [initialEmail, type]); // Handle file input change const handleAttachmentChange = (event: React.ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { handleAttachmentAdd(event.target.files); } }; // Add attachment const handleAttachmentAdd = async (files: FileList) => { // Implementation for adding attachments // Code omitted for brevity }; // Remove attachment const handleAttachmentRemove = (index: number) => { setAttachments(prev => prev.filter((_, i) => i !== index)); }; // Handle send email const handleSend = async () => { if (!to.length) { // Validation error: no recipients alert('Please add at least one recipient'); return; } try { setSending(true); // Format addresses to strings for the API const formattedTo = to.map(addr => addr.name && addr.name !== addr.address ? `${addr.name} <${addr.address}>` : addr.address).join(', '); const formattedCc = cc.length ? cc.map(addr => addr.name && addr.name !== addr.address ? `${addr.name} <${addr.address}>` : addr.address).join(', ') : undefined; const formattedBcc = bcc.length ? bcc.map(addr => addr.name && addr.name !== addr.address ? `${addr.name} <${addr.address}>` : addr.address).join(', ') : undefined; await onSend({ to: formattedTo, cc: formattedCc, bcc: formattedBcc, subject, body: emailContent, attachments }); onClose(); } catch (error) { console.error('Error sending email:', error); alert('Failed to send email. Please try again.'); } finally { setSending(false); } }; // Render the modern compose form return (
{/* Header */}

{type === 'reply' && } {type === 'reply-all' && } {type === 'forward' && } {type === 'reply' ? 'Reply' : type === 'reply-all' ? 'Reply All' : type === 'forward' ? 'Forward' : 'New Message'}

{/* Body */}
{/* To */}
addr.name && addr.name !== addr.address ? `${addr.name} <${addr.address}>` : addr.address).join(', ')} onChange={(e) => { // Parse email addresses const addresses = e.target.value.split(',').map(addr => { addr = addr.trim(); const match = addr.match(/(.*)<(.*)>/); if (match) { return { name: match[1].trim(), address: match[2].trim() }; } return { name: addr, address: addr }; }); setTo(addresses); }} placeholder="Recipients" className="w-full" />
{/* CC and BCC toggles */}
{!showCc && ( )} {!showBcc && ( )}
{/* CC */} {showCc && (
addr.name && addr.name !== addr.address ? `${addr.name} <${addr.address}>` : addr.address).join(', ')} onChange={(e) => { // Parse email addresses const addresses = e.target.value.split(',').map(addr => { addr = addr.trim(); const match = addr.match(/(.*)<(.*)>/); if (match) { return { name: match[1].trim(), address: match[2].trim() }; } return { name: addr, address: addr }; }); setCc(addresses); }} placeholder="Carbon copy recipients" className="w-full" />
)} {/* BCC */} {showBcc && (
addr.name && addr.name !== addr.address ? `${addr.name} <${addr.address}>` : addr.address).join(', ')} onChange={(e) => { // Parse email addresses const addresses = e.target.value.split(',').map(addr => { addr = addr.trim(); const match = addr.match(/(.*)<(.*)>/); if (match) { return { name: match[1].trim(), address: match[2].trim() }; } return { name: addr, address: addr }; }); setBcc(addresses); }} placeholder="Blind carbon copy recipients" className="w-full" />
)} {/* Subject */}
setSubject(e.target.value)} placeholder="Subject" className="w-full" />
{/* Email Content */}
setEmailContent(e.currentTarget.innerHTML)} /> {/* Quoted content for replies and forwards */} {initialEmail && (type === 'reply' || type === 'reply-all' || type === 'forward') && ( )}
{/* Attachments */}
{attachments.length > 0 && (
{attachments.map((attachment, index) => (
{attachment.name}
))}
)}
{/* Footer */}
); } // Legacy adapter to maintain backward compatibility function LegacyAdapter({ showCompose, setShowCompose, composeTo, setComposeTo, composeCc, setComposeCc, composeBcc, setComposeBcc, composeSubject, setComposeSubject, composeBody, setComposeBody, showCc, setShowCc, showBcc, setShowBcc, attachments, setAttachments, handleSend, originalEmail, onSend, onCancel, replyTo, forwardFrom }: LegacyComposeEmailProps) { const [sending, setSending] = useState(false); // Determine the type based on legacy props const determineType = (): 'new' | 'reply' | 'reply-all' | 'forward' => { if (originalEmail?.type === 'forward') return 'forward'; if (originalEmail?.type === 'reply-all') return 'reply-all'; if (originalEmail?.type === 'reply') return 'reply'; if (replyTo) return 'reply'; if (forwardFrom) return 'forward'; return 'new'; }; // Format legacy content on mount if necessary useEffect(() => { // Only format if we have original email and no content was set yet if ((originalEmail || replyTo || forwardFrom) && (!composeBody || composeBody === '

' || composeBody === '
')) { const type = determineType(); if (type === 'reply' || type === 'reply-all') { // For reply, format with sender info and original content const content = originalEmail?.content || ''; const sender = replyTo?.name || replyTo?.email || 'Unknown sender'; const date = new Date().toLocaleString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); const replyContent = `




On ${date}, ${sender} wrote:
${content}
`; setComposeBody(replyContent); } else if (type === 'forward') { // For forward, format with original message details const content = originalEmail?.content || ''; const fromString = forwardFrom?.name || forwardFrom?.email || 'Unknown'; const toString = 'Recipients'; const date = new Date().toLocaleString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); const forwardContent = `




---------- Forwarded message ---------
From: ${fromString}
Date: ${date}
Subject: ${composeSubject || ''}
To: ${toString}
`; setComposeBody(forwardContent); } } }, [originalEmail, replyTo, forwardFrom, composeBody, determineType, composeSubject]); // Converts attachments to the expected format const convertAttachments = () => { return attachments.map(att => ({ name: att.name || att.filename || 'attachment', content: att.content || '', type: att.type || att.contentType || 'application/octet-stream' })); }; // Handle sending in the legacy format const handleLegacySend = async () => { setSending(true); try { if (onSend) { // New API await onSend({ to: composeTo, cc: composeCc, bcc: composeBcc, subject: composeSubject, body: composeBody, attachments: convertAttachments() }); } else if (handleSend) { // Old API await handleSend(); } // Close compose window setShowCompose(false); } catch (error) { console.error('Error sending email:', error); alert('Failed to send email. Please try again.'); } finally { setSending(false); } }; // Handle file selection for legacy interface const handleFileSelection = (files: FileList) => { const newAttachments = Array.from(files).map(file => ({ name: file.name, type: file.type, content: URL.createObjectURL(file), size: file.size })); setAttachments([...attachments, ...newAttachments]); }; if (!showCompose) return null; return (
{/* Modal Header */}

{determineType() === 'reply' ? 'Reply' : determineType() === 'forward' ? 'Forward' : determineType() === 'reply-all' ? 'Reply All' : '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 */}
{/* Attachments */} {attachments.length > 0 && (

Attachments

{attachments.map((file, index) => (
{file.name || file.filename}
))}
)}
{/* Modal Footer */}
{/* File Input for Attachments */} { if (e.target.files && e.target.files.length > 0) { handleFileSelection(e.target.files); } }} /> {sending && Preparing attachment...}
); }