Neah/components/email/EmailContentDisplay.tsx
2025-04-27 11:03:34 +02:00

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;