197 lines
6.6 KiB
TypeScript
197 lines
6.6 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useState, useRef } from 'react';
|
|
import DOMPurify from 'isomorphic-dompurify';
|
|
import { parseRawEmail } from '@/lib/utils/email-mime-decoder';
|
|
|
|
interface EmailContentDisplayProps {
|
|
content: string;
|
|
type?: 'html' | 'text' | 'auto';
|
|
className?: string;
|
|
showQuotedText?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Component for displaying properly formatted email content
|
|
* Handles MIME decoding, sanitization, and proper rendering
|
|
*/
|
|
const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
|
content,
|
|
type = 'auto',
|
|
className = '',
|
|
showQuotedText = true
|
|
}) => {
|
|
const [processedContent, setProcessedContent] = useState<{
|
|
html: string;
|
|
text: string;
|
|
isHtml: boolean;
|
|
}>({
|
|
html: '',
|
|
text: '',
|
|
isHtml: false
|
|
});
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Process and sanitize email content
|
|
useEffect(() => {
|
|
if (!content) {
|
|
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);
|
|
|
|
// Check which content to use based on type and availability
|
|
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
|
|
});
|
|
|
|
setProcessedContent({
|
|
html: sanitizedHtml,
|
|
text: parsed.text,
|
|
isHtml: true
|
|
});
|
|
} else {
|
|
// Format plain text with line breaks
|
|
const formattedText = parsed.text.replace(/\n/g, '<br />');
|
|
setProcessedContent({
|
|
html: formattedText,
|
|
text: parsed.text,
|
|
isHtml: false
|
|
});
|
|
}
|
|
} 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');
|
|
|
|
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
|
|
});
|
|
|
|
setProcessedContent({
|
|
html: sanitizedHtml,
|
|
text: content,
|
|
isHtml: true
|
|
});
|
|
} else {
|
|
// Format plain text with line breaks
|
|
const formattedText = content.replace(/\n/g, '<br />');
|
|
setProcessedContent({
|
|
html: formattedText,
|
|
text: content,
|
|
isHtml: false
|
|
});
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Error processing email content:', err);
|
|
// Fallback to plain text
|
|
setProcessedContent({
|
|
html: content.replace(/\n/g, '<br />'),
|
|
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]);
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={`email-content-display ${className}`}
|
|
dangerouslySetInnerHTML={{ __html: processedContent.html }}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export default EmailContentDisplay;
|