diff --git a/components/email/RichEmailEditor.tsx b/components/email/RichEmailEditor.tsx index e661a1e2..6cfb0693 100644 --- a/components/email/RichEmailEditor.tsx +++ b/components/email/RichEmailEditor.tsx @@ -79,80 +79,74 @@ const RichEmailEditor: React.FC = ({ theme: 'snow', }); - // Set initial content (sanitized) + // Set initial content properly if (initialContent) { try { - // First, ensure we preserve the raw HTML structure - const preservedContent = sanitizeHtml(initialContent); + console.log('Setting initial content in editor', { + length: initialContent.length, + startsWithHtml: initialContent.trim().startsWith('<') + }); - // Check if there are tables in the content - const hasTables = preservedContent.includes(')<[^<]*)*<\/script>/gi, '') // Remove scripts + .replace(/on\w+="[^"]*"/g, '') // Remove event handlers + .replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:'); // Remove protocol handlers - // For content with tables, we need special handling - if (hasTables && preserveFormatting && tableModule) { - // First, set the content directly to the root - quillRef.current.root.innerHTML = preservedContent; - - // Initialize better table module after content is set - setTimeout(() => { - try { - // Clean up any existing tables first - const tables = quillRef.current.root.querySelectorAll('table'); - tables.forEach((table: HTMLTableElement) => { - // Add required data attributes that the module expects - if (!table.getAttribute('data-table')) { - table.setAttribute('data-table', 'true'); - } - }); - - // Initialize the module now that content is already in place - const betterTableModule = { - operationMenu: { - items: { - unmergeCells: { - text: 'Unmerge cells' - } - } - } - }; - - // Force a refresh - quillRef.current.update(); - - // Ensure the cursor and scroll position is at the top of the editor - quillRef.current.setSelection(0, 0); - - // Also scroll the container to the top - if (editorRef.current) { - editorRef.current.scrollTop = 0; - - // Also find and scroll parent containers that might have scroll - const scrollContainer = editorRef.current.closest('.ql-container'); - if (scrollContainer) { - scrollContainer.scrollTop = 0; - } - - // One more check for nested scroll containers (like overflow divs) - const parentScrollContainer = editorRef.current.closest('.rich-email-editor-container'); - if (parentScrollContainer) { - parentScrollContainer.scrollTop = 0; - } - } - } catch (tableErr) { - console.error('Error initializing table module:', tableErr); - } - }, 100); - } else { - // For content without tables, use the standard paste method - quillRef.current.clipboard.dangerouslyPasteHTML(0, preservedContent); - quillRef.current.setSelection(0, 0); + // First, directly set the content + if (editorRef.current) { + editorRef.current.innerHTML = cleanContent; } + + // Then let Quill parse and format it correctly + setTimeout(() => { + // Only proceed if editor ref is still available + if (!editorRef.current) return; + + // Get the content from the editor element + const content = editorRef.current.innerHTML; + + // Clear the editor + quillRef.current.setText(''); + + // Insert clean content + quillRef.current.clipboard.dangerouslyPasteHTML(0, content); + + // Set cursor at the beginning (before the quoted content) + quillRef.current.setSelection(0, 0); + + // Ensure the cursor and scroll position is at the top of the editor + if (editorRef.current) { + editorRef.current.scrollTop = 0; + + // Find and scroll parent containers that might have scroll + const scrollable = [ + editorRef.current.closest('.ql-container'), + editorRef.current.closest('.rich-email-editor-container'), + editorRef.current.closest('.overflow-y-auto'), + document.querySelector('.overflow-y-auto') + ]; + + scrollable.forEach(el => { + if (el instanceof HTMLElement) { + el.scrollTop = 0; + } + }); + } + }, 100); } catch (err) { console.error('Error setting initial content:', err); - // Fallback method if the above fails + // Fallback: just set text quillRef.current.setText(''); - quillRef.current.clipboard.dangerouslyPasteHTML(sanitizeHtml(initialContent)); - quillRef.current.setSelection(0, 0); + + // Try simplest approach + try { + quillRef.current.clipboard.dangerouslyPasteHTML(initialContent); + } catch (e) { + console.error('Fallback failed too:', e); + // Last resort: strip all HTML + quillRef.current.setText(initialContent.replace(/<[^>]*>/g, '')); + } } } diff --git a/lib/utils/email-utils.ts b/lib/utils/email-utils.ts index dcd2c24a..94bd5408 100644 --- a/lib/utils/email-utils.ts +++ b/lib/utils/email-utils.ts @@ -242,10 +242,23 @@ export function formatForwardedEmail(email: EmailMessage): { const toString = formatEmailAddresses(email.to || []); const dateString = formatEmailDate(email.date); - // Get original content as HTML - const originalContent = email.content.isHtml && email.content.html - ? email.content.html - : formatPlainTextToHtml(email.content.text); + // Get original content - use the raw content if possible to preserve formatting + let originalContent = ''; + + if (email.content) { + if (email.content.isHtml && email.content.html) { + originalContent = email.content.html; + } else if (email.content.text) { + // Format plain text with basic HTML formatting + originalContent = formatPlainTextToHtml(email.content.text); + } + } else if (email.html) { + originalContent = email.html; + } else if (email.text) { + originalContent = formatPlainTextToHtml(email.text); + } else { + originalContent = '

