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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { EmailMessage, EmailContent, EmailAddress, LegacyEmailMessage } from '@/types/email'; 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 * 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 * Normalizes content from various formats into the standard EmailContent format
*/ */
function normalizeContent(email: LegacyEmailMessage): EmailContent { function normalizeContent(email: LegacyEmailMessage): EmailContent {
// Default content structure
const normalizedContent: EmailContent = {
html: undefined,
text: '',
isHtml: false,
direction: 'ltr'
};
try { try {
// Extract content based on standardized property hierarchy // Extract content based on possible formats to pass to the centralized processor
let htmlContent = ''; let initialContent: any = {};
let textContent = '';
let isHtml = false;
// Step 1: Extract content from the various possible formats
if (email.content && typeof email.content === 'object') { if (email.content && typeof email.content === 'object') {
isHtml = !!email.content.html; initialContent = email.content;
htmlContent = email.content.html || '';
textContent = email.content.text || '';
} else if (typeof email.content === 'string') { } else if (typeof email.content === 'string') {
// Check if the string content is HTML // Check if the string content is HTML
isHtml = email.content.trim().startsWith('<') && if (email.content.trim().startsWith('<') &&
(email.content.includes('<html') || (email.content.includes('<html') ||
email.content.includes('<body') || email.content.includes('<body') ||
email.content.includes('<div') || email.content.includes('<div') ||
email.content.includes('<p>')); email.content.includes('<p>'))) {
htmlContent = isHtml ? email.content : ''; initialContent.html = 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 || '';
} else { } else {
// Server environment - do simple strip initialContent.text = email.content;
normalizedContent.text = htmlContent
.replace(/<[^>]*>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim();
} }
} 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) // Use the centralized content processor
if (isHtml && htmlContent) { const processedContent = processContentWithDirection(initialContent);
normalizedContent.html = htmlContent;
}
// Determine text direction return {
normalizedContent.direction = detectTextDirection(normalizedContent.text); html: processedContent.html,
text: processedContent.text,
return normalizedContent; isHtml: !!processedContent.html,
direction: processedContent.direction
};
} catch (error) { } catch (error) {
console.error('Error normalizing email content:', error); console.error('Error normalizing email content:', error);

View File

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

View File

@ -5,6 +5,9 @@
* to ensure consistent behavior across the application. * 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 * 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 * Uses a comprehensive regex pattern that covers Arabic, Hebrew, and other RTL scripts
@ -62,3 +65,115 @@ export function applyTextDirection(htmlContent: string, textContent?: string): s
// Otherwise, wrap the content with a direction-aware container // Otherwise, wrap the content with a direction-aware container
return `<div class="email-content" dir="${direction}">${htmlContent}</div>`; 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
};
}