/**
* Unified Email Utilities
*
* This file contains all email-related utility functions:
* - Content normalization
* - Email formatting (replies, forwards)
* - Text direction handling
*/
// Import from centralized configuration
import { sanitizeHtml } from './dom-purify-config';
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,
extractEmailContent,
processContentWithDirection
} from '@/lib/utils/text-direction';
import { format } from 'date-fns';
// Export the sanitizeHtml function from the centralized config
export { sanitizeHtml };
/**
* Standard interface for formatted email responses
*/
export interface FormattedEmail {
to: string;
cc?: string;
subject: string;
content: EmailContent;
attachments?: Array<{
filename: string;
contentType: string;
content?: string;
}>;
}
/**
* 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();
}
}
/**
* 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, '
/g, '
'); // Fix any
combinations
}
/**
* Normalize email content to our standard format regardless of input format
*/
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...');
// We need to force cast here due to type incompatibility between EmailMessage and the mime result
const adaptedEmail = adaptMimeEmail(email);
return {
...adaptedEmail,
flags: adaptedEmail.flags || [] // Ensure flags is always an array
} as EmailMessage;
} 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
// We need to force cast here due to type incompatibility
const adaptedEmail = adaptLegacyEmail(email);
return {
...adaptedEmail,
flags: adaptedEmail.flags || [] // Ensure flags is always an array
} as EmailMessage;
}
/**
* Render normalized email content into HTML for display
*/
export function renderEmailContent(content: EmailContent | null): string {
if (!content) {
return '
${truncatedText.replace(/\n/g, '`; textReply = ` On ${dateStr}, ${fromStr} wrote: > ${truncatedText.split('\n').join('\n> ')} `; } // If no text, try to sanitize and simplify HTML else if (originalHtmlContent) { try { // Sanitize the original HTML to remove problematic elements and simplify const sanitizedHtml = sanitizeHtml(originalHtmlContent); // Extract the text content from the sanitized HTML for the plaintext version const tempDiv = document.createElement('div'); tempDiv.innerHTML = sanitizedHtml; const extractedText = tempDiv.textContent || tempDiv.innerText || ''; // Limit to a reasonable size const maxChars = 1500; const truncatedText = extractedText.length > maxChars ? extractedText.slice(0, maxChars) + '... [message truncated]' : extractedText; replyBody = ` ${headerHtml}
')}
${truncatedText.replace(/\n/g, '`; textReply = ` On ${dateStr}, ${fromStr} wrote: > ${truncatedText.split('\n').join('\n> ')} `; } catch (error) { console.error('Error processing HTML for reply:', error); // Fallback to a basic template if everything fails replyBody = ` ${headerHtml}
')}
[Original message content could not be processed]`; textReply = ` On ${dateStr}, ${fromStr} wrote: > [Original message content could not be processed] `; } } else { // Empty or unrecognized content replyBody = ` ${headerHtml}
[Original message content not available]`; textReply = ` On ${dateStr}, ${fromStr} wrote: > [Original message content not available] `; } // Process the content with proper direction const processed = processContentWithDirection(replyBody); // Extract any inline images as attachments const inlineImages = extractInlineImages(originalHtmlContent); return { to, cc, subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`, content: { text: textReply.trim(), html: processed.html, isHtml: true, direction: processed.direction }, // Include inline images as attachments if any were found attachments: inlineImages.length > 0 ? inlineImages : undefined }; } /** * 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 } }; } // Get header information const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail); // Extract content using centralized utility - get simpler text version when possible const { text: originalTextContent, html: originalHtmlContent } = extractEmailContent(originalEmail); // Simpler approach - prefer text content when available for clean forwards let forwardBody = ''; let textForward = ''; // Create metadata header that works in both HTML and plain text const headerHtml = `
${truncatedText.replace(/\n/g, '`; textForward = ` ---------- Forwarded message --------- From: ${fromStr} Date: ${dateStr} Subject: ${subject || ''} To: ${toStr} ${ccStr ? `Cc: ${ccStr}\n` : ''} ${truncatedText} `; } // If no text, try to sanitize and simplify HTML else if (originalHtmlContent) { try { // Sanitize the original HTML to remove problematic elements and simplify const sanitizedHtml = sanitizeHtml(originalHtmlContent); // Extract the text content from the sanitized HTML for the plaintext version const tempDiv = document.createElement('div'); tempDiv.innerHTML = sanitizedHtml; const extractedText = tempDiv.textContent || tempDiv.innerText || ''; // Limit to a reasonable size const maxChars = 2000; const truncatedText = extractedText.length > maxChars ? extractedText.slice(0, maxChars) + '... [message truncated]' : extractedText; forwardBody = ` ${headerHtml}
')}
${truncatedText.replace(/\n/g, '`; textForward = ` ---------- Forwarded message --------- From: ${fromStr} Date: ${dateStr} Subject: ${subject || ''} To: ${toStr} ${ccStr ? `Cc: ${ccStr}\n` : ''} ${truncatedText} `; } catch (error) { console.error('Error processing HTML for forward:', error); // Fallback to a basic template if everything fails forwardBody = ` ${headerHtml}
')}
[Original message content could not be processed]`; textForward = ` ---------- Forwarded message --------- From: ${fromStr} Date: ${dateStr} Subject: ${subject || ''} To: ${toStr} ${ccStr ? `Cc: ${ccStr}\n` : ''} [Original message content could not be processed] `; } } else { // Empty or unrecognized content forwardBody = ` ${headerHtml}
[Original message content not available]`; textForward = ` ---------- Forwarded message --------- From: ${fromStr} Date: ${dateStr} Subject: ${subject || ''} To: ${toStr} ${ccStr ? `Cc: ${ccStr}\n` : ''} [Original message content not available] `; } // Process the content with proper direction const processed = processContentWithDirection(forwardBody); // Check if the original email has attachments const originalAttachments = originalEmail.attachments || []; // Extract any inline images and add to attachments const inlineImages = extractInlineImages(originalHtmlContent); // Combine original attachments and inline images const combinedAttachments = [ ...originalAttachments.map(att => ({ filename: att.filename || 'attachment', contentType: att.contentType || 'application/octet-stream', content: att.content })), ...inlineImages ]; return { to: '', subject: subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`, content: { text: textForward.trim(), html: processed.html, isHtml: true, direction: 'ltr' }, // Include attachments if any were found attachments: combinedAttachments.length > 0 ? combinedAttachments : undefined }; } /** * Format an email for reply or reply-all - canonical implementation */ export function formatEmailForReplyOrForward( email: EmailMessage | LegacyEmailMessage | null, type: 'reply' | 'reply-all' | 'forward' ): FormattedEmail { // Use our dedicated formatters if (type === 'forward') { return formatForwardedEmail(email); } else { return formatReplyEmail(email, type as 'reply' | 'reply-all'); } }