From 0db546ad793fdf74a0d209616684f4e2205ced0a Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 1 May 2025 16:36:44 +0200 Subject: [PATCH] courrier preview --- components/email/RichEmailEditor.tsx | 23 ++++- lib/utils/dom-purify-config.ts | 128 ++++++++++++++++----------- lib/utils/email-content.ts | 17 ++++ lib/utils/email-utils.ts | 4 +- 4 files changed, 115 insertions(+), 57 deletions(-) diff --git a/components/email/RichEmailEditor.tsx b/components/email/RichEmailEditor.tsx index 1a4bf2d0..27d1ce96 100644 --- a/components/email/RichEmailEditor.tsx +++ b/components/email/RichEmailEditor.tsx @@ -28,12 +28,27 @@ function cleanupTableStructures(htmlContent: string): string { // Find tables with problematic nested structures const tables = tempDiv.querySelectorAll('table'); - // Simple approach: if we have tables in forwarded content, convert them to divs - // to prevent quill-better-table from trying to interpret them - if (tables.length > 0 && htmlContent.includes('---------- Forwarded message ----------')) { - console.log(`Converting ${tables.length} tables in forwarded email to prevent Quill errors`); + // Check if content looks like a forwarded email or reply content + const isForwardedEmail = + htmlContent.includes('---------- Forwarded message ----------') || + htmlContent.includes('Forwarded message') || + htmlContent.includes('forwarded message') || + (htmlContent.includes('From:') && htmlContent.includes('Date:') && htmlContent.includes('Subject:')); + + // Check if content has complex tables that might cause issues + const hasComplexTables = tables.length > 0 && + (isForwardedEmail || htmlContent.includes('gmail_quote') || + htmlContent.includes('blockquote') || htmlContent.includes('wrote:')); + + if (hasComplexTables) { + console.log(`Converting ${tables.length} tables in complex email content to prevent Quill errors`); tables.forEach(table => { + // Skip simple tables that are likely to work fine with Quill + if (table.rows.length <= 1 && table.querySelectorAll('td, th').length <= 3) { + return; + } + // Create a replacement div const replacementDiv = document.createElement('div'); replacementDiv.className = 'converted-table'; diff --git a/lib/utils/dom-purify-config.ts b/lib/utils/dom-purify-config.ts index d7409634..a57b366c 100644 --- a/lib/utils/dom-purify-config.ts +++ b/lib/utils/dom-purify-config.ts @@ -11,64 +11,90 @@ import DOMPurify from 'isomorphic-dompurify'; // Reset any existing hooks to start with a clean slate DOMPurify.removeAllHooks(); -// Configure DOMPurify with settings appropriate for email content -DOMPurify.setConfig({ - ADD_TAGS: [ - 'html', 'head', 'body', 'style', 'link', 'meta', 'title', - 'table', 'caption', 'col', 'colgroup', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th', - 'div', 'span', 'img', 'br', 'hr', 'section', 'article', 'header', 'footer', - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre', 'code', - 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a', 'b', 'i', 'u', 'em', - 'strong', 'del', 'ins', 'mark', 'small', 'sub', 'sup', 'q', 'abbr', - 'font' // Allow legacy font tag often found in emails - ], - ADD_ATTR: [ - 'style', 'class', 'id', 'name', 'href', 'src', 'alt', 'title', 'width', 'height', - 'border', 'cellspacing', 'cellpadding', 'bgcolor', 'background', 'color', - 'align', 'valign', 'dir', 'lang', 'target', 'rel', 'charset', 'media', - 'colspan', 'rowspan', 'scope', 'span', 'size', 'face', 'hspace', 'vspace', - 'data-*', - 'start', 'type', 'value', 'cite', 'datetime', 'wrap', 'summary' - ], - KEEP_CONTENT: true, - WHOLE_DOCUMENT: false, - ALLOW_DATA_ATTR: true, - ALLOW_UNKNOWN_PROTOCOLS: true, // Needed for some email clients - FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'select', 'textarea'], - FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout'], - FORCE_BODY: false, - USE_PROFILES: { html: true } // Use HTML profile for more permissive sanitization for emails -}); +/** + * Configure DOMPurify with safe defaults for email content + * This balances security with the need to display rich email content + */ +export function configureDOMPurify() { + // Enhanced configuration for email content + DOMPurify.setConfig({ + ADD_TAGS: [ + // SVG elements for simple charts/logos that might be in emails + 'svg', 'path', 'g', 'circle', 'rect', 'line', 'polygon', 'ellipse', + // Common email-specific elements + 'o:p', 'font', + // Allow comments for conditional HTML in emails + '!--...--' + ], + ADD_ATTR: [ + // SVG attributes + 'viewbox', 'd', 'cx', 'cy', 'r', 'fill', 'stroke', 'stroke-width', 'x', 'y', 'width', 'height', + // Additional HTML attributes commonly used in emails + 'align', 'valign', 'bgcolor', 'color', 'cellpadding', 'cellspacing', 'colspan', 'rowspan', + 'face', 'size', 'direction', 'role', 'aria-label', 'aria-hidden', + // List attributes + 'start', 'type', 'value', + // Table attributes and styles + 'border', 'frame', 'rules', 'summary', 'headers', 'scope', 'abbr', + // Blockquote attributes + 'cite', 'datetime', + // Form elements attributes (read-only) + 'readonly', 'disabled', 'selected', 'checked', 'multiple', 'wrap' + ], + FORBID_TAGS: [ + // Remove dangerous tags + 'script', 'object', 'iframe', 'embed', 'applet', 'meta', 'link', + // Form elements that could be used for phishing + 'form', 'button', 'input', 'textarea', 'select', 'option' + ], + FORBID_ATTR: [ + // Remove JavaScript and dangerous attributes + 'onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onmouseenter', 'onmouseleave', + 'onkeydown', 'onkeypress', 'onkeyup', 'onchange', 'onsubmit', 'onreset', 'onselect', 'onblur', + 'onfocus', 'onscroll', 'onbeforeunload', 'onunload', 'onhashchange', 'onpopstate', 'onpageshow', + 'onpagehide', 'onabort', 'oncanplay', 'oncanplaythrough', 'ondurationchange', 'onemptied', + 'onended', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying', + 'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled', 'onsuspend', 'ontimeupdate', + 'onvolumechange', 'onwaiting', 'animationend', 'animationiteration', 'animationstart', + // Dangerous attributes + 'formaction', 'xlink:href' + ], + ALLOW_DATA_ATTR: false, // Disable data-* attributes which can be used for XSS + WHOLE_DOCUMENT: false, // Don't parse the entire document - just fragments + SANITIZE_DOM: true, // Sanitize the DOM to prevent XSS + KEEP_CONTENT: true, // Keep content of elements that are removed + RETURN_DOM: false, // Return a DOM object rather than HTML string + RETURN_DOM_FRAGMENT: false, // Return a DocumentFragment rather than HTML string + FORCE_BODY: false, // Add a tag if one doesn't exist + ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|data|irc):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i, + ALLOW_UNKNOWN_PROTOCOLS: true, // Some email clients use custom protocols for images/attachments + USE_PROFILES: { html: true } // Use the HTML profile for more permissive sanitization + }); + + return DOMPurify; +} + +// Singleton instance of configured DOMPurify for the app +export const purify = configureDOMPurify(); /** - * Sanitizes HTML content with the centralized DOMPurify configuration - * @param html HTML content to sanitize - * @returns Sanitized HTML + * Sanitize HTML content using our email-specific configuration */ -export function sanitizeHtml(html: string): string { - if (!html) return ''; +export function sanitizeHtml(content: string): string { + if (!content) return ''; try { - // Use DOMPurify with our central configuration - const clean = DOMPurify.sanitize(html, { - ADD_ATTR: ['style', 'class', 'id', 'align', 'valign', 'colspan', 'rowspan', 'cellspacing', 'cellpadding', 'bgcolor'] + // Sanitize with our configured instance + return purify.sanitize(content, { + ADD_TAGS: ['style'], // Allow internal styles temporarily for cleaning + ADD_ATTR: ['style', 'class'] // Allow style and class attributes }); - - // Fix common email rendering issues - const fixedHtml = clean - // Fix for Outlook WebVML content - .replace(/