/** * CENTRAL EMAIL FORMATTING UTILITY * * This is the centralized email formatting utility used throughout the application. * It provides consistent handling of email content, sanitization, and text direction. * * All code that needs to format email content should import from this file. * Text direction is preserved based on content language for proper RTL/LTR display. */ import DOMPurify from 'isomorphic-dompurify'; // Instead of importing, implement the formatDateRelative function directly // import { formatDateRelative } from './date-formatter'; /** * Format a date in a relative format * Simple implementation for email display */ function formatDateRelative(date: Date): string { if (!date) return ''; try { return date.toLocaleString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch (e) { return date.toString(); } } // Reset any existing hooks to start clean DOMPurify.removeAllHooks(); // Configure DOMPurify for English-only content (always LTR) DOMPurify.addHook('afterSanitizeAttributes', function(node) { // We no longer force LTR direction on all elements // This allows the natural text direction to be preserved if (node instanceof HTMLElement) { // Only set direction if not already specified if (!node.hasAttribute('dir')) { // Add dir attribute only if not present node.setAttribute('dir', 'auto'); } // Don't forcibly modify text alignment or direction in style attributes // This allows the component to control text direction instead } }); // Configure DOMPurify to preserve direction attributes DOMPurify.setConfig({ ADD_ATTR: ['dir'], ALLOWED_ATTR: ['style', 'class', 'id', 'dir'] }); // Note: We ensure LTR text direction is applied in the component level // when rendering email content // Interface definitions export interface EmailAddress { name: string; address: string; } export interface EmailMessage { id: string; messageId?: string; subject: string; from: EmailAddress[]; to: EmailAddress[]; cc?: EmailAddress[]; bcc?: EmailAddress[]; date: Date | string; flags?: { seen: boolean; flagged: boolean; answered: boolean; deleted: boolean; draft: boolean; }; preview?: string; content?: string; html?: string; text?: string; hasAttachments?: boolean; attachments?: any[]; folder?: string; size?: number; contentFetched?: boolean; } /** * Format email addresses for display */ export function formatEmailAddresses(addresses: EmailAddress[]): string { if (!addresses || addresses.length === 0) return ''; return addresses.map(addr => addr.name && addr.name !== addr.address ? `${addr.name} <${addr.address}>` : addr.address ).join(', '); } /** * 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(); } } /** * Sanitize HTML content before processing or displaying * This ensures the content is properly sanitized while preserving text direction * @param html HTML content to sanitize * @returns Sanitized HTML with preserved text direction */ export function sanitizeHtml(html: string): string { if (!html) return ''; try { // Use DOMPurify but ensure we keep all elements and attributes that might be in emails const clean = DOMPurify.sanitize(html, { ADD_TAGS: ['button', 'style', 'img', 'iframe', 'meta', 'table', 'thead', 'tbody', 'tr', 'td', 'th'], ADD_ATTR: ['target', 'rel', 'style', 'class', 'id', 'href', 'src', 'alt', 'title', 'width', 'height', 'onclick', 'colspan', 'rowspan'], KEEP_CONTENT: true, WHOLE_DOCUMENT: false, ALLOW_DATA_ATTR: true, ALLOW_UNKNOWN_PROTOCOLS: true, FORCE_BODY: false, RETURN_DOM: false, RETURN_DOM_FRAGMENT: false, }); return clean; } catch (e) { console.error('Error sanitizing HTML:', e); // Fall back to a basic sanitization approach return html .replace(/)<[^<]*)*<\/script>/gi, '') .replace(/on\w+="[^"]*"/g, ''); } } /** * Format an email for forwarding - CENTRAL IMPLEMENTATION * All other formatting functions should be deprecated in favor of this one */ export function formatForwardedEmail(email: EmailMessage): { subject: string; content: string; } { // Format subject with Fwd: prefix if needed const subjectBase = email.subject || '(No subject)'; const subject = subjectBase.match(/^(Fwd|FW|Forward):/i) ? subjectBase : `Fwd: ${subjectBase}`; // Get sender and recipient information const fromString = formatEmailAddresses(email.from || []); const toString = formatEmailAddresses(email.to || []); const dateString = formatEmailDate(email.date); // Get and sanitize original content (sanitization preserves content direction) const originalContent = sanitizeHtml(email.content || email.html || email.text || ''); // Check if the content already has a forwarded message header const hasExistingHeader = originalContent.includes('---------- Forwarded message ---------'); // If there's already a forwarded message header, don't add another one if (hasExistingHeader) { // Just wrap the content without additional formatting const content = `
${originalContent}
`; return { subject, content }; } // Create formatted content for forwarded email const content = `
---------- Forwarded message ---------
From: ${fromString}
Date: ${dateString}
Subject: ${email.subject || ''}
To: ${toString}
`; return { subject, content }; } /** * Format an email for reply or reply-all - CENTRAL IMPLEMENTATION * All other formatting functions should be deprecated in favor of this one */ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all'): { to: string; cc?: string; subject: string; content: string; } { // Format subject with Re: prefix if needed const subjectBase = email.subject || '(No subject)'; const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`; // Get sender information for quote header const sender = email.from[0]; const fromText = sender?.name ? `${sender.name} <${sender.address}>` : sender?.address || 'Unknown sender'; // Format date for quote header const date = typeof email.date === 'string' ? new Date(email.date) : email.date; const formattedDate = date.toLocaleString('en-US', { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); // Create quote header const quoteHeader = `
On ${formattedDate}, ${fromText} wrote:
`; // Get and sanitize original content (sanitization preserves content direction) const originalContent = email.html || email.content || email.text || ''; const quotedContent = sanitizeHtml(originalContent); // Format recipients let to = formatEmailAddresses(email.from || []); let cc = ''; if (type === 'reply-all') { // For reply-all, add all original recipients to CC const allRecipients = [ ...(email.to || []), ...(email.cc || []) ]; cc = formatEmailAddresses(allRecipients); } // Format content for reply with improved styling const content = `
${quoteHeader}
${quotedContent}
`; return { to, cc: cc || undefined, subject, content }; } /** * COMPATIBILITY LAYER: For backward compatibility with the old email-formatter.ts * These functions map to our new implementation but preserve the old interface */ export function formatEmailForReplyOrForward( email: EmailMessage, type: 'reply' | 'reply-all' | 'forward' ): { to: string; cc?: string; subject: string; body: string; } { if (type === 'forward') { const { subject, content } = formatForwardedEmail(email); return { to: '', subject, body: content }; } else { const { to, cc, subject, content } = formatReplyEmail(email, type as 'reply' | 'reply-all'); return { to, cc, subject, body: content }; } } /** * Decode compose content from MIME format to HTML and text */ export async function decodeComposeContent(content: string): Promise<{ html: string | null; text: string | null; }> { if (!content.trim()) { return { html: null, text: null }; } try { const response = await fetch('/api/parse-email', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: content }), }); if (!response.ok) { throw new Error('Failed to parse email'); } const parsed = await response.json(); // Apply LTR sanitization to the parsed content return { html: parsed.html ? sanitizeHtml(parsed.html) : null, text: parsed.text || null }; } catch (error) { console.error('Error parsing email content:', error); // Fallback to basic content handling with sanitization return { html: sanitizeHtml(content), text: content }; } } /** * Encode compose content to MIME format for sending */ export function encodeComposeContent(content: string): string { if (!content.trim()) { throw new Error('Email content is empty'); } // Create MIME headers const mimeHeaders = { 'MIME-Version': '1.0', 'Content-Type': 'text/html; charset="utf-8"', 'Content-Transfer-Encoding': 'quoted-printable' }; // Combine headers and content return Object.entries(mimeHeaders) .map(([key, value]) => `${key}: ${value}`) .join('\n') + '\n\n' + content; } // Legacy email formatter functions - renamed to avoid conflicts export function formatReplyEmailLegacy(email: any): string { const originalSender = email.sender?.name || email.sender?.email || 'Unknown Sender'; const originalDate = formatDateRelative(new Date(email.date)); // Use our own sanitizeHtml function consistently const sanitizedBody = sanitizeHtml(email.content || ''); return `

On ${originalDate}, ${originalSender} wrote:

${sanitizedBody}
`.trim(); } export function formatForwardedEmailLegacy(email: any): string { const originalSender = email.sender?.name || email.sender?.email || 'Unknown Sender'; const originalRecipients = email.to?.map((recipient: any) => recipient.name || recipient.email ).join(', ') || 'Unknown Recipients'; const originalDate = formatDateRelative(new Date(email.date)); const originalSubject = email.subject || 'No Subject'; // Use our own sanitizeHtml function consistently const sanitizedBody = sanitizeHtml(email.content || ''); return `

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

From: ${originalSender}

Date: ${originalDate}

Subject: ${originalSubject}

To: ${originalRecipients}


${sanitizedBody}
`.trim(); } export function formatReplyToAllEmail(email: any): string { // For reply all, we use the same format as regular reply return formatReplyEmailLegacy(email); } // Utility function to get the reply subject line export function getReplySubject(subject: string): string { return subject.startsWith('Re:') ? subject : `Re: ${subject}`; } // Utility function to get the forward subject line export function getForwardSubject(subject: string): string { return subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`; }