diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index 8b0ec09f..a080df68 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -56,50 +56,122 @@ interface ComposeEmailProps { export default function ComposeEmail(props: ComposeEmailProps) { const { initialEmail, type = 'new', onClose, onSend, accounts = [] } = 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 [selectedAccount, setSelectedAccount] = useState<{ - id: string; - email: string; - display_name?: string; - } | null>(accounts.length > 0 ? accounts[0] : null); - const [attachments, setAttachments] = useState>([]); + // State for email form + const [selectedAccount, setSelectedAccount] = useState(accounts[0]); + 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>([]); + // Reference to editor const editorRef = useRef(null); + + // Helper function to get formatted info from email + function getFormattedInfoForEmail(email: any) { + // Format the subject + const subject = email.subject || ''; + + // Format the date + const dateStr = email.date ? new Date(email.date).toLocaleString() : 'Unknown Date'; + + // Format sender + const fromStr = Array.isArray(email.from) + ? email.from.map((addr: any) => { + if (typeof addr === 'string') return addr; + return addr.name ? `${addr.name} <${addr.address}>` : addr.address; + }).join(', ') + : typeof email.from === 'string' + ? email.from + : email.from?.address + ? email.from.name + ? `${email.from.name} <${email.from.address}>` + : email.from.address + : 'Unknown Sender'; + + // Format recipients + const toStr = Array.isArray(email.to) + ? email.to.map((addr: any) => { + if (typeof addr === 'string') return addr; + return addr.name ? `${addr.name} <${addr.address}>` : addr.address; + }).join(', ') + : typeof email.to === 'string' + ? email.to + : ''; + + // Format CC + const ccStr = Array.isArray(email.cc) + ? email.cc.map((addr: any) => { + if (typeof addr === 'string') return addr; + return addr.name ? `${addr.name} <${addr.address}>` : addr.address; + }).join(', ') + : typeof email.cc === 'string' + ? email.cc + : ''; + + return { fromStr, toStr, ccStr, dateStr, subject }; + } - // Initialize the form when replying to or forwarding an email + // Initialize email form based on initial email and type useEffect(() => { - if (initialEmail && type !== 'new') { + if (initialEmail) { try { - // Set recipients based on type + console.log('Initializing compose with email:', { + id: initialEmail.id, + subject: initialEmail.subject, + hasContent: !!initialEmail.content, + contentType: initialEmail.content ? typeof initialEmail.content : 'none' + }); + + // Set default account from original email - use type assertion since accountId might be custom property + const emailAny = initialEmail as any; + if (emailAny.accountId && accounts?.length) { + const account = accounts.find(a => a.id === emailAny.accountId); + if (account) { + setSelectedAccount(account); + } + } + + // Get recipients based on type if (type === 'reply' || type === 'reply-all') { // Get formatted data for reply const formatted = formatReplyEmail(initialEmail, type); - // Set the recipients + // Set reply addresses setTo(formatted.to); if (formatted.cc) { - setCc(formatted.cc); setShowCc(true); + setCc(formatted.cc); } // Set subject setSubject(formatted.subject); - // Set content with original email - const content = formatted.content.html || formatted.content.text; - setEmailContent(content); + // Set content with original email - ensure we have content + const content = formatted.content.html || formatted.content.text || ''; + + if (!content) { + console.warn('Reply content is empty, falling back to a basic template'); + // Provide a basic template if the content is empty + const { fromStr, dateStr } = getFormattedInfoForEmail(initialEmail); + const fallbackContent = ` +
On ${dateStr}, ${fromStr} wrote:
+
+ [Original message content could not be loaded] +
+ `; + setEmailContent(fallbackContent); + } else { + console.log('Setting reply content:', { + length: content.length, + isHtml: formatted.content.isHtml + }); + setEmailContent(content); + } } else if (type === 'forward') { // Get formatted data for forward @@ -108,9 +180,34 @@ export default function ComposeEmail(props: ComposeEmailProps) { // Set subject setSubject(formatted.subject); - // Set content with original email - const content = formatted.content.html || formatted.content.text; - setEmailContent(content); + // Set content with original email - ensure we have content + const content = formatted.content.html || formatted.content.text || ''; + + if (!content) { + console.warn('Forward content is empty, falling back to a basic template'); + // Provide a basic template if the content is empty + const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedInfoForEmail(initialEmail); + const fallbackContent = ` +
+
---------- Forwarded message ---------
+
From: ${fromStr}
+
Date: ${dateStr}
+
Subject: ${subject || ''}
+
To: ${toStr}
+ ${ccStr ? `
Cc: ${ccStr}
` : ''} +
+
+ [Original message content could not be loaded] +
+ `; + setEmailContent(fallbackContent); + } else { + console.log('Setting forward content:', { + length: content.length, + isHtml: formatted.content.isHtml + }); + setEmailContent(content); + } // If the original email has attachments, include them if (initialEmail.attachments && initialEmail.attachments.length > 0) { @@ -124,9 +221,11 @@ export default function ComposeEmail(props: ComposeEmailProps) { } } catch (error) { console.error('Error initializing compose form:', error); + // Provide a fallback in case of error + setEmailContent('

Error loading email content

'); } } - }, [initialEmail, type]); + }, [initialEmail, type, accounts]); // Place cursor at beginning and ensure content is scrolled to top useEffect(() => { diff --git a/components/email/RichEmailEditor.tsx b/components/email/RichEmailEditor.tsx index f83906f6..b6a6d46b 100644 --- a/components/email/RichEmailEditor.tsx +++ b/components/email/RichEmailEditor.tsx @@ -99,13 +99,30 @@ const RichEmailEditor: React.FC = ({ // Simplify complex email content to something Quill can handle better const sanitizedContent = sanitizeHtml(processedContent || initialContent); - // Use direct innerHTML setting for the initial content - quillRef.current.root.innerHTML = sanitizedContent; - - // Set the direction for the content - quillRef.current.format('direction', direction); - if (direction === 'rtl') { - quillRef.current.format('align', 'right'); + // Check if sanitized content is valid + if (sanitizedContent.trim().length === 0) { + console.warn('Sanitized content is empty after processing, using fallback approach'); + // Try to extract text content if HTML processing failed + try { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = initialContent; + const textContent = tempDiv.textContent || tempDiv.innerText || 'Empty content'; + + // Set text directly to ensure something displays + quillRef.current.setText(textContent); + } catch (e) { + console.error('Text extraction fallback failed:', e); + quillRef.current.setText('Error loading content'); + } + } else { + // Use direct innerHTML setting for the initial content + quillRef.current.root.innerHTML = sanitizedContent; + + // Set the direction for the content + quillRef.current.format('direction', direction); + if (direction === 'rtl') { + quillRef.current.format('align', 'right'); + } } // Set cursor at the beginning @@ -131,19 +148,25 @@ const RichEmailEditor: React.FC = ({ } } catch (err) { console.error('Error setting initial content:', err); - // Fallback: just set text - quillRef.current.setText(''); - // Extract text as a last resort + // Enhanced fallback mechanism for complex content try { - // Create a temporary div to extract text from HTML + // First try to extract text from HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = initialContent; const textContent = tempDiv.textContent || tempDiv.innerText || ''; - quillRef.current.setText(textContent); + + if (textContent.trim()) { + console.log('Using extracted text fallback, length:', textContent.length); + quillRef.current.setText(textContent); + } else { + // If text extraction fails or returns empty, provide a message + console.log('Using empty content fallback'); + quillRef.current.setText('Unable to load original content'); + } } catch (e) { - console.error('Fallback failed too:', e); - quillRef.current.setText(''); + console.error('All fallbacks failed:', e); + quillRef.current.setText('Error loading content'); } } } @@ -184,19 +207,36 @@ const RichEmailEditor: React.FC = ({ // Only update if content changed to avoid editor position reset if (initialContent !== currentContent) { try { + console.log('Updating content in editor:', { + contentLength: initialContent.length, + startsWithHtml: initialContent.trim().startsWith('<') + }); + // Process content to ensure correct direction const { direction, html: processedContent } = processContentWithDirection(initialContent); // Sanitize the HTML const sanitizedContent = sanitizeHtml(processedContent || initialContent); - // SIMPLIFIED: Set content directly to the root element rather than using clipboard - quillRef.current.root.innerHTML = sanitizedContent; - - // Set the direction for the content - quillRef.current.format('direction', direction); - if (direction === 'rtl') { - quillRef.current.format('align', 'right'); + // Check if content is valid HTML + if (sanitizedContent.trim().length === 0) { + console.warn('Sanitized content is empty, using original content'); + // If sanitized content is empty, try to extract text from original + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = initialContent; + const textContent = tempDiv.textContent || tempDiv.innerText || ''; + + // Create simple HTML with text content + quillRef.current.setText(textContent); + } else { + // SIMPLIFIED: Set content directly to the root element rather than using clipboard + quillRef.current.root.innerHTML = sanitizedContent; + + // Set the direction for the content + quillRef.current.format('direction', direction); + if (direction === 'rtl') { + quillRef.current.format('align', 'right'); + } } // Force update @@ -215,6 +255,8 @@ const RichEmailEditor: React.FC = ({ quillRef.current.setText(textContent); } catch (e) { console.error('All fallbacks failed:', e); + // Last resort + quillRef.current.setText('Error loading content'); } } } diff --git a/lib/utils/email-content.ts b/lib/utils/email-content.ts index 947003cd..926755b8 100644 --- a/lib/utils/email-content.ts +++ b/lib/utils/email-content.ts @@ -75,7 +75,35 @@ export function formatEmailContent(email: any): string { } // Use the centralized sanitizeHtml function - const sanitizedContent = sanitizeHtml(content); + let sanitizedContent = sanitizeHtml(content); + + // Fix URL encoding issues that might occur during sanitization + try { + // Temporary element to manipulate the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = sanitizedContent; + + // Fix all links + const links = tempDiv.querySelectorAll('a'); + links.forEach(link => { + const href = link.getAttribute('href'); + if (href && href.includes('%')) { + try { + // Try to decode URLs that might have been double-encoded + const decodedHref = decodeURIComponent(href); + link.setAttribute('href', decodedHref); + } catch (e) { + // If decoding fails, keep the original + console.warn('Failed to decode href:', href); + } + } + }); + + // Get the fixed HTML + sanitizedContent = tempDiv.innerHTML; + } catch (e) { + console.error('Error fixing URLs in content:', e); + } // Fix common email client quirks let fixedContent = sanitizedContent diff --git a/lib/utils/email-utils.ts b/lib/utils/email-utils.ts index 78ec94e7..18d2aa51 100644 --- a/lib/utils/email-utils.ts +++ b/lib/utils/email-utils.ts @@ -279,16 +279,51 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag // Extract content using centralized utility const { text: originalTextContent, html: originalHtmlContent } = extractEmailContent(originalEmail); - // Create a simpler HTML structure that's easier for Quill to handle - const replyBody = ` -
- - `; + `; + } else { + // Empty or unrecognized content + replyBody = ` + ${headerHtml} +
+ [Original message content not available] +
+ `; + } // Process the content with proper direction const processed = processContentWithDirection(replyBody); @@ -335,21 +370,60 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe // Extract content using centralized utility const { text: originalTextContent, html: originalHtmlContent } = extractEmailContent(originalEmail); - // Create a simpler forwarded content structure for better Quill compatibility - const forwardBody = ` -
- - `; + `; + } else { + // Empty or unrecognized content + forwardBody = ` + ${headerHtml} +
+ [Original message content not available] +
+ `; + } // Process the content with proper direction const processed = processContentWithDirection(forwardBody); diff --git a/lib/utils/text-direction.ts b/lib/utils/text-direction.ts index ec5d4ddb..60a9abc5 100644 --- a/lib/utils/text-direction.ts +++ b/lib/utils/text-direction.ts @@ -78,8 +78,61 @@ export function extractEmailContent(email: any): { text: string; html: string } // Extract based on common formats if (email) { if (typeof email.content === 'object' && email.content) { + // Standard format with content object textContent = email.content.text || ''; htmlContent = email.content.html || ''; + + // Handle complex email formats where content might be nested + if (!textContent && !htmlContent) { + // Try to find content in deeper nested structure + if (email.content.body) { + if (typeof email.content.body === 'string') { + // Determine if body is HTML or text + if (email.content.body.includes('<') && ( + email.content.body.includes(']*>/g, ' ') + .replace(/\s+/g, ' ') + .trim() || '[Email content]'; + } + } + + // Add debug logging to help troubleshoot content extraction + console.log('Extracted email content:', { + hasHtml: !!htmlContent, + htmlLength: htmlContent.length, + hasText: !!textContent, + textLength: textContent.length + }); + return { text: textContent, html: htmlContent }; } @@ -133,7 +216,8 @@ export function processContentWithDirection(content: string | EmailContent | nul if (content.includes('<') && ( content.includes('') )) { htmlContent = content; } else { @@ -145,14 +229,73 @@ export function processContentWithDirection(content: string | EmailContent | nul htmlContent = content.html || ''; } + // Handle complex email content that might not be properly detected + if (!textContent && !htmlContent && typeof content === 'object') { + console.log('Processing complex content object:', content); + + // Try to extract content from complex object structure + try { + // Check for common email content formats + // Type assertion to 'any' since we need to handle various email formats + const contentAny = content as any; + + if (contentAny.body) { + if (typeof contentAny.body === 'string') { + // Detect if body is HTML or text + if (contentAny.body.includes('<') && ( + contentAny.body.includes(']*>/g, '') - .replace(/ /g, ' ') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/&/g, '&'); + try { + // Use DOM API if available + if (typeof document !== 'undefined') { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = htmlContent; + textContent = tempDiv.textContent || tempDiv.innerText || ''; + } else { + // Simple regex fallback for non-browser environments + textContent = htmlContent.replace(/<[^>]*>/g, ' ') + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/\s+/g, ' ') + .trim(); + } + } catch (e) { + console.error('Error extracting text from HTML:', e); + textContent = 'Failed to extract text content'; + } } // Detect direction from text @@ -160,16 +303,35 @@ export function processContentWithDirection(content: string | EmailContent | nul // Sanitize HTML if present if (htmlContent) { - // Sanitize HTML first - htmlContent = sanitizeHtml(htmlContent); - - // Then apply direction - htmlContent = applyTextDirection(htmlContent, textContent); + try { + // Sanitize HTML first using the centralized function + htmlContent = sanitizeHtml(htmlContent); + + // Then apply direction + htmlContent = applyTextDirection(htmlContent, textContent); + } catch (error) { + console.error('Error sanitizing HTML content:', error); + // Create fallback content if sanitization fails + htmlContent = `
${ + textContent ? + textContent.replace(/\n/g, '
') : + 'Could not process HTML content' + }
`; + } } else if (textContent) { // Convert plain text to HTML with proper direction htmlContent = `
${textContent.replace(/\n/g, '
')}
`; } + // Add debug logging for troubleshooting + console.log('Processed content:', { + direction, + htmlLength: htmlContent.length, + textLength: textContent.length, + hasHtml: !!htmlContent, + hasText: !!textContent + }); + // Return processed content return { text: textContent,