courrier preview
This commit is contained in:
parent
d582a2ba30
commit
0db546ad79
@ -28,12 +28,27 @@ function cleanupTableStructures(htmlContent: string): string {
|
|||||||
// Find tables with problematic nested structures
|
// Find tables with problematic nested structures
|
||||||
const tables = tempDiv.querySelectorAll('table');
|
const tables = tempDiv.querySelectorAll('table');
|
||||||
|
|
||||||
// Simple approach: if we have tables in forwarded content, convert them to divs
|
// Check if content looks like a forwarded email or reply content
|
||||||
// to prevent quill-better-table from trying to interpret them
|
const isForwardedEmail =
|
||||||
if (tables.length > 0 && htmlContent.includes('---------- Forwarded message ----------')) {
|
htmlContent.includes('---------- Forwarded message ----------') ||
|
||||||
console.log(`Converting ${tables.length} tables in forwarded email to prevent Quill errors`);
|
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 => {
|
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
|
// Create a replacement div
|
||||||
const replacementDiv = document.createElement('div');
|
const replacementDiv = document.createElement('div');
|
||||||
replacementDiv.className = 'converted-table';
|
replacementDiv.className = 'converted-table';
|
||||||
|
|||||||
@ -11,64 +11,90 @@ import DOMPurify from 'isomorphic-dompurify';
|
|||||||
// Reset any existing hooks to start with a clean slate
|
// Reset any existing hooks to start with a clean slate
|
||||||
DOMPurify.removeAllHooks();
|
DOMPurify.removeAllHooks();
|
||||||
|
|
||||||
// Configure DOMPurify with settings appropriate for email content
|
/**
|
||||||
DOMPurify.setConfig({
|
* Configure DOMPurify with safe defaults for email content
|
||||||
ADD_TAGS: [
|
* This balances security with the need to display rich email content
|
||||||
'html', 'head', 'body', 'style', 'link', 'meta', 'title',
|
*/
|
||||||
'table', 'caption', 'col', 'colgroup', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
|
export function configureDOMPurify() {
|
||||||
'div', 'span', 'img', 'br', 'hr', 'section', 'article', 'header', 'footer',
|
// Enhanced configuration for email content
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre', 'code',
|
DOMPurify.setConfig({
|
||||||
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a', 'b', 'i', 'u', 'em',
|
ADD_TAGS: [
|
||||||
'strong', 'del', 'ins', 'mark', 'small', 'sub', 'sup', 'q', 'abbr',
|
// SVG elements for simple charts/logos that might be in emails
|
||||||
'font' // Allow legacy font tag often found in emails
|
'svg', 'path', 'g', 'circle', 'rect', 'line', 'polygon', 'ellipse',
|
||||||
],
|
// Common email-specific elements
|
||||||
ADD_ATTR: [
|
'o:p', 'font',
|
||||||
'style', 'class', 'id', 'name', 'href', 'src', 'alt', 'title', 'width', 'height',
|
// Allow comments for conditional HTML in emails
|
||||||
'border', 'cellspacing', 'cellpadding', 'bgcolor', 'background', 'color',
|
'!--...--'
|
||||||
'align', 'valign', 'dir', 'lang', 'target', 'rel', 'charset', 'media',
|
],
|
||||||
'colspan', 'rowspan', 'scope', 'span', 'size', 'face', 'hspace', 'vspace',
|
ADD_ATTR: [
|
||||||
'data-*',
|
// SVG attributes
|
||||||
'start', 'type', 'value', 'cite', 'datetime', 'wrap', 'summary'
|
'viewbox', 'd', 'cx', 'cy', 'r', 'fill', 'stroke', 'stroke-width', 'x', 'y', 'width', 'height',
|
||||||
],
|
// Additional HTML attributes commonly used in emails
|
||||||
KEEP_CONTENT: true,
|
'align', 'valign', 'bgcolor', 'color', 'cellpadding', 'cellspacing', 'colspan', 'rowspan',
|
||||||
WHOLE_DOCUMENT: false,
|
'face', 'size', 'direction', 'role', 'aria-label', 'aria-hidden',
|
||||||
ALLOW_DATA_ATTR: true,
|
// List attributes
|
||||||
ALLOW_UNKNOWN_PROTOCOLS: true, // Needed for some email clients
|
'start', 'type', 'value',
|
||||||
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'select', 'textarea'],
|
// Table attributes and styles
|
||||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout'],
|
'border', 'frame', 'rules', 'summary', 'headers', 'scope', 'abbr',
|
||||||
FORCE_BODY: false,
|
// Blockquote attributes
|
||||||
USE_PROFILES: { html: true } // Use HTML profile for more permissive sanitization for emails
|
'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
|
* Sanitize HTML content using our email-specific configuration
|
||||||
* @param html HTML content to sanitize
|
|
||||||
* @returns Sanitized HTML
|
|
||||||
*/
|
*/
|
||||||
export function sanitizeHtml(html: string): string {
|
export function sanitizeHtml(content: string): string {
|
||||||
if (!html) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use DOMPurify with our central configuration
|
// Sanitize with our configured instance
|
||||||
const clean = DOMPurify.sanitize(html, {
|
return purify.sanitize(content, {
|
||||||
ADD_ATTR: ['style', 'class', 'id', 'align', 'valign', 'colspan', 'rowspan', 'cellspacing', 'cellpadding', 'bgcolor']
|
ADD_TAGS: ['style'], // Allow internal styles temporarily for cleaning
|
||||||
|
ADD_ATTR: ['style', 'class'] // Allow style and class attributes
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
// Fix common email rendering issues
|
console.error('Failed to sanitize HTML content:', error);
|
||||||
const fixedHtml = clean
|
// Fallback to basic sanitization
|
||||||
// Fix for Outlook WebVML content
|
return 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
|
|
||||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||||
.replace(/on\w+="[^"]*"/g, '')
|
.replace(/javascript:/gi, 'blocked:');
|
||||||
.replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -287,6 +287,23 @@ export function processHtmlContent(
|
|||||||
isEmpty: !sanitizedContent || sanitizedContent.trim().length === 0
|
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
|
// Fix common email client quirks without breaking cid: URLs
|
||||||
sanitizedContent = sanitizedContent
|
sanitizedContent = sanitizedContent
|
||||||
// Fix for Outlook WebVML content
|
// Fix for Outlook WebVML content
|
||||||
|
|||||||
@ -590,9 +590,9 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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}
|
${sanitizedOriginalContent}
|
||||||
</div>
|
</blockquote>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Now we have the full forwarded email structure without sanitizing it again
|
// Now we have the full forwarded email structure without sanitizing it again
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user