diff --git a/app/api/mail/route.ts b/app/api/mail/route.ts index 138aff4b..ed816f44 100644 --- a/app/api/mail/route.ts +++ b/app/api/mail/route.ts @@ -48,70 +48,22 @@ export async function GET() { await client.connect(); const mailbox = await client.mailboxOpen('INBOX'); - // Fetch messages with body content + // Fetch only essential message data const messages = await client.fetch('1:20', { envelope: true, - flags: true, - bodyStructure: true, - bodyParts: ['HEADER', 'TEXT'] + flags: true }); const result = []; for await (const message of messages) { - // Get the body content - let body = ''; - if (message.bodyParts && Array.isArray(message.bodyParts)) { - for (const [partType, content] of message.bodyParts) { - if (partType === 'text') { - // Convert buffer to string and decode if needed - const contentStr = content.toString('utf-8'); - // Check if content is base64 encoded - if (contentStr.includes('Content-Transfer-Encoding: base64')) { - const base64Content = contentStr.split('\r\n\r\n')[1]; - body = Buffer.from(base64Content, 'base64').toString('utf-8'); - } else { - body = contentStr; - } - break; - } - } - } - - // If no body found, try to get it from the message structure - if (!body && message.bodyStructure) { - try { - const fetch = await client.fetchOne(message.uid.toString(), { - bodyStructure: true, - bodyParts: ['TEXT'] - }); - if (fetch?.bodyParts) { - for (const [partType, content] of fetch.bodyParts) { - if (partType === 'text') { - body = content.toString('utf-8'); - break; - } - } - } - } catch (error) { - console.error('Error fetching message content:', error); - } - } - result.push({ id: message.uid.toString(), - accountId: 1, // Default account ID - from: message.envelope.from?.[0]?.address || '', - fromName: message.envelope.from?.[0]?.name || message.envelope.from?.[0]?.address?.split('@')[0] || '', - to: message.envelope.to?.[0]?.address || '', + from: message.envelope.from[0].address, subject: message.envelope.subject || '(No subject)', - body: body, date: message.envelope.date.toISOString(), read: message.flags.has('\\Seen'), starred: message.flags.has('\\Flagged'), - folder: mailbox.path, - cc: message.envelope.cc?.map(addr => addr.address).join(', '), - bcc: message.envelope.bcc?.map(addr => addr.address).join(', '), - flags: Array.from(message.flags) + folder: mailbox.path }); } diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 983f1baf..64779b35 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -153,29 +153,9 @@ function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders: const partContent = emailRaw.substring(startPos, endPos).trim(); if (partContent) { - // Check if this is a nested multipart - const nestedBoundaryMatch = partContent.match(/boundary="?([^"\r\n;]+)"?/i); - if (nestedBoundaryMatch) { - const nestedResult = processMultipartEmail(partContent, nestedBoundaryMatch[1]); - result.text = result.text || nestedResult.text; - result.html = result.html || nestedResult.html; - result.attachments.push(...nestedResult.attachments); - continue; - } - const decoded = processSinglePartEmail(partContent); - const contentDisposition = extractHeader(partContent, 'Content-Disposition'); - const isAttachment = contentDisposition.toLowerCase().includes('attachment'); - if (isAttachment) { - const filename = extractFilename(partContent) || 'attachment'; - result.attachments.push({ - filename, - contentType: decoded.contentType, - encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit', - content: decoded.raw?.body || '' - }); - } else if (decoded.contentType.includes('text/plain')) { + if (decoded.contentType.includes('text/plain')) { result.text = decoded.text || ''; } else if (decoded.contentType.includes('text/html')) { result.html = cleanHtml(decoded.html || ''); @@ -243,37 +223,23 @@ function parseEmailHeaders(headers: string): { contentType: string; encoding: st charset: 'utf-8' }; - // Extract content type and charset with better handling of quoted strings - const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*([^=]+)=([^;"\r\n]+|"[^"]*"))*/gi); + // Extract content type and charset + const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;"\r\n]+)|(?:;\s*charset="([^"]+)"))?/i); if (contentTypeMatch) { - const fullContentType = contentTypeMatch[0]; - const typeMatch = fullContentType.match(/Content-Type:\s*([^;]+)/i); - if (typeMatch) { - result.contentType = typeMatch[1].trim().toLowerCase(); - } - - // Extract charset with better handling of quoted values - const charsetMatch = fullContentType.match(/charset=([^;"\r\n]+|"[^"]*")/i); - if (charsetMatch) { - result.charset = charsetMatch[1].replace(/"/g, '').trim().toLowerCase(); + result.contentType = contentTypeMatch[1].trim().toLowerCase(); + if (contentTypeMatch[2]) { + result.charset = contentTypeMatch[2].trim().toLowerCase(); + } else if (contentTypeMatch[3]) { + result.charset = contentTypeMatch[3].trim().toLowerCase(); } } - // Extract content transfer encoding with better pattern matching + // Extract content transfer encoding const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s;\r\n]+)/i); if (encodingMatch) { result.encoding = encodingMatch[1].trim().toLowerCase(); } - // Normalize charset names - if (result.charset === 'iso-8859-1' || result.charset === 'latin1') { - result.charset = 'iso-8859-1'; - } else if (result.charset === 'utf-8' || result.charset === 'utf8') { - result.charset = 'utf-8'; - } else if (result.charset === 'windows-1252' || result.charset === 'cp1252') { - result.charset = 'windows-1252'; - } - return result; } @@ -289,34 +255,17 @@ function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'): if (encoding === 'quoted-printable') { return decodeQuotedPrintable(text, charset); } else if (encoding === 'base64') { - // Handle line breaks in base64 content - const cleanText = text.replace(/[\r\n]/g, ''); - return decodeBase64(cleanText, charset); + return decodeBase64(text, charset); } else if (encoding === '7bit' || encoding === '8bit' || encoding === 'binary') { // For these encodings, we still need to handle the character set return convertCharset(text, charset); } else { - // Unknown encoding, try to detect and handle - if (text.match(/^[A-Za-z0-9+/=]+$/)) { - // Looks like base64 - return decodeBase64(text, charset); - } else if (text.includes('=') && text.match(/=[A-F0-9]{2}/)) { - // Looks like quoted-printable - return decodeQuotedPrintable(text, charset); - } else { - // Unknown encoding, return as is but still handle charset - return convertCharset(text, charset); - } + // Unknown encoding, return as is but still handle charset + return convertCharset(text, charset); } } catch (error) { console.error('Error decoding MIME:', error); - // Try to recover by returning the original text with charset conversion - try { - return convertCharset(text, charset); - } catch (e) { - console.error('Error in fallback charset conversion:', e); - return text; - } + return text; } } @@ -328,14 +277,7 @@ function decodeBase64(text: string, charset: string): string { binaryString = atob(cleanText); } catch (e) { console.error('Base64 decoding error:', e); - // Try to recover by removing invalid characters - const validBase64 = cleanText.replace(/[^A-Za-z0-9+/=]/g, ''); - try { - binaryString = atob(validBase64); - } catch (e2) { - console.error('Base64 recovery failed:', e2); - return text; - } + return text; } return convertCharset(binaryString, charset); @@ -471,11 +413,51 @@ function decodeMimeContent(content: string): string { // Add this helper function const renderEmailContent = (email: Email) => { - const decodedContent = decodeMimeContent(email.body); - if (email.body.includes('Content-Type: text/html')) { - return
; + try { + const parsed = parseFullEmail(email.body); + const content = parsed.text || parsed.html || email.body; + const isHtml = parsed.html || content.includes('<'); + + if (isHtml) { + // Sanitize HTML content + const sanitizedHtml = content + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/)<[^<]*)*<\/style>/gi, '') + .replace(/on\w+="[^"]*"/g, '') + .replace(/on\w+='[^']*'/g, '') + .replace(/javascript:/gi, '') + .replace(/data:/gi, '') + .replace(/]*>/gi, '') + .replace(/]*>/gi, ''); + + return ( +
+ ); + } else { + // Format plain text content + const formattedText = content + .replace(/\n/g, '
') + .replace(/\t/g, '    ') + .replace(/ /g, '  '); + + return ( +
+ ); + } + } catch (e) { + console.error('Error rendering email content:', e); + return ( +
+ Error rendering email content. Please try refreshing the page. +
+ ); } - return
{decodedContent}
; }; // Add this helper function @@ -528,7 +510,7 @@ const initialSidebarItems = [ } ]; -export default function MailPage() { +export default function CourrierPage() { const router = useRouter(); const [loading, setLoading] = useState(true); const [accounts, setAccounts] = useState([ @@ -642,7 +624,7 @@ export default function MailPage() { } // Process emails keeping exact folder names - const processedEmails = data.emails.map((email: any) => ({ + const processedEmails = (data.emails || []).map((email: any) => ({ id: Number(email.id), accountId: 1, from: email.from || '', @@ -908,20 +890,20 @@ export default function MailPage() { // Update the email count in the header to show filtered count const renderEmailListHeader = () => (
-
+
- + setSearchQuery(e.target.value)} />
-
-
+
+
0 && selectedEmails.length === filteredEmails.length} onCheckedChange={toggleSelectAll} @@ -1086,39 +1068,7 @@ export default function MailPage() {
- {(() => { - try { - const parsed = parseFullEmail(selectedEmail.body); - return ( -
- {/* Display HTML content if available, otherwise fallback to text */} -
- - {/* Display attachments if present */} - {parsed.attachments && parsed.attachments.length > 0 && ( -
-

Attachments

-
- {parsed.attachments.map((attachment, index) => ( -
- - - {attachment.filename} - -
- ))} -
-
- )} -
- ); - } catch (e) { - console.error('Error parsing email:', e); - return selectedEmail.body; - } - })()} + {renderEmailContent(selectedEmail)}
@@ -1199,23 +1149,22 @@ export default function MailPage() {
{(() => { - // Get clean preview of the actual message content - let preview = ''; try { + // First try to parse the full email const parsed = parseFullEmail(email.body); - // Try to get content from parsed email - if (parsed.html) { - // Extract text from HTML + // Get text content from parsed email + let preview = ''; + if (parsed.text) { + preview = parsed.text; + } else if (parsed.html) { + // If only HTML is available, extract text content preview = parsed.html .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/<[^>]+>/g, ' ') - .replace(/ |‌|»|«|>/g, ' ') .replace(/\s+/g, ' ') .trim(); - } else if (parsed.text) { - preview = parsed.text; } // If no preview from parsed content, try direct body @@ -1227,7 +1176,7 @@ export default function MailPage() { .trim(); } - // Remove email artifacts and clean up + // Clean up the preview preview = preview .replace(/^>+/gm, '') .replace(/Content-Type:[^\n]+/g, '') @@ -1250,12 +1199,11 @@ export default function MailPage() { preview += '...'; } + return preview || 'No preview available'; } catch (e) { console.error('Error generating preview:', e); - preview = '(Error generating preview)'; + return 'Error loading preview'; } - - return preview || 'No preview available'; })()}
@@ -1687,92 +1635,93 @@ export default function MailPage() { } return ( - <> - {/* Main layout */} -
- {/* Sidebar */} -
- {/* Courrier Title */} -
-
- - COURRIER -
-
- - {/* Compose button and refresh button */} -
- - -
+
- {/* Accounts Section */} -
- - - {accountsDropdownOpen && ( -
- {accounts.map(account => ( -
- + +
+ + {/* Accounts Section */} +
+ + + {accountsDropdownOpen && ( +
+ {accounts.map(account => ( +
+
- -
- ))} -
- )} + +
+ ))} +
+ )} +
+ + {/* Navigation */} + {renderSidebarNav()}
- {/* Navigation */} - {renderSidebarNav()} -
- - {/* Main content area */} -
- {/* Email list panel */} - {renderEmailListWrapper()} + {/* Main content area */} +
+ {/* Email list panel */} + {renderEmailListWrapper()} +
@@ -1937,6 +1886,6 @@ export default function MailPage() {
)} {renderDeleteConfirmDialog()} - + ); -} \ No newline at end of file +}