From ddcb428233c80f12052ff60430253cb32d0c8334 Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 1 May 2025 17:01:26 +0200 Subject: [PATCH] courrier preview --- components/email/RichEmailEditor.tsx | 81 +++++++++++++++-- lib/utils/dom-purify-config.ts | 17 +++- lib/utils/email-content.ts | 18 +++- lib/utils/email-utils.ts | 131 +++++++++++++-------------- 4 files changed, 160 insertions(+), 87 deletions(-) diff --git a/components/email/RichEmailEditor.tsx b/components/email/RichEmailEditor.tsx index 64b12a6e..3605cb9f 100644 --- a/components/email/RichEmailEditor.tsx +++ b/components/email/RichEmailEditor.tsx @@ -35,16 +35,38 @@ function cleanupTableStructures(htmlContent: string): string { htmlContent.includes('forwarded message') || (htmlContent.includes('From:') && htmlContent.includes('Date:') && htmlContent.includes('Subject:')); + const isReplyEmail = + htmlContent.includes('wrote:') || + htmlContent.includes(' 0 && - (isForwardedEmail || htmlContent.includes('gmail_quote') || - htmlContent.includes('blockquote') || htmlContent.includes('wrote:')); + (isForwardedEmail || isReplyEmail); if (hasComplexTables) { console.log(`Found ${tables.length} tables in complex email content`); let convertedCount = 0; tables.forEach(table => { + // Special handling for tables inside blockquotes (quoted content) + if (table.closest('blockquote') || + (isReplyEmail && table.innerHTML.includes('wrote:'))) { + console.log('Preserving table inside quoted content'); + // Apply minimal styling to ensure it renders correctly + table.setAttribute('style', 'border-collapse: collapse; width: 100%; max-width: 100%; margin: 8px 0;'); + // Make sure all cells have some basic styling + const cells = table.querySelectorAll('td, th'); + cells.forEach(cell => { + if (cell instanceof HTMLTableCellElement) { + cell.style.padding = '4px'; + cell.style.textAlign = 'left'; + cell.style.verticalAlign = 'top'; + } + }); + return; + } + // Preserve the main forwarded email header table if (isForwardedEmail && table.innerHTML.includes('From:') && @@ -180,8 +202,18 @@ const RichEmailEditor: React.FC = ({ hasBlockquote: initialContent.includes(' = ({ containsQuoteHeader: sanitizedContent.includes('wrote:'), hasTable: sanitizedContent.includes(' = ({ quillRef.current.setText('Error loading content'); } } else { - // Use direct innerHTML setting for the initial content - quillRef.current.root.innerHTML = sanitizedContent; + // Special handling for reply or forwarded content + if (isReplyOrForward) { + console.log('Using special handling for reply/forward content'); + + // Clean up any problematic table structures, with special care for quoted content + const cleanedContent = cleanupTableStructures(sanitizedContent); + + // Use direct innerHTML setting with minimal processing for reply/forward content + quillRef.current.root.innerHTML = cleanedContent; + } else { + // Use direct innerHTML setting for regular content + quillRef.current.root.innerHTML = sanitizedContent; + } // Set the direction for the content if (quillRef.current && quillRef.current.format) { @@ -319,8 +363,18 @@ const RichEmailEditor: React.FC = ({ firstNChars: initialContent.substring(0, 100).replace(/\n/g, '\\n') }); + // Check if content is reply or forward to use special handling + const isReplyOrForward = + initialContent.includes('wrote:') || + initialContent.includes(' = ({ containsQuoteHeader: sanitizedContent.includes('wrote:'), hasTable: sanitizedContent.includes(' = ({ } else { // SIMPLIFIED: Set content directly to the root element rather than using clipboard if (quillRef.current && quillRef.current.root) { - // Clean up any problematic table structures first - const cleanedContent = cleanupTableStructures(sanitizedContent); + // Special handling for reply or forward content + let contentToSet = sanitizedContent; + + if (isReplyOrForward) { + console.log('Using special handling for reply/forward content update'); + // Clean up tables with special care for quoted content + contentToSet = cleanupTableStructures(sanitizedContent); + } // First set the content - quillRef.current.root.innerHTML = cleanedContent; + quillRef.current.root.innerHTML = contentToSet; // Then safely apply formatting only if quillRef is valid try { diff --git a/lib/utils/dom-purify-config.ts b/lib/utils/dom-purify-config.ts index 04686d19..47842171 100644 --- a/lib/utils/dom-purify-config.ts +++ b/lib/utils/dom-purify-config.ts @@ -84,14 +84,23 @@ export const purify = configureDOMPurify(); /** * Sanitize HTML content using our email-specific configuration */ -export function sanitizeHtml(content: string): string { +export function sanitizeHtml(content: string, options?: { preserveReplyFormat?: boolean }): string { if (!content) return ''; try { - // Sanitize with our configured instance + // Special handling for reply/forward emails to be less aggressive with sanitization + const extraTags = options?.preserveReplyFormat + ? ['style', 'blockquote', 'table', 'thead', 'tbody', 'tr', 'td', 'th'] + : ['style']; + + const extraAttrs = options?.preserveReplyFormat + ? ['style', 'class', 'align', 'valign', 'bgcolor', 'colspan', 'rowspan', 'width', 'height', 'border'] + : ['style', 'class']; + + // Sanitize with our configured instance and options return purify.sanitize(content, { - ADD_TAGS: ['style'], // Allow internal styles temporarily for cleaning - ADD_ATTR: ['style', 'class'] // Allow style and class attributes + ADD_TAGS: extraTags, + ADD_ATTR: extraAttrs }); } catch (error) { console.error('Failed to sanitize HTML content:', error); diff --git a/lib/utils/email-content.ts b/lib/utils/email-content.ts index 46decf34..64c4edce 100644 --- a/lib/utils/email-content.ts +++ b/lib/utils/email-content.ts @@ -226,6 +226,7 @@ export function processHtmlContent( options?: { sanitize?: boolean; blockExternalContent?: boolean; + preserveReplyFormat?: boolean; attachments?: Array<{ filename?: string; name?: string; @@ -252,6 +253,7 @@ export function processHtmlContent( containsForwardedMessage: htmlContent?.includes('---------- Forwarded message ----------'), containsQuoteHeader: htmlContent?.includes('
On ${date}, ${sender} wrote:
- ${sanitizeHtml(htmlContent)} + ${sanitizedOriginal}
`; } @@ -477,7 +489,7 @@ export function processCidReferences(htmlContent: string, attachments?: Array<{ * Format email for forwarding */ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMessage | null): FormattedEmail { - console.log('formatForwardedEmail called:', { emailId: originalEmail?.id }); + console.log('formatForwardedEmail called, emailId:', originalEmail?.id); if (!originalEmail) { console.warn('formatForwardedEmail: No original email provided'); @@ -496,14 +508,6 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe (email.subject.toLowerCase().startsWith('fwd:') ? email.subject : `Fwd: ${email.subject}`) : 'Fwd: '; - // Get original email info for headers - const { fromStr, toStr, ccStr, dateStr } = getFormattedHeaderInfo(email); - - console.log('Forward header info:', { fromStr, toStr, dateStr, subject }); - - // Original sent date - const date = dateStr; - // Get email content const originalContent = email.content; @@ -544,75 +548,66 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe htmlContent = formatPlainTextToHtml(textContent); } } - - // Process embedded images with CID references - if (htmlContent && email.attachments && email.attachments.length > 0) { - console.log('Processing CID references before sanitization'); - htmlContent = processCidReferences(htmlContent, email.attachments); - } - - // Create the forwarded email HTML content + + // Get header info for the forwarded message + const headerInfo = getFormattedHeaderInfo(email); + + // Create the forwarded content if (htmlContent) { - console.log('Formatting HTML forward, original content length:', htmlContent.length); + console.log('Formatting HTML forward, content length:', htmlContent.length); - // Important: First sanitize the content portion only - const sanitizedOriginalContent = sanitizeHtml(htmlContent); - console.log('Sanitized original content length:', sanitizedOriginalContent.length); + // Apply minimal sanitization to the original content - preserve more structure + // We'll do a more comprehensive sanitization later in the flow + const sanitizedOriginal = sanitizeHtml(htmlContent, { preserveReplyFormat: true }); - // Create the complete forwarded email with header info - const fullForwardedEmail = ` + // Create forwarded message with header info + htmlContent = `
- ---------- Forwarded message ----------
- - - - - - - - - - - - - - - - - - - ${ccStr ? ` - - - - - ` : ''} - +
+
---------------------------- Forwarded Message ----------------------------
+
+
From:${fromStr}
Date:${date}
Subject:${email.subject || ''}
To:${toStr}
Cc:${ccStr}
+ + + + + + + + + + + + + + + + + ${headerInfo.ccStr ? ` + + + + ` : ''}
From:${headerInfo.fromStr}
Date:${headerInfo.dateStr}
Subject:${headerInfo.subject}
To:${headerInfo.toStr}
Cc:${headerInfo.ccStr}
+
+
----------------------------------------------------------------------
+
+
+
+ ${sanitizedOriginal}
-
- ${sanitizedOriginalContent} -
`; - - // Now we have the full forwarded email structure without sanitizing it again - htmlContent = fullForwardedEmail; - - console.log('Final forward HTML content length:', htmlContent.length, - 'contains table:', htmlContent.includes('