courrier formatting
This commit is contained in:
parent
a7b023e359
commit
53e6c3f7c8
@ -1,51 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import DOMPurify from 'isomorphic-dompurify';
|
||||
import { parseRawEmail } from '@/lib/utils/email-mime-decoder';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { formatEmailContent } from '@/lib/utils/email-content';
|
||||
import { sanitizeHtml } from '@/lib/utils/email-formatter';
|
||||
|
||||
interface EmailContentDisplayProps {
|
||||
content: string;
|
||||
type?: 'html' | 'text' | 'auto';
|
||||
className?: string;
|
||||
showQuotedText?: boolean;
|
||||
/**
|
||||
* Interface for email content types
|
||||
*/
|
||||
interface ProcessedContent {
|
||||
html: string;
|
||||
text: string;
|
||||
isHtml: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for displaying properly formatted email content
|
||||
* Handles MIME decoding, sanitization, and proper rendering
|
||||
* Interface for component props
|
||||
*/
|
||||
interface EmailContentDisplayProps {
|
||||
content: string;
|
||||
type?: 'html' | 'text' | 'auto';
|
||||
isRawEmail?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw email content into HTML and text parts
|
||||
* This is a helper function used when processing raw email formats
|
||||
*/
|
||||
function parseRawEmail(rawContent: string): { html: string; text: string } {
|
||||
// Simple parser for demonstration - in production, use a proper MIME parser
|
||||
const hasHtmlPart = rawContent.includes('<html') ||
|
||||
rawContent.includes('<body') ||
|
||||
rawContent.includes('Content-Type: text/html');
|
||||
|
||||
if (hasHtmlPart) {
|
||||
// Extract HTML part
|
||||
let htmlPart = '';
|
||||
const htmlMatch = rawContent.match(/<html[\s\S]*<\/html>/i) ||
|
||||
rawContent.match(/<body[\s\S]*<\/body>/i);
|
||||
|
||||
if (htmlMatch) {
|
||||
htmlPart = htmlMatch[0];
|
||||
} else {
|
||||
// Fallback extraction
|
||||
const parts = rawContent.split(/(?:--boundary|\r\n\r\n)/);
|
||||
htmlPart = parts.find(part =>
|
||||
part.includes('Content-Type: text/html') ||
|
||||
part.includes('<html') ||
|
||||
part.includes('<body')
|
||||
) || '';
|
||||
}
|
||||
|
||||
return {
|
||||
html: htmlPart,
|
||||
text: rawContent.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
html: '',
|
||||
text: rawContent
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* EmailContentDisplay component - displays formatted email content
|
||||
* with proper security, styling and support for different email formats
|
||||
*/
|
||||
const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
||||
content,
|
||||
type = 'auto',
|
||||
className = '',
|
||||
showQuotedText = true
|
||||
isRawEmail = false,
|
||||
className = ''
|
||||
}) => {
|
||||
const [processedContent, setProcessedContent] = useState<{
|
||||
html: string;
|
||||
text: string;
|
||||
isHtml: boolean;
|
||||
}>({
|
||||
const [processedContent, setProcessedContent] = useState<ProcessedContent>({
|
||||
html: '',
|
||||
text: '',
|
||||
isHtml: false
|
||||
});
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Process and sanitize email content
|
||||
// Process the email content when it changes
|
||||
useEffect(() => {
|
||||
if (!content) {
|
||||
setProcessedContent({ html: '', text: '', isHtml: false });
|
||||
setProcessedContent({
|
||||
html: '',
|
||||
text: '',
|
||||
isHtml: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if this is raw email content
|
||||
const isRawEmail = content.includes('Content-Type:') ||
|
||||
content.includes('MIME-Version:') ||
|
||||
content.includes('From:') && content.includes('To:');
|
||||
|
||||
if (isRawEmail) {
|
||||
// Parse raw email content
|
||||
const parsed = parseRawEmail(content);
|
||||
@ -54,12 +100,8 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
||||
const useHtml = (type === 'html' || (type === 'auto' && parsed.html)) && !!parsed.html;
|
||||
|
||||
if (useHtml) {
|
||||
// Sanitize HTML content
|
||||
const sanitizedHtml = DOMPurify.sanitize(parsed.html, {
|
||||
ADD_TAGS: ['table', 'thead', 'tbody', 'tr', 'td', 'th'],
|
||||
ADD_ATTR: ['target', 'rel', 'colspan', 'rowspan'],
|
||||
ALLOW_DATA_ATTR: false
|
||||
});
|
||||
// Use the enhanced sanitizeHtml function from email-formatter
|
||||
const sanitizedHtml = sanitizeHtml(parsed.html);
|
||||
|
||||
setProcessedContent({
|
||||
html: sanitizedHtml,
|
||||
@ -67,8 +109,9 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
||||
isHtml: true
|
||||
});
|
||||
} else {
|
||||
// Format plain text with line breaks
|
||||
const formattedText = parsed.text.replace(/\n/g, '<br />');
|
||||
// Format plain text properly
|
||||
const formattedText = formatEmailContent({ text: parsed.text });
|
||||
|
||||
setProcessedContent({
|
||||
html: formattedText,
|
||||
text: parsed.text,
|
||||
@ -77,28 +120,29 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
||||
}
|
||||
} else {
|
||||
// Treat as direct content (not raw email)
|
||||
const isHtmlContent = content.includes('<html') ||
|
||||
content.includes('<body') ||
|
||||
content.includes('<div') ||
|
||||
content.includes('<p>') ||
|
||||
content.includes('<br');
|
||||
const isHtmlContent = type === 'html' || (
|
||||
type === 'auto' && (
|
||||
content.includes('<html') ||
|
||||
content.includes('<body') ||
|
||||
content.includes('<div') ||
|
||||
content.includes('<p>') ||
|
||||
content.includes('<br')
|
||||
)
|
||||
);
|
||||
|
||||
if (isHtmlContent || type === 'html') {
|
||||
// Sanitize HTML content
|
||||
const sanitizedHtml = DOMPurify.sanitize(content, {
|
||||
ADD_TAGS: ['table', 'thead', 'tbody', 'tr', 'td', 'th'],
|
||||
ADD_ATTR: ['target', 'rel', 'colspan', 'rowspan', 'style', 'class', 'id', 'border'],
|
||||
ALLOW_DATA_ATTR: false
|
||||
});
|
||||
if (isHtmlContent) {
|
||||
// Use the enhanced sanitizeHtml function
|
||||
const sanitizedHtml = sanitizeHtml(content);
|
||||
|
||||
setProcessedContent({
|
||||
html: sanitizedHtml,
|
||||
text: content,
|
||||
text: content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(),
|
||||
isHtml: true
|
||||
});
|
||||
} else {
|
||||
// Format plain text with line breaks
|
||||
const formattedText = content.replace(/\n/g, '<br />');
|
||||
// Format plain text properly using formatEmailContent
|
||||
const formattedText = formatEmailContent({ text: content });
|
||||
|
||||
setProcessedContent({
|
||||
html: formattedText,
|
||||
text: content,
|
||||
@ -108,89 +152,29 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error processing email content:', err);
|
||||
// Fallback to plain text
|
||||
// Fallback to plain text with basic formatting
|
||||
setProcessedContent({
|
||||
html: content.replace(/\n/g, '<br />'),
|
||||
html: `<div style="white-space: pre-wrap; font-family: monospace;">${content.replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br />')}</div>`,
|
||||
text: content,
|
||||
isHtml: false
|
||||
});
|
||||
}
|
||||
}, [content, type]);
|
||||
|
||||
// Process quoted content visibility and fix table styling
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !processedContent.html) return;
|
||||
|
||||
const container = containerRef.current;
|
||||
|
||||
// Handle quoted text visibility
|
||||
if (!showQuotedText) {
|
||||
// Add toggle buttons for quoted text sections
|
||||
const quotedSections = container.querySelectorAll('blockquote');
|
||||
|
||||
quotedSections.forEach((quote, index) => {
|
||||
// Check if this quoted section already has a toggle
|
||||
if (quote.previousElementSibling?.classList.contains('quoted-toggle-btn')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create toggle button
|
||||
const toggleBtn = document.createElement('button');
|
||||
toggleBtn.innerText = '▼ Show quoted text';
|
||||
toggleBtn.className = 'quoted-toggle-btn';
|
||||
toggleBtn.style.cssText = 'background: none; border: none; color: #666; font-size: 12px; cursor: pointer; padding: 4px 0; display: block;';
|
||||
|
||||
// Hide quoted section initially
|
||||
quote.style.display = 'none';
|
||||
|
||||
// Add click handler
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const isHidden = quote.style.display === 'none';
|
||||
quote.style.display = isHidden ? 'block' : 'none';
|
||||
toggleBtn.innerText = isHidden ? '▲ Hide quoted text' : '▼ Show quoted text';
|
||||
});
|
||||
|
||||
// Insert before the blockquote
|
||||
quote.parentNode?.insertBefore(toggleBtn, quote);
|
||||
});
|
||||
}
|
||||
|
||||
// Process tables and ensure they're properly formatted
|
||||
const tables = container.querySelectorAll('table');
|
||||
tables.forEach(table => {
|
||||
// Cast to HTMLTableElement to access style property
|
||||
const tableElement = table as HTMLTableElement;
|
||||
|
||||
// Only apply styling if the table doesn't already have border styles
|
||||
if (!tableElement.hasAttribute('border') &&
|
||||
(!tableElement.style.border || tableElement.style.border === '')) {
|
||||
// Apply proper table styling
|
||||
tableElement.style.width = '100%';
|
||||
tableElement.style.borderCollapse = 'collapse';
|
||||
tableElement.style.margin = '10px 0';
|
||||
tableElement.style.border = '1px solid #ddd';
|
||||
}
|
||||
|
||||
const cells = table.querySelectorAll('td, th');
|
||||
cells.forEach(cell => {
|
||||
// Cast to HTMLTableCellElement to access style property
|
||||
const cellElement = cell as HTMLTableCellElement;
|
||||
|
||||
// Only apply styling if the cell doesn't already have border styles
|
||||
if (!cellElement.style.border || cellElement.style.border === '') {
|
||||
cellElement.style.border = '1px solid #ddd';
|
||||
cellElement.style.padding = '6px';
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [processedContent.html, showQuotedText]);
|
||||
}, [content, type, isRawEmail]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`email-content-display ${className}`}
|
||||
dangerouslySetInnerHTML={{ __html: processedContent.html }}
|
||||
/>
|
||||
<div className={`email-content-container ${className}`}>
|
||||
<div
|
||||
className="email-content-viewer"
|
||||
style={{
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
fontSize: '15px',
|
||||
lineHeight: '1.6',
|
||||
color: '#1A202C',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: processedContent.html }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
EmailMessage as FormatterEmailMessage,
|
||||
sanitizeHtml
|
||||
} from '@/lib/utils/email-formatter';
|
||||
import { formatEmailContent } from '@/lib/utils/email-content';
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { AvatarImage } from '@/components/ui/avatar';
|
||||
import { Card } from '@/components/ui/card';
|
||||
@ -103,43 +104,11 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
||||
// Format the email content
|
||||
const formattedContent = useMemo(() => {
|
||||
if (!email) {
|
||||
console.log('EmailPreview: No email provided');
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('EmailPreview: Full email object:', JSON.stringify(email, null, 2));
|
||||
|
||||
// Get the content in order of preference
|
||||
let content = '';
|
||||
|
||||
// If content is an object with html/text
|
||||
if (email.content && typeof email.content === 'object') {
|
||||
console.log('EmailPreview: Using object content:', JSON.stringify(email.content, null, 2));
|
||||
content = email.content.html || email.content.text || '';
|
||||
}
|
||||
// If content is a string
|
||||
else if (typeof email.content === 'string') {
|
||||
console.log('EmailPreview: Using direct string content:', email.content);
|
||||
content = email.content;
|
||||
}
|
||||
|
||||
console.log('EmailPreview: Final content before sanitization:', content);
|
||||
|
||||
// Sanitize the content for display
|
||||
const sanitizedContent = DOMPurify.sanitize(content, {
|
||||
ADD_TAGS: ['style', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
|
||||
ADD_ATTR: ['class', 'style', 'dir', 'colspan', 'rowspan'],
|
||||
ALLOW_DATA_ATTR: false
|
||||
});
|
||||
|
||||
console.log('EmailPreview: Final sanitized content:', sanitizedContent);
|
||||
|
||||
return sanitizedContent;
|
||||
} catch (error) {
|
||||
console.error('EmailPreview: Error formatting email content:', error);
|
||||
return '';
|
||||
}
|
||||
// Use the improved, standardized email content formatter
|
||||
return formatEmailContent(email);
|
||||
}, [email]);
|
||||
|
||||
// Display loading state
|
||||
@ -249,12 +218,28 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
||||
|
||||
{/* Email content */}
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="space-y-2 p-6">
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable={false}
|
||||
className="w-full p-4 min-h-[300px] focus:outline-none email-content-display"
|
||||
<div className="p-6">
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable={false}
|
||||
className="w-full email-content-container"
|
||||
style={{
|
||||
backgroundColor: '#ffffff',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #e2e8f0',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
|
||||
overflow: 'hidden',
|
||||
minHeight: '300px'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="email-content-body p-6"
|
||||
style={{
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
||||
fontSize: '15px',
|
||||
lineHeight: '1.6',
|
||||
color: '#1A202C'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: formattedContent }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
/**
|
||||
* Format and standardize email content for display following email industry standards.
|
||||
* This function handles various email content formats and ensures proper display
|
||||
* including support for HTML emails, plain text emails, RTL languages, and email client quirks.
|
||||
*/
|
||||
export function formatEmailContent(email: any): string {
|
||||
if (!email) {
|
||||
console.log('formatEmailContent: No email provided');
|
||||
@ -7,47 +12,91 @@ export function formatEmailContent(email: any): string {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('formatEmailContent: Raw email content:', {
|
||||
content: email.content,
|
||||
html: email.html,
|
||||
text: email.text,
|
||||
formattedContent: email.formattedContent
|
||||
});
|
||||
|
||||
// Get the content in order of preference
|
||||
// Get the content in order of preference with proper fallbacks
|
||||
let content = '';
|
||||
let isHtml = false;
|
||||
let textContent = '';
|
||||
|
||||
// Extract content based on standardized property hierarchy
|
||||
if (email.content && typeof email.content === 'object') {
|
||||
console.log('formatEmailContent: Using object content:', email.content);
|
||||
content = email.content.html || email.content.text || '';
|
||||
isHtml = !!email.content.html;
|
||||
content = email.content.html || '';
|
||||
textContent = email.content.text || '';
|
||||
} else if (typeof email.content === 'string') {
|
||||
console.log('formatEmailContent: Using direct string content');
|
||||
// Check if the string content is HTML
|
||||
isHtml = email.content.trim().startsWith('<') &&
|
||||
(email.content.includes('<html') ||
|
||||
email.content.includes('<body') ||
|
||||
email.content.includes('<div') ||
|
||||
email.content.includes('<p>'));
|
||||
content = email.content;
|
||||
textContent = email.content;
|
||||
} else if (email.html) {
|
||||
console.log('formatEmailContent: Using html content');
|
||||
isHtml = true;
|
||||
content = email.html;
|
||||
textContent = email.text || '';
|
||||
} else if (email.text) {
|
||||
console.log('formatEmailContent: Using text content');
|
||||
content = email.text;
|
||||
isHtml = false;
|
||||
content = '';
|
||||
textContent = email.text;
|
||||
} else if (email.formattedContent) {
|
||||
console.log('formatEmailContent: Using formattedContent');
|
||||
// Assume formattedContent is already HTML
|
||||
isHtml = true;
|
||||
content = email.formattedContent;
|
||||
textContent = '';
|
||||
}
|
||||
|
||||
console.log('formatEmailContent: Content before sanitization:', content);
|
||||
// If we have HTML content, sanitize and standardize it
|
||||
if (isHtml && content) {
|
||||
// Sanitize with industry-standard email tags and attributes
|
||||
const sanitizedContent = DOMPurify.sanitize(content, {
|
||||
ADD_TAGS: [
|
||||
'style', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
|
||||
'caption', 'col', 'colgroup', 'div', 'span', 'img', 'br', 'hr',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre',
|
||||
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a', 'b', 'i', 'u', 'em',
|
||||
'strong', 'del', 'ins', 'sub', 'sup', 'small', 'mark', 'q'
|
||||
],
|
||||
ADD_ATTR: [
|
||||
'class', 'style', 'id', 'href', 'src', 'alt', 'title', 'width', 'height',
|
||||
'border', 'cellspacing', 'cellpadding', 'bgcolor', 'color', 'dir', 'lang',
|
||||
'align', 'valign', 'span', 'colspan', 'rowspan', 'target', 'rel',
|
||||
'background', 'data-*'
|
||||
],
|
||||
ALLOW_DATA_ATTR: true,
|
||||
WHOLE_DOCUMENT: false,
|
||||
RETURN_DOM: false,
|
||||
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button'],
|
||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout']
|
||||
});
|
||||
|
||||
// Wrap the content in standard email container with responsive styling
|
||||
return `
|
||||
<div class="email-content" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; word-wrap: break-word;">
|
||||
${sanitizedContent}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
// If we only have text content, format it properly
|
||||
else if (textContent) {
|
||||
// Format plain text with proper line breaks and paragraphs
|
||||
const formattedText = textContent
|
||||
.replace(/\r\n|\r|\n/g, '<br>') // Convert all newlines to <br>
|
||||
.replace(/((?:<br>){2,})/g, '</p><p>') // Convert multiple newlines to paragraphs
|
||||
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
|
||||
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
|
||||
|
||||
return `
|
||||
<div class="email-content plain-text" style="font-family: monospace; white-space: pre-wrap; line-height: 1.5; color: #333; max-width: 100%; word-wrap: break-word;">
|
||||
<p>${formattedText}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Sanitize the content for display while preserving formatting
|
||||
const sanitizedContent = DOMPurify.sanitize(content, {
|
||||
ADD_TAGS: ['style', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
|
||||
ADD_ATTR: ['class', 'style', 'dir', 'colspan', 'rowspan'],
|
||||
ALLOW_DATA_ATTR: false
|
||||
});
|
||||
|
||||
console.log('formatEmailContent: Final sanitized content:', sanitizedContent);
|
||||
|
||||
return sanitizedContent;
|
||||
// Default case: empty or unrecognized content
|
||||
return '<div class="email-content-empty">No content available</div>';
|
||||
} catch (error) {
|
||||
console.error('formatEmailContent: Error formatting email content:', error);
|
||||
return '';
|
||||
return '<div class="email-content-error">Error displaying email content</div>';
|
||||
}
|
||||
}
|
||||
@ -135,34 +135,54 @@ export function formatEmailDate(date: Date | string | undefined): string {
|
||||
|
||||
/**
|
||||
* Sanitize HTML content before processing or displaying
|
||||
* This ensures the content is properly sanitized while preserving text direction
|
||||
* Implements email industry standards for proper, consistent, and secure rendering
|
||||
*
|
||||
* @param html HTML content to sanitize
|
||||
* @returns Sanitized HTML with preserved text direction
|
||||
* @returns Sanitized HTML with preserved styling and structure
|
||||
*/
|
||||
export function sanitizeHtml(html: string): string {
|
||||
if (!html) return '';
|
||||
|
||||
try {
|
||||
// Use DOMPurify but ensure we keep all elements and attributes that might be in emails
|
||||
// Use DOMPurify with comprehensive email HTML standards
|
||||
const clean = DOMPurify.sanitize(html, {
|
||||
ADD_TAGS: ['button', 'style', 'img', 'iframe', 'meta', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
|
||||
ADD_ATTR: ['target', 'rel', 'style', 'class', 'id', 'href', 'src', 'alt', 'title', 'width', 'height', 'onclick', 'colspan', 'rowspan'],
|
||||
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'
|
||||
],
|
||||
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-*'
|
||||
],
|
||||
KEEP_CONTENT: true,
|
||||
WHOLE_DOCUMENT: false,
|
||||
ALLOW_DATA_ATTR: true,
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
FORCE_BODY: false,
|
||||
RETURN_DOM: false,
|
||||
RETURN_DOM_FRAGMENT: false,
|
||||
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
|
||||
});
|
||||
|
||||
return clean;
|
||||
// Fix common email rendering issues
|
||||
return 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://');
|
||||
} catch (e) {
|
||||
console.error('Error sanitizing HTML:', e);
|
||||
// Fall back to a basic sanitization approach
|
||||
return html
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/on\w+="[^"]*"/g, '');
|
||||
.replace(/on\w+="[^"]*"/g, '')
|
||||
.replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user