courrier preview

This commit is contained in:
alma 2025-05-01 16:36:44 +02:00
parent d582a2ba30
commit 0db546ad79
4 changed files with 115 additions and 57 deletions

View File

@ -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';

View File

@ -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 <body> 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(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
// Fix for broken image paths that might be relative
.replace(/(src|background)="(?!http|data|https|cid)/gi, '$1="https://');
return fixedHtml;
} catch (e) {
console.error('Error sanitizing HTML:', e);
// Fall back to a basic sanitization approach
return html
} catch (error) {
console.error('Failed to sanitize HTML content:', error);
// Fallback to basic sanitization
return content
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+="[^"]*"/g, '')
.replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:');
.replace(/javascript:/gi, 'blocked:');
}
}

View File

@ -287,6 +287,23 @@ export function processHtmlContent(
isEmpty: !sanitizedContent || sanitizedContent.trim().length === 0
});
// Check if content is a forwarded message to ensure special handling for tables
const isForwardedEmail =
sanitizedContent.includes('---------- Forwarded message ----------') ||
sanitizedContent.includes('Forwarded message') ||
(sanitizedContent.includes('From:') && sanitizedContent.includes('Date:') &&
sanitizedContent.includes('Subject:') && sanitizedContent.includes('To:'));
// Special processing for forwarded email styling
if (isForwardedEmail) {
console.log('Detected forwarded email content, preserving table structure');
// Make sure we're not removing important table structures
sanitizedContent = sanitizedContent
// Preserve table styling for email headers
.replace(/<table([^>]*)>/g, '<table$1 style="margin: 10px 0; border-collapse: collapse; font-size: 13px; color: #333;">')
.replace(/<td([^>]*)>/g, '<td$1 style="padding: 3px 5px; vertical-align: top;">');
}
// Fix common email client quirks without breaking cid: URLs
sanitizedContent = sanitizedContent
// Fix for Outlook WebVML content

View File

@ -590,9 +590,9 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
</tbody>
</table>
</div>
<div style="padding: 10px 0; border-top: 1px solid #ddd;">
<blockquote style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">
${sanitizedOriginalContent}
</div>
</blockquote>
`;
// Now we have the full forwarded email structure without sanitizing it again