mail page rest

This commit is contained in:
alma 2025-04-21 15:24:59 +02:00
parent 8d9694ba95
commit f4502f15ff

View File

@ -78,10 +78,14 @@ interface EmailAttachment {
}
interface ParsedEmail {
text: string;
html: string;
attachments: EmailAttachment[];
headers?: string;
text: string | null;
html: string | null;
attachments: Array<{
filename: string;
contentType: string;
encoding: string;
content: string;
}>;
}
interface EmailMessage {
@ -99,47 +103,91 @@ interface EmailMessage {
};
}
function parseFullEmail(emailRaw: string): ParsedEmail | EmailMessage {
// Check if this is a multipart message by looking for boundary definition
const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) ||
emailRaw.match(/boundary=([^\r\n;]+)/i);
if (boundaryMatch) {
const boundary = boundaryMatch[1].trim();
function parseFullEmail(content: string): ParsedEmail {
try {
// First, try to parse the email headers
const headers = parseEmailHeaders(content);
// Check if there's a preamble before the first boundary
let mainHeaders = '';
let mainContent = emailRaw;
// Extract the headers before the first boundary if they exist
const firstBoundaryPos = emailRaw.indexOf('--' + boundary);
if (firstBoundaryPos > 0) {
const headerSeparatorPos = emailRaw.indexOf('\r\n\r\n');
if (headerSeparatorPos > 0 && headerSeparatorPos < firstBoundaryPos) {
mainHeaders = emailRaw.substring(0, headerSeparatorPos);
// If it's a multipart email, process each part
if (headers.contentType?.includes('multipart')) {
const boundary = extractBoundary(headers.contentType);
if (!boundary) {
throw new Error('No boundary found in multipart content');
}
}
return processMultipartEmail(emailRaw, boundary, mainHeaders);
} else {
// Split headers and body
const [headers, body] = emailRaw.split(/\r?\n\r?\n/, 2);
// If no boundary is found, treat as a single part message
const emailInfo = parseEmailHeaders(headers);
return {
subject: extractHeader(headers, 'Subject'),
from: extractHeader(headers, 'From'),
to: extractHeader(headers, 'To'),
date: extractHeader(headers, 'Date'),
contentType: emailInfo.contentType,
text: emailInfo.contentType.includes('text/plain') ? body : null,
html: emailInfo.contentType.includes('text/html') ? body : null,
attachments: [], // Add empty attachments array for single part messages
raw: {
headers,
body
const parts = content.split(boundary);
const result: ParsedEmail = {
text: null,
html: null,
attachments: []
};
for (const part of parts) {
if (!part.trim()) continue;
const partHeaders = parseEmailHeaders(part);
const partContent = part.split('\r\n\r\n')[1] || '';
// Handle HTML content
if (partHeaders.contentType?.includes('text/html')) {
const decoded = decodeMIME(
partContent,
partHeaders.encoding || '7bit',
partHeaders.charset || 'utf-8'
);
result.html = cleanHtml(decoded);
}
// Handle plain text content
else if (partHeaders.contentType?.includes('text/plain')) {
const decoded = decodeMIME(
partContent,
partHeaders.encoding || '7bit',
partHeaders.charset || 'utf-8'
);
result.text = decoded;
}
// Handle attachments
else if (partHeaders.contentType && !partHeaders.contentType.includes('text/')) {
const filename = extractFilename(partHeaders.contentType) || 'attachment';
result.attachments.push({
filename,
contentType: partHeaders.contentType,
encoding: partHeaders.encoding || '7bit',
content: partContent
});
}
}
return result;
}
// If it's not multipart, handle as a single part
const body = content.split('\r\n\r\n')[1] || '';
const decoded = decodeMIME(
body,
headers.encoding || '7bit',
headers.charset || 'utf-8'
);
if (headers.contentType?.includes('text/html')) {
return {
html: cleanHtml(decoded),
text: null,
attachments: []
};
}
return {
html: null,
text: decoded,
attachments: []
};
} catch (e) {
console.error('Error parsing email:', e);
return {
html: null,
text: content,
attachments: []
};
}
}
@ -251,14 +299,71 @@ function decodeMimeContent(content: string): string {
return cleanHtml(content);
}
// Add this helper function
const renderEmailContent = (email: Email) => {
const decodedContent = decodeMimeContent(email.body);
if (email.body.includes('Content-Type: text/html')) {
return <div dangerouslySetInnerHTML={{ __html: decodedContent }} />;
function renderEmailContent(email: Email) {
try {
// First, parse the full email to get headers and body
const parsed = parseFullEmail(email.body);
// If we have HTML content, render it
if (parsed.html) {
return (
<div
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: parsed.html }}
/>
);
}
// If we have text content, render it
if (parsed.text) {
return (
<div className="whitespace-pre-wrap font-sans text-base leading-relaxed">
{parsed.text.split('\n').map((line, i) => (
<p key={i} className="mb-2">{line}</p>
))}
</div>
);
}
// If we have attachments, display them
if (parsed.attachments && parsed.attachments.length > 0) {
return (
<div className="mt-6 border-t border-gray-200 pt-6">
<h3 className="text-sm font-semibold text-gray-900 mb-4">Attachments</h3>
<div className="space-y-2">
{parsed.attachments.map((attachment, index) => (
<div key={index} className="flex items-center space-x-2 p-2 border rounded">
<Paperclip className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-600 truncate">
{attachment.filename}
</span>
</div>
))}
</div>
</div>
);
}
// If we couldn't parse the content, try to decode and clean the raw body
const decodedBody = decodeMIME(email.body, 'quoted-printable', 'utf-8');
const cleanedContent = cleanHtml(decodedBody);
return (
<div className="whitespace-pre-wrap font-sans text-base leading-relaxed">
{cleanedContent.split('\n').map((line, i) => (
<p key={i} className="mb-2">{line}</p>
))}
</div>
);
} catch (e) {
console.error('Error rendering email content:', e);
return (
<div className="text-red-500">
Error rendering email content. Please try again later.
</div>
);
}
return <div className="whitespace-pre-wrap">{decodedContent}</div>;
};
}
// Add this helper function
const decodeEmailContent = (content: string, charset: string = 'utf-8') => {
@ -868,61 +973,7 @@ export default function CourrierPage() {
</div>
<div className="prose max-w-none">
{(() => {
try {
const parsed = parseFullEmail(selectedEmail.body);
return (
<div>
{/* Display HTML content if available, otherwise fallback to text */}
{parsed.html ? (
<div
className="prose prose-sm sm:prose lg:prose-lg xl:prose-xl dark:prose-invert max-w-none"
dangerouslySetInnerHTML={{
__html: parsed.html
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<base[^>]*>/gi, '')
.replace(/<meta[^>]*>/gi, '')
.replace(/<link[^>]*>/gi, '')
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
.replace(/<body[^>]*>/gi, '')
.replace(/<\/body>/gi, '')
.replace(/<html[^>]*>/gi, '')
.replace(/<\/html>/gi, '')
}}
/>
) : (
<div className="whitespace-pre-wrap font-sans text-base leading-relaxed">
{(parsed.text || '').split('\n').map((line, i) => (
<p key={i} className="mb-2">{line}</p>
))}
</div>
)}
{/* Display attachments if present */}
{parsed.attachments && parsed.attachments.length > 0 && (
<div className="mt-6 border-t border-gray-200 pt-6">
<h3 className="text-sm font-semibold text-gray-900 mb-4">Attachments</h3>
<div className="grid grid-cols-2 gap-4">
{parsed.attachments.map((attachment, index) => (
<div key={index} className="flex items-center space-x-2 p-2 border rounded">
<Paperclip className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-600 truncate">
{attachment.filename}
</span>
</div>
))}
</div>
</div>
)}
</div>
);
} catch (e) {
console.error('Error parsing email:', e);
return <div className="text-gray-500">Error displaying email content</div>;
}
})()}
{renderEmailContent(selectedEmail)}
</div>
</ScrollArea>
</>