courrier formatting
This commit is contained in:
parent
3fa6c68464
commit
4c39241d3c
@ -222,27 +222,46 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
<div
|
<div
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
className="w-full email-content-container"
|
className="w-full email-content-container rounded-md overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
borderRadius: '4px',
|
|
||||||
border: '1px solid #e2e8f0',
|
border: '1px solid #e2e8f0',
|
||||||
boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
|
boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
|
||||||
overflow: 'hidden',
|
|
||||||
minHeight: '300px'
|
minHeight: '300px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="email-content-body p-4 sm:p-6">
|
||||||
|
{formattedContent ? (
|
||||||
<div
|
<div
|
||||||
className="email-content-body p-6"
|
className="email-content-rendered"
|
||||||
style={{
|
|
||||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
||||||
fontSize: '15px',
|
|
||||||
lineHeight: '1.6',
|
|
||||||
color: '#1A202C'
|
|
||||||
}}
|
|
||||||
dangerouslySetInnerHTML={{ __html: formattedContent }}
|
dangerouslySetInnerHTML={{ __html: formattedContent }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="email-content-empty py-8 text-center text-muted-foreground">
|
||||||
|
<p>This email does not contain any content.</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<details className="mt-6 text-xs text-muted-foreground border rounded-md p-2">
|
||||||
|
<summary className="cursor-pointer">Email Debug Info</summary>
|
||||||
|
<div className="mt-2 overflow-auto max-h-40">
|
||||||
|
<p><strong>Email ID:</strong> {email.id}</p>
|
||||||
|
<p><strong>Content Type:</strong> {
|
||||||
|
typeof email.content === 'object' && email.content?.html
|
||||||
|
? 'HTML'
|
||||||
|
: 'Plain Text'
|
||||||
|
}</p>
|
||||||
|
<p><strong>Content Size:</strong> {
|
||||||
|
typeof email.content === 'object'
|
||||||
|
? `HTML: ${email.content?.html?.length || 0} chars, Text: ${email.content?.text?.length || 0} chars`
|
||||||
|
: `${typeof email.content === 'string' ? email.content.length : 0} chars`
|
||||||
|
}</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -46,22 +46,50 @@ export function formatEmailContent(email: any): string {
|
|||||||
textContent = '';
|
textContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log what we found for debugging
|
||||||
|
console.log(`Email content detected: isHtml=${isHtml}, contentLength=${content.length}, textLength=${textContent.length}`);
|
||||||
|
|
||||||
// If we have HTML content, sanitize and standardize it
|
// If we have HTML content, sanitize and standardize it
|
||||||
if (isHtml && content) {
|
if (isHtml && content) {
|
||||||
|
// Make sure we have a complete HTML structure
|
||||||
|
const hasHtmlTag = content.includes('<html');
|
||||||
|
const hasBodyTag = content.includes('<body');
|
||||||
|
|
||||||
|
// Extract body content if we have a complete HTML document
|
||||||
|
if (hasHtmlTag && hasBodyTag) {
|
||||||
|
try {
|
||||||
|
// Create a DOM parser to extract just the body content
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(content, 'text/html');
|
||||||
|
const bodyContent = doc.body.innerHTML;
|
||||||
|
|
||||||
|
if (bodyContent) {
|
||||||
|
content = bodyContent;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If extraction fails, continue with the original content
|
||||||
|
console.error('Error extracting body content:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sanitize with industry-standard email tags and attributes
|
// Sanitize with industry-standard email tags and attributes
|
||||||
|
// Use a more permissive configuration for email HTML
|
||||||
const sanitizedContent = DOMPurify.sanitize(content, {
|
const sanitizedContent = DOMPurify.sanitize(content, {
|
||||||
ADD_TAGS: [
|
ADD_TAGS: [
|
||||||
'style', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
|
'style', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
|
||||||
'caption', 'col', 'colgroup', 'div', 'span', 'img', 'br', 'hr',
|
'caption', 'col', 'colgroup', 'div', 'span', 'img', 'br', 'hr',
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre',
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre',
|
||||||
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a', 'b', 'i', 'u', 'em',
|
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a', 'b', 'i', 'u', 'em',
|
||||||
'strong', 'del', 'ins', 'sub', 'sup', 'small', 'mark', 'q'
|
'strong', 'del', 'ins', 'sub', 'sup', 'small', 'mark', 'q',
|
||||||
|
'section', 'article', 'header', 'footer', 'aside', 'nav', 'figure',
|
||||||
|
'figcaption', 'address', 'main', 'center', 'font'
|
||||||
],
|
],
|
||||||
ADD_ATTR: [
|
ADD_ATTR: [
|
||||||
'class', 'style', 'id', 'href', 'src', 'alt', 'title', 'width', 'height',
|
'class', 'style', 'id', 'href', 'src', 'alt', 'title', 'width', 'height',
|
||||||
'border', 'cellspacing', 'cellpadding', 'bgcolor', 'color', 'dir', 'lang',
|
'border', 'cellspacing', 'cellpadding', 'bgcolor', 'color', 'dir', 'lang',
|
||||||
'align', 'valign', 'span', 'colspan', 'rowspan', 'target', 'rel',
|
'align', 'valign', 'span', 'colspan', 'rowspan', 'target', 'rel',
|
||||||
'background', 'data-*'
|
'background', 'data-*', 'face', 'size', 'bgcolor', 'hspace', 'vspace',
|
||||||
|
'marginheight', 'marginwidth', 'frameborder'
|
||||||
],
|
],
|
||||||
ALLOW_DATA_ATTR: true,
|
ALLOW_DATA_ATTR: true,
|
||||||
WHOLE_DOCUMENT: false,
|
WHOLE_DOCUMENT: false,
|
||||||
@ -70,26 +98,63 @@ export function formatEmailContent(email: any): string {
|
|||||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout']
|
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fix common email client quirks
|
||||||
|
let fixedContent = sanitizedContent
|
||||||
|
// 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|https|data|cid)/gi, '$1="https://');
|
||||||
|
|
||||||
|
// Fix for inline image references (CID)
|
||||||
|
if (fixedContent.includes('cid:')) {
|
||||||
|
console.log('Email contains CID references - these cannot be displayed properly in the web UI');
|
||||||
|
// We can't actually render CID references in the web UI
|
||||||
|
// Just log a message for now - a more comprehensive fix would involve
|
||||||
|
// extracting and converting these images
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for RTL content and set appropriate direction
|
||||||
|
const rtlLangPattern = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||||
|
const containsRtlText = rtlLangPattern.test(textContent);
|
||||||
|
const dirAttribute = containsRtlText ? 'dir="rtl"' : 'dir="ltr"';
|
||||||
|
|
||||||
// Wrap the content in standard email container with responsive styling
|
// Wrap the content in standard email container with responsive styling
|
||||||
return `
|
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;">
|
<div class="email-content-wrapper" style="max-width: 100%; overflow-x: auto;">
|
||||||
${sanitizedContent}
|
<div class="email-content" ${dirAttribute} style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word;">
|
||||||
|
${fixedContent}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
// If we only have text content, format it properly
|
// If we only have text content, format it properly
|
||||||
else if (textContent) {
|
else if (textContent) {
|
||||||
|
// Check for RTL content and set appropriate direction
|
||||||
|
const rtlLangPattern = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||||
|
const containsRtlText = rtlLangPattern.test(textContent);
|
||||||
|
const dirAttribute = containsRtlText ? 'dir="rtl"' : 'dir="ltr"';
|
||||||
|
|
||||||
|
// Escape HTML characters to prevent XSS
|
||||||
|
const escapedText = textContent
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
// Format plain text with proper line breaks and paragraphs
|
// Format plain text with proper line breaks and paragraphs
|
||||||
const formattedText = textContent
|
const formattedText = escapedText
|
||||||
.replace(/\r\n|\r|\n/g, '<br>') // Convert all newlines to <br>
|
.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>){2,})/g, '</p><p>') // Convert multiple newlines to paragraphs
|
||||||
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
|
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
|
||||||
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
|
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
|
||||||
|
|
||||||
return `
|
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;">
|
<div class="email-content-wrapper" style="max-width: 100%; overflow-x: auto;">
|
||||||
|
<div class="email-content plain-text" ${dirAttribute} style="font-family: -apple-system, BlinkMacSystemFont, Menlo, Monaco, Consolas, 'Courier New', monospace; white-space: pre-wrap; line-height: 1.5; color: #333; padding: 15px; background-color: #f8f9fa; border-radius: 4px; max-width: 100%; overflow-wrap: break-word; word-wrap: break-word;">
|
||||||
<p>${formattedText}</p>
|
<p>${formattedText}</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +162,11 @@ export function formatEmailContent(email: any): string {
|
|||||||
return '<div class="email-content-empty">No content available</div>';
|
return '<div class="email-content-empty">No content available</div>';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('formatEmailContent: Error formatting email content:', error);
|
console.error('formatEmailContent: Error formatting email content:', error);
|
||||||
return '<div class="email-content-error">Error displaying email content</div>';
|
return `
|
||||||
|
<div class="email-content-error" style="padding: 15px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;">
|
||||||
|
<p>Error displaying email content</p>
|
||||||
|
<p style="font-size: 12px; margin-top: 10px;">${error instanceof Error ? error.message : 'Unknown error'}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user