diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index c477ebb5..b1c1c994 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -61,15 +61,14 @@ interface Attachment { } interface ParsedEmailContent { - text: string; - html: string; - attachments: { + text: string | null; + html: string | null; + attachments: Array<{ filename: string; contentType: string; encoding: string; content: string; - }[]; - headers?: string; + }>; } interface ParsedEmailMetadata { @@ -122,119 +121,115 @@ function decodeQuotedPrintable(text: string, charset: string): string { } } -function parseFullEmail(emailRaw: string) { +function parseFullEmail(emailRaw: string): ParsedEmailContent { console.log('=== parseFullEmail Debug ==='); console.log('Input email length:', emailRaw.length); console.log('First 200 chars:', emailRaw.substring(0, 200)); - - // Check if this is a multipart message by looking for boundary definition - const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i) || - emailRaw.match(/boundary=([^\r\n;]+)/i); - - console.log('Boundary found:', boundaryMatch ? boundaryMatch[1] : 'No boundary'); - - if (boundaryMatch) { - const boundary = boundaryMatch[1].trim(); - - // Check if there's a preamble before the first boundary - let mainHeaders = ''; - let mainContent = emailRaw; - - // Extract the headers before the first boundary if they exist - const firstBoundaryPos = emailRaw.indexOf('--' + boundary); - if (firstBoundaryPos > 0) { - const headerSeparatorPos = emailRaw.indexOf('\r\n\r\n'); - if (headerSeparatorPos > 0 && headerSeparatorPos < firstBoundaryPos) { - mainHeaders = emailRaw.substring(0, headerSeparatorPos); - } - } - - return processMultipartEmail(emailRaw, boundary, mainHeaders); - } else { - // This is a single part message - return processSinglePartEmail(emailRaw); - } -} -function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders: string = ''): { - text: string; - html: string; - attachments: { filename: string; contentType: string; encoding: string; content: string; }[]; - headers?: string; -} { - const result = { - text: '', - html: '', - attachments: [] as { filename: string; contentType: string; encoding: string; content: string; }[], - headers: mainHeaders + // Split headers and body + const headerBodySplit = emailRaw.split(/\r?\n\r?\n/); + const headers = headerBodySplit[0]; + const body = headerBodySplit.slice(1).join('\n\n'); + + // Parse content type from headers + const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)/i); + const contentType = contentTypeMatch ? contentTypeMatch[1].trim().toLowerCase() : 'text/plain'; + + // Initialize result + const result: ParsedEmailContent = { + text: null, + html: null, + attachments: [] }; - - // Split by boundary (more robust pattern) - const boundaryRegex = new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`, 'g'); - - // Get all boundary positions - const matches = Array.from(emailRaw.matchAll(boundaryRegex)); - const boundaryPositions = matches.map(match => match.index!); - - // Extract content between boundaries - for (let i = 0; i < boundaryPositions.length - 1; i++) { - const startPos = boundaryPositions[i] + matches[i][0].length; - const endPos = boundaryPositions[i + 1]; - - if (endPos > startPos) { - const partContent = emailRaw.substring(startPos, endPos).trim(); + + // Handle multipart content + if (contentType.includes('multipart')) { + const boundaryMatch = emailRaw.match(/boundary="?([^"\r\n;]+)"?/i); + if (boundaryMatch) { + const boundary = boundaryMatch[1].trim(); + const parts = emailRaw.split(new RegExp(`--${boundary}(?:--)?(\\r?\\n|$)`)); - if (partContent) { - const decoded = processSinglePartEmail(partContent); + for (const part of parts) { + if (!part.trim()) continue; - if (decoded.contentType.includes('text/plain')) { - result.text = decoded.text || ''; - } else if (decoded.contentType.includes('text/html')) { - result.html = cleanHtml(decoded.html || ''); - } else if ( - decoded.contentType.startsWith('image/') || - decoded.contentType.startsWith('application/') - ) { - const filename = extractFilename(partContent); + const partHeaderBodySplit = part.split(/\r?\n\r?\n/); + const partHeaders = partHeaderBodySplit[0]; + const partBody = partHeaderBodySplit.slice(1).join('\n\n'); + + const partContentTypeMatch = partHeaders.match(/Content-Type:\s*([^;]+)/i); + const partContentType = partContentTypeMatch ? partContentTypeMatch[1].trim().toLowerCase() : 'text/plain'; + + if (partContentType.includes('text/plain')) { + result.text = decodeEmailBody(partBody, partContentType); + } else if (partContentType.includes('text/html')) { + result.html = decodeEmailBody(partBody, partContentType); + } else if (partContentType.startsWith('image/') || partContentType.startsWith('application/')) { + const filenameMatch = partHeaders.match(/filename="?([^"\r\n;]+)"?/i); + const filename = filenameMatch ? filenameMatch[1] : 'attachment'; + result.attachments.push({ filename, - contentType: decoded.contentType, - encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit', - content: decoded.raw?.body || '' + contentType: partContentType, + encoding: 'base64', + content: partBody }); } } } + } else { + // Single part content + if (contentType.includes('text/html')) { + result.html = decodeEmailBody(body, contentType); + } else { + result.text = decodeEmailBody(body, contentType); + } } - + return result; } -function processSinglePartEmail(rawEmail: string) { - // Split headers and body - const headerBodySplit = rawEmail.split(/\r?\n\r?\n/); - const headers = headerBodySplit[0]; - const body = headerBodySplit.slice(1).join('\n\n'); - - // Parse headers to get content type, encoding, etc. - const emailInfo = parseEmailHeaders(headers); - - // Decode the body based on its encoding - const decodedBody = decodeMIME(body, emailInfo.encoding, emailInfo.charset); - - return { - subject: extractHeader(headers, 'Subject'), - from: extractHeader(headers, 'From'), - to: extractHeader(headers, 'To'), - date: extractHeader(headers, 'Date'), - contentType: emailInfo.contentType, - text: emailInfo.contentType.includes('html') ? null : decodedBody, - html: emailInfo.contentType.includes('html') ? decodedBody : null, - raw: { - headers, - body +function decodeEmailBody(content: string, contentType: string): string { + try { + // Remove email client-specific markers + content = content.replace(/\r\n/g, '\n') + .replace(/=\n/g, '') + .replace(/=3D/g, '=') + .replace(/=09/g, '\t'); + + // If it's HTML content + if (contentType.includes('text/html')) { + return extractTextFromHtml(content); } - }; + + return content; + } catch (error) { + console.error('Error decoding email body:', error); + return content; + } +} + +function extractTextFromHtml(html: string): string { + // Remove scripts and style tags + html = html.replace(/]*>[\s\S]*?<\/script>/gi, '') + .replace(/]*>[\s\S]*?<\/style>/gi, ''); + + // Convert
and

to newlines + html = html.replace(/]*>/gi, '\n') + .replace(/]*>/gi, '\n') + .replace(/<\/p>/gi, '\n'); + + // Remove all other HTML tags + html = html.replace(/<[^>]+>/g, ''); + + // Decode HTML entities + html = html.replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"'); + + // Clean up whitespace + return html.replace(/\n\s*\n/g, '\n\n').trim(); } function extractHeader(headers: string, headerName: string): string { @@ -454,54 +449,26 @@ const renderEmailContent = (email: Email) => { try { const parsed = parseFullEmail(email.body); console.log('Parsed content:', { - hasText: 'text' in parsed ? !!parsed.text : false, - hasHtml: 'html' in parsed ? !!parsed.html : false, - textPreview: 'text' in parsed ? parsed.text?.substring(0, 100) : 'No text', - htmlPreview: 'html' in parsed ? parsed.html?.substring(0, 100) : 'No HTML' + hasText: !!parsed.text, + hasHtml: !!parsed.html, + textPreview: parsed.text?.substring(0, 100) || 'No text', + htmlPreview: parsed.html?.substring(0, 100) || 'No HTML' }); - const isHtml = 'html' in parsed ? !!parsed.html : email.body.includes('<'); - const content = 'text' in parsed ? parsed.text : ('html' in parsed ? parsed.html || '' : email.body); - + const content = parsed.html || parsed.text || email.body; + const isHtml = !!parsed.html; + console.log('Selected content type:', isHtml ? 'HTML' : 'Plain text'); console.log('Content preview:', content.substring(0, 100) + '...'); if (isHtml) { - // Enhanced HTML sanitization - 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, '') - // Fix common email client quirks - .replace(/=3D/g, '=') - .replace(/=20/g, ' ') - .replace(/=E2=80=99/g, "'") - .replace(/=E2=80=9C/g, '"') - .replace(/=E2=80=9D/g, '"') - .replace(/=E2=80=93/g, '–') - .replace(/=E2=80=94/g, '—') - .replace(/=C2=A0/g, ' ') - .replace(/=C3=A0/g, 'à') - .replace(/=C3=A9/g, 'é') - .replace(/=C3=A8/g, 'è') - .replace(/=C3=AA/g, 'ê') - .replace(/=C3=AB/g, 'ë') - .replace(/=C3=B4/g, 'ô') - .replace(/=C3=B9/g, 'ù') - .replace(/=C3=BB/g, 'û'); - return (

