courrier preview

This commit is contained in:
alma 2025-05-01 12:13:20 +02:00
parent fba2b1213f
commit b149c52931
7 changed files with 326 additions and 714 deletions

View File

@ -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 (
<div className="flex flex-col h-full max-h-[80vh] bg-white border rounded-md shadow-md">
{/* Header */}

View File

@ -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<EmailContentDisplayProps> = ({
type = 'auto',
debug = false
}) => {
// Process content with centralized utility
// Process content if provided
const processedContent = useMemo(() => {
// Default empty content
if (!content) {
return {
text: '',
html: '<div class="text-gray-400">No content available</div>',
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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>');
return processContentWithDirection(formattedText);
return { __html: '<div class="email-content-empty">No content available</div>' };
}
// 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: '<div class="email-content-error">Error displaying email content</div>' };
}
}, [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(/<blockquote[^>]*>[\s\S]*?<\/blockquote>/gi,
const htmlWithoutQuotes = processedContent.__html.replace(/<blockquote[^>]*>[\s\S]*?<\/blockquote>/gi,
'<div class="text-gray-400">[Quoted text hidden]</div>');
return { __html: htmlWithoutQuotes };
}
return processedContent.html;
}, [processedContent.html, showQuotedText]);
return processedContent;
}, [processedContent, showQuotedText]);
return (
<div className={`email-content-display ${className}`}>
<div
className="email-content-inner"
dangerouslySetInnerHTML={{ __html: displayHTML }}
dangerouslySetInnerHTML={displayHTML}
/>
{/* Debug output if enabled */}
{debug && (
<div className="mt-4 p-2 text-xs bg-gray-100 border rounded">
<p><strong>Content Type:</strong> {content?.isHtml ? 'HTML' : 'Text'}</p>
<p><strong>Direction:</strong> {processedContent.direction}</p>
<p><strong>HTML Length:</strong> {content?.html?.length || 0}</p>
<p><strong>Text Length:</strong> {content?.text?.length || 0}</p>
<p><strong>Content Type:</strong> {typeof content === 'string' ? 'Text' : 'HTML'}</p>
<p><strong>HTML Length:</strong> {typeof content === 'string' ? content.length : content?.html?.length || 0}</p>
<p><strong>Text Length:</strong> {typeof content === 'string' ? content.length : content?.text?.length || 0}</p>
</div>
)}

View File

@ -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<RichEmailEditorProps> = ({
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<RichEmailEditorProps> = ({
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) {

View File

@ -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(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/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('<html') ||
content.includes('<body') ||
content.includes('<div') ||
content.includes('<p>') ||
content.includes('<br>'));
}
/**
* 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('<html') ||
email.content.includes('<body') ||
email.content.includes('<div') ||
email.content.includes('<p>'));
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('<html');
const hasBodyTag = content.includes('<body');
if (html) {
// Process HTML content
let processedHtml = processHtmlContent(html, text);
// Extract body content if we have a complete HTML document and in browser environment
if (hasHtmlTag && hasBodyTag && typeof window !== 'undefined' && typeof DOMParser !== 'undefined') {
try {
// Create a DOM parser to extract just the body content
const parser = new DOMParser();
const doc = parser.parseFromString(content, 'text/html');
const bodyContent = doc.body.innerHTML;
if (bodyContent) {
content = bodyContent;
}
} catch (error) {
// If extraction fails, continue with the original content
console.error('Error extracting body content:', error);
}
}
// Use the centralized sanitizeHtml function
let sanitizedContent = sanitizeHtml(content);
// Fix URL encoding issues that might occur during sanitization
// Apply styling
return `<div class="email-content" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-x: auto; overflow-wrap: break-word; word-wrap: break-word;" dir="${detectTextDirection(text)}">${processedHtml}</div>`;
}
// If we only have text content, format it properly
else if (text) {
return formatPlainTextToHtml(text);
}
// Default case: empty or unrecognized content
return '<div class="email-content-empty" style="padding: 20px; text-align: center; color: #666;">No content available</div>';
} catch (error) {
console.error('formatEmailContent: Error formatting email content:', error);
return `<div class="email-content-error" style="padding: 15px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;"><p>Error displaying email content</p><p style="font-size: 12px; margin-top: 10px;">${error instanceof Error ? error.message : 'Unknown error'}</p></div>`;
}
}
/**
* 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('<html');
const hasBodyTag = htmlContent.includes('<body');
// Extract body content if we have a complete HTML document and in browser environment
if (hasHtmlTag && hasBodyTag && typeof window !== 'undefined' && typeof DOMParser !== 'undefined') {
try {
// Create a DOM parser to extract just the body content
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const bodyContent = doc.body.innerHTML;
if (bodyContent) {
htmlContent = bodyContent;
}
} catch (error) {
console.error('Error extracting body content:', error);
}
}
// Use the centralized sanitizeHtml function
let sanitizedContent = sanitizeHtml(htmlContent);
// Fix URL encoding issues
try {
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
// Temporary element to manipulate the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = sanitizedContent;
// Fix all links
// Fix all links that might have been double-encoded
const links = tempDiv.querySelectorAll('a');
links.forEach(link => {
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(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
// Fix for broken image paths that might be relative
.replace(/(src|background)="(?!(?:https?:|data:|cid:))/gi, '$1="https://')
// Fix for base64 images that might be broken across lines
.replace(/src="data:image\/[^;]+;base64,\s*([^"]+)\s*"/gi, (match, p1) => {
return `src="data:image/png;base64,${p1.replace(/\s+/g, '')}"`;
});
// Use the centralized text direction utility
const styledContent = `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-x: auto; overflow-wrap: break-word; word-wrap: break-word;">${fixedContent}</div>`;
// Apply correct text direction using the centralized utility
return applyTextDirection(styledContent, textContent);
}
// If we only have text content, format it properly
else if (textContent) {
// Escape HTML characters to prevent XSS
const escapedText = textContent
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// Format plain text with proper line breaks and paragraphs
const formattedText = escapedText
.replace(/\r\n|\r|\n/g, '<br>') // Convert all newlines to <br>
.replace(/((?:<br>){2,})/g, '</p><p>') // Convert multiple newlines to paragraphs
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
const styledPlainText = `<div style="font-family: -apple-system, BlinkMacSystemFont, Menlo, Monaco, Consolas, 'Courier New', monospace; white-space: pre-wrap; line-height: 1.5; color: #333; padding: 15px; max-width: 100%; overflow-wrap: break-word;"><p>${formattedText}</p></div>`;
// Apply correct text direction using the centralized utility
return applyTextDirection(styledPlainText, textContent);
} catch (e) {
console.error('Error fixing URLs in content:', e);
}
// Default case: empty or unrecognized content
return '<div class="email-content-empty" style="padding: 20px; text-align: center; color: #666;">No content available</div>';
// Fix common email client quirks
return sanitizedContent
// Fix for Outlook WebVML content
.replace(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
// Fix for broken image paths that might be relative
.replace(/(src|background)="(?!(?:https?:|data:|cid:))/gi, '$1="https://')
// Fix for base64 images that might be broken across lines
.replace(/src="data:image\/[^;]+;base64,\s*([^"]+)\s*"/gi, (match, p1) => {
return `src="data:image/png;base64,${p1.replace(/\s+/g, '')}"`;
});
} catch (error) {
console.error('formatEmailContent: Error formatting email content:', error);
return `<div class="email-content-error" style="padding: 15px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;"><p>Error displaying email content</p><p style="font-size: 12px; margin-top: 10px;">${error instanceof Error ? error.message : 'Unknown error'}</p></div>`;
console.error('Error processing HTML content:', error);
return htmlContent;
}
}
/**
* Format plain text to HTML with proper line breaks and styling
*/
export function formatPlainTextToHtml(text: string): string {
if (!text) return '';
// Detect text direction
const direction = detectTextDirection(text);
// Escape HTML characters to prevent XSS
const escapedText = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// Format plain text with proper line breaks and paragraphs
const formattedText = escapedText
.replace(/\r\n|\r|\n/g, '<br>') // Convert all newlines to <br>
.replace(/((?:<br>){2,})/g, '</p><p>') // Convert multiple newlines to paragraphs
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
return `<div class="email-content" style="font-family: -apple-system, BlinkMacSystemFont, Menlo, Monaco, Consolas, 'Courier New', monospace; white-space: pre-wrap; line-height: 1.5; color: #333; padding: 15px; max-width: 100%; overflow-wrap: break-word;" dir="${direction}"><p>${formattedText}</p></div>`;
}

View File

@ -1,154 +0,0 @@
/**
* DEPRECATED - USE email-utils.ts INSTEAD
*
* This file is maintained for backward compatibility only.
* New code should import directly from email-utils.ts, which contains
* the canonical implementations of these functions.
*/
import { sanitizeHtml } from './dom-purify-config';
import { formatEmailAddresses, formatEmailDate, formatReplyEmail, formatForwardedEmail, formatEmailForReplyOrForward } from './email-utils';
import { applyTextDirection } from './text-direction';
import type { EmailMessage, EmailAddress } from '@/types/email';
// Re-export the functions from email-utils for backward compatibility
export {
formatEmailAddresses,
formatEmailDate,
formatReplyEmail,
formatForwardedEmail,
formatEmailForReplyOrForward,
sanitizeHtml
};
// Re-export types for backward compatibility
export type { EmailAddress, EmailMessage };
/**
* 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();
}
}
/**
* Parse text and find URLs to turn into clickable links
* This is a utility function used only in this file
*/
function parseUrlsToLinks(text: string): string {
const urlRegex = /(https?:\/\/[^\s]+)/g;
return text.replace(
urlRegex,
url => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`
);
}
/**
* Convert plain text email content to HTML with proper line breaks
* This is a utility function used only in this file
*/
function textToHtml(text: string): string {
if (!text) return '';
// Escape HTML characters first
const escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
// Convert line breaks and wrap in a div
const withLineBreaks = escaped.replace(/\n/g, '<br>');
// Parse URLs to make them clickable
const withLinks = parseUrlsToLinks(withLineBreaks);
return withLinks;
}
/**
* 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 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;
}
// 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}`;
}

View File

@ -1,14 +1,25 @@
/**
* Unified Email Utilities
*
* This file contains all email-related utility functions:
* - Content normalization
* - Email formatting (replies, forwards)
* - Text direction handling
* This file provides backward compatibility for email utilities.
* New code should import directly from the specialized modules:
* - email-content.ts (content processing)
* - text-direction.ts (direction handling)
* - dom-purify-config.ts (sanitization)
*/
// Import from centralized configuration
// Import from specialized modules
import { sanitizeHtml } from './dom-purify-config';
import { detectTextDirection, applyTextDirection } from './text-direction';
import {
extractEmailContent,
formatEmailContent,
processHtmlContent,
formatPlainTextToHtml,
isHtmlContent,
extractTextFromHtml
} from './email-content';
import {
EmailMessage,
EmailContent,
@ -17,16 +28,18 @@ import {
} 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 };
// Re-export important functions for backward compatibility
export {
sanitizeHtml,
extractEmailContent,
formatEmailContent,
processHtmlContent,
formatPlainTextToHtml,
detectTextDirection,
applyTextDirection
};
/**
* Standard interface for formatted email responses
@ -88,28 +101,6 @@ export function formatEmailDate(date: Date | string | undefined): string {
}
}
/**
* 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// Format plain text with proper line breaks and paragraphs
return escapedText
.replace(/\r\n|\r|\n/g, '<br>') // Convert all newlines to <br>
.replace(/((?:<br>){2,})/g, '</p><p>') // Convert multiple newlines to paragraphs
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
}
/**
* Normalize email content to our standard format regardless of input format
*/
@ -158,11 +149,11 @@ export function renderEmailContent(content: EmailContent | null): string {
return '<div class="email-content-empty">No content available</div>';
}
// Use the centralized content processing function
const processed = processContentWithDirection(content);
// Create a simple object that can be processed by formatEmailContent
const emailObj = { content };
// Return the processed HTML with proper direction
return processed.html || '<div class="email-content-empty">No content available</div>';
// Use the centralized formatting function
return formatEmailContent(emailObj);
}
/**
@ -257,71 +248,6 @@ function getFormattedHeaderInfo(email: any): {
return { fromStr, toStr, ccStr, dateStr, subject };
}
/**
* Extract image attachments from HTML content
*/
function extractInlineImages(htmlContent: string): Array<{
filename: string;
contentType: string;
content?: string;
}> {
const images: Array<{
filename: string;
contentType: string;
content?: string;
}> = [];
try {
if (!htmlContent || typeof window === 'undefined') return images;
// Create a temporary DOM element to parse the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
// Find all image elements
const imgElements = tempDiv.querySelectorAll('img');
// Process each image
imgElements.forEach((img, index) => {
const src = img.getAttribute('src');
if (!src) return;
// Only process data URLs and non-tracking pixels
if (src.startsWith('data:image')) {
const contentType = src.split(',')[0].split(':')[1].split(';')[0];
const imageData = src.split(',')[1];
// Skip tiny images (likely tracking pixels)
if (imageData.length < 100) return;
images.push({
filename: `inline-image-${index + 1}.${contentType.split('/')[1] || 'png'}`,
contentType,
content: imageData
});
// Replace the image source with a placeholder
img.setAttribute('src', `cid:inline-image-${index + 1}`);
}
else if (src.startsWith('cid:')) {
// Already a CID reference, just add a placeholder
const cid = src.substring(4);
images.push({
filename: `${cid}.png`,
contentType: 'image/png'
});
}
});
// Update the HTML content to use the placeholders
htmlContent = tempDiv.innerHTML;
} catch (error) {
console.error('Error extracting inline images:', error);
}
return images;
}
/**
* Format email for reply
*/
@ -349,15 +275,9 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
// Extract just the text content for a clean reply
let emailText = '';
// Try to get text directly from content.text first
if (originalEmail.content && typeof originalEmail.content === 'object' && originalEmail.content.text) {
emailText = originalEmail.content.text;
}
// Otherwise, fall back to extractEmailContent which tries various formats
else {
const { text } = extractEmailContent(originalEmail);
emailText = text;
}
// Extract content using the centralized extraction function
const { text, html } = extractEmailContent(originalEmail);
emailText = text;
// Create simple reply with header
const cleanReplyHeader = `<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #eee; padding-bottom: 10px;">On ${dateStr}, ${fromStr} wrote:</div>`;
@ -417,15 +337,9 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
// Extract just the text content for a clean forward
let emailText = '';
// Try to get text directly from content.text first
if (originalEmail.content && typeof originalEmail.content === 'object' && originalEmail.content.text) {
emailText = originalEmail.content.text;
}
// Otherwise, fall back to extractEmailContent which tries various formats
else {
const { text } = extractEmailContent(originalEmail);
emailText = text;
}
// Extract content using the centralized extraction function
const { text, html } = extractEmailContent(originalEmail);
emailText = text;
// Create simple forward with metadata header
const cleanForwardHeader = `

View File

@ -1,13 +1,10 @@
/**
* Text Direction Utilities
*
* Centralized utilities for handling text direction (RTL/LTR)
* Core utilities for handling text direction (RTL/LTR)
* to ensure consistent behavior across the application.
*/
import { sanitizeHtml } from './dom-purify-config';
import { EmailContent } from '@/types/email';
/**
* Detects if text contains RTL characters and should be displayed right-to-left
* Uses a comprehensive regex pattern that covers Arabic, Hebrew, and other RTL scripts
@ -64,278 +61,4 @@ export function applyTextDirection(htmlContent: string, textContent?: string): s
// Otherwise, wrap the content with a direction-aware container
return `<div class="email-content" dir="${direction}">${htmlContent}</div>`;
}
/**
* Extracts content from various possible email formats
* Reduces duplication across the codebase for content extraction
*/
export function extractEmailContent(email: any): { text: string; html: string } {
// Default empty values
let textContent = '';
let htmlContent = '';
// Extract based on common formats
if (email) {
if (typeof email.content === 'object' && email.content) {
// 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 (email.content.body.includes('<') && (
email.content.body.includes('<html') ||
email.content.body.includes('<body') ||
email.content.body.includes('<div')
)) {
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 (email.content.data.includes('<') && (
email.content.data.includes('<html') ||
email.content.data.includes('<body') ||
email.content.data.includes('<div')
)) {
htmlContent = email.content.data;
} else {
textContent = email.content.data;
}
}
}
// Last resort: try to convert the entire content object to string
if (!textContent && !htmlContent) {
try {
// Some email servers encode content as JSON string
const contentStr = JSON.stringify(email.content);
if (contentStr && contentStr !== '{}') {
textContent = `[Complex email content - please view in original format]`;
}
} catch (e) {
console.error('Error extracting content from complex object:', e);
}
}
}
} else if (typeof email.content === 'string') {
// Check if content is likely HTML
if (email.content.includes('<') && (
email.content.includes('<html') ||
email.content.includes('<body') ||
email.content.includes('<div')
)) {
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 || '';
}
}
}
// Ensure we always have at least some text content
if (!textContent && htmlContent) {
try {
// Create a helper function to extract text from HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
textContent = tempDiv.textContent || tempDiv.innerText || '';
} catch (e) {
// Fallback for non-browser environments or if extraction fails
textContent = htmlContent.replace(/<[^>]*>/g, ' ')
.replace(/\s+/g, ' ')
.trim() || '[Email content]';
}
}
// Add debug logging to help troubleshoot content extraction
console.log('Extracted email content:', {
hasHtml: !!htmlContent,
htmlLength: htmlContent.length,
hasText: !!textContent,
textLength: textContent.length
});
return { text: textContent, html: htmlContent };
}
/**
* Comprehensive utility that processes email content:
* - Sanitizes HTML content
* - Detects text direction
* - Applies direction attributes
*
* This reduces redundancy by combining these steps into one centralized function
*/
export function processContentWithDirection(content: string | EmailContent | null | undefined): {
html: string;
text: string;
direction: 'ltr' | 'rtl';
} {
// Default result with fallbacks
const result = {
html: '',
text: '',
direction: 'ltr' as const
};
// Handle null/undefined cases
if (!content) return result;
// Extract text and HTML content based on input type
let textContent = '';
let htmlContent = '';
if (typeof content === 'string') {
// Simple string content (check if it's HTML or plain text)
if (content.includes('<') && (
content.includes('<html') ||
content.includes('<body') ||
content.includes('<div') ||
content.includes('<p>')
)) {
htmlContent = content;
} else {
textContent = content;
}
} else {
// EmailContent object
textContent = content.text || '';
htmlContent = content.html || '';
}
// Handle complex email content that might not be properly detected
if (!textContent && !htmlContent && typeof content === 'object') {
console.log('Processing complex content object:', content);
// Try to extract content from complex object structure
try {
// Check for common email content formats
// Type assertion to 'any' since we need to handle various email formats
const contentAny = content as any;
if (contentAny.body) {
if (typeof contentAny.body === 'string') {
// Detect if body is HTML or text
if (contentAny.body.includes('<') && (
contentAny.body.includes('<html') ||
contentAny.body.includes('<body') ||
contentAny.body.includes('<div')
)) {
htmlContent = contentAny.body;
} else {
textContent = contentAny.body;
}
} else if (typeof contentAny.body === 'object' && contentAny.body) {
// Extract from nested body object
htmlContent = contentAny.body.html || '';
textContent = contentAny.body.text || '';
}
}
// Try to convert complex content to string for debugging
if (!textContent && !htmlContent) {
try {
const contentStr = JSON.stringify(content);
console.log('Complex content structure:', contentStr.slice(0, 300) + '...');
textContent = '[Complex email content]';
} catch (e) {
console.error('Failed to stringify complex content:', e);
}
}
} catch (error) {
console.error('Error processing complex content:', error);
}
}
// Always ensure we have text for direction detection
if (!textContent && htmlContent) {
// Extract text from HTML for direction detection
try {
// Use DOM API if available
if (typeof document !== 'undefined') {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
textContent = tempDiv.textContent || tempDiv.innerText || '';
} else {
// Simple regex fallback for non-browser environments
textContent = htmlContent.replace(/<[^>]*>/g, ' ')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/\s+/g, ' ')
.trim();
}
} catch (e) {
console.error('Error extracting text from HTML:', e);
textContent = 'Failed to extract text content';
}
}
// Detect direction from text
const direction = detectTextDirection(textContent);
// Sanitize HTML if present
if (htmlContent) {
try {
// Sanitize HTML first using the centralized function
htmlContent = sanitizeHtml(htmlContent);
// Then apply direction
htmlContent = applyTextDirection(htmlContent, textContent);
} catch (error) {
console.error('Error sanitizing HTML content:', error);
// Create fallback content if sanitization fails
htmlContent = `<div dir="${direction}">${
textContent ?
textContent.replace(/\n/g, '<br>') :
'Could not process HTML content'
}</div>`;
}
} else if (textContent) {
// Convert plain text to HTML with proper direction
htmlContent = `<div dir="${direction}">${textContent.replace(/\n/g, '<br>')}</div>`;
}
// Add debug logging for troubleshooting
console.log('Processed content:', {
direction,
htmlLength: htmlContent.length,
textLength: textContent.length,
hasHtml: !!htmlContent,
hasText: !!textContent
});
// Return processed content
return {
text: textContent,
html: htmlContent,
direction
};
}