diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index a7483d4d..a7ef986e 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -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(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>[\s\S]*?<\/title>/gi, '') - .replace(/]*>[\s\S]*?<\/head>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/body>/gi, '') - .replace(/]*>/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(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>[\s\S]*?<\/title>/gi, '') - .replace(/]*>[\s\S]*?<\/head>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/body>/gi, '') - .replace(/]*>/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 => `
${line}
`) - .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, '
') + .replace(/\t/g, '    ') + .replace(/ /g, '  '); + } + + if (type === 'forward') { + return ` +
+

---------- Forwarded message ---------

+

From: ${email.from}

+

Date: ${new Date(email.date).toLocaleString()}

+

Subject: ${email.subject}

+

To: ${email.to}

+
+
+ ${content} +
+
+ `; + } + + return ` +
+

On ${new Date(email.date).toLocaleString()}, ${email.from} wrote:

+
+ ${content} +
+
+ `; } 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 (
- - {currentView === 'Sent' ? email.to : ( - (() => { - const fromMatch = email.from.match(/^([^<]+)\s*<([^>]+)>$/); - return fromMatch ? fromMatch[1].trim() : email.from; - })() - )} - -
-
- - {formatDate(email.date)} - + + {email.fromName || email.from} +
+ + {formatDate(email.date)} +

{email.subject || '(No subject)'}

-
- {preview} +
+ {generateEmailPreview(email)}
@@ -1765,15 +1677,15 @@ export default function CourrierPage() { {/* Message Body */} -
- +
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); + }} />