179 lines
6.4 KiB
TypeScript
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>
|
|
);
|
|
}
|