diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 50f07e69..5cd64c11 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -63,6 +63,7 @@ interface Email { cc?: string; bcc?: string; flags?: string[]; + raw: string; } interface Attachment { @@ -327,64 +328,81 @@ const initialSidebarItems = [ ]; function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward'): string { - const { headers, body } = splitEmailHeadersAndBody(email.body); + if (!email.raw) return ''; + + const { headers, body } = splitEmailHeadersAndBody(email.raw); const { contentType, encoding, charset } = parseEmailHeaders(headers); - const isHtml = contentType.includes('text/html'); - const isMultipart = contentType.includes('multipart'); - const isQuotedPrintable = encoding === 'quoted-printable'; - - 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); + + let content = ''; + + if (contentType.includes('multipart/')) { + const boundary = contentType.match(/boundary="([^"]+)"/)?.[1]; + if (boundary) { + const parts = body.split('--' + boundary).filter(part => part.trim()); + + // Find HTML part first, fallback to text part + const htmlPart = parts.find(part => part.toLowerCase().includes('content-type: text/html')); + const textPart = parts.find(part => part.toLowerCase().includes('content-type: text/plain')); + + const selectedPart = htmlPart || textPart; + if (selectedPart) { + const partHeaders = selectedPart.split('\r\n\r\n')[0]; + const partBody = selectedPart.split('\r\n\r\n').slice(1).join('\r\n\r\n'); + const { encoding: partEncoding } = parseEmailHeaders(partHeaders); + + content = partEncoding === 'quoted-printable' + ? decodeQuotedPrintable(partBody, charset) + : partBody; + } } - } - - 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, '  '); + content = encoding === 'quoted-printable' + ? decodeQuotedPrintable(body, charset) + : body; } - + + // Convert plain text to HTML if needed + if (!contentType.includes('text/html')) { + content = content + .split('\n') + .map(line => `

${line}

`) + .join(''); + } + + // Sanitize HTML content + content = DOMPurify.sanitize(content, { + ALLOWED_TAGS: [ + 'p', 'br', 'div', 'span', 'b', 'i', 'u', 'strong', 'em', + 'blockquote', 'ul', 'ol', 'li', 'a', 'h1', 'h2', 'h3', 'h4', + 'table', 'thead', 'tbody', 'tr', 'td', 'th' + ], + ALLOWED_ATTR: ['href', 'style', 'class'], + }); + + const date = new Date(email.date).toLocaleString(); + if (type === 'forward') { return ` -
-

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

-

From: ${email.from}

-

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

-

Subject: ${email.subject}

-

To: ${email.to}

-
-
- ${content} -
-
+
+
+ ---------- Forwarded message ----------
+ From: ${email.from}
+ Date: ${date}
+ Subject: ${email.subject}
+ To: ${Array.isArray(email.to) ? email.to.join(', ') : email.to}
+
+ ${content} `; - } - - return ` -
-

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

-
+ } else { + return ` +
+
+ On ${date}, ${email.from} wrote:
+
${content}
-
- `; + `; + } } export default function CourrierPage() { @@ -555,7 +573,8 @@ export default function CourrierPage() { folder: email.folder || currentView, cc: email.cc, bcc: email.bcc, - flags: email.flags || [] + flags: email.flags || [], + raw: email.body || '' })); // Only update unread count if we're in the Inbox folder @@ -1444,7 +1463,8 @@ export default function CourrierPage() { folder: email.folder || newMailbox, cc: email.cc, bcc: email.bcc, - flags: email.flags || [] + flags: email.flags || [], + raw: email.body || '' })); setEmails(processedEmails); @@ -1680,26 +1700,13 @@ export default function CourrierPage() {
{ - // Preserve formatting by using a temporary div to clean the HTML - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = e.currentTarget.innerHTML; - - // Remove any potentially dangerous elements/attributes while preserving formatting - const cleanHtml = DOMPurify.sanitize(tempDiv.innerHTML, { - ALLOWED_TAGS: ['p', 'br', 'div', 'span', 'b', 'i', 'u', 'strong', 'em', 'blockquote', 'ul', 'ol', 'li', 'a'], - ALLOWED_ATTR: ['href', 'style', 'class'], - }); - - setComposeBody(cleanHtml); + suppressContentEditableWarning + onInput={(e: React.FormEvent) => { + setComposeBody((e.target as HTMLDivElement).innerHTML); }} - style={{ - minHeight: '200px', - overflowY: 'auto', - lineHeight: '1.5', - }} - /> + > + {composeBody} +