From 9a762927fce3faf1ea5561c1984df0d178a00f2e Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 25 Apr 2025 09:25:04 +0200 Subject: [PATCH] compose mime --- app/api/parse-email/route.ts | 43 +++-- app/courrier/page.tsx | 329 +++++++++++++++++++++-------------- components/ComposeEmail.tsx | 232 ++++++++++++------------ lib/compose-mime-decoder.ts | 119 ++++++------- lib/mail-parser-wrapper.ts | 66 ++++++- next.config.js | 11 +- 6 files changed, 457 insertions(+), 343 deletions(-) diff --git a/app/api/parse-email/route.ts b/app/api/parse-email/route.ts index 613b89ce..ad03828d 100644 --- a/app/api/parse-email/route.ts +++ b/app/api/parse-email/route.ts @@ -1,29 +1,48 @@ import { NextResponse } from 'next/server'; -import { parseEmail } from '@/lib/server/email-parser'; +import { simpleParser, AddressObject } from 'mailparser'; + +function getEmailAddress(address: AddressObject | AddressObject[] | undefined): string | null { + if (!address) return null; + if (Array.isArray(address)) { + return address.map(a => a.text).join(', '); + } + return address.text; +} export async function POST(request: Request) { try { const body = await request.json(); - console.log('Received request body:', body); + const { email } = body; - const { emailContent } = body; - console.log('Email content type:', typeof emailContent); - console.log('Email content length:', emailContent?.length); - - if (!emailContent || typeof emailContent !== 'string') { - console.log('Invalid email content:', { emailContent, type: typeof emailContent }); + if (!email || typeof email !== 'string') { return NextResponse.json( - { error: 'Invalid email content. Expected a string.', received: { type: typeof emailContent, length: emailContent?.length } }, + { error: 'Invalid email content' }, { status: 400 } ); } - const parsed = await parseEmail(emailContent); - return NextResponse.json(parsed); + const parsed = await simpleParser(email); + + return NextResponse.json({ + subject: parsed.subject || null, + from: getEmailAddress(parsed.from), + to: getEmailAddress(parsed.to), + cc: getEmailAddress(parsed.cc), + bcc: getEmailAddress(parsed.bcc), + date: parsed.date || null, + html: parsed.html || null, + text: parsed.textAsHtml || parsed.text || null, + attachments: parsed.attachments?.map(att => ({ + filename: att.filename, + contentType: att.contentType, + size: att.size + })) || [], + headers: parsed.headers || {} + }); } catch (error) { console.error('Error parsing email:', error); return NextResponse.json( - { error: 'Failed to parse email', details: error instanceof Error ? error.message : 'Unknown error' }, + { error: 'Failed to parse email' }, { status: 500 } ); } diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index baafd4cc..0a519b67 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -103,20 +103,30 @@ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: s function EmailContent({ email }: { email: Email }) { const [content, setContent] = useState(null); const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { let mounted = true; async function loadContent() { + if (!email) return; + + setIsLoading(true); try { if (!email.body) { - if (mounted) setContent(null); + if (mounted) { + setContent(
No content available
); + setIsLoading(false); + } return; } const formattedEmail = email.body.trim(); if (!formattedEmail) { - if (mounted) setContent(null); + if (mounted) { + setContent(
No content available
); + setIsLoading(false); + } return; } @@ -127,7 +137,7 @@ function EmailContent({ email }: { email: Email }) { setContent(
); } else if (parsedEmail.text) { @@ -137,15 +147,17 @@ function EmailContent({ email }: { email: Email }) {
); } else { - setContent(null); + setContent(
No content available
); } setError(null); + setIsLoading(false); } } catch (err) { console.error('Error rendering email content:', err); if (mounted) { setError('Error rendering email content. Please try again.'); setContent(null); + setIsLoading(false); } } } @@ -155,13 +167,21 @@ function EmailContent({ email }: { email: Email }) { return () => { mounted = false; }; - }, [email.body]); + }, [email?.body]); + + if (isLoading) { + return ( +
+
+
+ ); + } if (error) { return
{error}
; } - return content; + return content ||
No content available
; } function renderEmailContent(email: Email) { @@ -310,15 +330,29 @@ function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'r function EmailPreview({ email }: { email: Email }) { const [preview, setPreview] = useState(''); const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { let mounted = true; async function loadPreview() { + if (!email?.body) { + if (mounted) setPreview('No content available'); + return; + } + + setIsLoading(true); try { const decoded = await decodeEmail(email.body); if (mounted) { - setPreview(decoded.text || cleanHtml(decoded.html || '')); + if (decoded.text) { + setPreview(decoded.text.substring(0, 150) + '...'); + } else if (decoded.html) { + const cleanText = decoded.html.replace(/<[^>]*>/g, ' ').trim(); + setPreview(cleanText.substring(0, 150) + '...'); + } else { + setPreview('No preview available'); + } setError(null); } } catch (err) { @@ -327,6 +361,8 @@ function EmailPreview({ email }: { email: Email }) { setError('Error generating preview'); setPreview(''); } + } finally { + if (mounted) setIsLoading(false); } } @@ -335,7 +371,11 @@ function EmailPreview({ email }: { email: Email }) { return () => { mounted = false; }; - }, [email.body]); + }, [email?.body]); + + if (isLoading) { + return Loading preview...; + } if (error) { return {error}; @@ -507,38 +547,55 @@ export default function CourrierPage() { setAvailableFolders(data.folders); } - // Process emails keeping exact folder names - const processedEmails = (data.emails || []).map((email: any) => ({ - id: Number(email.id), - accountId: 1, - from: email.from || '', - fromName: email.fromName || email.from?.split('@')[0] || '', - to: email.to || '', - subject: email.subject || '(No subject)', - body: email.body || '', - date: email.date || new Date().toISOString(), - read: email.read || false, - starred: email.starred || false, - folder: email.folder || currentView, - cc: email.cc, - bcc: email.bcc, - flags: email.flags || [], - raw: email.body || '' - })); + // Process emails keeping exact folder names and sort by date + const processedEmails = (data.emails || []) + .map((email: any) => ({ + id: Number(email.id), + accountId: 1, + from: email.from || '', + fromName: email.fromName || email.from?.split('@')[0] || '', + to: email.to || '', + subject: email.subject || '(No subject)', + body: email.body || '', + date: email.date || new Date().toISOString(), + read: email.read || false, + starred: email.starred || false, + folder: email.folder || currentView, + cc: email.cc, + bcc: email.bcc, + flags: email.flags || [], + raw: email.body || '' + })); + + // Sort emails by date, ensuring most recent first + const sortedEmails = processedEmails.sort((a: Email, b: Email) => { + const dateA = new Date(a.date).getTime(); + const dateB = new Date(b.date).getTime(); + return dateB - dateA; // Most recent first + }); // Only update unread count if we're in the Inbox folder if (currentView === 'INBOX') { - const unreadInboxEmails = processedEmails.filter( + const unreadInboxEmails = sortedEmails.filter( (email: Email) => !email.read && email.folder === 'INBOX' ).length; setUnreadCount(unreadInboxEmails); } if (isLoadMore) { - setEmails(prev => [...prev, ...processedEmails]); + // When loading more, merge with existing emails and re-sort + setEmails(prev => { + const combined = [...prev, ...sortedEmails]; + return combined.sort((a: Email, b: Email) => { + const dateA = new Date(a.date).getTime(); + const dateB = new Date(b.date).getTime(); + return dateB - dateA; // Most recent first + }); + }); setPage(prev => prev + 1); } else { - setEmails(processedEmails); + // For initial load or refresh, just use the sorted emails + setEmails(sortedEmails); setPage(1); } @@ -572,53 +629,60 @@ export default function CourrierPage() { return; } - // Set the selected email first to show preview immediately - setSelectedEmail(email); - - // Fetch the full email content - const response = await fetch(`/api/mail/${emailId}`); - if (!response.ok) { - throw new Error('Failed to fetch full email content'); - } - - const fullEmail = await response.json(); - - // Update the email in the list and selected email with full content - setEmails(prevEmails => prevEmails.map(email => - email.id === emailId - ? { ...email, body: fullEmail.body } - : email - )); - - setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body } : prev); - - // Try to mark as read in the background try { - const markReadResponse = await fetch(`/api/mail/mark-read`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - emailId, - isRead: true, - }), - }); + // Set the selected email first to show preview immediately + setSelectedEmail(email); + setContentLoading(true); + + // Fetch the full email content + const response = await fetch(`/api/mail/${emailId}`); + if (!response.ok) { + throw new Error('Failed to fetch full email content'); + } + + const fullEmail = await response.json(); + + // Update the email in the list and selected email with full content + setEmails(prevEmails => prevEmails.map(email => + email.id === emailId + ? { ...email, body: fullEmail.body || email.body } + : email + )); + + setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body || prev.body } : prev); + setContentLoading(false); - if (markReadResponse.ok) { - // Only update the emails list if the API call was successful - setEmails((prevEmails: Email[]) => - prevEmails.map((email: Email): Email => - email.id === emailId - ? { ...email, read: true } - : email - ) - ); - } else { - console.error('Failed to mark email as read:', await markReadResponse.text()); + // Try to mark as read in the background + try { + const markReadResponse = await fetch(`/api/mail/mark-read`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + emailId, + isRead: true, + }), + }); + + if (markReadResponse.ok) { + // Only update the emails list if the API call was successful + setEmails((prevEmails: Email[]) => + prevEmails.map((email: Email): Email => + email.id === emailId + ? { ...email, read: true } + : email + ) + ); + } else { + console.error('Failed to mark email as read:', await markReadResponse.text()); + } + } catch (error) { + console.error('Error marking email as read:', error); } } catch (error) { - console.error('Error marking email as read:', error); + console.error('Error fetching email content:', error); + setContentLoading(false); } }; @@ -1153,66 +1217,80 @@ export default function CourrierPage() { ); // Add back the handleReply function - const handleReply = (type: 'reply' | 'reply-all' | 'forward') => { + const handleReply = async (type: 'reply' | 'reply-all' | 'forward') => { if (!selectedEmail) return; - const getReplyTo = () => { - if (type === 'forward') return ''; - return selectedEmail.from; - }; + try { + // Get the decoded content first + const decoded = await decodeEmail(selectedEmail.body); + + // Set up the reply details + const getReplyTo = () => { + if (type === 'forward') return ''; + return selectedEmail.from; + }; - const getReplyCc = () => { - if (type !== 'reply-all') return ''; - return selectedEmail.cc || ''; - }; + const getReplyCc = () => { + if (type !== 'reply-all') return ''; + return selectedEmail.cc || ''; + }; - const getReplySubject = () => { - const subject = selectedEmail.subject || ''; + const getReplySubject = () => { + const subject = selectedEmail.subject || ''; + if (type === 'forward') { + return subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`; + } + return subject.startsWith('Re:') ? subject : `Re: ${subject}`; + }; + + // Create the appropriate email content based on type + let formattedContent = ''; if (type === 'forward') { - return subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`; + formattedContent = ` +
+
+
+ ---------- Forwarded message ---------
+ From: ${selectedEmail.from}
+ Date: ${new Date(selectedEmail.date).toLocaleString()}
+ Subject: ${selectedEmail.subject}
+ To: ${selectedEmail.to}
+ ${selectedEmail.cc ? `Cc: ${selectedEmail.cc}
` : ''} +
+
+ ${decoded.html || decoded.text || ''} +
+
+
+ `; + } else { + // For reply and reply-all + formattedContent = ` +
+
+
+ On ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote:
+
+ ${decoded.html || decoded.text || ''} +
+
+ `; } - return subject.startsWith('Re:') ? subject : `Re: ${subject}`; - }; - // Get the formatted original email content - const originalContent = getReplyBody(selectedEmail, type); - - // Create a clean structure with clear separation - const formattedContent = ` -
-
- ${type === 'forward' ? ` -
- ---------- Forwarded message ---------
- From: ${selectedEmail.from}
- Date: ${new Date(selectedEmail.date).toLocaleString()}
- Subject: ${selectedEmail.subject}
- To: ${selectedEmail.to}
- ${selectedEmail.cc ? `Cc: ${selectedEmail.cc}
` : ''} -
- ` : ` -
- On ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote: -
- `} -
- ${originalContent} -
-
- `; + // Update the compose form + setComposeTo(getReplyTo()); + setComposeCc(getReplyCc()); + setComposeSubject(getReplySubject()); + setComposeBody(formattedContent); + setComposeBcc(''); + setShowCompose(true); + setShowCc(type === 'reply-all'); + setShowBcc(false); + setAttachments([]); - // Update the compose form - setComposeTo(getReplyTo()); - setComposeCc(getReplyCc()); - setComposeSubject(getReplySubject()); - setComposeBody(formattedContent); - setComposeBcc(''); - - // Show the compose form and CC field for Reply All - setShowCompose(true); - setShowCc(type === 'reply-all'); - setShowBcc(false); - setAttachments([]); + } catch (error) { + console.error('Error preparing reply:', error); + } }; // Add back the toggleStarred function @@ -1445,10 +1523,7 @@ export default function CourrierPage() { attachments={attachments} setAttachments={setAttachments} handleSend={handleSend} - replyTo={selectedEmail || undefined} - forwardFrom={selectedEmail || undefined} onSend={(email) => { - // Handle the sent email console.log('Email sent:', email); setShowCompose(false); }} diff --git a/components/ComposeEmail.tsx b/components/ComposeEmail.tsx index ea64142f..92847375 100644 --- a/components/ComposeEmail.tsx +++ b/components/ComposeEmail.tsx @@ -81,157 +81,145 @@ export default function ComposeEmail({ }: ComposeEmailProps) { const composeBodyRef = useRef(null); const [localContent, setLocalContent] = useState(''); - const [isInitialized, setIsInitialized] = useState(false); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { - if (composeBodyRef.current && !isInitialized) { - let content = ''; - - if (replyTo || forwardFrom) { - const originalContent = replyTo?.body || forwardFrom?.body || ''; + if (replyTo || forwardFrom) { + const initializeContent = async () => { + if (!composeBodyRef.current) return; - fetch('/api/parse-email', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ emailContent: originalContent }), - }) - .then(response => response.json()) - .then(parsed => { - content = ` -
-


- ${forwardFrom ? ` -
- ---------- Forwarded message ---------
- From: ${forwardFrom.from}
- Date: ${new Date(forwardFrom.date).toLocaleString()}
- Subject: ${forwardFrom.subject}
- To: ${forwardFrom.to}
- ${forwardFrom.cc ? `Cc: ${forwardFrom.cc}
` : ''} -
- ${parsed.html || parsed.text} -
- ` : ` -
- On ${new Date(replyTo?.date || '').toLocaleString()}, ${replyTo?.from} wrote: -
-
- ${parsed.html || parsed.text} -
- `} + try { + const emailToProcess = replyTo || forwardFrom; + if (!emailToProcess?.body) { + console.error('No email body found to process'); + return; + } + + // Set initial loading state + composeBodyRef.current.innerHTML = ` +
+
+
Loading original message...
+
+ `; + + // Parse the original email using the API + const response = await fetch('/api/parse-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email: emailToProcess.body }), + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || 'Failed to parse email'); + } + + const emailContent = data.html || data.text || ''; + + // Format the reply/forward content + const quotedContent = forwardFrom ? ` +
+ ---------- Forwarded message ---------
+ From: ${emailToProcess.from}
+ Date: ${new Date(emailToProcess.date).toLocaleString()}
+ Subject: ${emailToProcess.subject}
+ To: ${emailToProcess.to}
+ ${emailToProcess.cc ? `Cc: ${emailToProcess.cc}
` : ''} +
+
+ ${emailContent} +
+ ` : ` +
+ On ${new Date(emailToProcess.date).toLocaleString()}, ${emailToProcess.from} wrote: +
+
+ ${emailContent} +
+ `; + + // Set the content in the compose area with proper structure + const formattedContent = ` +
+

+ ${quotedContent}
`; if (composeBodyRef.current) { - composeBodyRef.current.innerHTML = content; - setIsInitialized(true); - - // 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(); - } - } - }) - .catch(error => { - console.error('Error parsing email:', error); - }); - } else { - content = `
`; - composeBodyRef.current.innerHTML = content; - setIsInitialized(true); - - 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(); - } - } - } - }, [composeBody, replyTo, forwardFrom, isInitialized]); + composeBodyRef.current.innerHTML = formattedContent; + + // Place cursor at the beginning before the quoted content + const selection = window.getSelection(); + const range = document.createRange(); + const firstDiv = composeBodyRef.current.querySelector('div[style*="min-height: 20px;"]'); + if (firstDiv) { + range.setStart(firstDiv, 0); + range.collapse(true); + selection?.removeAllRanges(); + selection?.addRange(range); + (firstDiv as HTMLElement).focus(); + } + + // Update compose state + setComposeBody(formattedContent); + setLocalContent(formattedContent); + } + } catch (error) { + console.error('Error initializing compose content:', error); + if (composeBodyRef.current) { + const errorContent = ` +
+
+
Error loading original message.
+
+ `; + composeBodyRef.current.innerHTML = errorContent; + setComposeBody(errorContent); + setLocalContent(errorContent); + } + } + }; + + initializeContent(); + } + }, [replyTo, forwardFrom]); - // Modified input handler to work with the single contentEditable area const handleInput = (e: React.FormEvent) => { if (!composeBodyRef.current) return; - // Get the compose area content - const composeArea = composeBodyRef.current.querySelector('.compose-area'); - if (!composeArea) return; - - const content = composeArea.innerHTML; - + const content = composeBodyRef.current.innerHTML; if (!content.trim()) { - console.warn('Email content is empty'); - return; + setLocalContent(''); + setComposeBody(''); + } else { + setLocalContent(content); + setComposeBody(content); } - // 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); - if (onBodyChange) { - onBodyChange(mimeContent); + onBodyChange(content); } }; 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..04bc1fd5 100644 --- a/lib/compose-mime-decoder.ts +++ b/lib/compose-mime-decoder.ts @@ -3,73 +3,58 @@ * 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; +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 response = await fetch('/api/parse-email', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ emailContent: content }), + }); + + if (!response.ok) { + throw new Error('Failed to parse email'); + } + + const parsed = await response.json(); + return { + html: parsed.html || null, + text: parsed.text || null + }; + } catch (error) { + console.error('Error parsing email content:', error); + // Fallback to basic content handling + 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 diff --git a/lib/mail-parser-wrapper.ts b/lib/mail-parser-wrapper.ts index c400a011..ea78b01b 100644 --- a/lib/mail-parser-wrapper.ts +++ b/lib/mail-parser-wrapper.ts @@ -22,9 +22,20 @@ export interface ParsedEmail { export async function decodeEmail(emailContent: string): Promise { try { // Ensure the email content is properly formatted - const formattedContent = emailContent.trim(); + const formattedContent = emailContent?.trim(); if (!formattedContent) { - throw new Error('Email content is empty'); + return { + subject: null, + from: null, + to: null, + cc: null, + bcc: null, + date: null, + html: null, + text: 'No content available', + attachments: [], + headers: {} + }; } const response = await fetch('/api/parse-email', { @@ -32,22 +43,61 @@ export async function decodeEmail(emailContent: string): Promise { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ emailContent: formattedContent }), + body: JSON.stringify({ email: formattedContent }), }); + const data = await response.json(); + if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to parse email'); + console.error('API Error:', data); + return { + subject: null, + from: null, + to: null, + cc: null, + bcc: null, + date: null, + html: null, + text: data.error || 'Failed to parse email', + attachments: [], + headers: {} + }; + } + + // If we have a successful response but no content + if (!data.html && !data.text) { + return { + ...data, + date: data.date ? new Date(data.date) : null, + html: null, + text: 'No content available', + attachments: data.attachments || [], + headers: data.headers || {} + }; } - const data = await response.json(); return { ...data, - date: data.date ? new Date(data.date) : null + date: data.date ? new Date(data.date) : null, + text: data.text || null, + html: data.html || null, + attachments: data.attachments || [], + headers: data.headers || {} }; } catch (error) { console.error('Error parsing email:', error); - throw error; + return { + subject: null, + from: null, + to: null, + cc: null, + bcc: null, + date: null, + html: null, + text: 'Error parsing email content', + attachments: [], + headers: {} + }; } } diff --git a/next.config.js b/next.config.js index 93c15381..d48bee4a 100644 --- a/next.config.js +++ b/next.config.js @@ -1,16 +1,13 @@ /** @type {import('next').NextConfig} */ const nextConfig = { webpack: (config, { isServer }) => { + // Handle node: protocol imports if (!isServer) { config.resolve.fallback = { ...config.resolve.fallback, - net: false, - tls: false, - fs: false, - dns: false, - child_process: false, - http2: false, - module: false, + buffer: require.resolve('buffer/'), + stream: require.resolve('stream-browserify'), + util: require.resolve('util/'), }; } return config;