- {'attachments' in parsed && parsed.attachments && parsed.attachments.length > 0 && ( + {parsed.attachments.length > 0 && (

Attachments:

- {parsed.attachments.map((attachment, index: number) => ( + {parsed.attachments.map((attachment, index) => (
{attachment.filename} @@ -513,40 +480,17 @@ const renderEmailContent = (email: Email) => {
)} -
+
); } else { - // Enhanced plain text formatting - const formattedText = content - .replace(/\n/g, '
') - .replace(/\t/g, '    ') - .replace(/ /g, '  ') - // Fix common email client quirks - .replace(/=3D/g, '=') - .replace(/=20/g, ' ') - .replace(/=E2=80=99/g, "'") - .replace(/=E2=80=9C/g, '"') - .replace(/=E2=80=9D/g, '"') - .replace(/=E2=80=93/g, '–') - .replace(/=E2=80=94/g, '—') - .replace(/=C2=A0/g, ' ') - .replace(/=C3=A0/g, 'à') - .replace(/=C3=A9/g, 'é') - .replace(/=C3=A8/g, 'è') - .replace(/=C3=AA/g, 'ê') - .replace(/=C3=AB/g, 'ë') - .replace(/=C3=B4/g, 'ô') - .replace(/=C3=B9/g, 'ù') - .replace(/=C3=BB/g, 'û'); - return (
- {'attachments' in parsed && parsed.attachments && parsed.attachments.length > 0 && ( + {parsed.attachments.length > 0 && (

Attachments:

- {parsed.attachments.map((attachment, index: number) => ( + {parsed.attachments.map((attachment, index) => (
{attachment.filename} @@ -558,7 +502,7 @@ const renderEmailContent = (email: Email) => {
)} -
+
{content}
); } @@ -1287,17 +1231,17 @@ export default function MailPage() { try { const parsed = parseFullEmail(email.body); console.log('Parsed content:', { - hasText: 'text' in parsed ? !!parsed.text : false, - hasHtml: 'html' in parsed ? !!parsed.html : false, - textPreview: 'text' in parsed ? parsed.text?.substring(0, 100) : 'No text', - htmlPreview: 'html' in parsed ? parsed.html?.substring(0, 100) : 'No HTML' + hasText: !!parsed.text, + hasHtml: !!parsed.html, + textPreview: parsed.text?.substring(0, 100) || 'No text', + htmlPreview: parsed.html?.substring(0, 100) || 'No HTML' }); let preview = ''; - if ('text' in parsed && parsed.text) { + if (parsed.text) { preview = parsed.text; console.log('Using text content for preview'); - } else if ('html' in parsed && parsed.html) { + } else if (parsed.html) { preview = parsed.html .replace(/]*>[\s\S]*?<\/style>/gi, '') .replace(/]*>[\s\S]*?<\/script>/gi, '') @@ -1327,22 +1271,6 @@ export default function MailPage() { .replace(/boundary=[^\n]+/g, '') .replace(/charset=[^\n]+/g, '') .replace(/[\r\n]+/g, ' ') - .replace(/=3D/g, '=') - .replace(/=20/g, ' ') - .replace(/=E2=80=99/g, "'") - .replace(/=E2=80=9C/g, '"') - .replace(/=E2=80=9D/g, '"') - .replace(/=E2=80=93/g, '–') - .replace(/=E2=80=94/g, '—') - .replace(/=C2=A0/g, ' ') - .replace(/=C3=A0/g, 'à') - .replace(/=C3=A9/g, 'é') - .replace(/=C3=A8/g, 'è') - .replace(/=C3=AA/g, 'ê') - .replace(/=C3=AB/g, 'ë') - .replace(/=C3=B4/g, 'ô') - .replace(/=C3=B9/g, 'ù') - .replace(/=C3=BB/g, 'û') .trim(); // Take first 100 characters @@ -1357,11 +1285,10 @@ export default function MailPage() { preview += '...'; } - console.log('Final preview:', preview); - return preview || 'No preview available'; + return preview; } catch (e) { - console.error('Error in generateEmailPreview:', e); - return 'Error generating preview'; + console.error('Error generating preview:', e); + return '(No preview available)'; } };