diff --git a/lib/utils/email-formatter.ts b/lib/utils/email-formatter.ts index 2fbf2d07..f3c1b139 100644 --- a/lib/utils/email-formatter.ts +++ b/lib/utils/email-formatter.ts @@ -4,84 +4,50 @@ * This is the primary and only email formatting utility to be used. * All email formatting should go through here to ensure consistent * handling of text direction and HTML sanitization. + * + * IMPORTANT: This formatter is configured for English-only content + * and enforces left-to-right text direction. */ import DOMPurify from 'isomorphic-dompurify'; -// Configure DOMPurify to preserve direction attributes -DOMPurify.addHook('afterSanitizeAttributes', function(node) { - // Preserve direction attributes - if (node.hasAttribute('dir')) { - node.setAttribute('dir', node.getAttribute('dir') || 'ltr'); - } - // Preserve text-align in styles - if (node.hasAttribute('style')) { - const style = node.getAttribute('style') || ''; - if (style.includes('text-align')) { - // Keep existing alignment - } else if (node.hasAttribute('dir') && node.getAttribute('dir') === 'rtl') { - node.setAttribute('style', style + '; text-align: right;'); - } - } -}); - -// Configure DOMPurify to enforce LTR for English-only content -DOMPurify.addHook('afterSanitizeAttributes', function(node) { - // Always set direction to LTR for all elements - if (node.hasAttribute('dir')) { - node.setAttribute('dir', 'ltr'); - } - - // Ensure text alignment is left-aligned for all elements - if (node.hasAttribute('style')) { - let style = node.getAttribute('style') || ''; - - // Remove any right-to-left text alignment - if (style.includes('text-align: right') || style.includes('text-align:right')) { - style = style.replace(/text-align:\s*right\s*;?/gi, ''); - style = style.trim(); - // Add semicolon if needed - if (style && !style.endsWith(';')) { - style += ';'; - } - } - - // Add left alignment if not already specified - if (!style.includes('text-align:')) { - style += (style ? ' ' : '') + 'text-align: left;'; - } - - node.setAttribute('style', style); - } -}); - -// Clear existing hooks first -DOMPurify.removeHook('afterSanitizeAttributes'); +// Reset any existing hooks to start clean +DOMPurify.removeAllHooks(); // Configure DOMPurify for English-only content (always LTR) DOMPurify.addHook('afterSanitizeAttributes', function(node) { - // Always set direction to LTR for all elements - node.setAttribute('dir', 'ltr'); - - // Ensure text alignment is left-aligned for all elements - if (node.hasAttribute('style')) { - let style = node.getAttribute('style') || ''; + // Force LTR direction on all elements that can have a dir attribute + if (node instanceof HTMLElement) { + node.setAttribute('dir', 'ltr'); - // Remove any right-to-left text alignment - if (style.includes('text-align: right') || style.includes('text-align:right')) { - style = style.replace(/text-align:\s*right\s*;?/gi, ''); + // Handle style attribute + if (node.hasAttribute('style')) { + let style = node.getAttribute('style') || ''; + + // Remove any RTL-related styles + style = style.replace(/direction\s*:\s*rtl\s*;?/gi, ''); + style = style.replace(/text-align\s*:\s*right\s*;?/gi, ''); + style = style.replace(/unicode-bidi\s*:[^;]*;?/gi, ''); + + // Add explicit LTR styles style = style.trim(); + if (style && !style.endsWith(';')) style += ';'; + style += ' direction: ltr; text-align: left;'; + + node.setAttribute('style', style); + } else { + // If no style exists, add default LTR styles + node.setAttribute('style', 'direction: ltr; text-align: left;'); } - - // Add left alignment - style = (style ? style + '; ' : '') + 'text-align: left;'; - node.setAttribute('style', style); - } else { - // If no style exists, add default left alignment - node.setAttribute('style', 'text-align: left;'); } }); +// Configure DOMPurify to add certain attributes and forbid others +DOMPurify.setConfig({ + ADD_ATTR: ['dir'], + FORBID_ATTR: ['lang', 'bidi'] +}); + // Interface definitions export interface EmailAddress { name: string; @@ -151,13 +117,14 @@ export function formatEmailDate(date: Date | string | undefined): string { /** * Sanitize HTML content before processing or displaying + * This ensures the content is properly formatted for LTR display * @param content HTML content to sanitize - * @returns Sanitized HTML + * @returns Sanitized HTML with LTR formatting */ export function sanitizeHtml(content: string): string { if (!content) return ''; - // Sanitize the HTML using our configured DOMPurify + // Sanitize the HTML using our configured DOMPurify with LTR enforced return DOMPurify.sanitize(content); } @@ -180,7 +147,7 @@ export function formatForwardedEmail(email: EmailMessage): { const toString = formatEmailAddresses(email.to || []); const dateString = formatEmailDate(email.date); - // Get and sanitize original content + // Get and sanitize original content (sanitization enforces LTR) const originalContent = sanitizeHtml(email.content || email.html || email.text || ''); // Check if the content already has a forwarded message header @@ -188,7 +155,7 @@ export function formatForwardedEmail(email: EmailMessage): { // If there's already a forwarded message header, don't add another one if (hasExistingHeader) { - // Just wrap the content in appropriate styling without adding another header + // Just wrap the content without additional formatting const content = `
@@ -256,7 +223,7 @@ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all // Create quote header const quoteHeader = `
On ${formattedDate}, ${fromText} wrote:
`; - // Get and sanitize original content + // Get and sanitize original content (sanitization enforces LTR) const quotedContent = sanitizeHtml(email.html || email.content || email.text || ''); // Format recipients @@ -298,8 +265,6 @@ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all * COMPATIBILITY LAYER: For backward compatibility with the old email-formatter.ts * These functions map to our new implementation but preserve the old interface */ - -// For compatibility with old code that might be using the other email-formatter.ts export function formatEmailForReplyOrForward( email: EmailMessage, type: 'reply' | 'reply-all' | 'forward' @@ -329,7 +294,6 @@ export function formatEmailForReplyOrForward( /** * Decode compose content from MIME format to HTML and text - * This replaces the functionality previously in lib/compose-mime-decoder.ts */ export async function decodeComposeContent(content: string): Promise<{ html: string | null; @@ -353,15 +317,17 @@ export async function decodeComposeContent(content: string): Promise<{ } const parsed = await response.json(); + + // Apply LTR sanitization to the parsed content return { - html: parsed.html || null, + 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 + // Fallback to basic content handling with sanitization return { - html: content, + html: sanitizeHtml(content), text: content }; } @@ -369,7 +335,6 @@ export async function decodeComposeContent(content: string): Promise<{ /** * Encode compose content to MIME format for sending - * This replaces the functionality previously in lib/compose-mime-decoder.ts */ export function encodeComposeContent(content: string): string { if (!content.trim()) {