diff --git a/app/mail/page.tsx b/app/mail/page.tsx index 027d9c3..c7bcdd6 100644 --- a/app/mail/page.tsx +++ b/app/mail/page.tsx @@ -41,62 +41,171 @@ interface Email { category: string; } -// Improved MIME decoding function for all emails -const decodeMimeContent = (content: string): string => { +// MIME Decoder Implementation +function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'): string { + if (!text) return ''; + + encoding = (encoding || '').toLowerCase(); + charset = (charset || 'utf-8').toLowerCase(); + try { - // Handle forwarded message headers specially - if (content.includes('-------- Forwarded message ----------')) { - const parts: string[] = content.split('-------- Forwarded message ----------'); - if (parts.length > 1) { - // Clean the forwarded headers section - const headers = parts[1].split('\n') - .filter(line => line.trim()) - .map(line => line.replace(/=\n/g, '')) - .map(line => line.replace(/=([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))) - .join('\n'); - - // Clean the message body - const messageBody = parts[1].split('\n\n').slice(1).join('\n\n') - .replace(/^This is a multi-part message.*?charset="utf-8"/s, '') - .replace(/---InfomaniakPhpMail.*?Content-Transfer-Encoding:.*?\n/g, '') - .replace(/Content-Type:.*?\n/g, '') - .replace(/Content-Transfer-Encoding:.*?\n/g, '') - .replace(/=C2=A0/g, ' ') // non-breaking space - .replace(/=E2=80=(93|94)/g, '-') // dashes - .replace(/=\n/g, '') // soft line breaks - .replace(/=([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))) - .replace(/\[IMG:(.*?)\]/g, '') // remove image placeholders - .replace(/\[ ?LINK: ([^\]]+?) ?\]/g, '$1') // clean up links - .replace(/\*(.*?)\*/g, '$1') // remove asterisk formatting - .replace(/\n{3,}/g, '\n\n') // reduce multiple line breaks - .replace(/^\s+|\s+$/gm, '') // trim each line - .trim(); - - return `-------- Forwarded message ----------\n${headers}\n\n${messageBody}`; - } + if (encoding === 'quoted-printable') { + return decodeQuotedPrintable(text, charset); + } else if (encoding === 'base64') { + return decodeBase64(text, charset); + } else { + return text; } - - // Regular email content cleaning - return content - .replace(/^This is a multi-part message.*?charset="utf-8"/s, '') - .replace(/---InfomaniakPhpMail.*?Content-Transfer-Encoding:.*?\n/g, '') - .replace(/Content-Type:.*?\n/g, '') - .replace(/Content-Transfer-Encoding:.*?\n/g, '') - .replace(/=C2=A0/g, ' ') // non-breaking space - .replace(/=E2=80=(93|94)/g, '-') // dashes - .replace(/=\n/g, '') // soft line breaks - .replace(/=([0-9A-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))) - .replace(/\[IMG:(.*?)\]/g, '') // remove image placeholders - .replace(/\[ ?LINK: ([^\]]+?) ?\]/g, '$1') // clean up links - .replace(/\*(.*?)\*/g, '$1') // remove asterisk formatting - .replace(/\n{3,}/g, '\n\n') // reduce multiple line breaks - .replace(/^\s+|\s+$/gm, '') // trim each line - .trim(); } catch (error) { - console.error('Error decoding MIME content:', error); + console.error('Error decoding MIME:', error); + return text; + } +} + +function decodeQuotedPrintable(text: string, charset: string): string { + let decoded = text.replace(/=(?:\r\n|\n)/g, ''); + + decoded = decoded.replace(/=([0-9A-F]{2})/gi, (match, p1) => { + return String.fromCharCode(parseInt(p1, 16)); + }); + + if (charset !== 'utf-8' && typeof window !== 'undefined' && typeof TextDecoder !== 'undefined') { + try { + const bytes = new Uint8Array(decoded.length); + for (let i = 0; i < decoded.length; i++) { + bytes[i] = decoded.charCodeAt(i); + } + return new TextDecoder(charset).decode(bytes); + } catch (e) { + console.error('Charset decoding error:', e); + return decoded; + } + } + + return decoded; +} + +function decodeBase64(text: string, charset: string): string { + const cleanText = text.replace(/\s/g, ''); + + try { + const binary = atob(cleanText); + if (charset !== 'utf-8' && typeof TextDecoder !== 'undefined') { + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return new TextDecoder(charset).decode(bytes); + } + return binary; + } catch (e) { + console.error('Base64 decoding error:', e); + return text; + } +} + +function parseEmailHeaders(headers: string): { contentType: string; encoding: string; charset: string } { + const result = { + contentType: 'text/plain', + encoding: 'quoted-printable', + charset: 'utf-8' + }; + + const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;]+))?/i); + if (contentTypeMatch) { + result.contentType = contentTypeMatch[1].trim().toLowerCase(); + if (contentTypeMatch[2]) { + result.charset = contentTypeMatch[2].trim().replace(/"/g, '').toLowerCase(); + } + } + + const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s]+)/i); + if (encodingMatch) { + result.encoding = encodingMatch[1].trim().toLowerCase(); + } + + return result; +} + +function decodeEmail(emailRaw: string): { contentType: string; charset: string; encoding: string; decodedBody: string; headers: string } { + const parts = emailRaw.split(/\r?\n\r?\n/); + const headers = parts[0]; + const body = parts.slice(1).join('\n\n'); + + const { contentType, encoding, charset } = parseEmailHeaders(headers); + const decodedBody = decodeMIME(body, encoding, charset); + + return { + contentType, + charset, + encoding, + decodedBody, + headers + }; +} + +function processMultipartEmail(emailRaw: string, boundary: string): { text: string; html: string; attachments: Array<{ contentType: string; content: string }> } { + const result = { + text: '', + html: '', + attachments: [] + }; + + const boundaryRegex = new RegExp(`--${boundary}\\r?\\n|--${boundary}--\\r?\\n?`, 'g'); + const parts = emailRaw.split(boundaryRegex).filter(part => part.trim()); + + parts.forEach(part => { + const decoded = decodeEmail(part); + + if (decoded.contentType === 'text/plain') { + result.text = decoded.decodedBody; + } else if (decoded.contentType === 'text/html') { + result.html = decoded.decodedBody; + } else if (decoded.contentType.startsWith('image/') || decoded.contentType.startsWith('application/')) { + result.attachments.push({ + contentType: decoded.contentType, + content: decoded.decodedBody + }); + } + }); + + return result; +} + +// Replace the old decodeMimeContent function with a new implementation that uses the above functions +function decodeMimeContent(content: string): string { + if (!content) return ''; + + try { + // Check if the content includes headers + if (content.includes('Content-Type:') || content.includes('Content-Transfer-Encoding:')) { + // If it's a complete email with headers, use the full decoding process + const decoded = decodeEmail(content); + return decoded.decodedBody; + } + + // If no headers are present, try to detect the encoding and decode accordingly + if (content.includes('=?UTF-8?B?') || content.includes('=?utf-8?B?')) { + // Base64 encoded content + return decodeMIME(content, 'base64', 'utf-8'); + } else if (content.includes('=?UTF-8?Q?') || content.includes('=?utf-8?Q?') || content.includes('=20')) { + // Quoted-printable content + 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 quoted-printable didn't change anything, return the original content + return content; + } catch (error) { + console.error('Error decoding email content:', error); return content; } -}; +} export default function MailPage() { // Single IMAP account for now