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(/