Neah/lib/utils/email-content.ts
2025-04-30 20:32:24 +02:00

172 lines
7.7 KiB
TypeScript

import DOMPurify from 'dompurify';
/**
* 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) {
// Make sure we have a complete HTML structure
const hasHtmlTag = content.includes('<html');
const hasBodyTag = content.includes('<body');
// Extract body content if we have a complete HTML document
if (hasHtmlTag && hasBodyTag) {
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);
}
}
// Sanitize with industry-standard email tags and attributes
// Use a more permissive configuration for email HTML
const sanitizedContent = DOMPurify.sanitize(content, {
ADD_TAGS: [
'style', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
'caption', 'col', 'colgroup', 'div', 'span', 'img', 'br', 'hr',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre',
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a', 'b', 'i', 'u', 'em',
'strong', 'del', 'ins', 'sub', 'sup', 'small', 'mark', 'q',
'section', 'article', 'header', 'footer', 'aside', 'nav', 'figure',
'figcaption', 'address', 'main', 'center', 'font'
],
ADD_ATTR: [
'class', 'style', 'id', 'href', 'src', 'alt', 'title', 'width', 'height',
'border', 'cellspacing', 'cellpadding', 'bgcolor', 'color', 'dir', 'lang',
'align', 'valign', 'span', 'colspan', 'rowspan', 'target', 'rel',
'background', 'data-*', 'face', 'size', 'bgcolor', 'hspace', 'vspace',
'marginheight', 'marginwidth', 'frameborder'
],
ALLOW_DATA_ATTR: true,
WHOLE_DOCUMENT: false,
RETURN_DOM: false,
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout']
});
// 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)="(?!http|https|data|cid)/gi, '$1="https://');
// Fix for inline image references (CID)
if (fixedContent.includes('cid:')) {
console.log('Email contains CID references - these cannot be displayed properly in the web UI');
// We can't actually render CID references in the web UI
// Just log a message for now - a more comprehensive fix would involve
// extracting and converting these images
}
// Check for RTL content and set appropriate direction
const rtlLangPattern = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
const containsRtlText = rtlLangPattern.test(textContent);
const dirAttribute = containsRtlText ? 'dir="rtl"' : 'dir="ltr"';
// Wrap the content in standard email container with responsive styling
return `
<div class="email-content-wrapper" style="max-width: 100%; overflow-x: auto;">
<div class="email-content" ${dirAttribute} style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word;">
${fixedContent}
</div>
</div>
`;
}
// If we only have text content, format it properly
else if (textContent) {
// Check for RTL content and set appropriate direction
const rtlLangPattern = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
const containsRtlText = rtlLangPattern.test(textContent);
const dirAttribute = containsRtlText ? 'dir="rtl"' : 'dir="ltr"';
// Escape HTML characters to prevent XSS
const escapedText = textContent
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// 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-wrapper" style="max-width: 100%; overflow-x: auto;">
<div class="email-content plain-text" ${dirAttribute} style="font-family: -apple-system, BlinkMacSystemFont, Menlo, Monaco, Consolas, 'Courier New', monospace; white-space: pre-wrap; line-height: 1.5; color: #333; padding: 15px; background-color: #f8f9fa; border-radius: 4px; max-width: 100%; overflow-wrap: break-word; word-wrap: break-word;">
<p>${formattedText}</p>
</div>
</div>
`;
}
// Default case: empty or unrecognized content
return '<div class="email-content-empty">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>
`;
}
}