From 9a46e8839cd4356fdd845a12c9b96fe93fd7e86a Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 24 Apr 2025 19:38:44 +0200 Subject: [PATCH] compose mime --- components/ComposeEmail.tsx | 112 +++++++++++++++++------------------- lib/compose-mime-decoder.ts | 108 +++++++++++++--------------------- 2 files changed, 93 insertions(+), 127 deletions(-) diff --git a/components/ComposeEmail.tsx b/components/ComposeEmail.tsx index 208777bf..2bd0b68b 100644 --- a/components/ComposeEmail.tsx +++ b/components/ComposeEmail.tsx @@ -94,23 +94,35 @@ export default function ComposeEmail({ try { const originalContent = replyTo?.body || forwardFrom?.body || ''; - const response = await fetch('/api/parse-email', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ emailContent: originalContent }), - }); - - if (!response.ok) { - throw new Error('Failed to parse email'); - } - - const parsed = await response.json(); - + // Create initial content without waiting for parsing content = `

+
Loading original message...
+
+ `; + + // Set initial content immediately + composeBodyRef.current.innerHTML = content; + setLocalContent(content); + + // Place cursor at the beginning + const composeArea = composeBodyRef.current.querySelector('.compose-area'); + if (composeArea) { + const range = document.createRange(); + const sel = window.getSelection(); + range.setStart(composeArea, 0); + range.collapse(true); + sel?.removeAllRanges(); + sel?.addRange(range); + (composeArea as HTMLElement).focus(); + } + + // Now parse the email content + if (originalContent.trim()) { + const decodedContent = await decodeComposeContent(originalContent); + + const quotedContent = ` ${forwardFrom ? `
---------- Forwarded message ---------
@@ -120,43 +132,38 @@ export default function ComposeEmail({ To: ${forwardFrom.to}
${forwardFrom.cc ? `Cc: ${forwardFrom.cc}
` : ''}
- ${parsed.html || parsed.text} + ${decodedContent.html || decodedContent.text || 'No content available'}
` : `
On ${new Date(replyTo?.date || '').toLocaleString()}, ${replyTo?.from} wrote:
- ${parsed.html || parsed.text} + ${decodedContent.html || decodedContent.text || 'No content available'}
`} - - `; + `; + + // Replace placeholder with actual content + const placeholder = composeBodyRef.current.querySelector('#reply-placeholder'); + if (placeholder) { + placeholder.insertAdjacentHTML('beforebegin', quotedContent); + placeholder.remove(); + } + } } catch (error) { console.error('Error parsing email:', error); - content = `
`; + const placeholder = composeBodyRef.current.querySelector('#reply-placeholder'); + if (placeholder) { + placeholder.textContent = 'Error loading original message.'; + } } finally { setIsLoading(false); } } else { content = `
`; - } - - if (composeBodyRef.current) { composeBodyRef.current.innerHTML = content; setLocalContent(content); - - // Place cursor at the beginning of the compose area - const composeArea = composeBodyRef.current.querySelector('.compose-area'); - if (composeArea) { - const range = document.createRange(); - const sel = window.getSelection(); - range.setStart(composeArea, 0); - range.collapse(true); - sel?.removeAllRanges(); - sel?.addRange(range); - (composeArea as HTMLElement).focus(); - } } }; @@ -170,8 +177,13 @@ export default function ComposeEmail({ if (!composeArea) return; const content = composeArea.innerHTML; - setLocalContent(content); - setComposeBody(content); + if (!content.trim()) { + setLocalContent(''); + setComposeBody(''); + } else { + setLocalContent(content); + setComposeBody(content); + } if (onBodyChange) { onBodyChange(content); @@ -179,40 +191,20 @@ export default function ComposeEmail({ }; const handleSendEmail = async () => { - // Ensure we have content before sending - if (!composeBodyRef.current) { - console.error('Compose body ref is not available'); - return; - } + if (!composeBodyRef.current) return; const composeArea = composeBodyRef.current.querySelector('.compose-area'); - if (!composeArea) { - console.error('Compose area not found'); - return; - } + if (!composeArea) return; - // Get the current content const content = composeArea.innerHTML; if (!content.trim()) { console.error('Email content is empty'); return; } - // Create MIME headers - const mimeHeaders = { - 'MIME-Version': '1.0', - 'Content-Type': 'text/html; charset="utf-8"', - 'Content-Transfer-Encoding': 'quoted-printable' - }; - - // Combine headers and content - const mimeContent = Object.entries(mimeHeaders) - .map(([key, value]) => `${key}: ${value}`) - .join('\n') + '\n\n' + content; - - setComposeBody(mimeContent); - try { + const encodedContent = await encodeComposeContent(content); + setComposeBody(encodedContent); await handleSend(); setShowCompose(false); } catch (error) { diff --git a/lib/compose-mime-decoder.ts b/lib/compose-mime-decoder.ts index 96b4c140..d6048b89 100644 --- a/lib/compose-mime-decoder.ts +++ b/lib/compose-mime-decoder.ts @@ -3,73 +3,47 @@ * Handles basic email content without creating nested structures */ -export function decodeComposeContent(content: string): string { - if (!content) return ''; - - // Basic HTML cleaning without creating nested structures - let cleaned = content - // Remove script and style tags - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/]*>[\s\S]*?<\/style>/gi, '') - // Remove meta tags - .replace(/]*>/gi, '') - // Remove head and title - .replace(/]*>[\s\S]*?<\/head>/gi, '') - .replace(/]*>[\s\S]*?<\/title>/gi, '') - // Remove body tags - .replace(/]*>/gi, '') - .replace(/<\/body>/gi, '') - // Remove html tags - .replace(/]*>/gi, '') - .replace(/<\/html>/gi, '') - // Handle basic formatting - .replace(//gi, '\n') - .replace(/]*>/gi, '\n') - .replace(/<\/p>/gi, '\n') - // Handle lists - .replace(/]*>/gi, '\n') - .replace(/<\/ul>/gi, '\n') - .replace(/]*>/gi, '\n') - .replace(/<\/ol>/gi, '\n') - .replace(/]*>/gi, '• ') - .replace(/<\/li>/gi, '\n') - // Handle basic text formatting - .replace(/]*>/gi, '**') - .replace(/<\/strong>/gi, '**') - .replace(/]*>/gi, '**') - .replace(/<\/b>/gi, '**') - .replace(/]*>/gi, '*') - .replace(/<\/em>/gi, '*') - .replace(/]*>/gi, '*') - .replace(/<\/i>/gi, '*') - // Handle links - .replace(/]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '$2 ($1)') - // Handle basic entities - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - // Clean up whitespace - .replace(/\s+/g, ' ') - .trim(); - - // Do NOT wrap in additional divs - return cleaned; +import { simpleParser } from 'mailparser'; + +interface ParsedContent { + html: string | null; + text: string | null; } -export function encodeComposeContent(content: string): string { - if (!content) return ''; - - // Basic HTML encoding without adding structure - const encoded = content - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(/\n/g, '
'); - - return encoded; +export async function decodeComposeContent(content: string): Promise { + if (!content.trim()) { + return { html: null, text: null }; + } + + try { + const parsed = await simpleParser(content); + return { + html: parsed.html || null, + text: parsed.text || null + }; + } catch (error) { + console.error('Error parsing email content:', error); + return { + html: content, + text: content + }; + } +} + +export async function encodeComposeContent(content: string): Promise { + if (!content.trim()) { + throw new Error('Email content is empty'); + } + + // Create MIME headers + const mimeHeaders = { + 'MIME-Version': '1.0', + 'Content-Type': 'text/html; charset="utf-8"', + 'Content-Transfer-Encoding': 'quoted-printable' + }; + + // Combine headers and content + return Object.entries(mimeHeaders) + .map(([key, value]) => `${key}: ${value}`) + .join('\n') + '\n\n' + content; } \ No newline at end of file