mail page fix

This commit is contained in:
alma 2025-04-21 16:36:20 +02:00
parent 192ae462de
commit 2891ec3e11

View File

@ -97,6 +97,14 @@ interface ParsedEmailMetadata {
};
}
function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: string } {
const [headers, ...bodyParts] = emailBody.split('\r\n\r\n');
return {
headers: headers || '',
body: bodyParts.join('\r\n\r\n')
};
}
function renderEmailContent(email: Email) {
if (!email.body) {
console.warn('No email body provided');
@ -317,145 +325,65 @@ const initialSidebarItems = [
}
];
function getReplyBody(email: Email | null, type: 'reply' | 'replyAll' | 'forward'): string {
if (!email?.body) return '';
function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward'): string {
const { headers, body } = splitEmailHeadersAndBody(email.body);
const { contentType, encoding, charset } = parseEmailHeaders(headers);
const isHtml = contentType.includes('text/html');
const isMultipart = contentType.includes('multipart');
const isQuotedPrintable = encoding === 'quoted-printable';
try {
// Split email into headers and body
const [headersPart, ...bodyParts] = email.body.split('\r\n\r\n');
if (!headersPart || bodyParts.length === 0) {
throw new Error('Invalid email format: missing headers or body');
}
const body = bodyParts.join('\r\n\r\n');
// Parse headers using Infomaniak MIME decoder
const headerInfo = parseEmailHeaders(headersPart);
const boundary = extractBoundary(headersPart);
let content = '';
let isHtml = false;
// If it's a multipart email
if (boundary) {
const parts = body.split(`--${boundary}`);
for (const part of parts) {
if (!part.trim()) continue;
const [partHeaders, ...partBodyParts] = part.split('\r\n\r\n');
if (!partHeaders || partBodyParts.length === 0) continue;
const partBody = partBodyParts.join('\r\n\r\n');
const contentType = extractHeader(partHeaders, 'Content-Type').toLowerCase();
const encoding = extractHeader(partHeaders, 'Content-Transfer-Encoding').toLowerCase();
const charset = extractHeader(partHeaders, 'charset') || 'utf-8';
try {
let decodedContent = '';
if (encoding === 'base64') {
decodedContent = decodeBase64(partBody, charset);
} else if (encoding === 'quoted-printable') {
decodedContent = decodeQuotedPrintable(partBody, charset);
} else {
decodedContent = convertCharset(partBody, charset);
}
if (contentType.includes('text/html')) {
// For HTML content, preserve the HTML structure
content = decodedContent
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<meta[^>]*>/gi, '')
.replace(/<link[^>]*>/gi, '')
.replace(/<base[^>]*>/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, '');
isHtml = true;
break; // Prefer HTML content
} else if (contentType.includes('text/plain') && !content) {
content = decodedContent;
}
} catch (partError) {
console.error('Error processing email part:', partError);
continue;
}
}
}
// If no content found or not multipart, try to decode the whole body
if (!content) {
const encoding = extractHeader(headersPart, 'Content-Transfer-Encoding').toLowerCase();
const charset = extractHeader(headersPart, 'charset') || 'utf-8';
if (encoding === 'base64') {
content = decodeBase64(body, charset);
} else if (encoding === 'quoted-printable') {
content = decodeQuotedPrintable(body, charset);
} else {
content = convertCharset(body, charset);
}
if (headerInfo.contentType.includes('text/html')) {
content = content
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<meta[^>]*>/gi, '')
.replace(/<link[^>]*>/gi, '')
.replace(/<base[^>]*>/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, '');
isHtml = true;
}
}
// Format the reply
const date = new Date(email.date);
const formattedDate = date.toLocaleString('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
const from = email.fromName || email.from;
const subject = email.subject || '(No subject)';
if (type === 'forward') {
return `---------- Forwarded message ---------
From: ${from}
Date: ${formattedDate}
Subject: ${subject}
To: ${email.to}
${content}`;
} else {
if (isHtml) {
// For HTML content, wrap each line in a blockquote
const quotedHtml = content
.split('\n')
.map(line => `<blockquote>${line}</blockquote>`)
.join('\n');
return `\n\nOn ${formattedDate}, ${from} wrote:\n${quotedHtml}`;
} else {
return `\n\nOn ${formattedDate}, ${from} wrote:\n> ${content.split('\n').join('\n> ')}`;
}
}
} catch (error) {
console.error('Error formatting reply:', error);
return '';
let content = body;
if (isQuotedPrintable) {
content = decodeQuotedPrintable(content, charset);
}
if (isMultipart) {
const parts = content.split('--boundary');
content = parts.find((part: string) => part.includes('text/html')) || parts.find((part: string) => part.includes('text/plain')) || '';
const partHeaders = content.split('\n\n')[0];
const partContent = content.split('\n\n').slice(1).join('\n\n');
const { contentType: partContentType, encoding: partEncoding, charset: partCharset } = parseEmailHeaders(partHeaders);
content = partContent;
if (partEncoding === 'quoted-printable') {
content = decodeQuotedPrintable(content, partCharset);
}
}
if (isHtml) {
// Preserve HTML structure while cleaning potentially dangerous elements
content = cleanHtml(content);
} else {
// Convert plain text to HTML while preserving formatting
content = content
.replace(/\n/g, '<br>')
.replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;')
.replace(/ /g, '&nbsp;&nbsp;');
}
if (type === 'forward') {
return `
<div class="forwarded-message">
<p>---------- Forwarded message ---------</p>
<p>From: ${email.from}</p>
<p>Date: ${new Date(email.date).toLocaleString()}</p>
<p>Subject: ${email.subject}</p>
<p>To: ${email.to}</p>
<br>
<div class="prose">
${content}
</div>
</div>
`;
}
return `
<div class="reply-message">
<p>On ${new Date(email.date).toLocaleString()}, ${email.from} wrote:</p>
<blockquote class="prose">
${content}
</blockquote>
</div>
`;
}
export default function CourrierPage() {
@ -1120,15 +1048,6 @@ export default function CourrierPage() {
// Update the email list item to match header checkbox alignment
const renderEmailListItem = (email: Email) => {
console.log('=== Email List Item Debug ===');
console.log('Email ID:', email.id);
console.log('Subject:', email.subject);
console.log('Body length:', email.body.length);
console.log('First 100 chars of body:', email.body.substring(0, 100));
const preview = generateEmailPreview(email);
console.log('Generated preview:', preview);
return (
<div
key={email.id}
@ -1149,19 +1068,6 @@ export default function CourrierPage() {
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<span className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}>
{currentView === 'Sent' ? email.to : (
(() => {
const fromMatch = email.from.match(/^([^<]+)\s*<([^>]+)>$/);
return fromMatch ? fromMatch[1].trim() : email.from;
})()
)}
</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<span className="text-xs text-gray-500 whitespace-nowrap">
{formatDate(email.date)}
</span>
<Button
variant="ghost"
size="icon"
@ -1170,13 +1076,19 @@ export default function CourrierPage() {
>
<Star className={`h-4 w-4 ${email.starred ? 'fill-yellow-400 text-yellow-400' : ''}`} />
</Button>
<span className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}>
{email.fromName || email.from}
</span>
</div>
<span className="text-xs text-gray-500 whitespace-nowrap">
{formatDate(email.date)}
</span>
</div>
<h3 className="text-sm text-gray-900 truncate">
{email.subject || '(No subject)'}
</h3>
<div className="text-xs text-gray-500 truncate">
{preview}
<div className="text-xs text-gray-500 line-clamp-2">
{generateEmailPreview(email)}
</div>
</div>
</div>
@ -1765,15 +1677,15 @@ export default function CourrierPage() {
</div>
{/* Message Body */}
<div>
<Label htmlFor="message" className="block text-sm font-medium text-gray-700">Message</Label>
<div className="flex-1 overflow-auto">
<div
id="message"
contentEditable
className="prose max-w-none min-h-[200px] p-4 focus:outline-none"
dangerouslySetInnerHTML={{ __html: composeBody }}
onInput={(e) => setComposeBody(e.currentTarget.innerHTML)}
className="w-full mt-1 min-h-[200px] bg-white border border-gray-300 text-gray-900 p-2 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
style={{ whiteSpace: 'pre-wrap' }}
onInput={(e) => {
const content = e.currentTarget.innerHTML;
setComposeBody(content);
}}
/>
</div>
</div>