154 lines
6.8 KiB
TypeScript
154 lines
6.8 KiB
TypeScript
import DOMPurify from 'dompurify';
|
|
import { detectTextDirection, applyTextDirection } from './text-direction';
|
|
import { sanitizeHtml } from './email-utils';
|
|
|
|
/**
|
|
* Format and standardize email content for display following email industry standards.
|
|
* This function handles various email content formats and ensures proper display
|
|
* including support for HTML emails, plain text emails, RTL languages, and email client quirks.
|
|
*/
|
|
export function formatEmailContent(email: any): string {
|
|
if (!email) {
|
|
console.log('formatEmailContent: No email provided');
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
// Get the content in order of preference with proper fallbacks
|
|
let content = '';
|
|
let isHtml = false;
|
|
let textContent = '';
|
|
|
|
// Extract content based on standardized property hierarchy
|
|
if (email.content && typeof email.content === 'object') {
|
|
isHtml = !!email.content.html;
|
|
content = email.content.html || '';
|
|
textContent = email.content.text || '';
|
|
} else if (typeof email.content === 'string') {
|
|
// Check if the string content is HTML
|
|
isHtml = email.content.trim().startsWith('<') &&
|
|
(email.content.includes('<html') ||
|
|
email.content.includes('<body') ||
|
|
email.content.includes('<div') ||
|
|
email.content.includes('<p>'));
|
|
content = email.content;
|
|
textContent = email.content;
|
|
} else if (email.html) {
|
|
isHtml = true;
|
|
content = email.html;
|
|
textContent = email.text || '';
|
|
} else if (email.text) {
|
|
isHtml = false;
|
|
content = '';
|
|
textContent = email.text;
|
|
} else if (email.formattedContent) {
|
|
// Assume formattedContent is already HTML
|
|
isHtml = true;
|
|
content = email.formattedContent;
|
|
textContent = '';
|
|
}
|
|
|
|
// Log what we found for debugging
|
|
console.log(`Email content detected: isHtml=${isHtml}, contentLength=${content.length}, textLength=${textContent.length}`);
|
|
|
|
// If we have HTML content, sanitize and standardize it
|
|
if (isHtml && content) {
|
|
// CRITICAL FIX: Check for browser environment since DOMParser is browser-only
|
|
const hasHtmlTag = content.includes('<html');
|
|
const hasBodyTag = content.includes('<body');
|
|
|
|
// Extract body content if we have a complete HTML document and in browser environment
|
|
if (hasHtmlTag && hasBodyTag && typeof window !== 'undefined' && typeof DOMParser !== 'undefined') {
|
|
try {
|
|
// Create a DOM parser to extract just the body content
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(content, 'text/html');
|
|
const bodyContent = doc.body.innerHTML;
|
|
|
|
if (bodyContent) {
|
|
content = bodyContent;
|
|
}
|
|
} catch (error) {
|
|
// If extraction fails, continue with the original content
|
|
console.error('Error extracting body content:', error);
|
|
}
|
|
}
|
|
|
|
// Use the centralized sanitizeHtml function
|
|
let sanitizedContent = sanitizeHtml(content);
|
|
|
|
// Fix URL encoding issues that might occur during sanitization
|
|
try {
|
|
// Temporary element to manipulate the HTML
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = sanitizedContent;
|
|
|
|
// Fix all links
|
|
const links = tempDiv.querySelectorAll('a');
|
|
links.forEach(link => {
|
|
const href = link.getAttribute('href');
|
|
if (href && href.includes('%')) {
|
|
try {
|
|
// Try to decode URLs that might have been double-encoded
|
|
const decodedHref = decodeURIComponent(href);
|
|
link.setAttribute('href', decodedHref);
|
|
} catch (e) {
|
|
// If decoding fails, keep the original
|
|
console.warn('Failed to decode href:', href);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Get the fixed HTML
|
|
sanitizedContent = tempDiv.innerHTML;
|
|
} catch (e) {
|
|
console.error('Error fixing URLs in content:', e);
|
|
}
|
|
|
|
// Fix common email client quirks
|
|
let fixedContent = sanitizedContent
|
|
// Fix for Outlook WebVML content
|
|
.replace(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
|
|
// Fix for broken image paths that might be relative
|
|
.replace(/(src|background)="(?!(?:https?:|data:|cid:))/gi, '$1="https://')
|
|
// Fix for base64 images that might be broken across lines
|
|
.replace(/src="data:image\/[^;]+;base64,\s*([^"]+)\s*"/gi, (match, p1) => {
|
|
return `src="data:image/png;base64,${p1.replace(/\s+/g, '')}"`;
|
|
});
|
|
|
|
// Use the centralized text direction utility
|
|
const styledContent = `<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-x: auto; overflow-wrap: break-word; word-wrap: break-word;">${fixedContent}</div>`;
|
|
|
|
// Apply correct text direction using the centralized utility
|
|
return applyTextDirection(styledContent, textContent);
|
|
}
|
|
// If we only have text content, format it properly
|
|
else if (textContent) {
|
|
// Escape HTML characters to prevent XSS
|
|
const escapedText = textContent
|
|
.replace(/&/g, '&')
|
|
.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
|
|
return '<div class="email-content-empty" style="padding: 20px; text-align: center; color: #666;">No content available</div>';
|
|
} catch (error) {
|
|
console.error('formatEmailContent: Error formatting email content:', error);
|
|
return `<div class="email-content-error" style="padding: 15px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;"><p>Error displaying email content</p><p style="font-size: 12px; margin-top: 10px;">${error instanceof Error ? error.message : 'Unknown error'}</p></div>`;
|
|
}
|
|
}
|