courrier formatting

This commit is contained in:
alma 2025-04-30 20:39:11 +02:00
parent 4c39241d3c
commit 3f017f82f8
4 changed files with 74 additions and 268 deletions

View File

@ -1,181 +0,0 @@
'use client';
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 for email content types
*/
interface ProcessedContent {
html: string;
text: string;
isHtml: boolean;
}
/**
* 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',
isRawEmail = false,
className = ''
}) => {
const [processedContent, setProcessedContent] = useState<ProcessedContent>({
html: '',
text: '',
isHtml: false
});
// Process the email content when it changes
useEffect(() => {
if (!content) {
setProcessedContent({
html: '',
text: '',
isHtml: false
});
return;
}
try {
if (isRawEmail) {
// Parse raw email content
const parsed = parseRawEmail(content);
// Check which content to use based on type and availability
const useHtml = (type === 'html' || (type === 'auto' && parsed.html)) && !!parsed.html;
if (useHtml) {
// Use the enhanced sanitizeHtml function from email-formatter
const sanitizedHtml = sanitizeHtml(parsed.html);
setProcessedContent({
html: sanitizedHtml,
text: parsed.text,
isHtml: true
});
} else {
// Format plain text properly
const formattedText = formatEmailContent({ text: parsed.text });
setProcessedContent({
html: formattedText,
text: parsed.text,
isHtml: false
});
}
} else {
// Treat as direct content (not raw email)
const isHtmlContent = type === 'html' || (
type === 'auto' && (
content.includes('<html') ||
content.includes('<body') ||
content.includes('<div') ||
content.includes('<p>') ||
content.includes('<br')
)
);
if (isHtmlContent) {
// Use the enhanced sanitizeHtml function
const sanitizedHtml = sanitizeHtml(content);
setProcessedContent({
html: sanitizedHtml,
text: content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(),
isHtml: true
});
} else {
// Format plain text properly using formatEmailContent
const formattedText = formatEmailContent({ text: content });
setProcessedContent({
html: formattedText,
text: content,
isHtml: false
});
}
}
} catch (err) {
console.error('Error processing email content:', err);
// Fallback to plain text with basic formatting
setProcessedContent({
html: `<div style="white-space: pre-wrap; font-family: monospace;">${content.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br />')}</div>`,
text: content,
isHtml: false
});
}
}, [content, type, isRawEmail]);
return (
<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>
);
};
export default EmailContentDisplay;

View File

@ -79,37 +79,26 @@ export default function EmailPanel({
console.log('EmailPanel: Raw email:', email);
// If content is already an object with html/text, use it directly
if (email.content && typeof email.content === 'object') {
console.log('EmailPanel: Using existing content object');
return {
...email,
content: {
text: email.content.text || '',
html: email.content.html || ''
}
};
// CRITICAL FIX: Simplify email formatting to prevent double processing
// Just normalize the content structure, don't try to format content here
// The actual formatting will happen in EmailPreview with formatEmailContent
// If all fields are already present, just return as is
if (email.content && typeof email.content === 'object' && email.content.html && email.content.text) {
return email;
}
// If content is a string, convert it to object format
if (typeof email.content === 'string') {
console.log('EmailPanel: Converting string content to object');
return {
...email,
content: {
text: email.text || email.content,
html: email.html || email.content
}
};
}
// Fallback to html/text properties
console.log('EmailPanel: Using html/text properties');
// Create a standardized email object with consistent content structure
return {
...email,
// Ensure content is an object with html and text properties
content: {
text: email.text || '',
html: email.html || ''
text: typeof email.content === 'object' ? email.content.text :
typeof email.text === 'string' ? email.text :
typeof email.content === 'string' ? email.content : '',
html: typeof email.content === 'object' ? email.content.html :
typeof email.html === 'string' ? email.html :
typeof email.content === 'string' ? email.content : ''
}
};
}, [email]);

View File

@ -107,8 +107,25 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
return '';
}
// Use the improved, standardized email content formatter
return formatEmailContent(email);
// CRITICAL FIX: Send consistent input format to formatEmailContent
try {
// Log what we're sending to formatEmailContent for debugging
console.log('EmailPreview: Calling formatEmailContent with email:',
JSON.stringify({
id: email.id,
contentType: typeof email.content,
hasHtml: typeof email.content === 'object' ? !!email.content.html : false,
hasText: typeof email.content === 'object' ? !!email.content.text : false,
hasHtmlProp: !!email.html,
hasTextProp: !!email.text
})
);
return formatEmailContent(email);
} catch (error) {
console.error('Error formatting email content:', error);
return `<div class="error-message p-4 text-red-500">Error rendering email content: ${error instanceof Error ? error.message : 'Unknown error'}</div>`;
}
}, [email]);
// Display loading state
@ -219,35 +236,33 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
{/* Email content */}
<ScrollArea className="flex-1">
<div className="p-6">
{/* IMPROVED: Simplified email content container with better styling */}
<div
ref={editorRef}
contentEditable={false}
className="w-full email-content-container rounded-md overflow-hidden"
className="email-content-container rounded-lg overflow-hidden bg-white shadow-sm"
style={{
backgroundColor: '#ffffff',
border: '1px solid #e2e8f0',
boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
minHeight: '300px'
minHeight: '300px',
border: '1px solid #e2e8f0'
}}
>
<div className="email-content-body p-4 sm:p-6">
{formattedContent ? (
<div
className="email-content-rendered"
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>
{/* Render the formatted content directly */}
{formattedContent ? (
<div
className="email-body"
dangerouslySetInnerHTML={{ __html: formattedContent }}
/>
) : (
<div className="p-8 text-center text-muted-foreground">
<p>This email does not contain any content.</p>
</div>
)}
</div>
{/* Only in development mode: Show debugging info */}
{process.env.NODE_ENV === 'development' && (
<details className="mt-6 text-xs text-muted-foreground border rounded-md p-2">
<details className="mt-4 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">
<div className="mt-2 overflow-auto max-h-40 p-2 bg-gray-50 rounded">
<p><strong>Email ID:</strong> {email.id}</p>
<p><strong>Content Type:</strong> {
typeof email.content === 'object' && email.content?.html

View File

@ -51,12 +51,12 @@ export function formatEmailContent(email: any): string {
// If we have HTML content, sanitize and standardize it
if (isHtml && content) {
// Make sure we have a complete HTML structure
// CRITICAL FIX: Check for browser environment since DOMParser is browser-only
const hasHtmlTag = content.includes('<html');
const hasBodyTag = content.includes('<body');
// Extract body content if we have a complete HTML document
if (hasHtmlTag && hasBodyTag) {
// Extract body content if we have a complete HTML document and in browser environment
if (hasHtmlTag && hasBodyTag && typeof window !== 'undefined' && typeof DOMParser !== 'undefined') {
try {
// Create a DOM parser to extract just the body content
const parser = new DOMParser();
@ -72,8 +72,8 @@ export function formatEmailContent(email: any): string {
}
}
// Sanitize with industry-standard email tags and attributes
// Use a more permissive configuration for email HTML
// CRITICAL FIX: Configure DOMPurify for maximum compatibility with email content
// This is a more permissive configuration that preserves common email HTML
const sanitizedContent = DOMPurify.sanitize(content, {
ADD_TAGS: [
'style', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
@ -85,15 +85,18 @@ export function formatEmailContent(email: any): string {
'figcaption', 'address', 'main', 'center', 'font'
],
ADD_ATTR: [
'class', 'style', 'id', 'href', 'src', 'alt', 'title', 'width', 'height',
'style', 'class', 'id', 'href', 'src', 'alt', 'title', 'width', 'height',
'border', 'cellspacing', 'cellpadding', 'bgcolor', 'color', 'dir', 'lang',
'align', 'valign', 'span', 'colspan', 'rowspan', 'target', 'rel',
'background', 'data-*', 'face', 'size', 'bgcolor', 'hspace', 'vspace',
'background', 'data-*', 'face', 'size', 'hspace', 'vspace',
'marginheight', 'marginwidth', 'frameborder'
],
ALLOW_DATA_ATTR: true,
ALLOW_UNKNOWN_PROTOCOLS: true, // Allow cid: and other protocols
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|cid|mailto|tel|callto|sms|bitcoin|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
WHOLE_DOCUMENT: false,
RETURN_DOM: false,
USE_PROFILES: { html: true, svg: false, svgFilters: false }, // Allow standard HTML
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout']
});
@ -103,29 +106,19 @@ export function formatEmailContent(email: any): string {
// 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
}
.replace(/(src|background)="(?!(?:https?:|data:|cid:))/gi, '$1="https://')
// Fix for base64 images that might be broken across lines
.replace(/src="data:image\/[^;]+;base64,\s*([^"]+)\s*"/gi, (match, p1) => {
return `src="data:image/png;base64,${p1.replace(/\s+/g, '')}"`;
});
// 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
return `
<div class="email-content-wrapper" style="max-width: 100%; overflow-x: auto;">
<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>
`;
// CRITICAL FIX: Use a single wrapper with all necessary styles for better email client compatibility
return `<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-x: auto; overflow-wrap: break-word; word-wrap: break-word;">${fixedContent}</div>`;
}
// If we only have text content, format it properly
else if (textContent) {
@ -148,25 +141,15 @@ export function formatEmailContent(email: any): string {
.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-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>
</div>
</div>
`;
// CRITICAL FIX: Use consistent structure with HTML emails for better compatibility
return `<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; max-width: 100%; overflow-wrap: break-word;"><p>${formattedText}</p></div>`;
}
// Default case: empty or unrecognized content
return '<div class="email-content-empty">No content available</div>';
return '<div class="email-content-empty" style="padding: 20px; text-align: center; color: #666;">No content available</div>';
} catch (error) {
console.error('formatEmailContent: Error formatting email content:', error);
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>
`;
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>`;
}
}