courrier preview
This commit is contained in:
parent
fba2b1213f
commit
b149c52931
@ -16,7 +16,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import RichEmailEditor from '@/components/email/RichEmailEditor';
|
import RichEmailEditor from '@/components/email/RichEmailEditor';
|
||||||
import { processContentWithDirection } from '@/lib/utils/text-direction';
|
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||||
|
|
||||||
// Import from the centralized utils
|
// Import from the centralized utils
|
||||||
import {
|
import {
|
||||||
@ -318,9 +318,6 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get initial direction for the content
|
|
||||||
const { direction } = processContentWithDirection(emailContent);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full max-h-[80vh] bg-white border rounded-md shadow-md">
|
<div className="flex flex-col h-full max-h-[80vh] bg-white border rounded-md shadow-md">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { EmailContent } from '@/types/email';
|
import { EmailContent } from '@/types/email';
|
||||||
import { processContentWithDirection } from '@/lib/utils/text-direction';
|
import { formatEmailContent } from '@/lib/utils/email-content';
|
||||||
|
|
||||||
interface EmailContentDisplayProps {
|
interface EmailContentDisplayProps {
|
||||||
content: EmailContent | null | undefined;
|
content: EmailContent | string | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
showQuotedText?: boolean;
|
showQuotedText?: boolean;
|
||||||
type?: 'html' | 'text' | 'auto';
|
type?: 'html' | 'text' | 'auto';
|
||||||
@ -23,58 +23,57 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
|||||||
type = 'auto',
|
type = 'auto',
|
||||||
debug = false
|
debug = false
|
||||||
}) => {
|
}) => {
|
||||||
// Process content with centralized utility
|
// Process content if provided
|
||||||
const processedContent = useMemo(() => {
|
const processedContent = useMemo(() => {
|
||||||
// Default empty content
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return {
|
return { __html: '<div class="email-content-empty">No content available</div>' };
|
||||||
text: '',
|
|
||||||
html: '<div class="text-gray-400">No content available</div>',
|
|
||||||
direction: 'ltr' as const
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// For text-only display, convert plain text to HTML first
|
|
||||||
if (type === 'text') {
|
|
||||||
const textContent = content.text || '';
|
|
||||||
const formattedText = textContent
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/\n/g, '<br>');
|
|
||||||
|
|
||||||
return processContentWithDirection(formattedText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For auto mode, let the centralized function handle the content
|
try {
|
||||||
return processContentWithDirection(content);
|
let formattedContent: string;
|
||||||
}, [content, type]);
|
|
||||||
|
// If it's a string, we need to determine if it's HTML or plain text
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
formattedContent = formatEmailContent({ content });
|
||||||
|
}
|
||||||
|
// If it's an EmailContent object
|
||||||
|
else {
|
||||||
|
formattedContent = formatEmailContent({ content });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { __html: formattedContent };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing email content:', error);
|
||||||
|
return { __html: '<div class="email-content-error">Error displaying email content</div>' };
|
||||||
|
}
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
// Handle quoted text display
|
// Handle quoted text display
|
||||||
const displayHTML = useMemo(() => {
|
const displayHTML = useMemo(() => {
|
||||||
if (!showQuotedText) {
|
if (!showQuotedText) {
|
||||||
|
// Hide quoted text (usually in blockquotes)
|
||||||
// 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 processedContent.html.replace(/<blockquote[^>]*>[\s\S]*?<\/blockquote>/gi,
|
const htmlWithoutQuotes = processedContent.__html.replace(/<blockquote[^>]*>[\s\S]*?<\/blockquote>/gi,
|
||||||
'<div class="text-gray-400">[Quoted text hidden]</div>');
|
'<div class="text-gray-400">[Quoted text hidden]</div>');
|
||||||
|
return { __html: htmlWithoutQuotes };
|
||||||
}
|
}
|
||||||
return processedContent.html;
|
return processedContent;
|
||||||
}, [processedContent.html, showQuotedText]);
|
}, [processedContent, showQuotedText]);
|
||||||
|
|
||||||
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: displayHTML }}
|
dangerouslySetInnerHTML={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> {content?.isHtml ? 'HTML' : 'Text'}</p>
|
<p><strong>Content Type:</strong> {typeof content === 'string' ? 'Text' : 'HTML'}</p>
|
||||||
<p><strong>Direction:</strong> {processedContent.direction}</p>
|
<p><strong>HTML Length:</strong> {typeof content === 'string' ? content.length : content?.html?.length || 0}</p>
|
||||||
<p><strong>HTML Length:</strong> {content?.html?.length || 0}</p>
|
<p><strong>Text Length:</strong> {typeof content === 'string' ? content.length : content?.text?.length || 0}</p>
|
||||||
<p><strong>Text Length:</strong> {content?.text?.length || 0}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import 'quill/dist/quill.snow.css';
|
import 'quill/dist/quill.snow.css';
|
||||||
import { sanitizeHtml } from '@/lib/utils/email-utils';
|
import { sanitizeHtml } from '@/lib/utils/dom-purify-config';
|
||||||
import { processContentWithDirection } from '@/lib/utils/text-direction';
|
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||||
|
import { processHtmlContent } from '@/lib/utils/email-content';
|
||||||
|
|
||||||
interface RichEmailEditorProps {
|
interface RichEmailEditorProps {
|
||||||
initialContent: string;
|
initialContent: string;
|
||||||
@ -84,20 +85,19 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
|||||||
theme: 'snow',
|
theme: 'snow',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process initial content to detect direction
|
|
||||||
const { direction, html: processedContent } = processContentWithDirection(initialContent);
|
|
||||||
|
|
||||||
// Set initial content properly
|
// Set initial content properly
|
||||||
if (initialContent) {
|
if (initialContent) {
|
||||||
try {
|
try {
|
||||||
console.log('Setting initial content in editor', {
|
console.log('Setting initial content in editor', {
|
||||||
length: initialContent.length,
|
length: initialContent.length,
|
||||||
startsWithHtml: initialContent.trim().startsWith('<'),
|
startsWithHtml: initialContent.trim().startsWith('<'),
|
||||||
direction
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Simplify complex email content to something Quill can handle better
|
// Detect text direction
|
||||||
const sanitizedContent = sanitizeHtml(processedContent || initialContent);
|
const direction = detectTextDirection(initialContent);
|
||||||
|
|
||||||
|
// Process HTML content using centralized utility
|
||||||
|
const sanitizedContent = processHtmlContent(initialContent);
|
||||||
|
|
||||||
// Check if sanitized content is valid
|
// Check if sanitized content is valid
|
||||||
if (sanitizedContent.trim().length === 0) {
|
if (sanitizedContent.trim().length === 0) {
|
||||||
@ -212,11 +212,11 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
|||||||
startsWithHtml: initialContent.trim().startsWith('<')
|
startsWithHtml: initialContent.trim().startsWith('<')
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process content to ensure correct direction
|
// Detect text direction
|
||||||
const { direction, html: processedContent } = processContentWithDirection(initialContent);
|
const direction = detectTextDirection(initialContent);
|
||||||
|
|
||||||
// Sanitize the HTML
|
// Process HTML content using centralized utility
|
||||||
const sanitizedContent = sanitizeHtml(processedContent || initialContent);
|
const sanitizedContent = processHtmlContent(initialContent);
|
||||||
|
|
||||||
// Check if content is valid HTML
|
// Check if content is valid HTML
|
||||||
if (sanitizedContent.trim().length === 0) {
|
if (sanitizedContent.trim().length === 0) {
|
||||||
|
|||||||
@ -1,11 +1,156 @@
|
|||||||
import DOMPurify from 'dompurify';
|
/**
|
||||||
import { detectTextDirection, applyTextDirection } from './text-direction';
|
* Centralized Email Content Utilities
|
||||||
import { sanitizeHtml } from './email-utils';
|
*
|
||||||
|
* This file contains all core functions for email content processing:
|
||||||
|
* - Content extraction
|
||||||
|
* - HTML sanitization
|
||||||
|
* - Text direction handling
|
||||||
|
* - URL fixing
|
||||||
|
*
|
||||||
|
* Other modules should import from this file rather than implementing their own versions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { sanitizeHtml } from './dom-purify-config';
|
||||||
|
import { detectTextDirection } from './text-direction';
|
||||||
|
import { EmailContent } from '@/types/email';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract content from various possible email formats
|
||||||
|
* Centralized implementation to reduce duplication across the codebase
|
||||||
|
*/
|
||||||
|
export function extractEmailContent(email: any): { text: string; html: string } {
|
||||||
|
// Default empty values
|
||||||
|
let textContent = '';
|
||||||
|
let htmlContent = '';
|
||||||
|
|
||||||
|
// Early exit if no email
|
||||||
|
if (!email) {
|
||||||
|
console.log('extractEmailContent: No email provided');
|
||||||
|
return { text: '', html: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract based on common formats
|
||||||
|
if (email.content && typeof email.content === 'object') {
|
||||||
|
// Standard format with content object
|
||||||
|
textContent = email.content.text || '';
|
||||||
|
htmlContent = email.content.html || '';
|
||||||
|
|
||||||
|
// Handle complex email formats where content might be nested
|
||||||
|
if (!textContent && !htmlContent) {
|
||||||
|
// Try to find content in deeper nested structure
|
||||||
|
if (email.content.body) {
|
||||||
|
if (typeof email.content.body === 'string') {
|
||||||
|
// Determine if body is HTML or text
|
||||||
|
if (isHtmlContent(email.content.body)) {
|
||||||
|
htmlContent = email.content.body;
|
||||||
|
} else {
|
||||||
|
textContent = email.content.body;
|
||||||
|
}
|
||||||
|
} else if (typeof email.content.body === 'object' && email.content.body) {
|
||||||
|
// Some email formats nest content inside body
|
||||||
|
htmlContent = email.content.body.html || '';
|
||||||
|
textContent = email.content.body.text || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for data property which some email services use
|
||||||
|
if (!textContent && !htmlContent && email.content.data) {
|
||||||
|
if (typeof email.content.data === 'string') {
|
||||||
|
// Check if data looks like HTML
|
||||||
|
if (isHtmlContent(email.content.data)) {
|
||||||
|
htmlContent = email.content.data;
|
||||||
|
} else {
|
||||||
|
textContent = email.content.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof email.content === 'string') {
|
||||||
|
// Check if content is likely HTML
|
||||||
|
if (isHtmlContent(email.content)) {
|
||||||
|
htmlContent = email.content;
|
||||||
|
} else {
|
||||||
|
textContent = email.content;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check other common properties
|
||||||
|
htmlContent = email.html || '';
|
||||||
|
textContent = email.text || '';
|
||||||
|
|
||||||
|
// If still no content, check for less common properties
|
||||||
|
if (!htmlContent && !textContent) {
|
||||||
|
// Try additional properties that some email clients use
|
||||||
|
htmlContent = email.body?.html || email.bodyHtml || email.htmlBody || '';
|
||||||
|
textContent = email.body?.text || email.bodyText || email.plainText || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error extracting email content:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we always have at least some text content
|
||||||
|
if (!textContent && htmlContent) {
|
||||||
|
textContent = extractTextFromHtml(htmlContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log extraction results
|
||||||
|
console.log('Extracted email content:', {
|
||||||
|
hasHtml: !!htmlContent,
|
||||||
|
htmlLength: htmlContent?.length || 0,
|
||||||
|
hasText: !!textContent,
|
||||||
|
textLength: textContent?.length || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
return { text: textContent, html: htmlContent };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract plain text from HTML content
|
||||||
|
*/
|
||||||
|
export function extractTextFromHtml(html: string): string {
|
||||||
|
if (!html) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use DOM API if available
|
||||||
|
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = html;
|
||||||
|
return tempDiv.textContent || tempDiv.innerText || '';
|
||||||
|
} else {
|
||||||
|
// Simple regex fallback for non-browser environments
|
||||||
|
return html.replace(/<[^>]*>/g, ' ')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error extracting text from HTML:', e);
|
||||||
|
// Fallback to basic strip
|
||||||
|
return html.replace(/<[^>]*>/g, ' ').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is likely HTML content
|
||||||
|
*/
|
||||||
|
export function isHtmlContent(content: string): boolean {
|
||||||
|
if (!content) return false;
|
||||||
|
|
||||||
|
return content.trim().startsWith('<') &&
|
||||||
|
(content.includes('<html') ||
|
||||||
|
content.includes('<body') ||
|
||||||
|
content.includes('<div') ||
|
||||||
|
content.includes('<p>') ||
|
||||||
|
content.includes('<br>'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format and standardize email content for display following email industry standards.
|
* Format and standardize email content for display following email industry standards.
|
||||||
* This function handles various email content formats and ensures proper display
|
* This is the main entry point for rendering email content.
|
||||||
* including support for HTML emails, plain text emails, RTL languages, and email client quirks.
|
|
||||||
*/
|
*/
|
||||||
export function formatEmailContent(email: any): string {
|
export function formatEmailContent(email: any): string {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
@ -14,76 +159,68 @@ export function formatEmailContent(email: any): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the content in order of preference with proper fallbacks
|
// Extract content from email
|
||||||
let content = '';
|
const { text, html } = extractEmailContent(email);
|
||||||
let isHtml = false;
|
|
||||||
let textContent = '';
|
|
||||||
|
|
||||||
// Extract content based on standardized property hierarchy
|
|
||||||
if (email.content && typeof email.content === 'object') {
|
|
||||||
isHtml = !!email.content.html;
|
|
||||||
content = email.content.html || '';
|
|
||||||
textContent = email.content.text || '';
|
|
||||||
} else if (typeof email.content === 'string') {
|
|
||||||
// Check if the string content is HTML
|
|
||||||
isHtml = email.content.trim().startsWith('<') &&
|
|
||||||
(email.content.includes('<html') ||
|
|
||||||
email.content.includes('<body') ||
|
|
||||||
email.content.includes('<div') ||
|
|
||||||
email.content.includes('<p>'));
|
|
||||||
content = email.content;
|
|
||||||
textContent = email.content;
|
|
||||||
} else if (email.html) {
|
|
||||||
isHtml = true;
|
|
||||||
content = email.html;
|
|
||||||
textContent = email.text || '';
|
|
||||||
} else if (email.text) {
|
|
||||||
isHtml = false;
|
|
||||||
content = '';
|
|
||||||
textContent = email.text;
|
|
||||||
} else if (email.formattedContent) {
|
|
||||||
// Assume formattedContent is already HTML
|
|
||||||
isHtml = true;
|
|
||||||
content = email.formattedContent;
|
|
||||||
textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log what we found for debugging
|
|
||||||
console.log(`Email content detected: isHtml=${isHtml}, contentLength=${content.length}, textLength=${textContent.length}`);
|
|
||||||
|
|
||||||
// If we have HTML content, sanitize and standardize it
|
// If we have HTML content, sanitize and standardize it
|
||||||
if (isHtml && content) {
|
if (html) {
|
||||||
// CRITICAL FIX: Check for browser environment since DOMParser is browser-only
|
// Process HTML content
|
||||||
const hasHtmlTag = content.includes('<html');
|
let processedHtml = processHtmlContent(html, text);
|
||||||
const hasBodyTag = content.includes('<body');
|
|
||||||
|
|
||||||
// Extract body content if we have a complete HTML document and in browser environment
|
// Apply styling
|
||||||
if (hasHtmlTag && hasBodyTag && typeof window !== 'undefined' && typeof DOMParser !== 'undefined') {
|
return `<div class="email-content" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-x: auto; overflow-wrap: break-word; word-wrap: break-word;" dir="${detectTextDirection(text)}">${processedHtml}</div>`;
|
||||||
try {
|
}
|
||||||
// Create a DOM parser to extract just the body content
|
// If we only have text content, format it properly
|
||||||
const parser = new DOMParser();
|
else if (text) {
|
||||||
const doc = parser.parseFromString(content, 'text/html');
|
return formatPlainTextToHtml(text);
|
||||||
const bodyContent = doc.body.innerHTML;
|
}
|
||||||
|
|
||||||
if (bodyContent) {
|
// Default case: empty or unrecognized content
|
||||||
content = bodyContent;
|
return '<div class="email-content-empty" style="padding: 20px; text-align: center; color: #666;">No content available</div>';
|
||||||
}
|
} catch (error) {
|
||||||
} catch (error) {
|
console.error('formatEmailContent: Error formatting email content:', error);
|
||||||
// If extraction fails, continue with the original content
|
return `<div class="email-content-error" style="padding: 15px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;"><p>Error displaying email content</p><p style="font-size: 12px; margin-top: 10px;">${error instanceof Error ? error.message : 'Unknown error'}</p></div>`;
|
||||||
console.error('Error extracting body content:', error);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
// Use the centralized sanitizeHtml function
|
* Process HTML content to fix common email rendering issues
|
||||||
let sanitizedContent = sanitizeHtml(content);
|
*/
|
||||||
|
export function processHtmlContent(htmlContent: string, textContent?: string): string {
|
||||||
// Fix URL encoding issues that might occur during sanitization
|
if (!htmlContent) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check for browser environment (DOMParser is browser-only)
|
||||||
|
const hasHtmlTag = htmlContent.includes('<html');
|
||||||
|
const hasBodyTag = htmlContent.includes('<body');
|
||||||
|
|
||||||
|
// Extract body content if we have a complete HTML document and in browser environment
|
||||||
|
if (hasHtmlTag && hasBodyTag && typeof window !== 'undefined' && typeof DOMParser !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
|
// Create a DOM parser to extract just the body content
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(htmlContent, 'text/html');
|
||||||
|
const bodyContent = doc.body.innerHTML;
|
||||||
|
|
||||||
|
if (bodyContent) {
|
||||||
|
htmlContent = bodyContent;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error extracting body content:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the centralized sanitizeHtml function
|
||||||
|
let sanitizedContent = sanitizeHtml(htmlContent);
|
||||||
|
|
||||||
|
// Fix URL encoding issues
|
||||||
|
try {
|
||||||
|
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||||
// Temporary element to manipulate the HTML
|
// Temporary element to manipulate the HTML
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = sanitizedContent;
|
tempDiv.innerHTML = sanitizedContent;
|
||||||
|
|
||||||
// Fix all links
|
// Fix all links that might have been double-encoded
|
||||||
const links = tempDiv.querySelectorAll('a');
|
const links = tempDiv.querySelectorAll('a');
|
||||||
links.forEach(link => {
|
links.forEach(link => {
|
||||||
const href = link.getAttribute('href');
|
const href = link.getAttribute('href');
|
||||||
@ -101,54 +238,50 @@ export function formatEmailContent(email: any): string {
|
|||||||
|
|
||||||
// Get the fixed HTML
|
// Get the fixed HTML
|
||||||
sanitizedContent = tempDiv.innerHTML;
|
sanitizedContent = tempDiv.innerHTML;
|
||||||
} catch (e) {
|
|
||||||
console.error('Error fixing URLs in content:', e);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
// Fix common email client quirks
|
console.error('Error fixing URLs in content:', e);
|
||||||
let fixedContent = sanitizedContent
|
|
||||||
// Fix for Outlook WebVML content
|
|
||||||
.replace(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
|
|
||||||
// Fix for broken image paths that might be relative
|
|
||||||
.replace(/(src|background)="(?!(?:https?:|data:|cid:))/gi, '$1="https://')
|
|
||||||
// Fix for base64 images that might be broken across lines
|
|
||||||
.replace(/src="data:image\/[^;]+;base64,\s*([^"]+)\s*"/gi, (match, p1) => {
|
|
||||||
return `src="data:image/png;base64,${p1.replace(/\s+/g, '')}"`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Use the centralized text direction utility
|
|
||||||
const styledContent = `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-x: auto; overflow-wrap: break-word; word-wrap: break-word;">${fixedContent}</div>`;
|
|
||||||
|
|
||||||
// Apply correct text direction using the centralized utility
|
|
||||||
return applyTextDirection(styledContent, textContent);
|
|
||||||
}
|
|
||||||
// If we only have text content, format it properly
|
|
||||||
else if (textContent) {
|
|
||||||
// Escape HTML characters to prevent XSS
|
|
||||||
const escapedText = textContent
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
|
|
||||||
// Format plain text with proper line breaks and paragraphs
|
|
||||||
const formattedText = escapedText
|
|
||||||
.replace(/\r\n|\r|\n/g, '<br>') // Convert all newlines to <br>
|
|
||||||
.replace(/((?:<br>){2,})/g, '</p><p>') // Convert multiple newlines to paragraphs
|
|
||||||
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
|
|
||||||
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
|
|
||||||
|
|
||||||
const styledPlainText = `<div style="font-family: -apple-system, BlinkMacSystemFont, Menlo, Monaco, Consolas, 'Courier New', monospace; white-space: pre-wrap; line-height: 1.5; color: #333; padding: 15px; max-width: 100%; overflow-wrap: break-word;"><p>${formattedText}</p></div>`;
|
|
||||||
|
|
||||||
// Apply correct text direction using the centralized utility
|
|
||||||
return applyTextDirection(styledPlainText, textContent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default case: empty or unrecognized content
|
// Fix common email client quirks
|
||||||
return '<div class="email-content-empty" style="padding: 20px; text-align: center; color: #666;">No content available</div>';
|
return sanitizedContent
|
||||||
|
// Fix for Outlook WebVML content
|
||||||
|
.replace(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
|
||||||
|
// Fix for broken image paths that might be relative
|
||||||
|
.replace(/(src|background)="(?!(?:https?:|data:|cid:))/gi, '$1="https://')
|
||||||
|
// Fix for base64 images that might be broken across lines
|
||||||
|
.replace(/src="data:image\/[^;]+;base64,\s*([^"]+)\s*"/gi, (match, p1) => {
|
||||||
|
return `src="data:image/png;base64,${p1.replace(/\s+/g, '')}"`;
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('formatEmailContent: Error formatting email content:', error);
|
console.error('Error processing HTML content:', error);
|
||||||
return `<div class="email-content-error" style="padding: 15px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;"><p>Error displaying email content</p><p style="font-size: 12px; margin-top: 10px;">${error instanceof Error ? error.message : 'Unknown error'}</p></div>`;
|
return htmlContent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format plain text to HTML with proper line breaks and styling
|
||||||
|
*/
|
||||||
|
export function formatPlainTextToHtml(text: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
// Detect text direction
|
||||||
|
const direction = detectTextDirection(text);
|
||||||
|
|
||||||
|
// Escape HTML characters to prevent XSS
|
||||||
|
const escapedText = text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
// Format plain text with proper line breaks and paragraphs
|
||||||
|
const formattedText = escapedText
|
||||||
|
.replace(/\r\n|\r|\n/g, '<br>') // Convert all newlines to <br>
|
||||||
|
.replace(/((?:<br>){2,})/g, '</p><p>') // Convert multiple newlines to paragraphs
|
||||||
|
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
|
||||||
|
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
|
||||||
|
|
||||||
|
return `<div class="email-content" style="font-family: -apple-system, BlinkMacSystemFont, Menlo, Monaco, Consolas, 'Courier New', monospace; white-space: pre-wrap; line-height: 1.5; color: #333; padding: 15px; max-width: 100%; overflow-wrap: break-word;" dir="${direction}"><p>${formattedText}</p></div>`;
|
||||||
}
|
}
|
||||||
@ -1,154 +0,0 @@
|
|||||||
/**
|
|
||||||
* DEPRECATED - USE email-utils.ts INSTEAD
|
|
||||||
*
|
|
||||||
* This file is maintained for backward compatibility only.
|
|
||||||
* New code should import directly from email-utils.ts, which contains
|
|
||||||
* the canonical implementations of these functions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { sanitizeHtml } from './dom-purify-config';
|
|
||||||
import { formatEmailAddresses, formatEmailDate, formatReplyEmail, formatForwardedEmail, formatEmailForReplyOrForward } from './email-utils';
|
|
||||||
import { applyTextDirection } from './text-direction';
|
|
||||||
import type { EmailMessage, EmailAddress } from '@/types/email';
|
|
||||||
|
|
||||||
// Re-export the functions from email-utils for backward compatibility
|
|
||||||
export {
|
|
||||||
formatEmailAddresses,
|
|
||||||
formatEmailDate,
|
|
||||||
formatReplyEmail,
|
|
||||||
formatForwardedEmail,
|
|
||||||
formatEmailForReplyOrForward,
|
|
||||||
sanitizeHtml
|
|
||||||
};
|
|
||||||
|
|
||||||
// Re-export types for backward compatibility
|
|
||||||
export type { EmailAddress, EmailMessage };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format a date in a relative format
|
|
||||||
* Simple implementation for email display
|
|
||||||
*/
|
|
||||||
function formatDateRelative(date: Date): string {
|
|
||||||
if (!date) return '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
return date.toLocaleString('en-US', {
|
|
||||||
weekday: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return date.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse text and find URLs to turn into clickable links
|
|
||||||
* This is a utility function used only in this file
|
|
||||||
*/
|
|
||||||
function parseUrlsToLinks(text: string): string {
|
|
||||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
||||||
return text.replace(
|
|
||||||
urlRegex,
|
|
||||||
url => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert plain text email content to HTML with proper line breaks
|
|
||||||
* This is a utility function used only in this file
|
|
||||||
*/
|
|
||||||
function textToHtml(text: string): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// Escape HTML characters first
|
|
||||||
const escaped = text
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
|
|
||||||
// Convert line breaks and wrap in a div
|
|
||||||
const withLineBreaks = escaped.replace(/\n/g, '<br>');
|
|
||||||
|
|
||||||
// Parse URLs to make them clickable
|
|
||||||
const withLinks = parseUrlsToLinks(withLineBreaks);
|
|
||||||
|
|
||||||
return withLinks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode compose content from MIME format to HTML and text
|
|
||||||
*/
|
|
||||||
export async function decodeComposeContent(content: string): Promise<{
|
|
||||||
html: string | null;
|
|
||||||
text: string | null;
|
|
||||||
}> {
|
|
||||||
if (!content.trim()) {
|
|
||||||
return { html: null, text: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/parse-email', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email: content }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to parse email');
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = await response.json();
|
|
||||||
|
|
||||||
// Apply sanitization to the parsed content
|
|
||||||
return {
|
|
||||||
html: parsed.html ? sanitizeHtml(parsed.html) : null,
|
|
||||||
text: parsed.text || null
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing email content:', error);
|
|
||||||
// Fallback to basic content handling with sanitization
|
|
||||||
return {
|
|
||||||
html: sanitizeHtml(content),
|
|
||||||
text: content
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode compose content to MIME format for sending
|
|
||||||
*/
|
|
||||||
export function encodeComposeContent(content: string): string {
|
|
||||||
if (!content.trim()) {
|
|
||||||
throw new Error('Email content is empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create MIME headers
|
|
||||||
const mimeHeaders = {
|
|
||||||
'MIME-Version': '1.0',
|
|
||||||
'Content-Type': 'text/html; charset="utf-8"',
|
|
||||||
'Content-Transfer-Encoding': 'quoted-printable'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Combine headers and content
|
|
||||||
return Object.entries(mimeHeaders)
|
|
||||||
.map(([key, value]) => `${key}: ${value}`)
|
|
||||||
.join('\n') + '\n\n' + content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to get the reply subject line
|
|
||||||
export function getReplySubject(subject: string): string {
|
|
||||||
return subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to get the forward subject line
|
|
||||||
export function getForwardSubject(subject: string): string {
|
|
||||||
return subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`;
|
|
||||||
}
|
|
||||||
@ -1,14 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* Unified Email Utilities
|
* Unified Email Utilities
|
||||||
*
|
*
|
||||||
* This file contains all email-related utility functions:
|
* This file provides backward compatibility for email utilities.
|
||||||
* - Content normalization
|
* New code should import directly from the specialized modules:
|
||||||
* - Email formatting (replies, forwards)
|
* - email-content.ts (content processing)
|
||||||
* - Text direction handling
|
* - text-direction.ts (direction handling)
|
||||||
|
* - dom-purify-config.ts (sanitization)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Import from centralized configuration
|
// Import from specialized modules
|
||||||
import { sanitizeHtml } from './dom-purify-config';
|
import { sanitizeHtml } from './dom-purify-config';
|
||||||
|
import { detectTextDirection, applyTextDirection } from './text-direction';
|
||||||
|
import {
|
||||||
|
extractEmailContent,
|
||||||
|
formatEmailContent,
|
||||||
|
processHtmlContent,
|
||||||
|
formatPlainTextToHtml,
|
||||||
|
isHtmlContent,
|
||||||
|
extractTextFromHtml
|
||||||
|
} from './email-content';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EmailMessage,
|
EmailMessage,
|
||||||
EmailContent,
|
EmailContent,
|
||||||
@ -17,16 +28,18 @@ 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,
|
|
||||||
extractEmailContent,
|
|
||||||
processContentWithDirection
|
|
||||||
} from '@/lib/utils/text-direction';
|
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
// Export the sanitizeHtml function from the centralized config
|
// Re-export important functions for backward compatibility
|
||||||
export { sanitizeHtml };
|
export {
|
||||||
|
sanitizeHtml,
|
||||||
|
extractEmailContent,
|
||||||
|
formatEmailContent,
|
||||||
|
processHtmlContent,
|
||||||
|
formatPlainTextToHtml,
|
||||||
|
detectTextDirection,
|
||||||
|
applyTextDirection
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard interface for formatted email responses
|
* Standard interface for formatted email responses
|
||||||
@ -88,28 +101,6 @@ export function formatEmailDate(date: Date | string | undefined): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Format plain text for HTML display with proper line breaks
|
|
||||||
*/
|
|
||||||
export function formatPlainTextToHtml(text: string | null | undefined): string {
|
|
||||||
if (!text) return '';
|
|
||||||
|
|
||||||
// Escape HTML characters to prevent XSS
|
|
||||||
const escapedText = text
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
|
|
||||||
// Format plain text with proper line breaks and paragraphs
|
|
||||||
return escapedText
|
|
||||||
.replace(/\r\n|\r|\n/g, '<br>') // Convert all newlines to <br>
|
|
||||||
.replace(/((?:<br>){2,})/g, '</p><p>') // Convert multiple newlines to paragraphs
|
|
||||||
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
|
|
||||||
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize email content to our standard format regardless of input format
|
* Normalize email content to our standard format regardless of input format
|
||||||
*/
|
*/
|
||||||
@ -158,11 +149,11 @@ export function renderEmailContent(content: EmailContent | null): string {
|
|||||||
return '<div class="email-content-empty">No content available</div>';
|
return '<div class="email-content-empty">No content available</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the centralized content processing function
|
// Create a simple object that can be processed by formatEmailContent
|
||||||
const processed = processContentWithDirection(content);
|
const emailObj = { content };
|
||||||
|
|
||||||
// Return the processed HTML with proper direction
|
// Use the centralized formatting function
|
||||||
return processed.html || '<div class="email-content-empty">No content available</div>';
|
return formatEmailContent(emailObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -257,71 +248,6 @@ function getFormattedHeaderInfo(email: any): {
|
|||||||
return { fromStr, toStr, ccStr, dateStr, subject };
|
return { fromStr, toStr, ccStr, dateStr, subject };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract image attachments from HTML content
|
|
||||||
*/
|
|
||||||
function extractInlineImages(htmlContent: string): Array<{
|
|
||||||
filename: string;
|
|
||||||
contentType: string;
|
|
||||||
content?: string;
|
|
||||||
}> {
|
|
||||||
const images: Array<{
|
|
||||||
filename: string;
|
|
||||||
contentType: string;
|
|
||||||
content?: string;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!htmlContent || typeof window === 'undefined') return images;
|
|
||||||
|
|
||||||
// Create a temporary DOM element to parse the HTML
|
|
||||||
const tempDiv = document.createElement('div');
|
|
||||||
tempDiv.innerHTML = htmlContent;
|
|
||||||
|
|
||||||
// Find all image elements
|
|
||||||
const imgElements = tempDiv.querySelectorAll('img');
|
|
||||||
|
|
||||||
// Process each image
|
|
||||||
imgElements.forEach((img, index) => {
|
|
||||||
const src = img.getAttribute('src');
|
|
||||||
if (!src) return;
|
|
||||||
|
|
||||||
// Only process data URLs and non-tracking pixels
|
|
||||||
if (src.startsWith('data:image')) {
|
|
||||||
const contentType = src.split(',')[0].split(':')[1].split(';')[0];
|
|
||||||
const imageData = src.split(',')[1];
|
|
||||||
|
|
||||||
// Skip tiny images (likely tracking pixels)
|
|
||||||
if (imageData.length < 100) return;
|
|
||||||
|
|
||||||
images.push({
|
|
||||||
filename: `inline-image-${index + 1}.${contentType.split('/')[1] || 'png'}`,
|
|
||||||
contentType,
|
|
||||||
content: imageData
|
|
||||||
});
|
|
||||||
|
|
||||||
// Replace the image source with a placeholder
|
|
||||||
img.setAttribute('src', `cid:inline-image-${index + 1}`);
|
|
||||||
}
|
|
||||||
else if (src.startsWith('cid:')) {
|
|
||||||
// Already a CID reference, just add a placeholder
|
|
||||||
const cid = src.substring(4);
|
|
||||||
images.push({
|
|
||||||
filename: `${cid}.png`,
|
|
||||||
contentType: 'image/png'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the HTML content to use the placeholders
|
|
||||||
htmlContent = tempDiv.innerHTML;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error extracting inline images:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return images;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format email for reply
|
* Format email for reply
|
||||||
*/
|
*/
|
||||||
@ -349,15 +275,9 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
|
|||||||
// Extract just the text content for a clean reply
|
// Extract just the text content for a clean reply
|
||||||
let emailText = '';
|
let emailText = '';
|
||||||
|
|
||||||
// Try to get text directly from content.text first
|
// Extract content using the centralized extraction function
|
||||||
if (originalEmail.content && typeof originalEmail.content === 'object' && originalEmail.content.text) {
|
const { text, html } = extractEmailContent(originalEmail);
|
||||||
emailText = originalEmail.content.text;
|
emailText = text;
|
||||||
}
|
|
||||||
// Otherwise, fall back to extractEmailContent which tries various formats
|
|
||||||
else {
|
|
||||||
const { text } = extractEmailContent(originalEmail);
|
|
||||||
emailText = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create simple reply with header
|
// Create simple reply with header
|
||||||
const cleanReplyHeader = `<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #eee; padding-bottom: 10px;">On ${dateStr}, ${fromStr} wrote:</div>`;
|
const cleanReplyHeader = `<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #eee; padding-bottom: 10px;">On ${dateStr}, ${fromStr} wrote:</div>`;
|
||||||
@ -417,15 +337,9 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
|
|||||||
// Extract just the text content for a clean forward
|
// Extract just the text content for a clean forward
|
||||||
let emailText = '';
|
let emailText = '';
|
||||||
|
|
||||||
// Try to get text directly from content.text first
|
// Extract content using the centralized extraction function
|
||||||
if (originalEmail.content && typeof originalEmail.content === 'object' && originalEmail.content.text) {
|
const { text, html } = extractEmailContent(originalEmail);
|
||||||
emailText = originalEmail.content.text;
|
emailText = text;
|
||||||
}
|
|
||||||
// Otherwise, fall back to extractEmailContent which tries various formats
|
|
||||||
else {
|
|
||||||
const { text } = extractEmailContent(originalEmail);
|
|
||||||
emailText = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create simple forward with metadata header
|
// Create simple forward with metadata header
|
||||||
const cleanForwardHeader = `
|
const cleanForwardHeader = `
|
||||||
|
|||||||
@ -1,13 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Text Direction Utilities
|
* Text Direction Utilities
|
||||||
*
|
*
|
||||||
* Centralized utilities for handling text direction (RTL/LTR)
|
* Core utilities for handling text direction (RTL/LTR)
|
||||||
* to ensure consistent behavior across the application.
|
* 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
|
||||||
@ -64,278 +61,4 @@ 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) {
|
|
||||||
// Standard format with content object
|
|
||||||
textContent = email.content.text || '';
|
|
||||||
htmlContent = email.content.html || '';
|
|
||||||
|
|
||||||
// Handle complex email formats where content might be nested
|
|
||||||
if (!textContent && !htmlContent) {
|
|
||||||
// Try to find content in deeper nested structure
|
|
||||||
if (email.content.body) {
|
|
||||||
if (typeof email.content.body === 'string') {
|
|
||||||
// Determine if body is HTML or text
|
|
||||||
if (email.content.body.includes('<') && (
|
|
||||||
email.content.body.includes('<html') ||
|
|
||||||
email.content.body.includes('<body') ||
|
|
||||||
email.content.body.includes('<div')
|
|
||||||
)) {
|
|
||||||
htmlContent = email.content.body;
|
|
||||||
} else {
|
|
||||||
textContent = email.content.body;
|
|
||||||
}
|
|
||||||
} else if (typeof email.content.body === 'object' && email.content.body) {
|
|
||||||
// Some email formats nest content inside body
|
|
||||||
htmlContent = email.content.body.html || '';
|
|
||||||
textContent = email.content.body.text || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for data property which some email services use
|
|
||||||
if (!textContent && !htmlContent && email.content.data) {
|
|
||||||
if (typeof email.content.data === 'string') {
|
|
||||||
// Check if data looks like HTML
|
|
||||||
if (email.content.data.includes('<') && (
|
|
||||||
email.content.data.includes('<html') ||
|
|
||||||
email.content.data.includes('<body') ||
|
|
||||||
email.content.data.includes('<div')
|
|
||||||
)) {
|
|
||||||
htmlContent = email.content.data;
|
|
||||||
} else {
|
|
||||||
textContent = email.content.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort: try to convert the entire content object to string
|
|
||||||
if (!textContent && !htmlContent) {
|
|
||||||
try {
|
|
||||||
// Some email servers encode content as JSON string
|
|
||||||
const contentStr = JSON.stringify(email.content);
|
|
||||||
if (contentStr && contentStr !== '{}') {
|
|
||||||
textContent = `[Complex email content - please view in original format]`;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error extracting content from complex object:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (typeof email.content === 'string') {
|
|
||||||
// Check if content is likely HTML
|
|
||||||
if (email.content.includes('<') && (
|
|
||||||
email.content.includes('<html') ||
|
|
||||||
email.content.includes('<body') ||
|
|
||||||
email.content.includes('<div')
|
|
||||||
)) {
|
|
||||||
htmlContent = email.content;
|
|
||||||
} else {
|
|
||||||
textContent = email.content;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check other common properties
|
|
||||||
htmlContent = email.html || '';
|
|
||||||
textContent = email.text || '';
|
|
||||||
|
|
||||||
// If still no content, check for less common properties
|
|
||||||
if (!htmlContent && !textContent) {
|
|
||||||
// Try additional properties that some email clients use
|
|
||||||
htmlContent = email.body?.html || email.bodyHtml || email.htmlBody || '';
|
|
||||||
textContent = email.body?.text || email.bodyText || email.plainText || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure we always have at least some text content
|
|
||||||
if (!textContent && htmlContent) {
|
|
||||||
try {
|
|
||||||
// Create a helper function to extract text from HTML
|
|
||||||
const tempDiv = document.createElement('div');
|
|
||||||
tempDiv.innerHTML = htmlContent;
|
|
||||||
textContent = tempDiv.textContent || tempDiv.innerText || '';
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback for non-browser environments or if extraction fails
|
|
||||||
textContent = htmlContent.replace(/<[^>]*>/g, ' ')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim() || '[Email content]';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add debug logging to help troubleshoot content extraction
|
|
||||||
console.log('Extracted email content:', {
|
|
||||||
hasHtml: !!htmlContent,
|
|
||||||
htmlLength: htmlContent.length,
|
|
||||||
hasText: !!textContent,
|
|
||||||
textLength: textContent.length
|
|
||||||
});
|
|
||||||
|
|
||||||
return { text: textContent, html: htmlContent };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comprehensive utility that processes email content:
|
|
||||||
* - Sanitizes HTML content
|
|
||||||
* - Detects text direction
|
|
||||||
* - Applies direction attributes
|
|
||||||
*
|
|
||||||
* This reduces redundancy by combining these steps into one centralized function
|
|
||||||
*/
|
|
||||||
export function processContentWithDirection(content: string | EmailContent | null | undefined): {
|
|
||||||
html: string;
|
|
||||||
text: string;
|
|
||||||
direction: 'ltr' | 'rtl';
|
|
||||||
} {
|
|
||||||
// Default result with fallbacks
|
|
||||||
const result = {
|
|
||||||
html: '',
|
|
||||||
text: '',
|
|
||||||
direction: 'ltr' as const
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle null/undefined cases
|
|
||||||
if (!content) return result;
|
|
||||||
|
|
||||||
// Extract text and HTML content based on input type
|
|
||||||
let textContent = '';
|
|
||||||
let htmlContent = '';
|
|
||||||
|
|
||||||
if (typeof content === 'string') {
|
|
||||||
// Simple string content (check if it's HTML or plain text)
|
|
||||||
if (content.includes('<') && (
|
|
||||||
content.includes('<html') ||
|
|
||||||
content.includes('<body') ||
|
|
||||||
content.includes('<div') ||
|
|
||||||
content.includes('<p>')
|
|
||||||
)) {
|
|
||||||
htmlContent = content;
|
|
||||||
} else {
|
|
||||||
textContent = content;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// EmailContent object
|
|
||||||
textContent = content.text || '';
|
|
||||||
htmlContent = content.html || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle complex email content that might not be properly detected
|
|
||||||
if (!textContent && !htmlContent && typeof content === 'object') {
|
|
||||||
console.log('Processing complex content object:', content);
|
|
||||||
|
|
||||||
// Try to extract content from complex object structure
|
|
||||||
try {
|
|
||||||
// Check for common email content formats
|
|
||||||
// Type assertion to 'any' since we need to handle various email formats
|
|
||||||
const contentAny = content as any;
|
|
||||||
|
|
||||||
if (contentAny.body) {
|
|
||||||
if (typeof contentAny.body === 'string') {
|
|
||||||
// Detect if body is HTML or text
|
|
||||||
if (contentAny.body.includes('<') && (
|
|
||||||
contentAny.body.includes('<html') ||
|
|
||||||
contentAny.body.includes('<body') ||
|
|
||||||
contentAny.body.includes('<div')
|
|
||||||
)) {
|
|
||||||
htmlContent = contentAny.body;
|
|
||||||
} else {
|
|
||||||
textContent = contentAny.body;
|
|
||||||
}
|
|
||||||
} else if (typeof contentAny.body === 'object' && contentAny.body) {
|
|
||||||
// Extract from nested body object
|
|
||||||
htmlContent = contentAny.body.html || '';
|
|
||||||
textContent = contentAny.body.text || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to convert complex content to string for debugging
|
|
||||||
if (!textContent && !htmlContent) {
|
|
||||||
try {
|
|
||||||
const contentStr = JSON.stringify(content);
|
|
||||||
console.log('Complex content structure:', contentStr.slice(0, 300) + '...');
|
|
||||||
textContent = '[Complex email content]';
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to stringify complex content:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error processing complex content:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always ensure we have text for direction detection
|
|
||||||
if (!textContent && htmlContent) {
|
|
||||||
// Extract text from HTML for direction detection
|
|
||||||
try {
|
|
||||||
// Use DOM API if available
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
const tempDiv = document.createElement('div');
|
|
||||||
tempDiv.innerHTML = htmlContent;
|
|
||||||
textContent = tempDiv.textContent || tempDiv.innerText || '';
|
|
||||||
} else {
|
|
||||||
// Simple regex fallback for non-browser environments
|
|
||||||
textContent = htmlContent.replace(/<[^>]*>/g, ' ')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error extracting text from HTML:', e);
|
|
||||||
textContent = 'Failed to extract text content';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect direction from text
|
|
||||||
const direction = detectTextDirection(textContent);
|
|
||||||
|
|
||||||
// Sanitize HTML if present
|
|
||||||
if (htmlContent) {
|
|
||||||
try {
|
|
||||||
// Sanitize HTML first using the centralized function
|
|
||||||
htmlContent = sanitizeHtml(htmlContent);
|
|
||||||
|
|
||||||
// Then apply direction
|
|
||||||
htmlContent = applyTextDirection(htmlContent, textContent);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error sanitizing HTML content:', error);
|
|
||||||
// Create fallback content if sanitization fails
|
|
||||||
htmlContent = `<div dir="${direction}">${
|
|
||||||
textContent ?
|
|
||||||
textContent.replace(/\n/g, '<br>') :
|
|
||||||
'Could not process HTML content'
|
|
||||||
}</div>`;
|
|
||||||
}
|
|
||||||
} else if (textContent) {
|
|
||||||
// Convert plain text to HTML with proper direction
|
|
||||||
htmlContent = `<div dir="${direction}">${textContent.replace(/\n/g, '<br>')}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add debug logging for troubleshooting
|
|
||||||
console.log('Processed content:', {
|
|
||||||
direction,
|
|
||||||
htmlLength: htmlContent.length,
|
|
||||||
textLength: textContent.length,
|
|
||||||
hasHtml: !!htmlContent,
|
|
||||||
hasText: !!textContent
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return processed content
|
|
||||||
return {
|
|
||||||
text: textContent,
|
|
||||||
html: htmlContent,
|
|
||||||
direction
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user