courrier preview
This commit is contained in:
parent
fbfababb22
commit
3420c5be5c
@ -136,64 +136,6 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Safety timeout to prevent endless loading
|
||||
const safetyTimeoutId = setTimeout(() => {
|
||||
const contentState = emailContent;
|
||||
if (!contentState || contentState === "") {
|
||||
console.warn('Email content initialization timed out after 5 seconds, using fallback template');
|
||||
// Create a basic fallback template
|
||||
const { fromStr, dateStr } = getFormattedInfoForEmail(initialEmail);
|
||||
|
||||
// Use a different template based on email type
|
||||
let fallbackContent = '';
|
||||
if (type === 'forward') {
|
||||
fallbackContent = `
|
||||
<div style="margin: 20px 0 10px 0; color: #666; font-family: Arial, sans-serif;">
|
||||
<div style="border-bottom: 1px solid #ccc; margin-bottom: 10px; padding-bottom: 5px;">
|
||||
<div>---------------------------- Forwarded Message ----------------------------</div>
|
||||
</div>
|
||||
<table style="margin-bottom: 10px; font-size: 14px;">
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">From:</td>
|
||||
<td style="padding: 3px 0;">${fromStr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Date:</td>
|
||||
<td style="padding: 3px 0;">${dateStr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Subject:</td>
|
||||
<td style="padding: 3px 0;">${initialEmail.subject || ''}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="border-bottom: 1px solid #ccc; margin-top: 5px; margin-bottom: 15px; padding-bottom: 5px;">
|
||||
<div>----------------------------------------------------------------------</div>
|
||||
</div>
|
||||
<div>[Could not load original content. Please use plain text or start a new message.]</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// For replies
|
||||
fallbackContent = `
|
||||
<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
|
||||
On ${dateStr}, ${fromStr} wrote:
|
||||
</div>
|
||||
<blockquote style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">
|
||||
[Could not load original content. Please use plain text or start a new message.]
|
||||
</blockquote>
|
||||
`;
|
||||
}
|
||||
|
||||
setEmailContent(fallbackContent);
|
||||
|
||||
// Also update the Quill editor directly if possible
|
||||
const editorElement = document.querySelector('.ql-editor');
|
||||
if (editorElement instanceof HTMLElement) {
|
||||
editorElement.innerHTML = fallbackContent;
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Get recipients based on type
|
||||
if (type === 'reply' || type === 'reply-all') {
|
||||
// Get formatted data for reply
|
||||
@ -316,9 +258,6 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
||||
setAttachments(formattedAttachments);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the safety timeout if we complete successfully
|
||||
return () => clearTimeout(safetyTimeoutId);
|
||||
} catch (error) {
|
||||
console.error('Error initializing compose form:', error);
|
||||
// Provide a fallback in case of error
|
||||
|
||||
@ -45,35 +45,11 @@ export default function EmailDetailView({
|
||||
// Render email content based on the email body
|
||||
const renderEmailContent = () => {
|
||||
try {
|
||||
// Enhanced debugging to trace exactly what's in the content
|
||||
console.log('EmailDetailView renderEmailContent - DETAILED DEBUG', {
|
||||
emailId: email.id,
|
||||
subject: email.subject,
|
||||
console.log('EmailDetailView renderEmailContent', {
|
||||
hasContent: !!email.content,
|
||||
contentType: typeof email.content,
|
||||
contentKeys: email.content && typeof email.content === 'object' ? Object.keys(email.content) : [],
|
||||
contentStringLength: typeof email.content === 'string' ? email.content.length : 'N/A',
|
||||
contentHtmlLength: email.content && typeof email.content === 'object' && 'html' in email.content && typeof (email.content as any).html === 'string'
|
||||
? ((email.content as any).html as string).length
|
||||
: 0,
|
||||
contentTextLength: email.content && typeof email.content === 'object' && 'text' in email.content && typeof (email.content as any).text === 'string'
|
||||
? ((email.content as any).text as string).length
|
||||
: 0,
|
||||
contentSample: typeof email.content === 'string'
|
||||
? email.content.substring(0, 100)
|
||||
: (email.content && typeof email.content === 'object' && 'html' in email.content && typeof (email.content as any).html === 'string'
|
||||
? ((email.content as any).html as string).substring(0, 100)
|
||||
: (email.content && typeof email.content === 'object' && 'text' in email.content && typeof (email.content as any).text === 'string'
|
||||
? ((email.content as any).text as string).substring(0, 100)
|
||||
: 'N/A')),
|
||||
hasHtml: !!email.html,
|
||||
htmlLength: email.html?.length || 0,
|
||||
htmlSample: email.html?.substring(0, 100) || 'N/A',
|
||||
hasText: !!email.text,
|
||||
textLength: email.text?.length || 0,
|
||||
textSample: email.text?.substring(0, 100) || 'N/A',
|
||||
contentIsNull: email.content === null,
|
||||
contentIsUndefined: email.content === undefined,
|
||||
hasText: !!email.text
|
||||
});
|
||||
|
||||
// Determine what content to use and how to handle it
|
||||
@ -83,29 +59,15 @@ export default function EmailDetailView({
|
||||
// If content is a string, use it directly
|
||||
if (typeof email.content === 'string') {
|
||||
contentToUse = email.content;
|
||||
console.log('Using email.content as string', contentToUse.substring(0, 50));
|
||||
}
|
||||
// If content is an object with html/text properties
|
||||
else if (typeof email.content === 'object') {
|
||||
const contentObj = email.content as {html?: string; text?: string};
|
||||
if (contentObj.html) {
|
||||
contentToUse = contentObj.html;
|
||||
console.log('Using email.content.html', contentToUse.substring(0, 50));
|
||||
} else if (contentObj.text) {
|
||||
// Convert plain text to HTML
|
||||
contentToUse = contentObj.text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
console.log('Using email.content.text (converted)', contentToUse.substring(0, 50));
|
||||
}
|
||||
contentToUse = email.content.html || email.content.text || '';
|
||||
}
|
||||
}
|
||||
// Fall back to html or text properties if content is not available
|
||||
else if (email.html) {
|
||||
contentToUse = email.html;
|
||||
console.log('Using fallback email.html', contentToUse.substring(0, 50));
|
||||
}
|
||||
else if (email.text) {
|
||||
// Convert plain text to HTML with line breaks
|
||||
@ -114,15 +76,6 @@ export default function EmailDetailView({
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
console.log('Using fallback email.text (converted)', contentToUse.substring(0, 50));
|
||||
}
|
||||
|
||||
// Log if no content was found
|
||||
if (!contentToUse) {
|
||||
console.error('No renderable content found in email!', {
|
||||
id: email.id,
|
||||
subject: email.subject
|
||||
});
|
||||
}
|
||||
|
||||
// Return content or fallback message
|
||||
|
||||
@ -5,8 +5,6 @@ import 'quill/dist/quill.snow.css';
|
||||
import { sanitizeHtml } from '@/lib/utils/dom-purify-config';
|
||||
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||
import { processHtmlContent } from '@/lib/utils/email-content';
|
||||
import Quill from 'quill';
|
||||
import QuillBetterTable from 'quill-better-table';
|
||||
|
||||
interface RichEmailEditorProps {
|
||||
initialContent: string;
|
||||
@ -15,175 +13,8 @@ interface RichEmailEditorProps {
|
||||
minHeight?: string;
|
||||
maxHeight?: string;
|
||||
preserveFormatting?: boolean;
|
||||
autofocus?: boolean;
|
||||
mode?: string;
|
||||
allowedFormats?: any;
|
||||
customKeyBindings?: any;
|
||||
}
|
||||
|
||||
// Register better table module
|
||||
function registerTableModule() {
|
||||
try {
|
||||
// Only attempt to register if the module exists
|
||||
if (typeof QuillBetterTable !== 'undefined') {
|
||||
Quill.register({
|
||||
'modules/better-table': QuillBetterTable
|
||||
}, true);
|
||||
return true;
|
||||
} else {
|
||||
console.warn('QuillBetterTable module is not available, skipping registration');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error registering table module:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up problematic table structures that cause issues with quill-better-table
|
||||
*/
|
||||
function cleanupTableStructures(htmlContent: string, isReplyOrForward: boolean = false): string {
|
||||
if (!htmlContent) return htmlContent;
|
||||
|
||||
try {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
|
||||
// Find tables with problematic nested structures
|
||||
const tables = tempDiv.querySelectorAll('table');
|
||||
|
||||
// Check if content looks like a forwarded email or reply content
|
||||
const isForwardedEmail =
|
||||
htmlContent.includes('---------- Forwarded message ----------') ||
|
||||
htmlContent.includes('Forwarded message') ||
|
||||
htmlContent.includes('forwarded message') ||
|
||||
(htmlContent.includes('From:') && htmlContent.includes('Date:') && htmlContent.includes('Subject:'));
|
||||
|
||||
const isReplyEmail =
|
||||
htmlContent.includes('wrote:') ||
|
||||
htmlContent.includes('<blockquote') ||
|
||||
htmlContent.includes('gmail_quote');
|
||||
|
||||
// For reply/forward content, force convert ALL tables to divs to avoid Quill errors
|
||||
const shouldConvertAllTables = isReplyOrForward || isForwardedEmail || isReplyEmail;
|
||||
|
||||
if (tables.length > 0) {
|
||||
console.log(`Found ${tables.length} tables in ${shouldConvertAllTables ? 'reply/forward' : 'regular'} content`);
|
||||
|
||||
let convertedCount = 0;
|
||||
tables.forEach(table => {
|
||||
// In reply/forward mode, convert ALL tables to divs to avoid quill-better-table issues
|
||||
if (shouldConvertAllTables) {
|
||||
const replacementDiv = document.createElement('div');
|
||||
replacementDiv.className = 'converted-table';
|
||||
replacementDiv.style.border = '1px solid #ddd';
|
||||
replacementDiv.style.margin = '10px 0';
|
||||
replacementDiv.style.padding = '10px';
|
||||
|
||||
// Preserve the original table structure visually
|
||||
// Create a simplified HTML representation of the table
|
||||
let tableHtml = '';
|
||||
|
||||
// Process each row
|
||||
const rows = table.querySelectorAll('tr');
|
||||
rows.forEach(row => {
|
||||
const rowDiv = document.createElement('div');
|
||||
rowDiv.style.display = 'flex';
|
||||
rowDiv.style.flexWrap = 'wrap';
|
||||
rowDiv.style.marginBottom = '5px';
|
||||
|
||||
// Process each cell in the row
|
||||
const cells = row.querySelectorAll('td, th');
|
||||
cells.forEach(cell => {
|
||||
const cellDiv = document.createElement('div');
|
||||
cellDiv.style.flex = '1';
|
||||
cellDiv.style.padding = '5px';
|
||||
cellDiv.style.borderBottom = '1px solid #eee';
|
||||
cellDiv.innerHTML = cell.innerHTML;
|
||||
rowDiv.appendChild(cellDiv);
|
||||
});
|
||||
|
||||
replacementDiv.appendChild(rowDiv);
|
||||
});
|
||||
|
||||
// If no rows were processed, just use the table's inner HTML
|
||||
if (rows.length === 0) {
|
||||
replacementDiv.innerHTML = table.innerHTML;
|
||||
}
|
||||
|
||||
// Replace the table with the div
|
||||
if (table.parentNode) {
|
||||
table.parentNode.replaceChild(replacementDiv, table);
|
||||
convertedCount++;
|
||||
}
|
||||
}
|
||||
// For regular content, just add width attributes to make quill-better-table happy
|
||||
else {
|
||||
// Skip simple tables that are likely to work fine with Quill
|
||||
// Check more conditions to identify simple tables
|
||||
const isSimpleTable =
|
||||
table.rows.length <= 3 &&
|
||||
table.querySelectorAll('td, th').length <= 6 &&
|
||||
!table.querySelector('table') && // No nested tables
|
||||
!table.innerHTML.includes('rowspan') && // No rowspan
|
||||
!table.innerHTML.includes('colspan'); // No colspan
|
||||
|
||||
if (isSimpleTable) {
|
||||
console.log('Preserving simple table structure');
|
||||
// Add width attribute
|
||||
table.setAttribute('width', '100%');
|
||||
|
||||
// Add width to cells
|
||||
const cells = table.querySelectorAll('td, th');
|
||||
cells.forEach(cell => {
|
||||
if (cell instanceof HTMLTableCellElement && !cell.hasAttribute('width')) {
|
||||
cell.setAttribute('width', '100');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Convert complex tables to divs
|
||||
const replacementDiv = document.createElement('div');
|
||||
replacementDiv.className = 'converted-table';
|
||||
replacementDiv.style.border = '1px solid #ddd';
|
||||
replacementDiv.style.margin = '10px 0';
|
||||
replacementDiv.style.padding = '10px';
|
||||
|
||||
// Copy the table's innerHTML
|
||||
replacementDiv.innerHTML = table.innerHTML;
|
||||
|
||||
// Replace the table with the div
|
||||
if (table.parentNode) {
|
||||
table.parentNode.replaceChild(replacementDiv, table);
|
||||
convertedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Converted ${convertedCount} tables to divs to prevent Quill errors`);
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
return htmlContent;
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up table structures:', error);
|
||||
return htmlContent;
|
||||
}
|
||||
}
|
||||
|
||||
// Define toolbar options for consistency
|
||||
const emailToolbarOptions = [
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
||||
[{ 'align': [] }],
|
||||
[{ 'direction': 'rtl' }],
|
||||
['link'],
|
||||
['clean'],
|
||||
];
|
||||
|
||||
const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
||||
initialContent,
|
||||
onChange,
|
||||
@ -191,329 +22,201 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
||||
minHeight = '200px',
|
||||
maxHeight = 'calc(100vh - 400px)',
|
||||
preserveFormatting = false,
|
||||
autofocus = false,
|
||||
mode = 'compose',
|
||||
allowedFormats,
|
||||
customKeyBindings,
|
||||
}) => {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
const quillRef = useRef<any>(null);
|
||||
const quillInitTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [isReplyOrForward, setIsReplyOrForward] = useState(false);
|
||||
|
||||
// Helper function to clean up existing editor
|
||||
const cleanupEditor = () => {
|
||||
try {
|
||||
// Clear any existing timeouts
|
||||
if (quillInitTimeoutRef.current) {
|
||||
clearTimeout(quillInitTimeoutRef.current);
|
||||
quillInitTimeoutRef.current = null;
|
||||
// Initialize Quill editor when component mounts
|
||||
useEffect(() => {
|
||||
// Import Quill dynamically (client-side only)
|
||||
const initializeQuill = async () => {
|
||||
if (!editorRef.current || !toolbarRef.current) return;
|
||||
|
||||
const Quill = (await import('quill')).default;
|
||||
|
||||
// Import quill-better-table
|
||||
let tableModule = null;
|
||||
try {
|
||||
const QuillBetterTable = await import('quill-better-table');
|
||||
|
||||
// Register the table module if available
|
||||
if (QuillBetterTable && QuillBetterTable.default) {
|
||||
Quill.register({
|
||||
'modules/better-table': QuillBetterTable.default
|
||||
}, true);
|
||||
tableModule = QuillBetterTable.default;
|
||||
console.log('Better Table module registered successfully');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Table module not available:', err);
|
||||
}
|
||||
|
||||
// Remove existing Quill instance if it exists
|
||||
if (quillRef.current) {
|
||||
console.log('Cleaning up existing Quill editor');
|
||||
|
||||
// Remove event listeners
|
||||
// Define custom formats/modules with table support
|
||||
const emailToolbarOptions = [
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
||||
[{ 'align': [] }],
|
||||
[{ 'direction': 'rtl' }], // Add direction to toolbar
|
||||
['link'],
|
||||
['clean'],
|
||||
];
|
||||
|
||||
// Create new Quill instance with the DOM element and custom toolbar
|
||||
const editorElement = editorRef.current;
|
||||
quillRef.current = new Quill(editorElement, {
|
||||
modules: {
|
||||
toolbar: {
|
||||
container: toolbarRef.current,
|
||||
handlers: {
|
||||
// Add any custom toolbar handlers here
|
||||
}
|
||||
},
|
||||
clipboard: {
|
||||
matchVisual: false // Disable clipboard matching for better HTML handling
|
||||
},
|
||||
// Don't initialize better-table yet - we'll do it after content is loaded
|
||||
'better-table': false,
|
||||
},
|
||||
placeholder: placeholder,
|
||||
theme: 'snow',
|
||||
});
|
||||
|
||||
// Set initial content properly
|
||||
if (initialContent) {
|
||||
try {
|
||||
quillRef.current.off('text-change');
|
||||
quillRef.current.off('selection-change');
|
||||
console.log('Setting initial content in editor', {
|
||||
length: initialContent.length,
|
||||
startsWithHtml: initialContent.trim().startsWith('<'),
|
||||
containsForwardedMessage: initialContent.includes('---------- Forwarded message ----------'),
|
||||
containsReplyIndicator: initialContent.includes('wrote:'),
|
||||
hasBlockquote: initialContent.includes('<blockquote')
|
||||
});
|
||||
|
||||
// Get the container of the editor
|
||||
// Detect text direction
|
||||
const direction = detectTextDirection(initialContent);
|
||||
|
||||
// Process HTML content using centralized utility
|
||||
const sanitizedContent = processHtmlContent(initialContent);
|
||||
|
||||
// Log sanitized content details for debugging
|
||||
console.log('Sanitized content details:', {
|
||||
length: sanitizedContent.length,
|
||||
isEmpty: sanitizedContent.trim().length === 0,
|
||||
startsWithDiv: sanitizedContent.trim().startsWith('<div'),
|
||||
containsForwardedMessage: sanitizedContent.includes('---------- Forwarded message ----------'),
|
||||
containsQuoteHeader: sanitizedContent.includes('wrote:'),
|
||||
hasTable: sanitizedContent.includes('<table'),
|
||||
hasBlockquote: sanitizedContent.includes('<blockquote'),
|
||||
firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n')
|
||||
});
|
||||
|
||||
// Check if sanitized content is valid
|
||||
if (sanitizedContent.trim().length === 0) {
|
||||
console.warn('Sanitized content is empty after processing, using fallback approach');
|
||||
// Try to extract text content if HTML processing failed
|
||||
try {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = initialContent;
|
||||
const textContent = tempDiv.textContent || tempDiv.innerText || 'Empty content';
|
||||
|
||||
// Set text directly to ensure something displays
|
||||
quillRef.current.setText(textContent);
|
||||
} catch (e) {
|
||||
console.error('Text extraction fallback failed:', e);
|
||||
quillRef.current.setText('Error loading content');
|
||||
}
|
||||
} else {
|
||||
// Use direct innerHTML setting for the initial content
|
||||
quillRef.current.root.innerHTML = sanitizedContent;
|
||||
|
||||
// Set the direction for the content
|
||||
if (quillRef.current && quillRef.current.format) {
|
||||
quillRef.current.format('direction', direction);
|
||||
if (direction === 'rtl') {
|
||||
quillRef.current.format('align', 'right');
|
||||
}
|
||||
} else {
|
||||
console.warn('Cannot format content: editor not fully initialized');
|
||||
}
|
||||
}
|
||||
|
||||
// Set cursor at the beginning
|
||||
quillRef.current.setSelection(0, 0);
|
||||
|
||||
// Ensure the cursor and scroll position is at the top of the editor
|
||||
if (editorRef.current) {
|
||||
// Clear the content
|
||||
editorRef.current.innerHTML = '';
|
||||
editorRef.current.scrollTop = 0;
|
||||
|
||||
// Find and scroll parent containers that might have scroll
|
||||
const scrollable = [
|
||||
editorRef.current.closest('.ql-container'),
|
||||
editorRef.current.closest('.rich-email-editor-container'),
|
||||
editorRef.current.closest('.overflow-y-auto'),
|
||||
document.querySelector('.overflow-y-auto')
|
||||
];
|
||||
|
||||
scrollable.forEach(el => {
|
||||
if (el instanceof HTMLElement) {
|
||||
el.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Error removing Quill event listeners:', err);
|
||||
}
|
||||
|
||||
// Set to null to ensure garbage collection
|
||||
quillRef.current = null;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up editor:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize editor effect
|
||||
useEffect(() => {
|
||||
let editorInstance: any = null;
|
||||
let initialContentSet = false;
|
||||
let initializationTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const initEditor = async () => {
|
||||
try {
|
||||
// First cleanup any existing editor instances to prevent memory leaks
|
||||
cleanupEditor();
|
||||
|
||||
if (!editorRef.current) {
|
||||
console.error('Editor reference is not available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the initialization
|
||||
console.log('Initializing editor in', mode, 'mode');
|
||||
|
||||
// Clear any existing content
|
||||
editorRef.current.innerHTML = '';
|
||||
|
||||
// Register better table module
|
||||
registerTableModule();
|
||||
console.log('Better Table module registered successfully');
|
||||
|
||||
// Create a modules configuration that works
|
||||
const modules = {
|
||||
toolbar: emailToolbarOptions,
|
||||
keyboard: {
|
||||
bindings: customKeyBindings,
|
||||
}
|
||||
};
|
||||
|
||||
// Set up Quill with configurations
|
||||
const quill = new Quill(editorRef.current, {
|
||||
modules,
|
||||
theme: 'snow',
|
||||
placeholder: placeholder || 'Write your message here...',
|
||||
formats: allowedFormats,
|
||||
});
|
||||
|
||||
// Store the instance for cleanup
|
||||
quillRef.current = quill;
|
||||
editorInstance = quill;
|
||||
|
||||
// Process and set initial content if available
|
||||
if (initialContent) {
|
||||
console.error('Error setting initial content:', err);
|
||||
|
||||
// Enhanced fallback mechanism for complex content
|
||||
try {
|
||||
console.log('Setting initial editor content');
|
||||
// First try to extract text from HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = initialContent;
|
||||
const textContent = tempDiv.textContent || tempDiv.innerText || '';
|
||||
|
||||
// Process the content to handle blocked elements like CID images
|
||||
const processedContent = handleBlockedContent(initialContent);
|
||||
|
||||
// Use dangerouslyPasteHTML which accepts string content directly
|
||||
quill.clipboard.dangerouslyPasteHTML(processedContent);
|
||||
|
||||
// Emit initial content with the string HTML
|
||||
if (onChange) {
|
||||
onChange(quill.root.innerHTML);
|
||||
}
|
||||
|
||||
initialContentSet = true;
|
||||
} catch (err) {
|
||||
console.error('Error setting initial content:', err);
|
||||
|
||||
// Fallback to direct HTML setting if conversion fails
|
||||
try {
|
||||
quill.root.innerHTML = handleBlockedContent(initialContent);
|
||||
|
||||
// Still emit the change even with the fallback approach
|
||||
if (onChange) {
|
||||
onChange(quill.root.innerHTML);
|
||||
}
|
||||
} catch (innerErr) {
|
||||
console.error('Fallback content setting failed:', innerErr);
|
||||
quill.root.innerHTML = '<p>Error loading content. Please start typing or paste content manually.</p>';
|
||||
|
||||
// Emit the fallback content
|
||||
if (onChange) {
|
||||
onChange(quill.root.innerHTML);
|
||||
}
|
||||
if (textContent.trim()) {
|
||||
console.log('Using extracted text fallback, length:', textContent.length);
|
||||
quillRef.current.setText(textContent);
|
||||
} else {
|
||||
// If text extraction fails or returns empty, provide a message
|
||||
console.log('Using empty content fallback');
|
||||
quillRef.current.setText('Unable to load original content');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('All fallbacks failed:', e);
|
||||
quillRef.current.setText('Error loading content');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle content change events
|
||||
quill.on('text-change', (delta: any, oldDelta: any, source: string) => {
|
||||
if (source === 'user') {
|
||||
const html = quill.root.innerHTML;
|
||||
onChange(html);
|
||||
}
|
||||
});
|
||||
|
||||
// Set focus if autofocus is true
|
||||
if (autofocus) {
|
||||
setTimeout(() => {
|
||||
quill.focus();
|
||||
quill.setSelection(0, 0);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Mark editor as ready
|
||||
setIsReady(true);
|
||||
} catch (error) {
|
||||
console.error('Error initializing editor:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Set a timeout to prevent endless waiting for editor initialization
|
||||
initializationTimeout = setTimeout(() => {
|
||||
if (!initialContentSet && editorRef.current) {
|
||||
console.warn('Editor initialization timed out after 3 seconds');
|
||||
|
||||
// Force cleanup and try one more time with minimal settings
|
||||
cleanupEditor();
|
||||
|
||||
try {
|
||||
// Create a simple editor without complex modules
|
||||
const fallbackQuill = new Quill(editorRef.current, {
|
||||
theme: 'snow',
|
||||
placeholder: placeholder || 'Write your message here...',
|
||||
});
|
||||
|
||||
// Add a simple placeholder message
|
||||
fallbackQuill.root.innerHTML = initialContent || '<p>Editor ready. Start typing...</p>';
|
||||
|
||||
// Handle content change events
|
||||
fallbackQuill.on('text-change', () => {
|
||||
const html = fallbackQuill.root.innerHTML;
|
||||
onChange(html);
|
||||
});
|
||||
|
||||
quillRef.current = fallbackQuill;
|
||||
editorInstance = fallbackQuill;
|
||||
setIsReady(true);
|
||||
} catch (fallbackError) {
|
||||
console.error('Fallback editor initialization also failed:', fallbackError);
|
||||
}
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// Store timeout reference to allow cleanup
|
||||
quillInitTimeoutRef.current = initializationTimeout;
|
||||
|
||||
// Initialize the editor
|
||||
initEditor();
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
if (initializationTimeout) {
|
||||
clearTimeout(initializationTimeout);
|
||||
}
|
||||
|
||||
if (quillInitTimeoutRef.current) {
|
||||
clearTimeout(quillInitTimeoutRef.current);
|
||||
quillInitTimeoutRef.current = null;
|
||||
}
|
||||
|
||||
if (editorInstance) {
|
||||
try {
|
||||
// Remove event listeners
|
||||
editorInstance.off('text-change');
|
||||
|
||||
// Properly destroy Quill instance if possible
|
||||
if (editorInstance.emitter) {
|
||||
editorInstance.emitter.removeAllListeners();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during editor cleanup:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Final cleanup of any Quill instances
|
||||
cleanupEditor();
|
||||
};
|
||||
}, [initialContent, onChange, placeholder, autofocus, mode, allowedFormats, customKeyBindings]);
|
||||
|
||||
// Handle blocked content like CID images that cause loading issues
|
||||
function handleBlockedContent(htmlContent: string): string {
|
||||
if (!htmlContent) return '';
|
||||
|
||||
try {
|
||||
// Create a DOM parser to work with the HTML
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlContent, 'text/html');
|
||||
|
||||
// Count images processed for logging
|
||||
let imageCount = 0;
|
||||
let cidCount = 0;
|
||||
|
||||
// Process all images to prevent loading issues
|
||||
const images = doc.querySelectorAll('img');
|
||||
images.forEach(img => {
|
||||
const src = img.getAttribute('src');
|
||||
imageCount++;
|
||||
|
||||
// Handle CID images which cause loading issues
|
||||
if (src && src.startsWith('cid:')) {
|
||||
cidCount++;
|
||||
// Replace with placeholder or remove src
|
||||
img.setAttribute('src', 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"%3E%3Cpath fill="%23ccc" d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/%3E%3C/svg%3E');
|
||||
img.setAttribute('data-original-cid', src);
|
||||
img.setAttribute('title', 'Image reference not available');
|
||||
img.setAttribute('alt', 'Image placeholder');
|
||||
img.style.border = '1px dashed #ccc';
|
||||
img.style.padding = '4px';
|
||||
img.style.maxWidth = '100%';
|
||||
}
|
||||
|
||||
// Handle possible blocked remote images
|
||||
if (src && (src.startsWith('http://') || src.startsWith('https://'))) {
|
||||
// Set alt text and a class for better UX
|
||||
if (!img.getAttribute('alt')) {
|
||||
img.setAttribute('alt', 'Remote image');
|
||||
}
|
||||
img.classList.add('remote-image');
|
||||
}
|
||||
// Add change listener
|
||||
quillRef.current.on('text-change', () => {
|
||||
const html = quillRef.current.root.innerHTML;
|
||||
onChange(html);
|
||||
});
|
||||
|
||||
// Process table elements to ensure consistent styling
|
||||
const tables = doc.querySelectorAll('table');
|
||||
tables.forEach(table => {
|
||||
// Ensure tables have consistent styling to prevent layout issues
|
||||
const htmlTable = table as HTMLTableElement;
|
||||
htmlTable.style.borderCollapse = 'collapse';
|
||||
htmlTable.style.maxWidth = '100%';
|
||||
|
||||
// Handle cell padding and borders for better readability
|
||||
const cells = htmlTable.querySelectorAll('td, th');
|
||||
cells.forEach(cell => {
|
||||
const htmlCell = cell as HTMLTableCellElement;
|
||||
if (!htmlCell.style.padding) {
|
||||
htmlCell.style.padding = '4px 8px';
|
||||
}
|
||||
// Only add border if not already styled
|
||||
if (!htmlCell.style.border) {
|
||||
htmlCell.style.border = '1px solid #e0e0e0';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Ensure all blockquotes have consistent styling
|
||||
const blockquotes = doc.querySelectorAll('blockquote');
|
||||
blockquotes.forEach(blockquote => {
|
||||
const htmlBlockquote = blockquote as HTMLElement;
|
||||
if (!htmlBlockquote.style.borderLeft) {
|
||||
htmlBlockquote.style.borderLeft = '3px solid #ddd';
|
||||
}
|
||||
if (!htmlBlockquote.style.paddingLeft) {
|
||||
htmlBlockquote.style.paddingLeft = '10px';
|
||||
}
|
||||
if (!htmlBlockquote.style.margin) {
|
||||
htmlBlockquote.style.margin = '10px 0';
|
||||
}
|
||||
if (!htmlBlockquote.style.color) {
|
||||
htmlBlockquote.style.color = '#505050';
|
||||
}
|
||||
});
|
||||
|
||||
// Log the number of images processed for debugging
|
||||
if (imageCount > 0) {
|
||||
console.log(`Processed ${imageCount} images in content (${cidCount} CID images replaced)`);
|
||||
|
||||
// Improve editor layout
|
||||
const editorContainer = editorElement.closest('.ql-container');
|
||||
if (editorContainer) {
|
||||
editorContainer.classList.add('email-editor-container');
|
||||
}
|
||||
|
||||
// Return the cleaned HTML
|
||||
return doc.body.innerHTML;
|
||||
} catch (error) {
|
||||
console.error('Error processing blocked content:', error);
|
||||
// Return original content if processing fails
|
||||
return htmlContent;
|
||||
}
|
||||
}
|
||||
|
||||
setIsReady(true);
|
||||
};
|
||||
|
||||
initializeQuill().catch(err => {
|
||||
console.error('Failed to initialize Quill editor:', err);
|
||||
});
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
if (quillRef.current) {
|
||||
// Clean up any event listeners or resources
|
||||
quillRef.current.off('text-change');
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update content from props if changed externally - using a simpler approach
|
||||
useEffect(() => {
|
||||
@ -533,57 +236,11 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
||||
firstNChars: initialContent.substring(0, 100).replace(/\n/g, '\\n')
|
||||
});
|
||||
|
||||
// Check if content is reply or forward to use special handling
|
||||
const contentIsReplyOrForward =
|
||||
initialContent.includes('wrote:') ||
|
||||
initialContent.includes('<blockquote') ||
|
||||
initialContent.includes('Forwarded message') ||
|
||||
initialContent.includes('---------- Forwarded message ----------');
|
||||
|
||||
// If content type changed (from reply to regular or vice versa), we need to reload
|
||||
if (contentIsReplyOrForward !== isReplyOrForward) {
|
||||
console.log('Content type changed from', isReplyOrForward ? 'reply/forward' : 'regular',
|
||||
'to', contentIsReplyOrForward ? 'reply/forward' : 'regular',
|
||||
'- reloading editor');
|
||||
setIsReplyOrForward(contentIsReplyOrForward);
|
||||
// Force a complete re-initialization of the editor by unmounting
|
||||
if (quillRef.current) {
|
||||
quillRef.current.off('text-change');
|
||||
|
||||
// Clear content to prevent flashing
|
||||
try {
|
||||
quillRef.current.root.innerHTML = '<div>Loading...</div>';
|
||||
} catch (e) {
|
||||
console.warn('Error clearing editor content:', e);
|
||||
}
|
||||
|
||||
quillRef.current = null;
|
||||
}
|
||||
setIsReady(false);
|
||||
|
||||
// Force a small delay before reinitializing to ensure cleanup completes
|
||||
setTimeout(() => {
|
||||
// Explicitly empty out the editor DOM node to ensure clean start
|
||||
if (editorRef.current) {
|
||||
while (editorRef.current.firstChild) {
|
||||
editorRef.current.removeChild(editorRef.current.firstChild);
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-process to handle blocked content
|
||||
const preProcessedContent = handleBlockedContent(initialContent);
|
||||
// Detect text direction
|
||||
const direction = detectTextDirection(initialContent);
|
||||
|
||||
// Process HTML content using centralized utility
|
||||
const processed = processHtmlContent(preProcessedContent, {
|
||||
sanitize: true,
|
||||
preserveReplyFormat: contentIsReplyOrForward
|
||||
});
|
||||
const sanitizedContent = processed.sanitizedContent;
|
||||
const direction = processed.direction; // Use direction from processed result
|
||||
const sanitizedContent = processHtmlContent(initialContent);
|
||||
|
||||
// Log sanitized content details for debugging
|
||||
console.log('Sanitized content details:', {
|
||||
@ -594,7 +251,6 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
||||
containsQuoteHeader: sanitizedContent.includes('wrote:'),
|
||||
hasTable: sanitizedContent.includes('<table'),
|
||||
hasBlockquote: sanitizedContent.includes('<blockquote'),
|
||||
isReplyOrForward: contentIsReplyOrForward,
|
||||
firstNChars: sanitizedContent.substring(0, 100).replace(/\n/g, '\\n')
|
||||
});
|
||||
|
||||
@ -603,7 +259,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
||||
console.warn('Sanitized content is empty, using original content');
|
||||
// If sanitized content is empty, try to extract text from original
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = preProcessedContent;
|
||||
tempDiv.innerHTML = initialContent;
|
||||
const textContent = tempDiv.textContent || tempDiv.innerText || '';
|
||||
|
||||
// Create simple HTML with text content
|
||||
@ -611,61 +267,32 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
||||
quillRef.current.setText(textContent || 'No content available');
|
||||
}
|
||||
} else {
|
||||
// Process content based on type
|
||||
if (contentIsReplyOrForward) {
|
||||
console.log('Using special handling for reply/forward content update');
|
||||
|
||||
// For reply/forward content, convert ALL tables to divs
|
||||
const cleanedContent = cleanupTableStructures(sanitizedContent, true);
|
||||
|
||||
// Set content without table handling
|
||||
// SIMPLIFIED: Set content directly to the root element rather than using clipboard
|
||||
if (quillRef.current && quillRef.current.root) {
|
||||
quillRef.current.root.innerHTML = cleanedContent;
|
||||
// First set the content
|
||||
quillRef.current.root.innerHTML = sanitizedContent;
|
||||
|
||||
// Then safely apply formatting only if quillRef is valid
|
||||
try {
|
||||
if (quillRef.current && quillRef.current.format && quillRef.current.root.innerHTML.trim().length > 0) {
|
||||
// Set the direction for the content
|
||||
quillRef.current.format('direction', direction);
|
||||
if (direction === 'rtl') {
|
||||
quillRef.current.format('align', 'right');
|
||||
}
|
||||
|
||||
// Delay applying formatting to ensure Quill is fully ready
|
||||
setTimeout(() => {
|
||||
if (quillRef.current) {
|
||||
try {
|
||||
// Set the direction for the content
|
||||
quillRef.current.format('direction', direction);
|
||||
if (direction === 'rtl') {
|
||||
quillRef.current.format('align', 'right');
|
||||
}
|
||||
|
||||
// Force update
|
||||
quillRef.current.update();
|
||||
|
||||
// Set selection to beginning
|
||||
quillRef.current.setSelection(0, 0);
|
||||
} catch (innerError) {
|
||||
console.error('Error applying delayed formatting:', innerError);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
// Force update
|
||||
quillRef.current.update();
|
||||
|
||||
// Set selection to beginning
|
||||
quillRef.current.setSelection(0, 0);
|
||||
} else {
|
||||
console.warn('Skipping format - either editor not ready or content empty');
|
||||
}
|
||||
} else {
|
||||
// For regular content, use normal processing
|
||||
const cleanedContent = cleanupTableStructures(sanitizedContent, false);
|
||||
|
||||
if (quillRef.current && quillRef.current.root) {
|
||||
quillRef.current.root.innerHTML = cleanedContent;
|
||||
|
||||
// Safely apply formatting
|
||||
try {
|
||||
quillRef.current.format('direction', direction);
|
||||
if (direction === 'rtl') {
|
||||
quillRef.current.format('align', 'right');
|
||||
}
|
||||
|
||||
// Force update
|
||||
quillRef.current.update();
|
||||
|
||||
// Set selection to beginning
|
||||
quillRef.current.setSelection(0, 0);
|
||||
} catch (formatError) {
|
||||
console.error('Error applying formatting:', formatError);
|
||||
}
|
||||
}
|
||||
// Continue without formatting if there's an error
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@ -690,7 +317,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [initialContent, isReady, isReplyOrForward]);
|
||||
}, [initialContent, isReady]);
|
||||
|
||||
return (
|
||||
<div className="rich-email-editor-wrapper">
|
||||
|
||||
@ -61,84 +61,6 @@ export const useEmailState = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Normalize email content structure to ensure consistency
|
||||
const normalizeEmailContent = useCallback((emailData: any): any => {
|
||||
if (!emailData) return emailData;
|
||||
|
||||
// Create a clone to avoid modifying the original
|
||||
const normalizedEmail = { ...emailData };
|
||||
|
||||
// Log the incoming email structure
|
||||
console.log(`[NORMALIZE_EMAIL] Processing email ${normalizedEmail.id || 'unknown'}: ${normalizedEmail.subject || 'No subject'}`);
|
||||
|
||||
try {
|
||||
// Handle content field normalization
|
||||
if (!normalizedEmail.content) {
|
||||
// Create content object if it doesn't exist
|
||||
normalizedEmail.content = { html: '', text: '' };
|
||||
|
||||
// Try to populate content from html/text fields
|
||||
if (normalizedEmail.html) {
|
||||
normalizedEmail.content.html = normalizedEmail.html;
|
||||
console.log(`[NORMALIZE_EMAIL] Populated content.html from email.html (${normalizedEmail.html.length} chars)`);
|
||||
}
|
||||
|
||||
if (normalizedEmail.text) {
|
||||
normalizedEmail.content.text = normalizedEmail.text;
|
||||
console.log(`[NORMALIZE_EMAIL] Populated content.text from email.text (${normalizedEmail.text.length} chars)`);
|
||||
}
|
||||
}
|
||||
// If content is a string, convert to object format
|
||||
else if (typeof normalizedEmail.content === 'string') {
|
||||
const htmlContent = normalizedEmail.content;
|
||||
normalizedEmail.content = {
|
||||
html: htmlContent,
|
||||
text: htmlContent.replace(/<[^>]*>/g, '') // Simple HTML to text conversion
|
||||
};
|
||||
console.log(`[NORMALIZE_EMAIL] Converted string content to object (${htmlContent.length} chars)`);
|
||||
}
|
||||
// Ensure content object has both html and text properties
|
||||
else if (typeof normalizedEmail.content === 'object') {
|
||||
if (!normalizedEmail.content.html && normalizedEmail.content.text) {
|
||||
// Convert text to simple HTML if only text exists
|
||||
normalizedEmail.content.html = normalizedEmail.content.text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
console.log(`[NORMALIZE_EMAIL] Created HTML content from text (${normalizedEmail.content.text.length} chars)`);
|
||||
} else if (!normalizedEmail.content.text && normalizedEmail.content.html) {
|
||||
// Create plain text version if only HTML exists
|
||||
normalizedEmail.content.text = normalizedEmail.content.html.replace(/<[^>]*>/g, '');
|
||||
console.log(`[NORMALIZE_EMAIL] Created text content from HTML (${normalizedEmail.content.html.length} chars)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure html and text properties are also set for backward compatibility
|
||||
if (normalizedEmail.content?.html && !normalizedEmail.html) {
|
||||
normalizedEmail.html = normalizedEmail.content.html;
|
||||
}
|
||||
|
||||
if (normalizedEmail.content?.text && !normalizedEmail.text) {
|
||||
normalizedEmail.text = normalizedEmail.content.text;
|
||||
}
|
||||
|
||||
console.log(`[NORMALIZE_EMAIL] Normalized email content structure successfully`, {
|
||||
hasContentObj: !!normalizedEmail.content,
|
||||
contentHtmlLength: normalizedEmail.content?.html?.length || 0,
|
||||
contentTextLength: normalizedEmail.content?.text?.length || 0,
|
||||
hasHtml: !!normalizedEmail.html,
|
||||
hasText: !!normalizedEmail.text
|
||||
});
|
||||
|
||||
return normalizedEmail;
|
||||
} catch (error) {
|
||||
console.error(`[NORMALIZE_EMAIL] Error normalizing email content:`, error);
|
||||
// Return the original data if normalization fails
|
||||
return emailData;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load emails from the server
|
||||
const loadEmails = useCallback(async (page: number, perPage: number, isLoadMore: boolean = false) => {
|
||||
// CRITICAL FIX: Do important validation before setting loading state
|
||||
@ -155,407 +77,194 @@ export const useEmailState = () => {
|
||||
dispatch({ type: 'SET_LOADING', payload: true });
|
||||
|
||||
try {
|
||||
// CRITICAL FIX: Add more robust validation to prevent "toString of undefined" error
|
||||
if (!state.currentFolder) {
|
||||
logEmailOp('ERROR', 'Current folder is undefined, cannot load emails');
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: 'Invalid folder configuration'
|
||||
});
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Get normalized parameters using helper function with proper account ID handling
|
||||
const accountId = state.selectedAccount ? state.selectedAccount.id : undefined;
|
||||
const { normalizedFolder, effectiveAccountId, prefixedFolder } =
|
||||
normalizeFolderAndAccount(state.currentFolder, accountId);
|
||||
|
||||
// Additional validation for accountId
|
||||
if (accountId === undefined && state.currentFolder.includes(':')) {
|
||||
// Try to extract accountId from folder string as fallback
|
||||
const extractedAccountId = state.currentFolder.split(':')[0];
|
||||
if (extractedAccountId) {
|
||||
console.log(`[DEBUG-LOAD_EMAILS] Using extracted accountId ${extractedAccountId} from folder path as fallback`);
|
||||
const { normalizedFolder, effectiveAccountId, prefixedFolder } =
|
||||
normalizeFolderAndAccount(state.currentFolder, extractedAccountId);
|
||||
|
||||
logEmailOp('LOAD_EMAILS', `Loading emails for ${prefixedFolder} (account: ${effectiveAccountId}, isLoadMore: ${isLoadMore}, page: ${page})`);
|
||||
logEmailOp('LOAD_EMAILS', `Loading emails for ${prefixedFolder} (account: ${effectiveAccountId}, isLoadMore: ${isLoadMore}, page: ${page})`);
|
||||
|
||||
// Construct query parameters
|
||||
const queryParams = new URLSearchParams({
|
||||
folder: normalizedFolder,
|
||||
page: page.toString(),
|
||||
perPage: perPage.toString(),
|
||||
accountId: effectiveAccountId
|
||||
});
|
||||
|
||||
// Debug log existing emails count
|
||||
if (isLoadMore) {
|
||||
console.log(`[DEBUG-PAGINATION] Loading more emails. Current page: ${page}, existing emails: ${state.emails.length}`);
|
||||
}
|
||||
|
||||
// Try to get cached emails first
|
||||
logEmailOp('CACHE_CHECK', `Checking cache for ${prefixedFolder}, page: ${page}`);
|
||||
const cachedEmails = await getCachedEmailsWithTimeout(
|
||||
session.user.id,
|
||||
prefixedFolder,
|
||||
page,
|
||||
perPage,
|
||||
100,
|
||||
effectiveAccountId
|
||||
);
|
||||
|
||||
if (cachedEmails) {
|
||||
logEmailOp('CACHE_HIT', `Using cached data for ${prefixedFolder}, page: ${page}, emails: ${cachedEmails.emails?.length || 0}, isLoadMore: ${isLoadMore}`);
|
||||
|
||||
// Ensure cached data has emails array property
|
||||
if (Array.isArray(cachedEmails.emails)) {
|
||||
// CRITICAL FIX: Double check we're using the right action type based on isLoadMore param
|
||||
console.log(`[DEBUG-CACHE_HIT] Dispatching ${isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS'} with ${cachedEmails.emails.length} emails`);
|
||||
|
||||
// Continue with the extracted account ID...
|
||||
// Construct query parameters
|
||||
const queryParams = new URLSearchParams({
|
||||
folder: normalizedFolder,
|
||||
page: page.toString(),
|
||||
perPage: perPage.toString(),
|
||||
accountId: effectiveAccountId
|
||||
});
|
||||
|
||||
// Debug log existing emails count
|
||||
if (isLoadMore) {
|
||||
console.log(`[DEBUG-PAGINATION] Loading more emails. Current page: ${page}, existing emails: ${state.emails.length}`);
|
||||
}
|
||||
|
||||
// Try to get cached emails first
|
||||
logEmailOp('CACHE_CHECK', `Checking cache for ${prefixedFolder}, page: ${page}`);
|
||||
const cachedEmails = await getCachedEmailsWithTimeout(
|
||||
session.user.id,
|
||||
prefixedFolder,
|
||||
page,
|
||||
perPage,
|
||||
100,
|
||||
effectiveAccountId
|
||||
);
|
||||
|
||||
if (cachedEmails) {
|
||||
logEmailOp('CACHE_HIT', `Using cached data for ${prefixedFolder}, page: ${page}, emails: ${cachedEmails.emails?.length || 0}, isLoadMore: ${isLoadMore}`);
|
||||
|
||||
// Ensure cached data has emails array property
|
||||
if (Array.isArray(cachedEmails.emails)) {
|
||||
// CRITICAL FIX: Double check we're using the right action type based on isLoadMore param
|
||||
console.log(`[DEBUG-CACHE_HIT] Dispatching ${isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS'} with ${cachedEmails.emails.length} emails`);
|
||||
|
||||
// Dispatch appropriate action based on if we're loading more - DO NOT OVERRIDE isLoadMore!
|
||||
dispatch({
|
||||
type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS',
|
||||
payload: cachedEmails.emails
|
||||
});
|
||||
|
||||
// Set pagination info from cache if available
|
||||
if (cachedEmails.totalEmails) {
|
||||
dispatch({ type: 'SET_TOTAL_EMAILS', payload: cachedEmails.totalEmails });
|
||||
}
|
||||
|
||||
if (cachedEmails.totalPages) {
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: cachedEmails.totalPages });
|
||||
}
|
||||
|
||||
// Update available mailboxes if provided
|
||||
if (cachedEmails.mailboxes && cachedEmails.mailboxes.length > 0) {
|
||||
dispatch({ type: 'SET_MAILBOXES', payload: cachedEmails.mailboxes });
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL FIX: If this was a loadMore operation, check the result after the dispatch
|
||||
if (isLoadMore) {
|
||||
setTimeout(() => {
|
||||
console.log(`[DEBUG-CACHE_HIT_APPEND] After ${isLoadMore ? 'APPEND' : 'SET'}, email count is now: ${state.emails.length}`);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch emails from API if no cache hit
|
||||
logEmailOp('API_FETCH', `Fetching emails from API: ${queryParams.toString()}, isLoadMore: ${isLoadMore}`);
|
||||
console.log(`[DEBUG-API_FETCH] Fetching from /api/courrier?${queryParams.toString()}`);
|
||||
const response = await fetch(`/api/courrier?${queryParams.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
// CRITICAL FIX: Try to recover from fetch errors by retrying with different pagination
|
||||
if (isLoadMore && page > 1) {
|
||||
logEmailOp('ERROR_RECOVERY', `Failed to fetch emails for page ${page}, attempting to recover by decrementing page`);
|
||||
console.log(`[DEBUG-ERROR] API returned ${response.status} for page ${page}`);
|
||||
// If we're loading more and there's an error, just decrement the page to avoid getting stuck
|
||||
dispatch({ type: 'SET_PAGE', payload: page - 1 });
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
// Also reset total pages to try again
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: page });
|
||||
return;
|
||||
}
|
||||
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to fetch emails');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`[DEBUG-API_RESPONSE] Got response with ${data.emails?.length || 0} emails, totalPages: ${data.totalPages}, totalEmails: ${data.totalEmails}, isLoadMore: ${isLoadMore}`);
|
||||
|
||||
// CRITICAL FIX: Enhanced empty results handling
|
||||
if (!data.emails || data.emails.length === 0) {
|
||||
console.log(`[DEBUG-EMPTY] No emails in response for page ${page}`);
|
||||
// If we're at a page > 1 and got no results, the paging is off, so try again with page 1
|
||||
if (page > 1 && !isLoadMore) {
|
||||
logEmailOp('EMPTY_RESULTS', `No emails returned for page ${page}, resetting to page 1`);
|
||||
dispatch({ type: 'SET_PAGE', payload: 1 });
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're already at page 1, just update the state with no emails
|
||||
if (!isLoadMore) {
|
||||
logEmailOp('EMPTY_RESULTS', `No emails found in ${state.currentFolder}`);
|
||||
dispatch({ type: 'SET_EMAILS', payload: [] });
|
||||
dispatch({ type: 'SET_TOTAL_EMAILS', payload: 0 });
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: 0 });
|
||||
} else {
|
||||
// For load more, just set loading to false but keep existing emails
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure all emails have proper account ID and folder format
|
||||
if (Array.isArray(data.emails)) {
|
||||
// Log email dates for debugging
|
||||
if (data.emails.length > 0) {
|
||||
logEmailOp('EMAIL_DATES', `First few email dates before processing:`,
|
||||
data.emails.slice(0, 5).map((e: any) => ({
|
||||
id: e.id.substring(0, 8),
|
||||
subject: e.subject?.substring(0, 20),
|
||||
date: e.date,
|
||||
dateObj: new Date(e.date),
|
||||
timestamp: new Date(e.date).getTime()
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
data.emails.forEach((email: Email) => {
|
||||
// If email doesn't have an accountId, set it to the effective one
|
||||
if (!email.accountId) {
|
||||
email.accountId = effectiveAccountId;
|
||||
}
|
||||
|
||||
// Ensure folder has the proper prefix format
|
||||
if (email.folder && !email.folder.includes(':')) {
|
||||
email.folder = `${email.accountId}:${email.folder}`;
|
||||
}
|
||||
|
||||
// Ensure date is a valid Date object (handle strings or timestamps)
|
||||
if (email.date && !(email.date instanceof Date)) {
|
||||
try {
|
||||
// Convert to a proper Date object if it's a string or number
|
||||
const dateObj = new Date(email.date);
|
||||
// Verify it's a valid date
|
||||
if (!isNaN(dateObj.getTime())) {
|
||||
email.date = dateObj;
|
||||
}
|
||||
} catch (err) {
|
||||
// If conversion fails, log and use current date as fallback
|
||||
console.error(`Invalid date format for email ${email.id}: ${email.date}`);
|
||||
email.date = new Date();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Log what we're about to do
|
||||
console.log(`[DEBUG-DISPATCH] About to dispatch ${isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS'} with ${data.emails?.length || 0} emails`);
|
||||
|
||||
// Update state with fetched data
|
||||
// Dispatch appropriate action based on if we're loading more - DO NOT OVERRIDE isLoadMore!
|
||||
dispatch({
|
||||
type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS',
|
||||
payload: Array.isArray(data.emails) ? data.emails : []
|
||||
payload: cachedEmails.emails
|
||||
});
|
||||
|
||||
// Double-check that we've updated the email list correctly after dispatch
|
||||
setTimeout(() => {
|
||||
console.log(`[DEBUG-AFTER-DISPATCH] Email count is now: ${state.emails.length}, should include the ${data.emails?.length || 0} new emails we just loaded`);
|
||||
}, 0);
|
||||
|
||||
if (data.totalEmails) {
|
||||
dispatch({ type: 'SET_TOTAL_EMAILS', payload: data.totalEmails });
|
||||
// Set pagination info from cache if available
|
||||
if (cachedEmails.totalEmails) {
|
||||
dispatch({ type: 'SET_TOTAL_EMAILS', payload: cachedEmails.totalEmails });
|
||||
}
|
||||
|
||||
if (data.totalPages) {
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: data.totalPages });
|
||||
if (cachedEmails.totalPages) {
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: cachedEmails.totalPages });
|
||||
}
|
||||
|
||||
// Update available mailboxes if provided
|
||||
if (data.mailboxes && data.mailboxes.length > 0) {
|
||||
dispatch({ type: 'SET_MAILBOXES', payload: data.mailboxes });
|
||||
if (cachedEmails.mailboxes && cachedEmails.mailboxes.length > 0) {
|
||||
dispatch({ type: 'SET_MAILBOXES', payload: cachedEmails.mailboxes });
|
||||
}
|
||||
} else {
|
||||
// If we can't extract a valid accountId, throw an error
|
||||
throw new Error("Cannot determine account ID for loading emails");
|
||||
}
|
||||
} else {
|
||||
// Normal flow with valid accountId
|
||||
const { normalizedFolder, effectiveAccountId, prefixedFolder } =
|
||||
normalizeFolderAndAccount(state.currentFolder, accountId);
|
||||
|
||||
logEmailOp('LOAD_EMAILS', `Loading emails for ${prefixedFolder} (account: ${effectiveAccountId}, isLoadMore: ${isLoadMore}, page: ${page})`);
|
||||
|
||||
// Construct query parameters
|
||||
const queryParams = new URLSearchParams({
|
||||
folder: normalizedFolder,
|
||||
page: page.toString(),
|
||||
perPage: perPage.toString(),
|
||||
accountId: effectiveAccountId
|
||||
});
|
||||
|
||||
// Debug log existing emails count
|
||||
// CRITICAL FIX: If this was a loadMore operation, check the result after the dispatch
|
||||
if (isLoadMore) {
|
||||
console.log(`[DEBUG-PAGINATION] Loading more emails. Current page: ${page}, existing emails: ${state.emails.length}`);
|
||||
setTimeout(() => {
|
||||
console.log(`[DEBUG-CACHE_HIT_APPEND] After ${isLoadMore ? 'APPEND' : 'SET'}, email count is now: ${state.emails.length}`);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Try to get cached emails first
|
||||
logEmailOp('CACHE_CHECK', `Checking cache for ${prefixedFolder}, page: ${page}`);
|
||||
const cachedEmails = await getCachedEmailsWithTimeout(
|
||||
session.user.id,
|
||||
prefixedFolder,
|
||||
page,
|
||||
perPage,
|
||||
100,
|
||||
effectiveAccountId
|
||||
);
|
||||
|
||||
if (cachedEmails) {
|
||||
logEmailOp('CACHE_HIT', `Using cached data for ${prefixedFolder}, page: ${page}, emails: ${cachedEmails.emails?.length || 0}, isLoadMore: ${isLoadMore}`);
|
||||
|
||||
// Ensure cached data has emails array property
|
||||
if (Array.isArray(cachedEmails.emails)) {
|
||||
// CRITICAL FIX: Double check we're using the right action type based on isLoadMore param
|
||||
console.log(`[DEBUG-CACHE_HIT] Dispatching ${isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS'} with ${cachedEmails.emails.length} emails`);
|
||||
|
||||
// Dispatch appropriate action based on if we're loading more - DO NOT OVERRIDE isLoadMore!
|
||||
dispatch({
|
||||
type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS',
|
||||
payload: cachedEmails.emails
|
||||
});
|
||||
|
||||
// Set pagination info from cache if available
|
||||
if (cachedEmails.totalEmails) {
|
||||
dispatch({ type: 'SET_TOTAL_EMAILS', payload: cachedEmails.totalEmails });
|
||||
}
|
||||
|
||||
if (cachedEmails.totalPages) {
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: cachedEmails.totalPages });
|
||||
}
|
||||
|
||||
// Update available mailboxes if provided
|
||||
if (cachedEmails.mailboxes && cachedEmails.mailboxes.length > 0) {
|
||||
dispatch({ type: 'SET_MAILBOXES', payload: cachedEmails.mailboxes });
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL FIX: If this was a loadMore operation, check the result after the dispatch
|
||||
if (isLoadMore) {
|
||||
setTimeout(() => {
|
||||
console.log(`[DEBUG-CACHE_HIT_APPEND] After ${isLoadMore ? 'APPEND' : 'SET'}, email count is now: ${state.emails.length}`);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch emails from API if no cache hit
|
||||
logEmailOp('API_FETCH', `Fetching emails from API: ${queryParams.toString()}, isLoadMore: ${isLoadMore}`);
|
||||
console.log(`[DEBUG-API_FETCH] Fetching from /api/courrier?${queryParams.toString()}`);
|
||||
const response = await fetch(`/api/courrier?${queryParams.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
// CRITICAL FIX: Try to recover from fetch errors by retrying with different pagination
|
||||
if (isLoadMore && page > 1) {
|
||||
logEmailOp('ERROR_RECOVERY', `Failed to fetch emails for page ${page}, attempting to recover by decrementing page`);
|
||||
console.log(`[DEBUG-ERROR] API returned ${response.status} for page ${page}`);
|
||||
// If we're loading more and there's an error, just decrement the page to avoid getting stuck
|
||||
dispatch({ type: 'SET_PAGE', payload: page - 1 });
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
// Also reset total pages to try again
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: page });
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch emails from API if no cache hit
|
||||
logEmailOp('API_FETCH', `Fetching emails from API: ${queryParams.toString()}, isLoadMore: ${isLoadMore}`);
|
||||
console.log(`[DEBUG-API_FETCH] Fetching from /api/courrier?${queryParams.toString()}`);
|
||||
const response = await fetch(`/api/courrier?${queryParams.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
// CRITICAL FIX: Try to recover from fetch errors by retrying with different pagination
|
||||
if (isLoadMore && page > 1) {
|
||||
logEmailOp('ERROR_RECOVERY', `Failed to fetch emails for page ${page}, attempting to recover by decrementing page`);
|
||||
console.log(`[DEBUG-ERROR] API returned ${response.status} for page ${page}`);
|
||||
// If we're loading more and there's an error, just decrement the page to avoid getting stuck
|
||||
dispatch({ type: 'SET_PAGE', payload: page - 1 });
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
// Also reset total pages to try again
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: page });
|
||||
return;
|
||||
}
|
||||
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to fetch emails');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`[DEBUG-API_RESPONSE] Got response with ${data.emails?.length || 0} emails, totalPages: ${data.totalPages}, totalEmails: ${data.totalEmails}, isLoadMore: ${isLoadMore}`);
|
||||
|
||||
// CRITICAL FIX: Enhanced empty results handling
|
||||
if (!data.emails || data.emails.length === 0) {
|
||||
console.log(`[DEBUG-EMPTY] No emails in response for page ${page}`);
|
||||
// If we're at a page > 1 and got no results, the paging is off, so try again with page 1
|
||||
if (page > 1 && !isLoadMore) {
|
||||
logEmailOp('EMPTY_RESULTS', `No emails returned for page ${page}, resetting to page 1`);
|
||||
dispatch({ type: 'SET_PAGE', payload: 1 });
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're already at page 1, just update the state with no emails
|
||||
if (!isLoadMore) {
|
||||
logEmailOp('EMPTY_RESULTS', `No emails found in ${state.currentFolder}`);
|
||||
dispatch({ type: 'SET_EMAILS', payload: [] });
|
||||
dispatch({ type: 'SET_TOTAL_EMAILS', payload: 0 });
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: 0 });
|
||||
} else {
|
||||
// For load more, just set loading to false but keep existing emails
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
}
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to fetch emails');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`[DEBUG-API_RESPONSE] Got response with ${data.emails?.length || 0} emails, totalPages: ${data.totalPages}, totalEmails: ${data.totalEmails}, isLoadMore: ${isLoadMore}`);
|
||||
|
||||
// CRITICAL FIX: Enhanced empty results handling
|
||||
if (!data.emails || data.emails.length === 0) {
|
||||
console.log(`[DEBUG-EMPTY] No emails in response for page ${page}`);
|
||||
// If we're at a page > 1 and got no results, the paging is off, so try again with page 1
|
||||
if (page > 1 && !isLoadMore) {
|
||||
logEmailOp('EMPTY_RESULTS', `No emails returned for page ${page}, resetting to page 1`);
|
||||
dispatch({ type: 'SET_PAGE', payload: 1 });
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure all emails have proper account ID and folder format
|
||||
if (Array.isArray(data.emails)) {
|
||||
// Log email dates for debugging
|
||||
if (data.emails.length > 0) {
|
||||
logEmailOp('EMAIL_DATES', `First few email dates before processing:`,
|
||||
data.emails.slice(0, 5).map((e: any) => ({
|
||||
id: e.id.substring(0, 8),
|
||||
subject: e.subject?.substring(0, 20),
|
||||
date: e.date,
|
||||
dateObj: new Date(e.date),
|
||||
timestamp: new Date(e.date).getTime()
|
||||
}))
|
||||
);
|
||||
// If we're already at page 1, just update the state with no emails
|
||||
if (!isLoadMore) {
|
||||
logEmailOp('EMPTY_RESULTS', `No emails found in ${state.currentFolder}`);
|
||||
dispatch({ type: 'SET_EMAILS', payload: [] });
|
||||
dispatch({ type: 'SET_TOTAL_EMAILS', payload: 0 });
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: 0 });
|
||||
} else {
|
||||
// For load more, just set loading to false but keep existing emails
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure all emails have proper account ID and folder format
|
||||
if (Array.isArray(data.emails)) {
|
||||
// Log email dates for debugging
|
||||
if (data.emails.length > 0) {
|
||||
logEmailOp('EMAIL_DATES', `First few email dates before processing:`,
|
||||
data.emails.slice(0, 5).map((e: any) => ({
|
||||
id: e.id.substring(0, 8),
|
||||
subject: e.subject?.substring(0, 20),
|
||||
date: e.date,
|
||||
dateObj: new Date(e.date),
|
||||
timestamp: new Date(e.date).getTime()
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
data.emails.forEach((email: Email) => {
|
||||
// If email doesn't have an accountId, set it to the effective one
|
||||
if (!email.accountId) {
|
||||
email.accountId = effectiveAccountId;
|
||||
}
|
||||
|
||||
data.emails.forEach((email: Email) => {
|
||||
// If email doesn't have an accountId, set it to the effective one
|
||||
if (!email.accountId) {
|
||||
email.accountId = effectiveAccountId;
|
||||
}
|
||||
|
||||
// Ensure folder has the proper prefix format
|
||||
if (email.folder && !email.folder.includes(':')) {
|
||||
email.folder = `${email.accountId}:${email.folder}`;
|
||||
}
|
||||
|
||||
// Ensure date is a valid Date object (handle strings or timestamps)
|
||||
if (email.date && !(email.date instanceof Date)) {
|
||||
try {
|
||||
// Convert to a proper Date object if it's a string or number
|
||||
const dateObj = new Date(email.date);
|
||||
// Verify it's a valid date
|
||||
if (!isNaN(dateObj.getTime())) {
|
||||
email.date = dateObj;
|
||||
}
|
||||
} catch (err) {
|
||||
// If conversion fails, log and use current date as fallback
|
||||
console.error(`Invalid date format for email ${email.id}: ${email.date}`);
|
||||
email.date = new Date();
|
||||
// Ensure folder has the proper prefix format
|
||||
if (email.folder && !email.folder.includes(':')) {
|
||||
email.folder = `${email.accountId}:${email.folder}`;
|
||||
}
|
||||
|
||||
// Ensure date is a valid Date object (handle strings or timestamps)
|
||||
if (email.date && !(email.date instanceof Date)) {
|
||||
try {
|
||||
// Convert to a proper Date object if it's a string or number
|
||||
const dateObj = new Date(email.date);
|
||||
// Verify it's a valid date
|
||||
if (!isNaN(dateObj.getTime())) {
|
||||
email.date = dateObj;
|
||||
}
|
||||
} catch (err) {
|
||||
// If conversion fails, log and use current date as fallback
|
||||
console.error(`Invalid date format for email ${email.id}: ${email.date}`);
|
||||
email.date = new Date();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Log what we're about to do
|
||||
console.log(`[DEBUG-DISPATCH] About to dispatch ${isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS'} with ${data.emails?.length || 0} emails`);
|
||||
|
||||
// Update state with fetched data
|
||||
dispatch({
|
||||
type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS',
|
||||
payload: Array.isArray(data.emails) ? data.emails : []
|
||||
}
|
||||
});
|
||||
|
||||
// Double-check that we've updated the email list correctly after dispatch
|
||||
setTimeout(() => {
|
||||
console.log(`[DEBUG-AFTER-DISPATCH] Email count is now: ${state.emails.length}, should include the ${data.emails?.length || 0} new emails we just loaded`);
|
||||
}, 0);
|
||||
|
||||
if (data.totalEmails) {
|
||||
dispatch({ type: 'SET_TOTAL_EMAILS', payload: data.totalEmails });
|
||||
}
|
||||
|
||||
if (data.totalPages) {
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: data.totalPages });
|
||||
}
|
||||
|
||||
// Update available mailboxes if provided
|
||||
if (data.mailboxes && data.mailboxes.length > 0) {
|
||||
dispatch({ type: 'SET_MAILBOXES', payload: data.mailboxes });
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Log what we're about to do
|
||||
console.log(`[DEBUG-DISPATCH] About to dispatch ${isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS'} with ${data.emails?.length || 0} emails`);
|
||||
|
||||
// Update state with fetched data
|
||||
dispatch({
|
||||
type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS',
|
||||
payload: Array.isArray(data.emails) ? data.emails : []
|
||||
});
|
||||
|
||||
// Double-check that we've updated the email list correctly after dispatch
|
||||
setTimeout(() => {
|
||||
console.log(`[DEBUG-AFTER-DISPATCH] Email count is now: ${state.emails.length}, should include the ${data.emails?.length || 0} new emails we just loaded`);
|
||||
}, 0);
|
||||
|
||||
if (data.totalEmails) {
|
||||
dispatch({ type: 'SET_TOTAL_EMAILS', payload: data.totalEmails });
|
||||
}
|
||||
|
||||
if (data.totalPages) {
|
||||
dispatch({ type: 'SET_TOTAL_PAGES', payload: data.totalPages });
|
||||
}
|
||||
|
||||
// Update available mailboxes if provided
|
||||
if (data.mailboxes && data.mailboxes.length > 0) {
|
||||
dispatch({ type: 'SET_MAILBOXES', payload: data.mailboxes });
|
||||
}
|
||||
} catch (err) {
|
||||
logEmailOp('ERROR', `Failed to load emails: ${err instanceof Error ? err.message : String(err)}`);
|
||||
@ -646,11 +355,9 @@ export const useEmailState = () => {
|
||||
|
||||
if (existingEmail && existingEmail.contentFetched) {
|
||||
// Use the existing email if it has content already
|
||||
// ENHANCEMENT: Apply content normalization before selecting the email
|
||||
const normalizedEmail = normalizeEmailContent(existingEmail);
|
||||
dispatch({
|
||||
type: 'SELECT_EMAIL',
|
||||
payload: { emailId, accountId, folder, email: normalizedEmail }
|
||||
payload: { emailId, accountId, folder, email: existingEmail }
|
||||
});
|
||||
|
||||
// Mark as read if not already
|
||||
@ -679,13 +386,10 @@ export const useEmailState = () => {
|
||||
// Mark the email as read on the server
|
||||
markEmailAsRead(emailId, true, effectiveAccountId);
|
||||
|
||||
// ENHANCEMENT: Apply content normalization before selecting the email
|
||||
const normalizedEmailData = normalizeEmailContent(emailData);
|
||||
|
||||
// Select the email
|
||||
dispatch({
|
||||
type: 'SELECT_EMAIL',
|
||||
payload: { emailId, accountId: effectiveAccountId, folder, email: normalizedEmailData }
|
||||
payload: { emailId, accountId: effectiveAccountId, folder, email: emailData }
|
||||
});
|
||||
} catch (error) {
|
||||
logEmailOp('ERROR', `Failed to select email: ${error instanceof Error ? error.message : String(error)}`);
|
||||
@ -696,7 +400,7 @@ export const useEmailState = () => {
|
||||
} finally {
|
||||
dispatch({ type: 'SET_LOADING', payload: false });
|
||||
}
|
||||
}, [state.emails, logEmailOp, normalizeEmailContent]);
|
||||
}, [state.emails, logEmailOp]);
|
||||
|
||||
// Toggle email selection for multi-select
|
||||
const toggleEmailSelection = useCallback((emailId: string) => {
|
||||
|
||||
@ -718,32 +718,9 @@ export async function getEmailContent(
|
||||
// Convert flags from Set to boolean checks
|
||||
const flagsArray = Array.from(flags as Set<string>);
|
||||
|
||||
// Process the raw HTML with CID attachments
|
||||
// Preserve the raw HTML exactly as it was in the original email
|
||||
const rawHtml = parsedEmail.html || '';
|
||||
|
||||
// Import processHtmlContent if needed
|
||||
const { processHtmlContent } = await import('../utils/email-content');
|
||||
|
||||
// Process HTML content with attachments for CID image handling
|
||||
let processedHtml = rawHtml;
|
||||
let direction = 'ltr';
|
||||
|
||||
if (rawHtml) {
|
||||
const processed = processHtmlContent(rawHtml, {
|
||||
sanitize: true,
|
||||
blockExternalContent: false,
|
||||
attachments: parsedEmail.attachments?.map(att => ({
|
||||
filename: att.filename || 'attachment',
|
||||
contentType: att.contentType,
|
||||
content: att.content?.toString('base64'), // Convert Buffer to base64 string
|
||||
contentId: att.contentId
|
||||
}))
|
||||
});
|
||||
|
||||
processedHtml = processed.sanitizedContent;
|
||||
direction = processed.direction;
|
||||
}
|
||||
|
||||
const email: EmailMessage = {
|
||||
id: emailId,
|
||||
messageId: envelope.messageId,
|
||||
@ -776,15 +753,13 @@ export async function getEmailContent(
|
||||
attachments: parsedEmail.attachments?.map(att => ({
|
||||
filename: att.filename || 'attachment',
|
||||
contentType: att.contentType,
|
||||
contentId: att.contentId,
|
||||
content: att.content?.toString('base64'),
|
||||
size: att.size || 0
|
||||
})),
|
||||
content: {
|
||||
text: parsedEmail.text || '',
|
||||
html: processedHtml || '',
|
||||
isHtml: !!processedHtml,
|
||||
direction
|
||||
html: rawHtml || '',
|
||||
isHtml: !!rawHtml,
|
||||
direction: 'ltr' // Default to left-to-right
|
||||
},
|
||||
folder: normalizedFolder,
|
||||
contentFetched: true,
|
||||
|
||||
@ -11,103 +11,64 @@ import DOMPurify from 'isomorphic-dompurify';
|
||||
// Reset any existing hooks to start with a clean slate
|
||||
DOMPurify.removeAllHooks();
|
||||
|
||||
/**
|
||||
* Configure DOMPurify with safe defaults for email content
|
||||
* This balances security with the need to display rich email content
|
||||
*/
|
||||
export function configureDOMPurify() {
|
||||
// Enhanced configuration for email content
|
||||
DOMPurify.setConfig({
|
||||
ADD_TAGS: [
|
||||
// SVG elements for simple charts/logos that might be in emails
|
||||
'svg', 'path', 'g', 'circle', 'rect', 'line', 'polygon', 'ellipse',
|
||||
// Common email-specific elements
|
||||
'o:p', 'font',
|
||||
// Allow comments for conditional HTML in emails
|
||||
'!--...--'
|
||||
],
|
||||
ADD_ATTR: [
|
||||
// SVG attributes
|
||||
'viewbox', 'd', 'cx', 'cy', 'r', 'fill', 'stroke', 'stroke-width', 'x', 'y', 'width', 'height',
|
||||
// Additional HTML attributes commonly used in emails
|
||||
'align', 'valign', 'bgcolor', 'color', 'cellpadding', 'cellspacing', 'colspan', 'rowspan',
|
||||
'face', 'size', 'direction', 'role', 'aria-label', 'aria-hidden',
|
||||
// List attributes
|
||||
'start', 'type', 'value',
|
||||
// Table attributes and styles
|
||||
'border', 'frame', 'rules', 'summary', 'headers', 'scope', 'abbr',
|
||||
// Blockquote attributes
|
||||
'cite', 'datetime',
|
||||
// Form elements attributes (read-only)
|
||||
'readonly', 'disabled', 'selected', 'checked', 'multiple', 'wrap',
|
||||
// Additional attributes for forwarded emails
|
||||
'style', 'class', 'id', 'dir', 'lang', 'title',
|
||||
// Table attributes commonly used in email clients
|
||||
'background', 'bordercolor', 'width', 'height'
|
||||
],
|
||||
FORBID_TAGS: [
|
||||
// Remove dangerous tags
|
||||
'script', 'object', 'iframe', 'embed', 'applet', 'meta', 'link',
|
||||
// Form elements that could be used for phishing
|
||||
'form', 'button', 'input', 'textarea', 'select', 'option'
|
||||
],
|
||||
FORBID_ATTR: [
|
||||
// Remove JavaScript and dangerous attributes
|
||||
'onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onmouseenter', 'onmouseleave',
|
||||
'onkeydown', 'onkeypress', 'onkeyup', 'onchange', 'onsubmit', 'onreset', 'onselect', 'onblur',
|
||||
'onfocus', 'onscroll', 'onbeforeunload', 'onunload', 'onhashchange', 'onpopstate', 'onpageshow',
|
||||
'onpagehide', 'onabort', 'oncanplay', 'oncanplaythrough', 'ondurationchange', 'onemptied',
|
||||
'onended', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying',
|
||||
'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled', 'onsuspend', 'ontimeupdate',
|
||||
'onvolumechange', 'onwaiting', 'animationend', 'animationiteration', 'animationstart',
|
||||
// Dangerous attributes
|
||||
'formaction', 'xlink:href'
|
||||
],
|
||||
ALLOW_DATA_ATTR: false, // Disable data-* attributes which can be used for XSS
|
||||
WHOLE_DOCUMENT: false, // Don't parse the entire document - just fragments
|
||||
SANITIZE_DOM: true, // Sanitize the DOM to prevent XSS
|
||||
KEEP_CONTENT: true, // Keep content of elements that are removed
|
||||
RETURN_DOM: false, // Return a DOM object rather than HTML string
|
||||
RETURN_DOM_FRAGMENT: false, // Return a DocumentFragment rather than HTML string
|
||||
FORCE_BODY: false, // Add a <body> tag if one doesn't exist
|
||||
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|data|irc):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true, // Some email clients use custom protocols for images/attachments
|
||||
USE_PROFILES: { html: true } // Use the HTML profile for more permissive sanitization
|
||||
});
|
||||
|
||||
return DOMPurify;
|
||||
}
|
||||
|
||||
// Singleton instance of configured DOMPurify for the app
|
||||
export const purify = configureDOMPurify();
|
||||
// Configure DOMPurify with settings appropriate for email content
|
||||
DOMPurify.setConfig({
|
||||
ADD_TAGS: [
|
||||
'html', 'head', 'body', 'style', 'link', 'meta', 'title',
|
||||
'table', 'caption', 'col', 'colgroup', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
|
||||
'div', 'span', 'img', 'br', 'hr', 'section', 'article', 'header', 'footer',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre', 'code',
|
||||
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a', 'b', 'i', 'u', 'em',
|
||||
'strong', 'del', 'ins', 'mark', 'small', 'sub', 'sup', 'q', 'abbr',
|
||||
'font' // Allow legacy font tag often found in emails
|
||||
],
|
||||
ADD_ATTR: [
|
||||
'style', 'class', 'id', 'name', 'href', 'src', 'alt', 'title', 'width', 'height',
|
||||
'border', 'cellspacing', 'cellpadding', 'bgcolor', 'background', 'color',
|
||||
'align', 'valign', 'dir', 'lang', 'target', 'rel', 'charset', 'media',
|
||||
'colspan', 'rowspan', 'scope', 'span', 'size', 'face', 'hspace', 'vspace',
|
||||
'data-*',
|
||||
'start', 'type', 'value', 'cite', 'datetime', 'wrap', 'summary'
|
||||
],
|
||||
KEEP_CONTENT: true,
|
||||
WHOLE_DOCUMENT: false,
|
||||
ALLOW_DATA_ATTR: true,
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true, // Needed for some email clients
|
||||
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'select', 'textarea'],
|
||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout'],
|
||||
FORCE_BODY: false,
|
||||
USE_PROFILES: { html: true } // Use HTML profile for more permissive sanitization for emails
|
||||
});
|
||||
|
||||
/**
|
||||
* Sanitize HTML content using our email-specific configuration
|
||||
* Sanitizes HTML content with the centralized DOMPurify configuration
|
||||
* @param html HTML content to sanitize
|
||||
* @returns Sanitized HTML
|
||||
*/
|
||||
export function sanitizeHtml(content: string, options?: { preserveReplyFormat?: boolean }): string {
|
||||
if (!content) return '';
|
||||
export function sanitizeHtml(html: string): string {
|
||||
if (!html) return '';
|
||||
|
||||
try {
|
||||
// Special handling for reply/forward emails to be less aggressive with sanitization
|
||||
const extraTags = options?.preserveReplyFormat
|
||||
? ['style', 'blockquote', 'table', 'thead', 'tbody', 'tr', 'td', 'th']
|
||||
: ['style'];
|
||||
|
||||
const extraAttrs = options?.preserveReplyFormat
|
||||
? ['style', 'class', 'align', 'valign', 'bgcolor', 'colspan', 'rowspan', 'width', 'height', 'border']
|
||||
: ['style', 'class'];
|
||||
|
||||
// Sanitize with our configured instance and options
|
||||
return purify.sanitize(content, {
|
||||
ADD_TAGS: extraTags,
|
||||
ADD_ATTR: extraAttrs
|
||||
// Use DOMPurify with our central configuration
|
||||
const clean = DOMPurify.sanitize(html, {
|
||||
ADD_ATTR: ['style', 'class', 'id', 'align', 'valign', 'colspan', 'rowspan', 'cellspacing', 'cellpadding', 'bgcolor']
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to sanitize HTML content:', error);
|
||||
// Fallback to basic sanitization
|
||||
return content
|
||||
|
||||
// Fix common email rendering issues
|
||||
const fixedHtml = clean
|
||||
// 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|data|https|cid)/gi, '$1="https://');
|
||||
|
||||
return fixedHtml;
|
||||
} catch (e) {
|
||||
console.error('Error sanitizing HTML:', e);
|
||||
// Fall back to a basic sanitization approach
|
||||
return html
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/javascript:/gi, 'blocked:');
|
||||
.replace(/on\w+="[^"]*"/g, '')
|
||||
.replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,23 +13,20 @@
|
||||
import { sanitizeHtml } from './dom-purify-config';
|
||||
import { detectTextDirection } from './text-direction';
|
||||
import { EmailContent } from '@/types/email';
|
||||
import { processCidReferences } from './email-utils';
|
||||
|
||||
/**
|
||||
* Extract content from various possible email formats
|
||||
* Centralized implementation to reduce duplication across the codebase
|
||||
*/
|
||||
export function extractEmailContent(email: any): { text: string; html: string; isHtml: boolean; direction: 'ltr' | 'rtl'; } {
|
||||
export function extractEmailContent(email: any): { text: string; html: string } {
|
||||
// Default empty values
|
||||
let textContent = '';
|
||||
let htmlContent = '';
|
||||
let isHtml = false;
|
||||
let direction: 'ltr' | 'rtl' = 'ltr';
|
||||
|
||||
// Early exit if no email
|
||||
if (!email) {
|
||||
console.log('extractEmailContent: No email provided');
|
||||
return { text: '', html: '', isHtml: false, direction: 'ltr' };
|
||||
return { text: '', html: '' };
|
||||
}
|
||||
|
||||
try {
|
||||
@ -38,8 +35,6 @@ export function extractEmailContent(email: any): { text: string; html: string; i
|
||||
// Standard format with content object
|
||||
textContent = email.content.text || '';
|
||||
htmlContent = email.content.html || '';
|
||||
isHtml = email.content.isHtml || !!htmlContent;
|
||||
direction = email.content.direction || 'ltr';
|
||||
|
||||
// Handle complex email formats where content might be nested
|
||||
if (!textContent && !htmlContent) {
|
||||
@ -49,17 +44,13 @@ export function extractEmailContent(email: any): { text: string; html: string; i
|
||||
// Determine if body is HTML or text
|
||||
if (isHtmlContent(email.content.body)) {
|
||||
htmlContent = email.content.body;
|
||||
isHtml = true;
|
||||
} else {
|
||||
textContent = email.content.body;
|
||||
isHtml = false;
|
||||
}
|
||||
} 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 || '';
|
||||
isHtml = email.content.body.isHtml || !!htmlContent;
|
||||
direction = email.content.body.direction || 'ltr';
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,10 +60,8 @@ export function extractEmailContent(email: any): { text: string; html: string; i
|
||||
// Check if data looks like HTML
|
||||
if (isHtmlContent(email.content.data)) {
|
||||
htmlContent = email.content.data;
|
||||
isHtml = true;
|
||||
} else {
|
||||
textContent = email.content.data;
|
||||
isHtml = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,25 +70,19 @@ export function extractEmailContent(email: any): { text: string; html: string; i
|
||||
// Check if content is likely HTML
|
||||
if (isHtmlContent(email.content)) {
|
||||
htmlContent = email.content;
|
||||
isHtml = true;
|
||||
} else {
|
||||
textContent = email.content;
|
||||
isHtml = false;
|
||||
}
|
||||
} else {
|
||||
// Check other common properties
|
||||
htmlContent = email.html || '';
|
||||
textContent = email.text || '';
|
||||
isHtml = email.isHtml || !!htmlContent;
|
||||
direction = email.direction || 'ltr';
|
||||
|
||||
// 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 || '';
|
||||
isHtml = email.body?.isHtml || !!htmlContent;
|
||||
direction = email.body?.direction || 'ltr';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -116,12 +99,10 @@ export function extractEmailContent(email: any): { text: string; html: string; i
|
||||
hasHtml: !!htmlContent,
|
||||
htmlLength: htmlContent?.length || 0,
|
||||
hasText: !!textContent,
|
||||
textLength: textContent?.length || 0,
|
||||
isHtml,
|
||||
direction
|
||||
textLength: textContent?.length || 0
|
||||
});
|
||||
|
||||
return { text: textContent, html: htmlContent, isHtml, direction };
|
||||
return { text: textContent, html: htmlContent };
|
||||
}
|
||||
|
||||
/**
|
||||
@ -179,30 +160,28 @@ export function formatEmailContent(email: any): string {
|
||||
|
||||
try {
|
||||
// Extract content from email
|
||||
const { text, html, isHtml, direction } = extractEmailContent(email);
|
||||
const { text, html } = extractEmailContent(email);
|
||||
|
||||
console.log('formatEmailContent processing:', {
|
||||
hasHtml: !!html,
|
||||
htmlLength: html?.length || 0,
|
||||
hasText: !!text,
|
||||
textLength: text?.length || 0,
|
||||
emailType: typeof email === 'string' ? 'string' : 'object',
|
||||
isHtml,
|
||||
direction
|
||||
emailType: typeof email === 'string' ? 'string' : 'object'
|
||||
});
|
||||
|
||||
// If we have HTML content, sanitize and standardize it
|
||||
if (html) {
|
||||
// Process HTML content
|
||||
const processed = processHtmlContent(html, { sanitize: true });
|
||||
let processedHtml = processHtmlContent(html, text);
|
||||
|
||||
console.log('HTML content processed:', {
|
||||
processedLength: processed.sanitizedContent?.length || 0,
|
||||
isEmpty: !processed.sanitizedContent || processed.sanitizedContent.trim().length === 0
|
||||
processedLength: processedHtml?.length || 0,
|
||||
isEmpty: !processedHtml || processedHtml.trim().length === 0
|
||||
});
|
||||
|
||||
// Apply styling
|
||||
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="${processed.direction}">${processed.sanitizedContent}</div>`;
|
||||
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>`;
|
||||
}
|
||||
// If we only have text content, format it properly
|
||||
else if (text) {
|
||||
@ -219,142 +198,164 @@ export function formatEmailContent(email: any): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Process HTML content to ensure safe rendering and proper formatting
|
||||
* Process HTML content to fix common email rendering issues
|
||||
*/
|
||||
export function processHtmlContent(
|
||||
htmlContent: string,
|
||||
options?: {
|
||||
sanitize?: boolean;
|
||||
blockExternalContent?: boolean;
|
||||
preserveReplyFormat?: boolean;
|
||||
attachments?: Array<{
|
||||
filename?: string;
|
||||
name?: string;
|
||||
contentType?: string;
|
||||
content?: string;
|
||||
contentId?: string;
|
||||
}>;
|
||||
} | string // Support for legacy textContent parameter
|
||||
): {
|
||||
sanitizedContent: string;
|
||||
hasImages: boolean;
|
||||
hasExternalContent: boolean;
|
||||
direction: 'ltr' | 'rtl';
|
||||
} {
|
||||
// Handle legacy string parameter (textContent)
|
||||
if (typeof options === 'string') {
|
||||
options = { sanitize: true };
|
||||
}
|
||||
|
||||
console.log('Processing HTML content:', {
|
||||
contentLength: htmlContent?.length || 0,
|
||||
startsWithHtml: htmlContent?.startsWith('<html'),
|
||||
startsWithDiv: htmlContent?.startsWith('<div'),
|
||||
containsForwardedMessage: htmlContent?.includes('---------- Forwarded message ----------'),
|
||||
containsQuoteHeader: htmlContent?.includes('<div class="gmail_quote"'),
|
||||
sanitize: options?.sanitize,
|
||||
preserveReplyFormat: options?.preserveReplyFormat,
|
||||
blockExternalContent: options?.blockExternalContent,
|
||||
hasAttachments: options?.attachments?.length || 0
|
||||
});
|
||||
|
||||
if (!htmlContent) {
|
||||
return {
|
||||
sanitizedContent: '',
|
||||
hasImages: false,
|
||||
hasExternalContent: false,
|
||||
direction: 'ltr',
|
||||
};
|
||||
}
|
||||
|
||||
// Store the original content for comparison
|
||||
const originalContent = htmlContent;
|
||||
|
||||
// Process CID references before sanitization
|
||||
if (options?.attachments?.length) {
|
||||
console.log('Processing CID references in processHtmlContent');
|
||||
htmlContent = processCidReferences(htmlContent, options.attachments);
|
||||
}
|
||||
|
||||
export function processHtmlContent(htmlContent: string, textContent?: string): string {
|
||||
if (!htmlContent) return '';
|
||||
|
||||
try {
|
||||
// Special handling for reply/forwarded content with less aggressive sanitization
|
||||
const isReplyOrForward = options?.preserveReplyFormat === true;
|
||||
|
||||
// Apply sanitization by default unless explicitly turned off
|
||||
let sanitizedContent = (options?.sanitize !== false)
|
||||
? sanitizeHtml(htmlContent, { preserveReplyFormat: isReplyOrForward })
|
||||
: htmlContent;
|
||||
|
||||
// Log content changes from sanitization
|
||||
console.log('HTML sanitization results:', {
|
||||
originalLength: originalContent.length,
|
||||
sanitizedLength: sanitizedContent.length,
|
||||
difference: originalContent.length - sanitizedContent.length,
|
||||
percentRemoved: ((originalContent.length - sanitizedContent.length) / originalContent.length * 100).toFixed(2) + '%',
|
||||
isEmpty: !sanitizedContent || sanitizedContent.trim().length === 0,
|
||||
isReplyOrForward: isReplyOrForward
|
||||
console.log('processHtmlContent input:', {
|
||||
length: htmlContent.length,
|
||||
startsWithHtml: htmlContent.trim().startsWith('<html'),
|
||||
startsWithDiv: htmlContent.trim().startsWith('<div'),
|
||||
hasBody: htmlContent.includes('<body'),
|
||||
containsForwardedMessage: htmlContent.includes('---------- Forwarded message ----------'),
|
||||
containsQuoteHeader: htmlContent.includes('wrote:'),
|
||||
hasBlockquote: htmlContent.includes('<blockquote'),
|
||||
hasTable: htmlContent.includes('<table')
|
||||
});
|
||||
|
||||
// Detect if content is a forwarded message to ensure special handling for tables
|
||||
const isForwardedEmail =
|
||||
sanitizedContent.includes('---------- Forwarded message ----------') ||
|
||||
sanitizedContent.includes('Forwarded message') ||
|
||||
(sanitizedContent.includes('From:') && sanitizedContent.includes('Date:') &&
|
||||
sanitizedContent.includes('Subject:') && sanitizedContent.includes('To:'));
|
||||
// Check for browser environment (DOMParser is browser-only)
|
||||
const hasHtmlTag = htmlContent.includes('<html');
|
||||
const hasBodyTag = htmlContent.includes('<body');
|
||||
|
||||
// Special processing for forwarded email styling
|
||||
if (isForwardedEmail || isReplyOrForward) {
|
||||
console.log('Detected forwarded email or reply content, enhancing structure');
|
||||
// Make sure we're not removing important table structures
|
||||
sanitizedContent = sanitizedContent
|
||||
// Preserve table styling for email headers
|
||||
.replace(/<table([^>]*)>/g, '<table$1 style="margin: 10px 0; border-collapse: collapse; font-size: 13px; color: #333;">')
|
||||
.replace(/<td([^>]*)>/g, '<td$1 style="padding: 3px 5px; vertical-align: top;">')
|
||||
// Ensure blockquote styling is preserved
|
||||
.replace(/<blockquote([^>]*)>/g, '<blockquote$1 style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">');
|
||||
// Preserve original HTML for debugging
|
||||
let originalHtml = htmlContent;
|
||||
|
||||
// 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(htmlContent, 'text/html');
|
||||
const bodyContent = doc.body.innerHTML;
|
||||
|
||||
if (bodyContent) {
|
||||
console.log('Extracted body content from HTML document, length:', bodyContent.length);
|
||||
htmlContent = bodyContent;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error extracting body content:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Use the centralized sanitizeHtml function
|
||||
let sanitizedContent = sanitizeHtml(htmlContent);
|
||||
|
||||
console.log('After sanitizeHtml:', {
|
||||
originalLength: originalHtml.length,
|
||||
sanitizedLength: sanitizedContent.length,
|
||||
difference: originalHtml.length - sanitizedContent.length,
|
||||
percentRemoved: ((originalHtml.length - sanitizedContent.length) / originalHtml.length * 100).toFixed(2) + '%',
|
||||
containsForwardedMessage: sanitizedContent.includes('---------- Forwarded message ----------'),
|
||||
hasTable: sanitizedContent.includes('<table'),
|
||||
hasBlockquote: sanitizedContent.includes('<blockquote')
|
||||
});
|
||||
|
||||
// Fix URL encoding issues and clean up content
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||
// Temporary element to manipulate the HTML
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = sanitizedContent;
|
||||
|
||||
// Fix all links that might have been double-encoded
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fix image URLs - preserve cid: URLs for email attachments
|
||||
const images = tempDiv.querySelectorAll('img');
|
||||
images.forEach(img => {
|
||||
const src = img.getAttribute('src');
|
||||
if (src) {
|
||||
// Don't modify cid: URLs as they are handled specially in email clients
|
||||
if (src.startsWith('cid:')) {
|
||||
// Keep cid: URLs as they are
|
||||
console.log('Preserving CID reference:', src);
|
||||
}
|
||||
// Fix http:// URLs to https:// for security
|
||||
else if (src.startsWith('http://')) {
|
||||
img.setAttribute('src', src.replace('http://', 'https://'));
|
||||
}
|
||||
// Handle relative URLs that might be broken
|
||||
else if (!src.startsWith('https://') && !src.startsWith('data:')) {
|
||||
if (src.startsWith('/')) {
|
||||
img.setAttribute('src', `https://example.com${src}`);
|
||||
} else {
|
||||
img.setAttribute('src', `https://example.com/${src}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clean up excessive whitespace and empty elements
|
||||
// Find all text nodes and normalize whitespace
|
||||
const walker = document.createTreeWalker(
|
||||
tempDiv,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null
|
||||
);
|
||||
|
||||
const textNodes = [];
|
||||
while (walker.nextNode()) {
|
||||
textNodes.push(walker.currentNode);
|
||||
}
|
||||
|
||||
// Process text nodes to normalize whitespace
|
||||
textNodes.forEach(node => {
|
||||
if (node.nodeValue) {
|
||||
// Replace sequences of whitespace with a single space
|
||||
node.nodeValue = node.nodeValue.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Remove empty paragraphs and divs that contain only whitespace
|
||||
const emptyElements = tempDiv.querySelectorAll('p, div, span');
|
||||
emptyElements.forEach(el => {
|
||||
if (el.innerHTML.trim() === '' || el.innerHTML === ' ') {
|
||||
el.parentNode?.removeChild(el);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove excessive consecutive <br> tags (more than 2)
|
||||
let html = tempDiv.innerHTML;
|
||||
html = html.replace(/(<br\s*\/?>\s*){3,}/gi, '<br><br>');
|
||||
tempDiv.innerHTML = html;
|
||||
|
||||
// Get the fixed HTML
|
||||
sanitizedContent = tempDiv.innerHTML;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fixing content:', e);
|
||||
}
|
||||
|
||||
// Fix common email client quirks without breaking cid: URLs
|
||||
sanitizedContent = sanitizedContent
|
||||
return sanitizedContent
|
||||
// Fix for Outlook WebVML content
|
||||
.replace(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
|
||||
// Fix for broken image paths starting with // (add https:)
|
||||
.replace(/src="\/\//g, 'src="https://')
|
||||
// Handle mixed content issues by converting http:// to https://
|
||||
.replace(/src="http:\/\//g, 'src="https://')
|
||||
// Fix email signature line breaks
|
||||
.replace(/--<br>/g, '<hr style="border-top: 1px solid #ccc; margin: 15px 0;">')
|
||||
.replace(/-- <br>/g, '<hr style="border-top: 1px solid #ccc; margin: 15px 0;">')
|
||||
// Fix for broken image paths WITHOUT replacing cid: URLs
|
||||
.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, '')}"`;
|
||||
})
|
||||
// Remove excessive whitespace from the HTML string itself
|
||||
.replace(/>\s+</g, '> <');
|
||||
|
||||
// Additional processing for quoted content in replies/forwards
|
||||
if (sanitizedContent.includes('blockquote')) {
|
||||
console.log('Enhancing blockquote styling');
|
||||
sanitizedContent = sanitizedContent
|
||||
// Ensure blockquotes have proper styling
|
||||
.replace(/<blockquote([^>]*)>/g, (match, attrs) => {
|
||||
if (match.includes('style=')) {
|
||||
return match; // Already has style
|
||||
}
|
||||
return `<blockquote${attrs} style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">`;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
sanitizedContent,
|
||||
hasImages: sanitizedContent.includes('<img'),
|
||||
hasExternalContent: sanitizedContent.includes('https://'),
|
||||
direction: detectTextDirection(sanitizedContent)
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing HTML content:', error);
|
||||
return {
|
||||
sanitizedContent: htmlContent,
|
||||
hasImages: false,
|
||||
hasExternalContent: false,
|
||||
direction: 'ltr',
|
||||
};
|
||||
return htmlContent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -324,26 +324,14 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
|
||||
|
||||
// Create the quoted reply content
|
||||
if (htmlContent) {
|
||||
// Format HTML reply with better styling for quoted content
|
||||
// Format HTML reply
|
||||
console.log('Formatting HTML reply, quoted content length:', htmlContent.length);
|
||||
|
||||
// Apply minimal sanitization to the original content - preserve more structure
|
||||
// We'll do a more comprehensive sanitization later in the flow
|
||||
const sanitizedOriginal = sanitizeHtml(htmlContent, { preserveReplyFormat: true });
|
||||
|
||||
// Check if original content already contains blockquotes or is reply/forward
|
||||
const containsExistingQuote =
|
||||
sanitizedOriginal.includes('<blockquote') ||
|
||||
sanitizedOriginal.includes('wrote:') ||
|
||||
sanitizedOriginal.includes('---------- Forwarded message ----------');
|
||||
|
||||
// Preserve existing quotes and add outer quote
|
||||
htmlContent = `
|
||||
<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
|
||||
On ${date}, ${sender} wrote:
|
||||
</div>
|
||||
<blockquote style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">
|
||||
${sanitizedOriginal}
|
||||
${sanitizeHtml(htmlContent)}
|
||||
</blockquote>
|
||||
`;
|
||||
}
|
||||
@ -393,103 +381,11 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process and replace CID references with base64 data URLs using the email's attachments.
|
||||
* This function should be called before sanitizing the content.
|
||||
*/
|
||||
export function processCidReferences(htmlContent: string, attachments?: Array<{
|
||||
filename?: string;
|
||||
name?: string;
|
||||
contentType?: string;
|
||||
content?: string;
|
||||
contentId?: string;
|
||||
}>): string {
|
||||
if (!htmlContent || !attachments || !attachments.length) {
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
console.log(`Processing CID references with ${attachments.length} attachments available`);
|
||||
|
||||
try {
|
||||
// Create a map of content IDs to their attachment data
|
||||
const cidMap = new Map();
|
||||
attachments.forEach(att => {
|
||||
if (att.contentId) {
|
||||
// Content ID sometimes has <> brackets which need to be removed
|
||||
const cleanCid = att.contentId.replace(/[<>]/g, '');
|
||||
cidMap.set(cleanCid, {
|
||||
contentType: att.contentType || 'application/octet-stream',
|
||||
content: att.content
|
||||
});
|
||||
console.log(`Mapped CID: ${cleanCid} to attachment of type ${att.contentType || 'unknown'}`);
|
||||
}
|
||||
});
|
||||
|
||||
// If we have no content IDs mapped, return original content
|
||||
if (cidMap.size === 0) {
|
||||
console.log('No CID references found in attachments');
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
// Check if we're in a browser environment
|
||||
if (typeof document === 'undefined') {
|
||||
console.log('Not in browser environment, skipping CID processing');
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
// Parse the HTML content and replace CID references
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = htmlContent;
|
||||
|
||||
// Find all images with CID sources
|
||||
const imgElements = tempDiv.querySelectorAll('img[src^="cid:"]');
|
||||
|
||||
console.log(`Found ${imgElements.length} img elements with CID references`);
|
||||
|
||||
if (imgElements.length === 0) {
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
// Process each image with a CID reference
|
||||
let replacedCount = 0;
|
||||
imgElements.forEach(img => {
|
||||
const src = img.getAttribute('src');
|
||||
if (!src || !src.startsWith('cid:')) return;
|
||||
|
||||
// Extract the content ID from the src
|
||||
const cid = src.substring(4); // Remove 'cid:' prefix
|
||||
|
||||
// Find the matching attachment
|
||||
const attachment = cidMap.get(cid);
|
||||
|
||||
if (attachment && attachment.content) {
|
||||
// Convert the attachment content to a data URL
|
||||
const dataUrl = `data:${attachment.contentType};base64,${attachment.content}`;
|
||||
|
||||
// Replace the CID reference with the data URL
|
||||
img.setAttribute('src', dataUrl);
|
||||
replacedCount++;
|
||||
console.log(`Replaced CID ${cid} with data URL`);
|
||||
} else {
|
||||
console.log(`No matching attachment found for CID: ${cid}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Replaced ${replacedCount} CID references with data URLs`);
|
||||
|
||||
// Return the updated HTML content
|
||||
return tempDiv.innerHTML;
|
||||
} catch (error) {
|
||||
console.error('Error processing CID references:', error);
|
||||
return htmlContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format email for forwarding
|
||||
*/
|
||||
export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMessage | null): FormattedEmail {
|
||||
console.log('formatForwardedEmail called, emailId:', originalEmail?.id);
|
||||
console.log('formatForwardedEmail called:', { emailId: originalEmail?.id });
|
||||
|
||||
if (!originalEmail) {
|
||||
console.warn('formatForwardedEmail: No original email provided');
|
||||
@ -508,6 +404,14 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
|
||||
(email.subject.toLowerCase().startsWith('fwd:') ? email.subject : `Fwd: ${email.subject}`) :
|
||||
'Fwd: ';
|
||||
|
||||
// Get original email info for headers
|
||||
const { fromStr, toStr, ccStr, dateStr } = getFormattedHeaderInfo(email);
|
||||
|
||||
console.log('Forward header info:', { fromStr, toStr, dateStr, subject });
|
||||
|
||||
// Original sent date
|
||||
const date = dateStr;
|
||||
|
||||
// Get email content
|
||||
const originalContent = email.content;
|
||||
|
||||
@ -548,66 +452,68 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
|
||||
htmlContent = formatPlainTextToHtml(textContent);
|
||||
}
|
||||
}
|
||||
|
||||
// Get header info for the forwarded message
|
||||
const headerInfo = getFormattedHeaderInfo(email);
|
||||
|
||||
// Create the forwarded content
|
||||
|
||||
// Create the forwarded email HTML content
|
||||
if (htmlContent) {
|
||||
console.log('Formatting HTML forward, content length:', htmlContent.length);
|
||||
console.log('Formatting HTML forward, original content length:', htmlContent.length);
|
||||
|
||||
// Apply minimal sanitization to the original content - preserve more structure
|
||||
// We'll do a more comprehensive sanitization later in the flow
|
||||
const sanitizedOriginal = sanitizeHtml(htmlContent, { preserveReplyFormat: true });
|
||||
// Important: First sanitize the content portion only
|
||||
const sanitizedOriginalContent = sanitizeHtml(htmlContent);
|
||||
console.log('Sanitized original content length:', sanitizedOriginalContent.length);
|
||||
|
||||
// Create forwarded message with header info
|
||||
htmlContent = `
|
||||
// Create the complete forwarded email with header info
|
||||
const fullForwardedEmail = `
|
||||
<div style="margin: 20px 0 10px 0; color: #666; font-family: Arial, sans-serif;">
|
||||
<div style="border-bottom: 1px solid #ccc; margin-bottom: 10px; padding-bottom: 5px;">
|
||||
<div>---------------------------- Forwarded Message ----------------------------</div>
|
||||
</div>
|
||||
<table style="margin-bottom: 10px; font-size: 14px; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">From:</td>
|
||||
<td style="padding: 3px 0;">${headerInfo.fromStr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Date:</td>
|
||||
<td style="padding: 3px 0;">${headerInfo.dateStr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Subject:</td>
|
||||
<td style="padding: 3px 0;">${headerInfo.subject}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">To:</td>
|
||||
<td style="padding: 3px 0;">${headerInfo.toStr}</td>
|
||||
</tr>
|
||||
${headerInfo.ccStr ? `
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Cc:</td>
|
||||
<td style="padding: 3px 0;">${headerInfo.ccStr}</td>
|
||||
</tr>` : ''}
|
||||
---------- Forwarded message ----------<br>
|
||||
<table style="margin: 10px 0 15px 0; border-collapse: collapse; font-size: 13px; color: #333;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">From:</td>
|
||||
<td style="padding: 3px 0;">${fromStr}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Date:</td>
|
||||
<td style="padding: 3px 0;">${date}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Subject:</td>
|
||||
<td style="padding: 3px 0;">${email.subject || ''}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">To:</td>
|
||||
<td style="padding: 3px 0;">${toStr}</td>
|
||||
</tr>
|
||||
${ccStr ? `
|
||||
<tr>
|
||||
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top;">Cc:</td>
|
||||
<td style="padding: 3px 0;">${ccStr}</td>
|
||||
</tr>
|
||||
` : ''}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="border-bottom: 1px solid #ccc; margin-top: 5px; margin-bottom: 15px; padding-bottom: 5px;">
|
||||
<div>----------------------------------------------------------------------</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="forwarded-content" style="margin: 0; color: #333;">
|
||||
${sanitizedOriginal}
|
||||
<div style="padding: 10px 0; border-top: 1px solid #ddd;">
|
||||
${sanitizedOriginalContent}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Now we have the full forwarded email structure without sanitizing it again
|
||||
htmlContent = fullForwardedEmail;
|
||||
|
||||
console.log('Final forward HTML content length:', htmlContent.length,
|
||||
'contains table:', htmlContent.includes('<table'),
|
||||
'contains forwarded message:', htmlContent.includes('---------- Forwarded message ----------'));
|
||||
}
|
||||
|
||||
// Format the plain text version
|
||||
if (textContent) {
|
||||
// Format plain text forward
|
||||
textContent = `
|
||||
---------- Forwarded message ----------
|
||||
From: ${headerInfo.fromStr}
|
||||
Date: ${headerInfo.dateStr}
|
||||
Subject: ${headerInfo.subject}
|
||||
To: ${headerInfo.toStr}
|
||||
${headerInfo.ccStr ? `Cc: ${headerInfo.ccStr}` : ''}
|
||||
From: ${fromStr}
|
||||
Date: ${date}
|
||||
Subject: ${email.subject || ''}
|
||||
To: ${toStr}
|
||||
${ccStr ? `Cc: ${ccStr}\n` : ''}
|
||||
|
||||
${textContent}
|
||||
`.trim();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user