diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index cfcff3d7..983f1baf 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -153,9 +153,29 @@ function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders: const partContent = emailRaw.substring(startPos, endPos).trim(); if (partContent) { - const decoded = processSinglePartEmail(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; + } - if (decoded.contentType.includes('text/plain')) { + 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')) { result.text = decoded.text || ''; } else if (decoded.contentType.includes('text/html')) { result.html = cleanHtml(decoded.html || ''); @@ -223,23 +243,37 @@ function parseEmailHeaders(headers: string): { contentType: string; encoding: st charset: 'utf-8' }; - // Extract content type and charset - const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;"\r\n]+)|(?:;\s*charset="([^"]+)"))?/i); + // Extract content type and charset with better handling of quoted strings + const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*([^=]+)=([^;"\r\n]+|"[^"]*"))*/gi); if (contentTypeMatch) { - 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(); + 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(); } } - // Extract content transfer encoding + // Extract content transfer encoding with better pattern matching 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; } @@ -255,17 +289,34 @@ function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'): if (encoding === 'quoted-printable') { return decodeQuotedPrintable(text, charset); } else if (encoding === 'base64') { - return decodeBase64(text, charset); + // Handle line breaks in base64 content + const cleanText = text.replace(/[\r\n]/g, ''); + return decodeBase64(cleanText, 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, return as is but still handle charset - return convertCharset(text, charset); + // 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); + } } } catch (error) { console.error('Error decoding MIME:', error); - return text; + // 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; + } } } @@ -277,7 +328,14 @@ function decodeBase64(text: string, charset: string): string { binaryString = atob(cleanText); } catch (e) { console.error('Base64 decoding error:', e); - return text; + // 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 convertCharset(binaryString, charset); @@ -380,154 +438,44 @@ function cleanHtml(html: string): string { function decodeMimeContent(content: string): string { if (!content) return ''; - try { - // First, try to extract the content type and encoding - const contentTypeMatch = content.match(/Content-Type:\s*([^;\r\n]+)/i); - const encodingMatch = content.match(/Content-Transfer-Encoding:\s*([^\r\n]+)/i); - const charsetMatch = content.match(/charset="?([^"\r\n;]+)"?/i); - - const contentType = contentTypeMatch ? contentTypeMatch[1].toLowerCase() : 'text/plain'; - const encoding = encodingMatch ? encodingMatch[1].toLowerCase() : '7bit'; - const charset = charsetMatch ? charsetMatch[1].toLowerCase() : 'utf-8'; + // Check if this is an Infomaniak multipart message + if (content.includes('Content-Type: multipart/')) { + const boundary = content.match(/boundary="([^"]+)"/)?.[1]; + if (boundary) { + const parts = content.split('--' + boundary); + let htmlContent = ''; + let textContent = ''; - // Handle multipart messages - if (contentType.includes('multipart/')) { - const boundaryMatch = content.match(/boundary="?([^"\r\n;]+)"?/i); - if (boundaryMatch) { - const boundary = boundaryMatch[1]; - const parts = content.split('--' + boundary); - - let htmlContent = ''; - let textContent = ''; - - for (const part of parts) { - if (!part.trim()) continue; - - const partContentType = part.match(/Content-Type:\s*([^;\r\n]+)/i)?.[1]?.toLowerCase() || ''; - const partEncoding = part.match(/Content-Transfer-Encoding:\s*([^\r\n]+)/i)?.[1]?.toLowerCase() || '7bit'; - const partCharset = part.match(/charset="?([^"\r\n;]+)"?/i)?.[1]?.toLowerCase() || 'utf-8'; - - // Extract the actual content (after headers) - const contentMatch = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/); - if (!contentMatch) continue; - - let partContent = contentMatch[1]; - - // Decode based on encoding - if (partEncoding === 'quoted-printable') { - partContent = decodeQuotedPrintable(partContent, partCharset); - } else if (partEncoding === 'base64') { - partContent = decodeBase64(partContent, partCharset); + parts.forEach(part => { + if (part.includes('Content-Type: text/html')) { + const match = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/); + if (match) { + htmlContent = cleanHtml(match[1]); } - - if (partContentType.includes('text/html')) { - htmlContent = partContent; - } else if (partContentType.includes('text/plain')) { - textContent = partContent; + } else if (part.includes('Content-Type: text/plain')) { + const match = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/); + if (match) { + textContent = cleanHtml(match[1]); } } - - // Prefer HTML content if available - return htmlContent || textContent || content; - } - } + }); - // Handle single part messages - let decodedContent = content; - - // Find the actual content (after headers) - const contentMatch = content.match(/\r?\n\r?\n([\s\S]+)/); - if (contentMatch) { - decodedContent = contentMatch[1]; + // Prefer HTML content if available + return htmlContent || textContent; } - - // Decode based on encoding - if (encoding === 'quoted-printable') { - decodedContent = decodeQuotedPrintable(decodedContent, charset); - } else if (encoding === 'base64') { - decodedContent = decodeBase64(decodedContent, charset); - } - - // Clean up the content - return cleanHtml(decodedContent); - } catch (e) { - console.error('Error decoding MIME content:', e); - return content; } + + // If not multipart or no boundary found, clean the content directly + return cleanHtml(content); } // Add this helper function const renderEmailContent = (email: Email) => { - try { - // First try to decode the MIME content - const decodedContent = decodeMimeContent(email.body); - - // Check if the content is HTML - const isHtml = decodedContent.includes('<') && - (decodedContent.includes(']*>([\s\S]*?)<\/body>/i); - const content = bodyMatch ? bodyMatch[1] : decodedContent; - - // 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, '') - // Fix common encoding issues - .replace(/=C2=A0/g, ' ') - .replace(/=E2=80=93/g, '\u2013') - .replace(/=E2=80=94/g, '\u2014') - .replace(/=E2=80=98/g, '\u2018') - .replace(/=E2=80=99/g, '\u2019') - .replace(/=E2=80=9C/g, '\u201C') - .replace(/=E2=80=9D/g, '\u201D'); - - return ( -
- ); - } else { - // Format plain text content - const formattedText = decodedContent - .replace(/\n/g, '
') - .replace(/\t/g, '    ') - .replace(/ /g, '  ') - // Fix common encoding issues - .replace(/=C2=A0/g, ' ') - .replace(/=E2=80=93/g, '\u2013') - .replace(/=E2=80=94/g, '\u2014') - .replace(/=E2=80=98/g, '\u2018') - .replace(/=E2=80=99/g, '\u2019') - .replace(/=E2=80=9C/g, '\u201C') - .replace(/=E2=80=9D/g, '\u201D'); - - return ( -
- ); - } - } catch (e) { - console.error('Error rendering email content:', e); - return ( -
- Error rendering email content. Please try refreshing the page. -
- ); + const decodedContent = decodeMimeContent(email.body); + if (email.body.includes('Content-Type: text/html')) { + return
; } + return
{decodedContent}
; }; // Add this helper function @@ -580,7 +528,7 @@ const initialSidebarItems = [ } ]; -export default function CourrierPage() { +export default function MailPage() { const router = useRouter(); const [loading, setLoading] = useState(true); const [accounts, setAccounts] = useState([ @@ -694,7 +642,7 @@ export default function CourrierPage() { } // 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 || '', @@ -960,20 +908,20 @@ export default function CourrierPage() { // Update the email count in the header to show filtered count const renderEmailListHeader = () => (
-
+
- + setSearchQuery(e.target.value)} />
-
-
+
+
0 && selectedEmails.length === filteredEmails.length} onCheckedChange={toggleSelectAll} @@ -1138,7 +1086,39 @@ export default function CourrierPage() {
- {renderEmailContent(selectedEmail)} + {(() => { + 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; + } + })()}
@@ -1219,32 +1199,43 @@ export default function CourrierPage() {
{(() => { + // Get clean preview of the actual message content + let preview = ''; try { - // First decode the MIME content - const decodedContent = decodeMimeContent(email.body); + const parsed = parseFullEmail(email.body); - // Extract preview text - let preview = decodedContent - // Remove HTML tags - .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/<[^>]+>/g, ' ') - // Remove email headers - .replace(/^(From|To|Sent|Subject|Date|Cc|Bcc):.*$/gim, '') - // Remove quoted text - .replace(/^>.*$/gm, '') - // Remove multiple spaces - .replace(/\s+/g, ' ') - // Remove special characters - .replace(/ |‌|»|«|>/g, ' ') - // Fix common encoding issues - .replace(/=C2=A0/g, ' ') - .replace(/=E2=80=93/g, '\u2013') - .replace(/=E2=80=94/g, '\u2014') - .replace(/=E2=80=98/g, '\u2018') - .replace(/=E2=80=99/g, '\u2019') - .replace(/=E2=80=9C/g, '\u201C') - .replace(/=E2=80=9D/g, '\u201D') + // Try to get content from parsed email + if (parsed.html) { + // Extract text from HTML + 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 + if (!preview) { + preview = email.body + .replace(/<[^>]+>/g, ' ') + .replace(/ |‌|»|«|>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + // Remove email artifacts and clean up + preview = preview + .replace(/^>+/gm, '') + .replace(/Content-Type:[^\n]+/g, '') + .replace(/Content-Transfer-Encoding:[^\n]+/g, '') + .replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '') + .replace(/boundary=[^\n]+/g, '') + .replace(/charset=[^\n]+/g, '') + .replace(/[\r\n]+/g, ' ') .trim(); // Take first 100 characters @@ -1259,11 +1250,12 @@ export default function CourrierPage() { preview += '...'; } - return preview || 'No preview available'; } catch (e) { console.error('Error generating preview:', e); - return 'Error loading preview'; + preview = '(Error generating preview)'; } + + return preview || 'No preview available'; })()}
@@ -1695,93 +1687,92 @@ export default function CourrierPage() { } return ( -
-
-
- {/* Sidebar */} -
- {/* Courrier Title */} -
+ <> + {/* Main layout */} +
+ {/* Sidebar */} +
+ {/* Courrier Title */} +
+
+ + COURRIER +
+
+ + {/* Compose button and refresh button */} +
+
+ + +
- {/* Compose button and refresh button */} -
- - -
- - {/* Accounts Section */} -
- - - {accountsDropdownOpen && ( -
- {accounts.map(account => ( -
- + + {accountsDropdownOpen && ( +
+ {accounts.map(account => ( +
+ -
- ))} -
- )} -
- - {/* Navigation */} - {renderSidebarNav()} + {account.email} +
+ +
+ ))} +
+ )}
- {/* Main content area */} -
- {/* Email list panel */} - {renderEmailListWrapper()} -
+ {/* Navigation */} + {renderSidebarNav()} +
+ + {/* Main content area */} +
+ {/* Email list panel */} + {renderEmailListWrapper()}
@@ -1946,6 +1937,6 @@ export default function CourrierPage() {
)} {renderDeleteConfirmDialog()} -
+ ); -} +} \ No newline at end of file