Neah/components/email/EmailPreview.tsx
2025-04-26 09:56:48 +02:00

179 lines
6.4 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import DOMPurify from 'isomorphic-dompurify';
import { EmailMessage } from '@/lib/services/email-service';
import { Loader2, Paperclip, Download } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { cleanHtml } from '@/lib/mail-parser-wrapper';
interface EmailPreviewProps {
email: EmailMessage | null;
loading?: boolean;
onReply?: (type: 'reply' | 'reply-all' | 'forward') => void;
}
export default function EmailPreview({ email, loading = false, onReply }: EmailPreviewProps) {
const [contentLoading, setContentLoading] = useState<boolean>(false);
// Handle sanitizing and rendering HTML content
const renderContent = () => {
if (!email?.content) return <p>No content available</p>;
try {
// Use DOMPurify directly with enhanced sanitization options
const sanitizedContent = DOMPurify.sanitize(email.content, {
ADD_TAGS: ['style', 'meta', 'link', 'table', 'thead', 'tbody', 'tr', 'td', 'th', 'hr', 'font', 'div', 'span', 'a', 'img', 'b', 'strong', 'i', 'em', 'u', 'br', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code', 'center', 'section', 'header', 'footer', 'article', 'nav', 'keyframes'],
ADD_ATTR: ['*', 'colspan', 'rowspan', 'cellpadding', 'cellspacing', 'border', 'bgcolor', 'width', 'height', 'align', 'valign', 'class', 'id', 'style', 'color', 'face', 'size', 'background', 'src', 'href', 'target', 'rel', 'alt', 'title', 'name', 'animation', 'animation-name', 'animation-duration', 'animation-fill-mode'],
ALLOW_UNKNOWN_PROTOCOLS: true,
WHOLE_DOCUMENT: true,
KEEP_CONTENT: true,
RETURN_DOM: false,
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'select', 'option', 'textarea', 'canvas', 'video', 'audio'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onchange', 'onsubmit'],
USE_PROFILES: { html: true, svg: false, svgFilters: false, mathMl: false },
FORCE_BODY: true
});
return (
<div
className="email-content prose max-w-none dark:prose-invert"
dangerouslySetInnerHTML={{ __html: sanitizedContent }}
/>
);
} catch (error) {
console.error('Error rendering email content:', error);
return <p>Error displaying email content</p>;
}
};
// Format the date
const formatDate = (date: Date | string) => {
if (!date) return '';
const dateObj = date instanceof Date ? date : new Date(date);
return dateObj.toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Format email addresses
const formatEmailAddresses = (addresses: Array<{name: string, address: string}> | undefined) => {
if (!addresses || addresses.length === 0) return '';
return addresses.map(addr =>
addr.name && addr.name !== addr.address
? `${addr.name} <${addr.address}>`
: addr.address
).join(', ');
};
if (loading || contentLoading) {
return (
<div className="flex items-center justify-center h-full p-6">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-primary" />
<p>Loading email content...</p>
</div>
</div>
);
}
if (!email) {
return (
<div className="flex items-center justify-center h-full p-6">
<div className="text-center text-muted-foreground">
<p>Select an email to view</p>
</div>
</div>
);
}
return (
<div className="flex flex-col h-full overflow-hidden">
{/* Email header */}
<div className="p-4 border-b">
<div className="mb-3">
<h2 className="text-xl font-semibold mb-2">{email.subject}</h2>
<div className="flex items-center justify-between text-sm">
<div className="flex items-center">
<span className="font-medium mr-1">From:</span>
<span>{formatEmailAddresses(email.from)}</span>
</div>
<span className="text-muted-foreground">{formatDate(email.date)}</span>
</div>
{email.to && email.to.length > 0 && (
<div className="text-sm mt-1">
<span className="font-medium mr-1">To:</span>
<span>{formatEmailAddresses(email.to)}</span>
</div>
)}
{email.cc && email.cc.length > 0 && (
<div className="text-sm mt-1">
<span className="font-medium mr-1">Cc:</span>
<span>{formatEmailAddresses(email.cc)}</span>
</div>
)}
</div>
{/* Action buttons */}
{onReply && (
<div className="flex gap-2 mt-4">
<Button
size="sm"
variant="outline"
onClick={() => onReply('reply')}
>
Reply
</Button>
<Button
size="sm"
variant="outline"
onClick={() => onReply('reply-all')}
>
Reply All
</Button>
<Button
size="sm"
variant="outline"
onClick={() => onReply('forward')}
>
Forward
</Button>
</div>
)}
{/* Attachments */}
{email.attachments && email.attachments.length > 0 && (
<div className="mt-4 border-t pt-2">
<div className="text-sm font-medium mb-2">Attachments:</div>
<div className="flex flex-wrap gap-2">
{email.attachments.map((attachment, index) => (
<Badge key={index} variant="outline" className="flex items-center gap-1">
<Paperclip className="h-3 w-3" />
<span>{attachment.filename}</span>
<span className="text-xs text-muted-foreground ml-1">
({Math.round(attachment.size / 1024)}KB)
</span>
</Badge>
))}
</div>
</div>
)}
</div>
{/* Email content */}
<div className="flex-1 overflow-auto p-4">
{renderContent()}
</div>
</div>
);
}