courrier preview

This commit is contained in:
alma 2025-05-01 10:47:24 +02:00
parent 86581cea72
commit 6c9f2d86a6
7 changed files with 320 additions and 335 deletions

View File

@ -16,7 +16,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import RichTextEditor from '@/components/ui/rich-text-editor';
import { detectTextDirection } from '@/lib/utils/text-direction';
import { processContentWithDirection } from '@/lib/utils/text-direction';
// Import from the centralized utils
import {
@ -351,7 +351,7 @@ export default function ComposeEmail(props: ComposeEmailProps) {
<RichTextEditor
ref={editorRef}
initialContent={emailContent}
initialDirection={detectTextDirection(emailContent)}
initialDirection={processContentWithDirection(emailContent).direction}
onChange={(html) => {
// Store the content
setEmailContent(html);

View File

@ -2,8 +2,7 @@
import React, { useMemo } from 'react';
import { EmailContent } from '@/types/email';
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
import { sanitizeHtml, getDOMPurify } from '@/lib/utils/dom-purify-config';
import { processContentWithDirection } from '@/lib/utils/text-direction';
interface EmailContentDisplayProps {
content: EmailContent | null | undefined;
@ -24,99 +23,58 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
type = 'auto',
debug = false
}) => {
// Create a safe content object with fallback values for missing properties
const safeContent = useMemo(() => {
// Process content with centralized utility
const processedContent = useMemo(() => {
// Default empty content
if (!content) {
return {
text: '',
html: undefined,
isHtml: false,
html: '<div class="text-gray-400">No content available</div>',
direction: 'ltr' as const
};
}
return {
text: content.text || '',
html: content.html,
isHtml: content.isHtml,
// If direction is missing, detect it from the text content
direction: content.direction || detectTextDirection(content.text)
};
}, [content]);
// Determine what content to display based on type preference and available content
const htmlToDisplay = useMemo(() => {
// If no content is available, show a placeholder
if (!safeContent.text && !safeContent.html) {
return '<div class="text-gray-400">No content available</div>';
}
// If type is explicitly set to text, or we don't have HTML and auto mode
if (type === 'text' || (type === 'auto' && !safeContent.isHtml)) {
// Format plain text with line breaks for HTML display
if (safeContent.text) {
const formattedText = safeContent.text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>');
return formattedText;
}
}
// Otherwise use HTML content if available
if (safeContent.isHtml && safeContent.html) {
return safeContent.html;
}
// Fallback to text content if there's no HTML
if (safeContent.text) {
const formattedText = safeContent.text
// 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 formattedText;
return processContentWithDirection(formattedText);
}
return '<div class="text-gray-400">No content available</div>';
}, [safeContent, type]);
// For auto mode, let the centralized function handle the content
return processContentWithDirection(content);
}, [content, type]);
// Handle quoted text display
const processedHTML = useMemo(() => {
const displayHTML = useMemo(() => {
if (!showQuotedText) {
// This is simplified - a more robust approach would parse and handle
// quoted sections more intelligently
return htmlToDisplay.replace(/<blockquote[^>]*>[\s\S]*?<\/blockquote>/gi,
return processedContent.html.replace(/<blockquote[^>]*>[\s\S]*?<\/blockquote>/gi,
'<div class="text-gray-400">[Quoted text hidden]</div>');
}
return htmlToDisplay;
}, [htmlToDisplay, showQuotedText]);
// Sanitize HTML content and apply proper direction
const sanitizedHTML = useMemo(() => {
// First sanitize the HTML with our centralized utility
const cleanHtml = sanitizeHtml(processedHTML);
// Then apply text direction consistently
return applyTextDirection(cleanHtml, safeContent.text);
}, [processedHTML, safeContent.text]);
return processedContent.html;
}, [processedContent.html, showQuotedText]);
return (
<div className={`email-content-display ${className}`}>
<div
className="email-content-inner"
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
dangerouslySetInnerHTML={{ __html: displayHTML }}
/>
{/* Debug output if enabled */}
{debug && (
<div className="mt-4 p-2 text-xs bg-gray-100 border rounded">
<p><strong>Content Type:</strong> {safeContent.isHtml ? 'HTML' : 'Text'}</p>
<p><strong>Direction:</strong> {safeContent.direction}</p>
<p><strong>HTML Length:</strong> {safeContent.html?.length || 0}</p>
<p><strong>Text Length:</strong> {safeContent.text?.length || 0}</p>
<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>
</div>
)}

View File

@ -1,7 +1,7 @@
'use client';
import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
import { detectTextDirection } from '@/lib/utils/text-direction';
import { processContentWithDirection } from '@/lib/utils/text-direction';
import { getDOMPurify } from '@/lib/utils/dom-purify-config';
interface RichTextEditorProps {
@ -44,8 +44,13 @@ const RichTextEditor = forwardRef<HTMLDivElement, RichTextEditorProps>(({
initialDirection
}, ref) => {
const internalEditorRef = useRef<HTMLDivElement>(null);
// Process initial content to get its direction
const processedInitialContent = processContentWithDirection(initialContent);
// Set initial direction either from prop or detected from content
const [direction, setDirection] = useState<'ltr' | 'rtl'>(
initialDirection || detectTextDirection(initialContent)
initialDirection || processedInitialContent.direction
);
// Forward the ref to parent components
@ -54,12 +59,10 @@ const RichTextEditor = forwardRef<HTMLDivElement, RichTextEditorProps>(({
// Initialize editor with clean content
useEffect(() => {
if (internalEditorRef.current) {
// Clean the initial content using our centralized config
const cleanContent = DOMPurify.sanitize(initialContent, {
ADD_ATTR: ['dir'] // Ensure dir attributes are preserved
});
// Using centralized processing for initial content
const { html: cleanContent } = processContentWithDirection(initialContent);
internalEditorRef.current.innerHTML = cleanContent;
internalEditorRef.current.innerHTML = cleanContent || '';
// Set initial direction
internalEditorRef.current.setAttribute('dir', direction);
@ -75,15 +78,20 @@ const RichTextEditor = forwardRef<HTMLDivElement, RichTextEditorProps>(({
// Handle content changes and detect direction changes
const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
if (onChange && e.currentTarget.innerHTML !== initialContent) {
onChange(e.currentTarget.innerHTML);
const newContent = e.currentTarget.innerHTML;
// Notify parent of changes
if (onChange && newContent !== initialContent) {
onChange(newContent);
}
// Re-detect direction on significant content changes
// Only do this when the content length has changed significantly
const newContent = e.currentTarget.innerText;
if (newContent.length > 5 && newContent.length % 10 === 0) {
const newDirection = detectTextDirection(newContent);
// Only perform direction detection periodically to avoid constant recalculation
// Get the text content for direction detection
const newTextContent = e.currentTarget.innerText;
if (newTextContent.length > 5 && newTextContent.length % 10 === 0) {
// Process the content to determine direction
const { direction: newDirection } = processContentWithDirection(newTextContent);
if (newDirection !== direction) {
setDirection(newDirection);
e.currentTarget.setAttribute('dir', newDirection);

View File

@ -1,8 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useToast } from './use-toast';
import { EmailMessage, EmailContent } from '@/types/email';
import { detectTextDirection } from '@/lib/utils/text-direction';
import { sanitizeHtml } from '@/lib/utils/email-utils';
import { processContentWithDirection } from '@/lib/utils/text-direction';
interface EmailFetchState {
email: EmailMessage | null;
@ -76,46 +75,40 @@ export function useEmailFetch({ onEmailLoaded, onError }: UseEmailFetchProps = {
// Create a valid email message object with required fields
const processContent = (data: any): EmailContent => {
// Determine the text content - using all possible paths
let textContent = '';
if (typeof data.content === 'string') {
textContent = data.content;
} else if (data.content?.text) {
textContent = data.content.text;
} else if (data.text) {
textContent = data.text;
} else if (data.plainText) {
textContent = data.plainText;
}
// Determine the HTML content - using all possible paths
let htmlContent = undefined;
if (data.content?.html) {
htmlContent = data.content.html;
} else if (data.html) {
htmlContent = data.html;
} else if (typeof data.content === 'string' && data.content.includes('<')) {
// If the content string appears to be HTML
htmlContent = data.content;
// We should still keep the text version, will be extracted if needed
// Extract initial content from all possible sources
let initialContent: any = {};
if (typeof data.content === 'object' && data.content) {
// Use content object directly if available
initialContent = data.content;
} else {
// Build content object from separate properties
if (typeof data.content === 'string') {
// Check if content appears to be HTML
if (data.content.includes('<') &&
(data.content.includes('<html') ||
data.content.includes('<body') ||
data.content.includes('<div'))) {
initialContent.html = data.content;
} else {
initialContent.text = data.content;
}
} else {
// Check for separate html and text properties
if (data.html) initialContent.html = data.html;
if (data.text) initialContent.text = data.text;
else if (data.plainText) initialContent.text = data.plainText;
}
}
// Clean HTML content if present - use centralized sanitization
if (htmlContent) {
htmlContent = sanitizeHtml(htmlContent);
}
// Determine if content is HTML
const isHtml = !!htmlContent;
// Detect text direction - use centralized direction detection
const direction = data.content?.direction || detectTextDirection(textContent);
// Use the centralized content processing function
const processedContent = processContentWithDirection(initialContent);
return {
text: textContent,
html: htmlContent,
isHtml,
direction
text: processedContent.text,
html: processedContent.html,
isHtml: !!processedContent.html,
direction: processedContent.direction
};
};

View File

@ -1,5 +1,5 @@
import { EmailMessage, EmailContent, EmailAddress, LegacyEmailMessage } from '@/types/email';
import { detectTextDirection } from './text-direction';
import { processContentWithDirection } from './text-direction';
/**
* Adapts a legacy email format to the standardized EmailMessage format
@ -136,81 +136,40 @@ function formatAddressesToString(addresses: EmailAddress[]): string {
* Normalizes content from various formats into the standard EmailContent format
*/
function normalizeContent(email: LegacyEmailMessage): EmailContent {
// Default content structure
const normalizedContent: EmailContent = {
html: undefined,
text: '',
isHtml: false,
direction: 'ltr'
};
try {
// Extract content based on standardized property hierarchy
let htmlContent = '';
let textContent = '';
let isHtml = false;
// Extract content based on possible formats to pass to the centralized processor
let initialContent: any = {};
// Step 1: Extract content from the various possible formats
if (email.content && typeof email.content === 'object') {
isHtml = !!email.content.html;
htmlContent = email.content.html || '';
textContent = email.content.text || '';
initialContent = email.content;
} 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>'));
htmlContent = isHtml ? email.content : '';
textContent = isHtml ? '' : email.content;
} else if (email.html) {
isHtml = true;
htmlContent = email.html;
textContent = email.text || email.plainText || '';
} else if (email.text || email.plainText) {
isHtml = false;
htmlContent = '';
textContent = email.text || email.plainText || '';
} else if (email.formattedContent) {
// Assume formattedContent is already HTML
isHtml = true;
htmlContent = email.formattedContent;
textContent = '';
}
// Step 2: Set the normalized content properties
normalizedContent.isHtml = isHtml;
// Always ensure we have text content
if (textContent) {
normalizedContent.text = textContent;
} else if (htmlContent) {
// Extract text from HTML if we don't have plain text
if (typeof document !== 'undefined') {
// Browser environment
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent;
normalizedContent.text = tempDiv.textContent || tempDiv.innerText || '';
if (email.content.trim().startsWith('<') &&
(email.content.includes('<html') ||
email.content.includes('<body') ||
email.content.includes('<div') ||
email.content.includes('<p>'))) {
initialContent.html = email.content;
} else {
// Server environment - do simple strip
normalizedContent.text = htmlContent
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim();
initialContent.text = email.content;
}
} else {
// Extract from separate properties
if (email.html) initialContent.html = email.html;
if (email.text) initialContent.text = email.text;
else if (email.plainText) initialContent.text = email.plainText;
else if (email.formattedContent) initialContent.html = email.formattedContent;
}
// If we have HTML content, store it without sanitizing (sanitization will be done at display time)
if (isHtml && htmlContent) {
normalizedContent.html = htmlContent;
}
// Use the centralized content processor
const processedContent = processContentWithDirection(initialContent);
// Determine text direction
normalizedContent.direction = detectTextDirection(normalizedContent.text);
return normalizedContent;
return {
html: processedContent.html,
text: processedContent.text,
isHtml: !!processedContent.html,
direction: processedContent.direction
};
} catch (error) {
console.error('Error normalizing email content:', error);

View File

@ -4,11 +4,11 @@
* This file contains all email-related utility functions:
* - Content normalization
* - Email formatting (replies, forwards)
* - Text direction detection
* - Text direction handling
*/
// Import from centralized DOMPurify configuration instead of configuring directly
import { sanitizeHtml, getDOMPurify } from './dom-purify-config';
// Import from centralized configuration
import { sanitizeHtml } from './dom-purify-config';
import {
EmailMessage,
EmailContent,
@ -17,7 +17,13 @@ import {
} from '@/types/email';
import { adaptLegacyEmail } from '@/lib/utils/email-adapters';
import { decodeInfomaniakEmail, adaptMimeEmail, isMimeFormat } from './email-mime-decoder';
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
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 };
@ -32,25 +38,6 @@ export interface FormattedEmail {
content: EmailContent;
}
/**
* Utility type that combines EmailMessage and LegacyEmailMessage
* to allow access to properties that might exist in either type
*/
type AnyEmailMessage = {
id: string;
subject: string | undefined;
from: any;
to: any;
cc?: any;
date: any;
content?: any;
html?: string;
text?: string;
attachments?: any[];
flags?: any;
[key: string]: any;
};
/**
* Format email addresses for display
* Can handle both array of EmailAddress objects or a string
@ -130,7 +117,12 @@ export function normalizeEmailContent(email: any): EmailMessage {
if (email.content && isMimeFormat(email.content)) {
try {
console.log('Detected MIME format email, decoding...');
return adaptMimeEmail(email);
// 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
@ -144,8 +136,13 @@ export function normalizeEmailContent(email: any): EmailMessage {
return email as EmailMessage;
}
// Otherwise, adapt from legacy format and cast to EmailMessage
return adaptLegacyEmail(email as LegacyEmailMessage) as unknown 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;
}
/**
@ -156,47 +153,17 @@ export function renderEmailContent(content: EmailContent | null): string {
return '<div class="email-content-empty">No content available</div>';
}
const safeContent = {
text: content.text || '',
html: content.html || '',
isHtml: !!content.isHtml,
direction: content.direction || 'ltr'
};
// Use the centralized content processing function
const processed = processContentWithDirection(content);
// If we have HTML content and isHtml flag is true, use it
if (safeContent.isHtml && safeContent.html) {
return sanitizeHtml(safeContent.html);
}
// Otherwise, convert text to HTML with proper line breaks
if (safeContent.text) {
return `<div dir="${safeContent.direction}">${formatPlainTextToHtml(safeContent.text)}</div>`;
}
return '<div class="email-content-empty">No content available</div>';
// Return the processed HTML with proper direction
return processed.html || '<div class="email-content-empty">No content available</div>';
}
/**
* Format email for reply
* Get recipient addresses from an email for reply or forward
*/
export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessage | null, type: 'reply' | 'reply-all' = 'reply'): FormattedEmail {
if (!originalEmail) {
return {
to: '',
cc: '',
subject: '',
content: {
text: '',
html: '',
isHtml: false,
direction: 'ltr' as const
}
};
}
// Cast to AnyEmailMessage for property access
const email = originalEmail as AnyEmailMessage;
function getRecipientAddresses(email: any, type: 'reply' | 'reply-all'): { to: string; cc: string } {
// Format the recipients
const to = Array.isArray(email.from)
? email.from.map((addr: any) => {
@ -231,15 +198,28 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
cc = [...toRecipients, ...ccRecipients].join(', ');
}
return { to, cc };
}
/**
* Get formatted header information for reply or forward
*/
function getFormattedHeaderInfo(email: any): {
fromStr: string;
toStr: string;
ccStr: string;
dateStr: string;
subject: string;
} {
// Format the subject
const subject = email.subject && !email.subject.startsWith('Re:')
? `Re: ${email.subject}`
const subject = email.subject && !email.subject.startsWith('Re:') && !email.subject.startsWith('Fwd:')
? email.subject
: email.subject || '';
// Format the content
const originalDate = email.date ? new Date(email.date) : new Date();
const dateStr = originalDate.toLocaleString();
// Format the date
const dateStr = email.date ? new Date(email.date).toLocaleString() : 'Unknown Date';
// Format sender
const fromStr = Array.isArray(email.from)
? email.from.map((addr: any) => {
if (typeof addr === 'string') return addr;
@ -249,6 +229,7 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
? email.from
: 'Unknown Sender';
// Format recipients
const toStr = Array.isArray(email.to)
? email.to.map((addr: any) => {
if (typeof addr === 'string') return addr;
@ -257,24 +238,46 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
: typeof email.to === 'string'
? email.to
: '';
// Format CC
const ccStr = Array.isArray(email.cc)
? email.cc.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.cc === 'string'
? email.cc
: '';
// Extract original content
const originalTextContent =
typeof email.content === 'object' && email.content?.text ? email.content.text :
typeof email.content === 'string' ? email.content :
email.text || '';
const originalHtmlContent =
typeof email.content === 'object' && email.content?.html ? email.content.html :
email.html ||
(typeof email.content === 'string' && email.content.includes('<')
? email.content
: '');
return { fromStr, toStr, ccStr, dateStr, subject };
}
// Get the direction from the original email
const originalDirection =
typeof email.content === 'object' && email.content?.direction ? email.content.direction :
detectTextDirection(originalTextContent);
/**
* Format email for reply
*/
export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessage | null, type: 'reply' | 'reply-all' = 'reply'): FormattedEmail {
if (!originalEmail) {
return {
to: '',
cc: '',
subject: '',
content: {
text: '',
html: '',
isHtml: false,
direction: 'ltr' as const
}
};
}
// Extract recipient addresses
const { to, cc } = getRecipientAddresses(originalEmail, type);
// Get header information
const { fromStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail);
// Extract content using centralized utility
const { text: originalTextContent, html: originalHtmlContent } = extractEmailContent(originalEmail);
// Create content with appropriate quote formatting
const replyBody = `
@ -286,12 +289,11 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
</blockquote>
`;
// Apply consistent text direction
const htmlContent = applyTextDirection(replyBody);
// Process the content with proper direction
const processed = processContentWithDirection(replyBody);
// Create plain text content
const textContent = `
On ${dateStr}, ${fromStr} wrote:
> ${originalTextContent.split('\n').join('\n> ')}
`;
@ -299,12 +301,12 @@ On ${dateStr}, ${fromStr} wrote:
return {
to,
cc,
subject,
subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`,
content: {
text: textContent,
html: htmlContent,
html: processed.html,
isHtml: true,
direction: 'ltr' as const // Reply is LTR, but original content keeps its direction in the blockquote
direction: processed.direction
}
};
}
@ -326,61 +328,11 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
};
}
// Cast to AnyEmailMessage for property access
const email = originalEmail as AnyEmailMessage;
// Get header information
const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail);
// Format the subject
const subject = email.subject && !email.subject.startsWith('Fwd:')
? `Fwd: ${email.subject}`
: email.subject || '';
// Format from, to, cc for the header
const fromStr = Array.isArray(email.from)
? email.from.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.from === 'string'
? email.from
: 'Unknown Sender';
const toStr = Array.isArray(email.to)
? email.to.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.to === 'string'
? email.to
: '';
const ccStr = Array.isArray(email.cc)
? email.cc.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.cc === 'string'
? email.cc
: '';
const dateStr = email.date ? new Date(email.date).toLocaleString() : 'Unknown Date';
// Extract original content
const originalTextContent =
typeof email.content === 'object' && email.content?.text ? email.content.text :
typeof email.content === 'string' ? email.content :
email.text || '';
const originalHtmlContent =
typeof email.content === 'object' && email.content?.html ? email.content.html :
email.html ||
(typeof email.content === 'string' && email.content.includes('<')
? email.content
: '');
// Get the direction from the original email
const originalDirection =
typeof email.content === 'object' && email.content?.direction ? email.content.direction :
detectTextDirection(originalTextContent);
// Extract content using centralized utility
const { text: originalTextContent, html: originalHtmlContent } = extractEmailContent(originalEmail);
// Create forwarded content with header information
const forwardBody = `
@ -390,7 +342,7 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
<p>---------- Forwarded message ---------</p>
<p><strong>From:</strong> ${fromStr}</p>
<p><strong>Date:</strong> ${dateStr}</p>
<p><strong>Subject:</strong> ${email.subject || ''}</p>
<p><strong>Subject:</strong> ${subject || ''}</p>
<p><strong>To:</strong> ${toStr}</p>
${ccStr ? `<p><strong>Cc:</strong> ${ccStr}</p>` : ''}
<div style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;">
@ -399,8 +351,8 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
</div>
`;
// Apply consistent text direction
const htmlContent = applyTextDirection(forwardBody);
// Process the content with proper direction
const processed = processContentWithDirection(forwardBody);
// Create plain text content
const textContent = `
@ -408,7 +360,7 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
---------- Forwarded message ---------
From: ${fromStr}
Date: ${dateStr}
Subject: ${email.subject || ''}
Subject: ${subject || ''}
To: ${toStr}
${ccStr ? `Cc: ${ccStr}\n` : ''}
@ -417,12 +369,12 @@ ${originalTextContent}
return {
to: '',
subject,
subject: subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`,
content: {
text: textContent,
html: htmlContent,
html: processed.html,
isHtml: true,
direction: 'ltr' as const // Forward is LTR, but original content keeps its direction
direction: 'ltr' as const
}
};
}
@ -440,4 +392,4 @@ export function formatEmailForReplyOrForward(
} else {
return formatReplyEmail(email, type as 'reply' | 'reply-all');
}
}
}

View File

@ -5,6 +5,9 @@
* 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
@ -61,4 +64,116 @@ 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) {
textContent = email.content.text || '';
htmlContent = email.content.html || '';
} 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 || '';
}
}
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')
)) {
htmlContent = content;
} else {
textContent = content;
}
} else {
// EmailContent object
textContent = content.text || '';
htmlContent = content.html || '';
}
// Always ensure we have text for direction detection
if (!textContent && htmlContent) {
// Extract text from HTML for direction detection
textContent = htmlContent.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&');
}
// Detect direction from text
const direction = detectTextDirection(textContent);
// Sanitize HTML if present
if (htmlContent) {
// Sanitize HTML first
htmlContent = sanitizeHtml(htmlContent);
// Then apply direction
htmlContent = applyTextDirection(htmlContent, textContent);
} else if (textContent) {
// Convert plain text to HTML with proper direction
htmlContent = `<div dir="${direction}">${textContent.replace(/\n/g, '<br>')}</div>`;
}
// Return processed content
return {
text: textContent,
html: htmlContent,
direction
};
}