/** * Unified Email Utilities * * This file contains all email-related utility functions: * - Content normalization * - Content sanitization * - Email formatting (replies, forwards) * - Text direction detection */ import { EmailMessage, EmailContent, EmailAddress, LegacyEmailMessage } from '@/types/email'; import { adaptLegacyEmail } from '@/lib/utils/email-adapters'; import { decodeInfomaniakEmail, adaptMimeEmail, isMimeFormat } from './email-mime-decoder'; import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction'; import { sanitizeHtml } from '@/lib/utils/dom-sanitizer'; // Remove all local DOMPurify configuration - now using centralized version /** * Format email addresses for display * Can handle both array of EmailAddress objects or a string */ export function formatEmailAddresses(addresses: EmailAddress[] | string | undefined): string { if (!addresses) return ''; // If already a string, return as is if (typeof addresses === 'string') { return addresses; } // If array, format each address if (Array.isArray(addresses) && addresses.length > 0) { return addresses.map(addr => addr.name && addr.name !== addr.address ? `${addr.name} <${addr.address}>` : addr.address ).join(', '); } return ''; } /** * Format date for display */ export function formatEmailDate(date: Date | string | undefined): string { if (!date) return ''; try { const dateObj = typeof date === 'string' ? new Date(date) : date; return dateObj.toLocaleString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch (e) { return typeof date === 'string' ? date : date.toString(); } } // Re-export sanitizeHtml for convenience export { sanitizeHtml }; /** * Format plain text for HTML display with proper line breaks */ export function formatPlainTextToHtml(text: string | null | undefined): string { if (!text) return ''; // Escape HTML characters to prevent XSS const escapedText = text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); // Format plain text with proper line breaks and paragraphs return escapedText .replace(/\r\n|\r|\n/g, '
') // Convert all newlines to
.replace(/((?:
){2,})/g, '

') // Convert multiple newlines to paragraphs .replace(/
<\/p>/g, '

') // Fix any

combinations .replace(/


/g, '

'); // Fix any


combinations } /** * Normalize email content to our standard format regardless of input format * This is the key function that handles all the different email content formats */ export function normalizeEmailContent(email: any): EmailMessage { if (!email) { throw new Error('Cannot normalize null or undefined email'); } // First check if this is a MIME format email that needs decoding if (email.content && isMimeFormat(email.content)) { try { console.log('Detected MIME format email, decoding...'); return adaptMimeEmail(email); } catch (error) { console.error('Error decoding MIME email:', error); // Continue with regular normalization if MIME decoding fails } } // Check if it's already in the standardized format if (email.content && typeof email.content === 'object' && (email.content.html !== undefined || email.content.text !== undefined)) { // Already in the correct format return email as EmailMessage; } // Otherwise, adapt from legacy format return adaptLegacyEmail(email); } /** * Render normalized email content into HTML for display */ export function renderEmailContent(content: EmailContent | null): string { if (!content) { return '

No content available
'; } const safeContent = { text: content.text || '', html: content.html, isHtml: content.isHtml, direction: content.direction || 'ltr' }; // If we have HTML content and isHtml flag is true, use it if (safeContent.isHtml && safeContent.html) { // Apply text direction consistently using the utility return applyTextDirection(safeContent.html, safeContent.text); } // Otherwise, format the text content with basic HTML const text = safeContent.text; const formattedText = text .replace(/&/g, '&') .replace(//g, '>') .replace(/\n/g, '
'); // Apply text direction consistently return applyTextDirection(formattedText, text); } // Add interface for email formatting functions interface FormattedEmail { to: string; cc?: string; subject: string; content: EmailContent; } /** * Format email for reply */ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessage | null, type: 'reply' | 'reply-all' = 'reply'): FormattedEmail { if (!originalEmail) { return { to: '', cc: '', subject: '', content: { text: '', html: '', isHtml: false, direction: 'ltr' as const } }; } // Format the recipients const to = Array.isArray(originalEmail.from) ? originalEmail.from.map((addr: any) => { if (typeof addr === 'string') return addr; return addr.address ? addr.address : ''; }).filter(Boolean).join(', ') : typeof originalEmail.from === 'string' ? originalEmail.from : ''; // For reply-all, include other recipients in CC let cc = ''; if (type === 'reply-all') { const toRecipients = Array.isArray(originalEmail.to) ? originalEmail.to.map((addr: any) => { if (typeof addr === 'string') return addr; return addr.address ? addr.address : ''; }).filter(Boolean) : typeof originalEmail.to === 'string' ? [originalEmail.to] : []; const ccRecipients = Array.isArray(originalEmail.cc) ? originalEmail.cc.map((addr: any) => { if (typeof addr === 'string') return addr; return addr.address ? addr.address : ''; }).filter(Boolean) : typeof originalEmail.cc === 'string' ? [originalEmail.cc] : []; cc = [...toRecipients, ...ccRecipients].join(', '); } // Format the subject const subject = originalEmail.subject && !originalEmail.subject.startsWith('Re:') ? `Re: ${originalEmail.subject}` : originalEmail.subject || ''; // Format the content const originalDate = originalEmail.date ? new Date(originalEmail.date) : new Date(); const dateStr = originalDate.toLocaleString(); const fromStr = Array.isArray(originalEmail.from) ? originalEmail.from.map((addr: any) => { if (typeof addr === 'string') return addr; return addr.name ? `${addr.name} <${addr.address}>` : addr.address; }).join(', ') : typeof originalEmail.from === 'string' ? originalEmail.from : 'Unknown Sender'; const toStr = Array.isArray(originalEmail.to) ? originalEmail.to.map((addr: any) => { if (typeof addr === 'string') return addr; return addr.name ? `${addr.name} <${addr.address}>` : addr.address; }).join(', ') : typeof originalEmail.to === 'string' ? originalEmail.to : ''; // Extract original content const originalTextContent = typeof originalEmail?.content === 'object' ? originalEmail.content.text : typeof originalEmail?.content === 'string' ? originalEmail.content : originalEmail?.text || ''; const originalHtmlContent = typeof originalEmail?.content === 'object' ? originalEmail.content.html : originalEmail?.html || (typeof originalEmail?.content === 'string' && originalEmail?.content.includes('<') ? originalEmail.content : ''); // Get the direction from the original email const originalDirection = typeof originalEmail?.content === 'object' ? originalEmail.content.direction : detectTextDirection(originalTextContent); // Create content with appropriate quote formatting const replyBody = `

On ${dateStr}, ${fromStr} wrote:

${originalHtmlContent || originalTextContent.replace(/\n/g, '
')}
`; // Apply consistent text direction const htmlContent = applyTextDirection(replyBody); // Create plain text content const textContent = ` On ${dateStr}, ${fromStr} wrote: > ${originalTextContent.split('\n').join('\n> ')} `; return { to, cc, subject, content: { text: textContent, html: htmlContent, isHtml: true, direction: 'ltr' as const // Reply is LTR, but original content keeps its direction in the blockquote } }; } /** * Format email for forwarding */ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMessage | null): FormattedEmail { if (!originalEmail) { return { to: '', subject: '', content: { text: '', html: '', isHtml: false, direction: 'ltr' as const } }; } // Format the subject const subject = originalEmail.subject && !originalEmail.subject.startsWith('Fwd:') ? `Fwd: ${originalEmail.subject}` : originalEmail.subject || ''; // Format from, to, cc for the header const fromStr = Array.isArray(originalEmail.from) ? originalEmail.from.map((addr: any) => { if (typeof addr === 'string') return addr; return addr.name ? `${addr.name} <${addr.address}>` : addr.address; }).join(', ') : typeof originalEmail.from === 'string' ? originalEmail.from : 'Unknown Sender'; const toStr = Array.isArray(originalEmail.to) ? originalEmail.to.map((addr: any) => { if (typeof addr === 'string') return addr; return addr.name ? `${addr.name} <${addr.address}>` : addr.address; }).join(', ') : typeof originalEmail.to === 'string' ? originalEmail.to : ''; const ccStr = Array.isArray(originalEmail.cc) ? originalEmail.cc.map((addr: any) => { if (typeof addr === 'string') return addr; return addr.name ? `${addr.name} <${addr.address}>` : addr.address; }).join(', ') : typeof originalEmail.cc === 'string' ? originalEmail.cc : ''; const dateStr = originalEmail.date ? new Date(originalEmail.date).toLocaleString() : 'Unknown Date'; // Extract original content const originalTextContent = typeof originalEmail?.content === 'object' ? originalEmail.content.text : typeof originalEmail?.content === 'string' ? originalEmail.content : originalEmail?.text || ''; const originalHtmlContent = typeof originalEmail?.content === 'object' ? originalEmail.content.html : originalEmail?.html || (typeof originalEmail?.content === 'string' && originalEmail?.content.includes('<') ? originalEmail.content : ''); // Get the direction from the original email const originalDirection = typeof originalEmail?.content === 'object' ? originalEmail.content.direction : detectTextDirection(originalTextContent); // Create forwarded content with header information const forwardBody = `

---------- Forwarded message ---------

From: ${fromStr}

Date: ${dateStr}

Subject: ${originalEmail?.subject || ''}

To: ${toStr}

${ccStr ? `

Cc: ${ccStr}

` : ''}
${originalHtmlContent || originalTextContent.replace(/\n/g, '
')}
`; // Apply consistent text direction const htmlContent = applyTextDirection(forwardBody); // Create plain text content const textContent = ` ---------- Forwarded message --------- From: ${fromStr} Date: ${dateStr} Subject: ${originalEmail?.subject || ''} To: ${toStr} ${ccStr ? `Cc: ${ccStr}\n` : ''} ${originalTextContent} `; return { to: '', subject, content: { text: textContent, html: htmlContent, isHtml: true, direction: 'ltr' as const // Forward is LTR, but original content keeps its direction } }; } /** * Format an email for reply or reply-all */ export function formatEmailForReplyOrForward( email: EmailMessage, type: 'reply' | 'reply-all' | 'forward' ): { to?: string; cc?: string; subject: string; content: EmailContent; } { // Use our dedicated formatters but ensure the return is properly typed if (type === 'forward') { const formatted = formatForwardedEmail(email); return { to: formatted.to, subject: formatted.subject, content: formatted.content }; } else { const formatted = formatReplyEmail(email, type); return { to: formatted.to, cc: formatted.cc, subject: formatted.subject, content: formatted.content }; } }