diff --git a/app/mail/page.tsx b/app/mail/page.tsx index 917b098..0c5f4aa 100644 --- a/app/mail/page.tsx +++ b/app/mail/page.tsx @@ -42,19 +42,123 @@ interface Email { } // Improved MIME Decoder Implementation for Infomaniak -function decodeInfomaniakEmail(rawEmail: string) { - // Check if the email is multipart - const boundaryMatch = rawEmail.match(/boundary="?([^"\r\n;]+)"?/i); +function extractBoundary(headers: string): string | null { + const boundaryMatch = headers.match(/boundary="?([^"\r\n;]+)"?/i) || + headers.match(/boundary=([^\r\n;]+)/i); + + return boundaryMatch ? boundaryMatch[1].trim() : null; +} + +function decodeQuotedPrintable(text: string, charset: string): string { + if (!text) return ''; + + // Replace soft line breaks (=\r\n or =\n or =\r) + let decoded = text.replace(/=(?:\r\n|\n|\r)/g, ''); + + // Replace quoted-printable encoded characters (including non-ASCII characters) + decoded = decoded.replace(/=([0-9A-F]{2})/gi, (match, p1) => { + return String.fromCharCode(parseInt(p1, 16)); + }); + + // Handle character encoding + try { + // For browsers with TextDecoder support + if (typeof TextDecoder !== 'undefined') { + // Convert string to array of byte values + const bytes = new Uint8Array(Array.from(decoded).map(c => c.charCodeAt(0))); + return new TextDecoder(charset).decode(bytes); + } + + // Fallback for older browsers or when charset handling is not critical + return decoded; + } catch (e) { + console.warn('Charset conversion error:', e); + return decoded; + } +} + +function parseFullEmail(emailRaw: string) { + // 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); if (boundaryMatch) { - // Handle multipart email - return processMultipartEmail(rawEmail, boundaryMatch[1]); + 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 { - // Handle simple email - return processSinglePartEmail(rawEmail); + // 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 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(); + + if (partContent) { + const decoded = processSinglePartEmail(partContent); + + 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); + result.attachments.push({ + filename, + contentType: decoded.contentType, + encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit', + content: decoded.raw?.body || '' + }); + } + } + } + } + + return result; +} + function processSinglePartEmail(rawEmail: string) { // Split headers and body const headerBodySplit = rawEmail.split(/\r?\n\r?\n/); @@ -82,72 +186,6 @@ function processSinglePartEmail(rawEmail: string) { }; } -function processMultipartEmail(rawEmail: string, boundary: string): { - text: string; - html: string; - attachments: { filename: string; contentType: string; encoding: string; content: string; }[]; - subject?: string; - from?: string; - to?: string; - date?: string; -} { - // Split headers and body - const headerBodySplit = rawEmail.split(/\r?\n\r?\n/); - const headers = headerBodySplit[0]; - const fullBody = headerBodySplit.slice(1).join('\n\n'); - - // Create the result object - const result = { - subject: extractHeader(headers, 'Subject'), - from: extractHeader(headers, 'From'), - to: extractHeader(headers, 'To'), - date: extractHeader(headers, 'Date'), - text: '', - html: '', - attachments: [] as { filename: string; contentType: string; encoding: string; content: string; }[] - }; - - // Split the body by boundary - const boundaryRegex = new RegExp(`--${boundary}\\r?\\n|--${boundary}--\\r?\\n?`, 'g'); - const parts = fullBody.split(boundaryRegex).filter(part => part.trim()); - - // Process each part - parts.forEach(part => { - if (!part.trim()) return; - - // Split headers and content for this part - const partHeadersEnd = part.match(/\r?\n\r?\n/); - if (!partHeadersEnd) return; - - const partHeadersEndPos = partHeadersEnd.index!; - const partHeaders = part.substring(0, partHeadersEndPos); - const partContent = part.substring(partHeadersEndPos + partHeadersEnd[0].length); - - // Get content info for this part - const partInfo = parseEmailHeaders(partHeaders); - - // Handle different content types - if (partInfo.contentType.includes('text/plain')) { - result.text = decodeMIME(partContent, partInfo.encoding, partInfo.charset); - } else if (partInfo.contentType.includes('text/html')) { - result.html = cleanHtml(decodeMIME(partContent, partInfo.encoding, partInfo.charset)); - } else if ( - partInfo.contentType.startsWith('image/') || - partInfo.contentType.startsWith('application/') - ) { - const filename = extractFilename(partHeaders); - result.attachments.push({ - filename, - contentType: partInfo.contentType, - encoding: partInfo.encoding, - content: partContent - }); - } - }); - - return result; -} - function extractHeader(headers: string, headerName: string): string { const regex = new RegExp(`^${headerName}:\\s*(.+?)(?:\\r?\\n(?!\\s)|$)`, 'im'); const match = headers.match(regex); @@ -212,22 +250,6 @@ function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'): } } -function decodeQuotedPrintable(text: string, charset: string): string { - // Replace soft line breaks - let decoded = text.replace(/=(?:\r\n|\n)/g, ''); - - // Replace quoted-printable hex sequences - decoded = decoded.replace(/=([0-9A-F]{2})/gi, (match, p1) => { - return String.fromCharCode(parseInt(p1, 16)); - }); - - // Handle Infomaniak specific issues with special characters - decoded = decoded.replace(/\xA0/g, ' '); - - // Handle character set conversion - return convertCharset(decoded, charset); -} - function decodeBase64(text: string, charset: string): string { const cleanText = text.replace(/\s/g, ''); @@ -312,20 +334,25 @@ function decodeMimeContent(content: string): string { if (!content) return ''; try { - // Try to decode as a complete email first - const decoded = decodeInfomaniakEmail(content); - - // If we have HTML content, prefer that - if (decoded.html) { - return extractHtmlBody(decoded.html); + // Handle the special case with InfomaniakPhpMail boundary + if (content.includes('---InfomaniakPhpMail')) { + const boundaryMatch = content.match(/---InfomaniakPhpMail[\w\d]+/); + if (boundaryMatch) { + const boundary = boundaryMatch[0]; + const result = processMultipartEmail(content, boundary); + return result.html || result.text || content; + } } - // Otherwise use the text content - if (decoded.text) { - return decoded.text; + // Regular email parsing + const result = parseFullEmail(content); + if ('html' in result && result.html) { + return extractHtmlBody(result.html); + } else if ('text' in result && result.text) { + return result.text; } - // If neither HTML nor text was found, try simple decoding + // If parsing fails, try simple decoding if (content.includes('Content-Type:') || content.includes('Content-Transfer-Encoding:')) { const simpleDecoded = processSinglePartEmail(content); return simpleDecoded.text || simpleDecoded.html || content; @@ -338,12 +365,6 @@ function decodeMimeContent(content: string): string { return decodeMIME(content, 'quoted-printable', 'utf-8'); } - // If no specific encoding is detected, try quoted-printable first - const qpDecoded = decodeMIME(content, 'quoted-printable', 'utf-8'); - if (qpDecoded !== content) { - return qpDecoded; - } - // If nothing else worked, return the original content return content; } catch (error) {