courrier preview
This commit is contained in:
parent
86581cea72
commit
6c9f2d86a6
@ -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);
|
||||
|
||||
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.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>
|
||||
)}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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(/ /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);
|
||||
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(/ /g, ' ')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/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
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user