No content

'; + } // Check if the content already has a forwarded message header const hasExistingHeader = originalContent.includes('---------- Forwarded message ---------'); @@ -263,7 +276,7 @@ export function formatForwardedEmail(email: EmailMessage): { } else { // Create formatted content for forwarded email htmlContent = ` -
+
@@ -278,16 +291,22 @@ export function formatForwardedEmail(email: EmailMessage): {
-
`; } + // Ensure we have clean HTML + const cleanedHtml = DOMPurify.sanitize(htmlContent, { + ADD_TAGS: ['style'], + ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'], + ALLOW_DATA_ATTR: true + }); + // Create normalized content with HTML and extracted text const content: EmailContent = { - html: sanitizeHtml(htmlContent), + html: cleanedHtml, text: '', // Will be extracted when composing isHtml: true, - direction: email.content.direction || 'ltr' + direction: email.content?.direction || 'ltr' }; // Extract text from HTML if in browser environment @@ -322,13 +341,13 @@ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all let cc = undefined; if (type === 'reply-all' && (email.to || email.cc)) { const allRecipients = [ - ...(email.to || []), - ...(email.cc || []) + ...(typeof email.to === 'string' ? [{name: '', address: email.to}] : (email.to || [])), + ...(typeof email.cc === 'string' ? [{name: '', address: email.cc}] : (email.cc || [])) ]; // Remove duplicates, then convert to string const uniqueRecipients = [...new Map(allRecipients.map(addr => - [addr.address, addr] + [typeof addr === 'string' ? addr : addr.address, addr] )).values()]; cc = formatEmailAddresses(uniqueRecipients); @@ -338,14 +357,35 @@ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all const subjectBase = email.subject || '(No subject)'; const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`; - // Get original content as HTML - const originalContent = email.content.isHtml && email.content.html - ? email.content.html - : formatPlainTextToHtml(email.content.text); + // Get original content - use the raw content if possible to preserve formatting + let originalContent = ''; + + if (email.content) { + if (email.content.isHtml && email.content.html) { + originalContent = email.content.html; + } else if (email.content.text) { + // Format plain text with basic HTML formatting + originalContent = formatPlainTextToHtml(email.content.text); + } + } else if (email.html) { + originalContent = email.html; + } else if (email.text) { + originalContent = formatPlainTextToHtml(email.text); + } else { + originalContent = '

No content

'; + } // Format sender info - const sender = email.from && email.from.length > 0 ? email.from[0] : undefined; - const senderName = sender ? (sender.name || sender.address) : 'Unknown Sender'; + let senderName = 'Unknown Sender'; + if (email.from) { + if (Array.isArray(email.from) && email.from.length > 0) { + const sender = email.from[0]; + senderName = typeof sender === 'string' ? sender : (sender.name || sender.address); + } else if (typeof email.from === 'string') { + senderName = email.from; + } + } + const formattedDate = formatEmailDate(email.date); // Create the reply content with attribution line @@ -361,12 +401,19 @@ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all `; + // Ensure we have clean HTML + const cleanedHtml = DOMPurify.sanitize(htmlContent, { + ADD_TAGS: ['style'], + ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'], + ALLOW_DATA_ATTR: true + }); + // Create normalized content with HTML and extracted text const content: EmailContent = { - html: sanitizeHtml(htmlContent), + html: cleanedHtml, text: '', // Will be extracted when composing isHtml: true, - direction: email.content.direction || 'ltr' + direction: email.content?.direction || 'ltr' }; // Extract text from HTML if in browser environment