From b149c529316e95ec26508e89f909e9bf92623734 Mon Sep 17 00:00:00 2001 From: alma Date: Thu, 1 May 2025 12:13:20 +0200 Subject: [PATCH] courrier preview --- components/email/ComposeEmail.tsx | 5 +- components/email/EmailContentDisplay.tsx | 63 ++-- components/email/RichEmailEditor.tsx | 24 +- lib/utils/email-content.ts | 357 ++++++++++++++++------- lib/utils/email-formatter.ts | 154 ---------- lib/utils/email-utils.ts | 158 +++------- lib/utils/text-direction.ts | 279 +----------------- 7 files changed, 326 insertions(+), 714 deletions(-) delete mode 100644 lib/utils/email-formatter.ts diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index 8d4a4583..7085332d 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -16,7 +16,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import RichEmailEditor from '@/components/email/RichEmailEditor'; -import { processContentWithDirection } from '@/lib/utils/text-direction'; +import { detectTextDirection } from '@/lib/utils/text-direction'; // Import from the centralized utils import { @@ -318,9 +318,6 @@ export default function ComposeEmail(props: ComposeEmailProps) { } }; - // Get initial direction for the content - const { direction } = processContentWithDirection(emailContent); - return (
{/* Header */} diff --git a/components/email/EmailContentDisplay.tsx b/components/email/EmailContentDisplay.tsx index 738b88de..5ba2c26b 100644 --- a/components/email/EmailContentDisplay.tsx +++ b/components/email/EmailContentDisplay.tsx @@ -2,10 +2,10 @@ import React, { useMemo } from 'react'; import { EmailContent } from '@/types/email'; -import { processContentWithDirection } from '@/lib/utils/text-direction'; +import { formatEmailContent } from '@/lib/utils/email-content'; interface EmailContentDisplayProps { - content: EmailContent | null | undefined; + content: EmailContent | string | null; className?: string; showQuotedText?: boolean; type?: 'html' | 'text' | 'auto'; @@ -23,58 +23,57 @@ const EmailContentDisplay: React.FC = ({ type = 'auto', debug = false }) => { - // Process content with centralized utility + // Process content if provided const processedContent = useMemo(() => { - // Default empty content if (!content) { - return { - text: '', - html: '
No content available
', - direction: 'ltr' as const - }; - } - - // For text-only display, convert plain text to HTML first - if (type === 'text') { - const textContent = content.text || ''; - const formattedText = textContent - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/\n/g, '
'); - - return processContentWithDirection(formattedText); + return { __html: '' }; } - // For auto mode, let the centralized function handle the content - return processContentWithDirection(content); - }, [content, type]); + try { + let formattedContent: string; + + // If it's a string, we need to determine if it's HTML or plain text + if (typeof content === 'string') { + formattedContent = formatEmailContent({ content }); + } + // If it's an EmailContent object + else { + formattedContent = formatEmailContent({ content }); + } + + return { __html: formattedContent }; + } catch (error) { + console.error('Error processing email content:', error); + return { __html: '' }; + } + }, [content]); // Handle quoted text display const displayHTML = useMemo(() => { if (!showQuotedText) { + // Hide quoted text (usually in blockquotes) // This is simplified - a more robust approach would parse and handle // quoted sections more intelligently - return processedContent.html.replace(/]*>[\s\S]*?<\/blockquote>/gi, + const htmlWithoutQuotes = processedContent.__html.replace(/]*>[\s\S]*?<\/blockquote>/gi, '
[Quoted text hidden]
'); + return { __html: htmlWithoutQuotes }; } - return processedContent.html; - }, [processedContent.html, showQuotedText]); + return processedContent; + }, [processedContent, showQuotedText]); return (
{/* Debug output if enabled */} {debug && (
-

Content Type: {content?.isHtml ? 'HTML' : 'Text'}

-

Direction: {processedContent.direction}

-

HTML Length: {content?.html?.length || 0}

-

Text Length: {content?.text?.length || 0}

+

Content Type: {typeof content === 'string' ? 'Text' : 'HTML'}

+

HTML Length: {typeof content === 'string' ? content.length : content?.html?.length || 0}

+

Text Length: {typeof content === 'string' ? content.length : content?.text?.length || 0}

)} diff --git a/components/email/RichEmailEditor.tsx b/components/email/RichEmailEditor.tsx index b6a6d46b..e7a1c310 100644 --- a/components/email/RichEmailEditor.tsx +++ b/components/email/RichEmailEditor.tsx @@ -2,8 +2,9 @@ import React, { useEffect, useRef, useState } from 'react'; import 'quill/dist/quill.snow.css'; -import { sanitizeHtml } from '@/lib/utils/email-utils'; -import { processContentWithDirection } from '@/lib/utils/text-direction'; +import { sanitizeHtml } from '@/lib/utils/dom-purify-config'; +import { detectTextDirection } from '@/lib/utils/text-direction'; +import { processHtmlContent } from '@/lib/utils/email-content'; interface RichEmailEditorProps { initialContent: string; @@ -84,20 +85,19 @@ const RichEmailEditor: React.FC = ({ theme: 'snow', }); - // Process initial content to detect direction - const { direction, html: processedContent } = processContentWithDirection(initialContent); - // Set initial content properly if (initialContent) { try { console.log('Setting initial content in editor', { length: initialContent.length, startsWithHtml: initialContent.trim().startsWith('<'), - direction }); - // Simplify complex email content to something Quill can handle better - const sanitizedContent = sanitizeHtml(processedContent || initialContent); + // Detect text direction + const direction = detectTextDirection(initialContent); + + // Process HTML content using centralized utility + const sanitizedContent = processHtmlContent(initialContent); // Check if sanitized content is valid if (sanitizedContent.trim().length === 0) { @@ -212,11 +212,11 @@ const RichEmailEditor: React.FC = ({ startsWithHtml: initialContent.trim().startsWith('<') }); - // Process content to ensure correct direction - const { direction, html: processedContent } = processContentWithDirection(initialContent); + // Detect text direction + const direction = detectTextDirection(initialContent); - // Sanitize the HTML - const sanitizedContent = sanitizeHtml(processedContent || initialContent); + // Process HTML content using centralized utility + const sanitizedContent = processHtmlContent(initialContent); // Check if content is valid HTML if (sanitizedContent.trim().length === 0) { diff --git a/lib/utils/email-content.ts b/lib/utils/email-content.ts index 926755b8..93517166 100644 --- a/lib/utils/email-content.ts +++ b/lib/utils/email-content.ts @@ -1,11 +1,156 @@ -import DOMPurify from 'dompurify'; -import { detectTextDirection, applyTextDirection } from './text-direction'; -import { sanitizeHtml } from './email-utils'; +/** + * Centralized Email Content Utilities + * + * This file contains all core functions for email content processing: + * - Content extraction + * - HTML sanitization + * - Text direction handling + * - URL fixing + * + * Other modules should import from this file rather than implementing their own versions. + */ + +import { sanitizeHtml } from './dom-purify-config'; +import { detectTextDirection } from './text-direction'; +import { EmailContent } from '@/types/email'; + +/** + * Extract content from various possible email formats + * Centralized implementation to reduce duplication across the codebase + */ +export function extractEmailContent(email: any): { text: string; html: string } { + // Default empty values + let textContent = ''; + let htmlContent = ''; + + // Early exit if no email + if (!email) { + console.log('extractEmailContent: No email provided'); + return { text: '', html: '' }; + } + + try { + // Extract based on common formats + if (email.content && typeof email.content === 'object') { + // Standard format with content object + textContent = email.content.text || ''; + htmlContent = email.content.html || ''; + + // Handle complex email formats where content might be nested + if (!textContent && !htmlContent) { + // Try to find content in deeper nested structure + if (email.content.body) { + if (typeof email.content.body === 'string') { + // Determine if body is HTML or text + if (isHtmlContent(email.content.body)) { + htmlContent = email.content.body; + } else { + textContent = email.content.body; + } + } else if (typeof email.content.body === 'object' && email.content.body) { + // Some email formats nest content inside body + htmlContent = email.content.body.html || ''; + textContent = email.content.body.text || ''; + } + } + + // Check for data property which some email services use + if (!textContent && !htmlContent && email.content.data) { + if (typeof email.content.data === 'string') { + // Check if data looks like HTML + if (isHtmlContent(email.content.data)) { + htmlContent = email.content.data; + } else { + textContent = email.content.data; + } + } + } + } + } else if (typeof email.content === 'string') { + // Check if content is likely HTML + if (isHtmlContent(email.content)) { + htmlContent = email.content; + } else { + textContent = email.content; + } + } else { + // Check other common properties + htmlContent = email.html || ''; + textContent = email.text || ''; + + // If still no content, check for less common properties + if (!htmlContent && !textContent) { + // Try additional properties that some email clients use + htmlContent = email.body?.html || email.bodyHtml || email.htmlBody || ''; + textContent = email.body?.text || email.bodyText || email.plainText || ''; + } + } + } catch (error) { + console.error('Error extracting email content:', error); + } + + // Ensure we always have at least some text content + if (!textContent && htmlContent) { + textContent = extractTextFromHtml(htmlContent); + } + + // Log extraction results + console.log('Extracted email content:', { + hasHtml: !!htmlContent, + htmlLength: htmlContent?.length || 0, + hasText: !!textContent, + textLength: textContent?.length || 0 + }); + + return { text: textContent, html: htmlContent }; +} + +/** + * Extract plain text from HTML content + */ +export function extractTextFromHtml(html: string): string { + if (!html) return ''; + + try { + // Use DOM API if available + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + return tempDiv.textContent || tempDiv.innerText || ''; + } else { + // Simple regex fallback for non-browser environments + return html.replace(/<[^>]*>/g, ' ') + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&') + .replace(/\s+/g, ' ') + .trim(); + } + } catch (e) { + console.error('Error extracting text from HTML:', e); + // Fallback to basic strip + return html.replace(/<[^>]*>/g, ' ').trim(); + } +} + +/** + * Check if a string is likely HTML content + */ +export function isHtmlContent(content: string): boolean { + if (!content) return false; + + return content.trim().startsWith('<') && + (content.includes('') || + content.includes('
')); +} /** * Format and standardize email content for display following email industry standards. - * This function handles various email content formats and ensures proper display - * including support for HTML emails, plain text emails, RTL languages, and email client quirks. + * This is the main entry point for rendering email content. */ export function formatEmailContent(email: any): string { if (!email) { @@ -14,76 +159,68 @@ export function formatEmailContent(email: any): string { } try { - // Get the content in order of preference with proper fallbacks - let content = ''; - let isHtml = false; - let textContent = ''; - - // Extract content based on standardized property hierarchy - if (email.content && typeof email.content === 'object') { - isHtml = !!email.content.html; - content = email.content.html || ''; - textContent = email.content.text || ''; - } else if (typeof email.content === 'string') { - // Check if the string content is HTML - isHtml = email.content.trim().startsWith('<') && - (email.content.includes('')); - content = email.content; - textContent = email.content; - } else if (email.html) { - isHtml = true; - content = email.html; - textContent = email.text || ''; - } else if (email.text) { - isHtml = false; - content = ''; - textContent = email.text; - } else if (email.formattedContent) { - // Assume formattedContent is already HTML - isHtml = true; - content = email.formattedContent; - textContent = ''; - } - - // Log what we found for debugging - console.log(`Email content detected: isHtml=${isHtml}, contentLength=${content.length}, textLength=${textContent.length}`); + // Extract content from email + const { text, html } = extractEmailContent(email); // If we have HTML content, sanitize and standardize it - if (isHtml && content) { - // CRITICAL FIX: Check for browser environment since DOMParser is browser-only - const hasHtmlTag = content.includes('
`; + } + // If we only have text content, format it properly + else if (text) { + return formatPlainTextToHtml(text); + } + + // Default case: empty or unrecognized content + return ''; + } catch (error) { + console.error('formatEmailContent: Error formatting email content:', error); + return ``; + } +} + +/** + * Process HTML content to fix common email rendering issues + */ +export function processHtmlContent(htmlContent: string, textContent?: string): string { + if (!htmlContent) return ''; + + try { + // Check for browser environment (DOMParser is browser-only) + const hasHtmlTag = htmlContent.includes(' { const href = link.getAttribute('href'); @@ -101,54 +238,50 @@ export function formatEmailContent(email: any): string { // Get the fixed HTML sanitizedContent = tempDiv.innerHTML; - } catch (e) { - console.error('Error fixing URLs in content:', e); } - - // Fix common email client quirks - let fixedContent = sanitizedContent - // Fix for Outlook WebVML content - .replace(/