courrier preview

This commit is contained in:
alma 2025-04-30 23:13:14 +02:00
parent ad03dba3bf
commit f9e0b323ce
2 changed files with 126 additions and 85 deletions

View File

@ -79,80 +79,74 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
theme: 'snow', theme: 'snow',
}); });
// Set initial content (sanitized) // Set initial content properly
if (initialContent) { if (initialContent) {
try { try {
// First, ensure we preserve the raw HTML structure console.log('Setting initial content in editor', {
const preservedContent = sanitizeHtml(initialContent); length: initialContent.length,
startsWithHtml: initialContent.trim().startsWith('<')
// Check if there are tables in the content
const hasTables = preservedContent.includes('<table');
// For content with tables, we need special handling
if (hasTables && preserveFormatting && tableModule) {
// First, set the content directly to the root
quillRef.current.root.innerHTML = preservedContent;
// Initialize better table module after content is set
setTimeout(() => {
try {
// Clean up any existing tables first
const tables = quillRef.current.root.querySelectorAll('table');
tables.forEach((table: HTMLTableElement) => {
// Add required data attributes that the module expects
if (!table.getAttribute('data-table')) {
table.setAttribute('data-table', 'true');
}
}); });
// Initialize the module now that content is already in place // Make sure content is properly sanitized before injecting it
const betterTableModule = { const cleanContent = initialContent
operationMenu: { .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '') // Remove scripts
items: { .replace(/on\w+="[^"]*"/g, '') // Remove event handlers
unmergeCells: { .replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:'); // Remove protocol handlers
text: 'Unmerge cells'
}
}
}
};
// Force a refresh // First, directly set the content
quillRef.current.update(); if (editorRef.current) {
editorRef.current.innerHTML = cleanContent;
}
// Ensure the cursor and scroll position is at the top of the editor // Then let Quill parse and format it correctly
setTimeout(() => {
// Only proceed if editor ref is still available
if (!editorRef.current) return;
// Get the content from the editor element
const content = editorRef.current.innerHTML;
// Clear the editor
quillRef.current.setText('');
// Insert clean content
quillRef.current.clipboard.dangerouslyPasteHTML(0, content);
// Set cursor at the beginning (before the quoted content)
quillRef.current.setSelection(0, 0); quillRef.current.setSelection(0, 0);
// Also scroll the container to the top // Ensure the cursor and scroll position is at the top of the editor
if (editorRef.current) { if (editorRef.current) {
editorRef.current.scrollTop = 0; editorRef.current.scrollTop = 0;
// Also find and scroll parent containers that might have scroll // Find and scroll parent containers that might have scroll
const scrollContainer = editorRef.current.closest('.ql-container'); const scrollable = [
if (scrollContainer) { editorRef.current.closest('.ql-container'),
scrollContainer.scrollTop = 0; editorRef.current.closest('.rich-email-editor-container'),
} editorRef.current.closest('.overflow-y-auto'),
document.querySelector('.overflow-y-auto')
];
// One more check for nested scroll containers (like overflow divs) scrollable.forEach(el => {
const parentScrollContainer = editorRef.current.closest('.rich-email-editor-container'); if (el instanceof HTMLElement) {
if (parentScrollContainer) { el.scrollTop = 0;
parentScrollContainer.scrollTop = 0;
} }
} });
} catch (tableErr) {
console.error('Error initializing table module:', tableErr);
} }
}, 100); }, 100);
} else {
// For content without tables, use the standard paste method
quillRef.current.clipboard.dangerouslyPasteHTML(0, preservedContent);
quillRef.current.setSelection(0, 0);
}
} catch (err) { } catch (err) {
console.error('Error setting initial content:', err); console.error('Error setting initial content:', err);
// Fallback method if the above fails // Fallback: just set text
quillRef.current.setText(''); quillRef.current.setText('');
quillRef.current.clipboard.dangerouslyPasteHTML(sanitizeHtml(initialContent));
quillRef.current.setSelection(0, 0); // Try simplest approach
try {
quillRef.current.clipboard.dangerouslyPasteHTML(initialContent);
} catch (e) {
console.error('Fallback failed too:', e);
// Last resort: strip all HTML
quillRef.current.setText(initialContent.replace(/<[^>]*>/g, ''));
}
} }
} }

View File

@ -242,10 +242,23 @@ export function formatForwardedEmail(email: EmailMessage): {
const toString = formatEmailAddresses(email.to || []); const toString = formatEmailAddresses(email.to || []);
const dateString = formatEmailDate(email.date); const dateString = formatEmailDate(email.date);
// Get original content as HTML // Get original content - use the raw content if possible to preserve formatting
const originalContent = email.content.isHtml && email.content.html let originalContent = '';
? email.content.html
: formatPlainTextToHtml(email.content.text); if (email.content) {
if (email.content.isHtml && email.content.html) {
originalContent = email.content.html;
} else if (email.content.text) {
// Format plain text with basic HTML formatting
originalContent = formatPlainTextToHtml(email.content.text);
}
} else if (email.html) {
originalContent = email.html;
} else if (email.text) {
originalContent = formatPlainTextToHtml(email.text);
} else {
originalContent = '<p>No content</p>';
}
// Check if the content already has a forwarded message header // Check if the content already has a forwarded message header
const hasExistingHeader = originalContent.includes('---------- Forwarded message ---------'); const hasExistingHeader = originalContent.includes('---------- Forwarded message ---------');
@ -263,7 +276,7 @@ export function formatForwardedEmail(email: EmailMessage): {
} else { } else {
// Create formatted content for forwarded email // Create formatted content for forwarded email
htmlContent = ` htmlContent = `
<div style="min-height: 20px;"> <div style="min-height: 20px;"></div>
<div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px;"> <div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px;">
<div style="font-family: Arial, sans-serif; color: #333;"> <div style="font-family: Arial, sans-serif; color: #333;">
<div style="margin-bottom: 15px;"> <div style="margin-bottom: 15px;">
@ -278,16 +291,22 @@ export function formatForwardedEmail(email: EmailMessage): {
</div> </div>
</div> </div>
</div> </div>
</div>
`; `;
} }
// Ensure we have clean HTML
const cleanedHtml = DOMPurify.sanitize(htmlContent, {
ADD_TAGS: ['style'],
ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'],
ALLOW_DATA_ATTR: true
});
// Create normalized content with HTML and extracted text // Create normalized content with HTML and extracted text
const content: EmailContent = { const content: EmailContent = {
html: sanitizeHtml(htmlContent), html: cleanedHtml,
text: '', // Will be extracted when composing text: '', // Will be extracted when composing
isHtml: true, isHtml: true,
direction: email.content.direction || 'ltr' direction: email.content?.direction || 'ltr'
}; };
// Extract text from HTML if in browser environment // Extract text from HTML if in browser environment
@ -322,13 +341,13 @@ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all
let cc = undefined; let cc = undefined;
if (type === 'reply-all' && (email.to || email.cc)) { if (type === 'reply-all' && (email.to || email.cc)) {
const allRecipients = [ const allRecipients = [
...(email.to || []), ...(typeof email.to === 'string' ? [{name: '', address: email.to}] : (email.to || [])),
...(email.cc || []) ...(typeof email.cc === 'string' ? [{name: '', address: email.cc}] : (email.cc || []))
]; ];
// Remove duplicates, then convert to string // Remove duplicates, then convert to string
const uniqueRecipients = [...new Map(allRecipients.map(addr => const uniqueRecipients = [...new Map(allRecipients.map(addr =>
[addr.address, addr] [typeof addr === 'string' ? addr : addr.address, addr]
)).values()]; )).values()];
cc = formatEmailAddresses(uniqueRecipients); cc = formatEmailAddresses(uniqueRecipients);
@ -338,14 +357,35 @@ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all
const subjectBase = email.subject || '(No subject)'; const subjectBase = email.subject || '(No subject)';
const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`; const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`;
// Get original content as HTML // Get original content - use the raw content if possible to preserve formatting
const originalContent = email.content.isHtml && email.content.html let originalContent = '';
? email.content.html
: formatPlainTextToHtml(email.content.text); if (email.content) {
if (email.content.isHtml && email.content.html) {
originalContent = email.content.html;
} else if (email.content.text) {
// Format plain text with basic HTML formatting
originalContent = formatPlainTextToHtml(email.content.text);
}
} else if (email.html) {
originalContent = email.html;
} else if (email.text) {
originalContent = formatPlainTextToHtml(email.text);
} else {
originalContent = '<p>No content</p>';
}
// Format sender info // Format sender info
const sender = email.from && email.from.length > 0 ? email.from[0] : undefined; let senderName = 'Unknown Sender';
const senderName = sender ? (sender.name || sender.address) : 'Unknown Sender'; if (email.from) {
if (Array.isArray(email.from) && email.from.length > 0) {
const sender = email.from[0];
senderName = typeof sender === 'string' ? sender : (sender.name || sender.address);
} else if (typeof email.from === 'string') {
senderName = email.from;
}
}
const formattedDate = formatEmailDate(email.date); const formattedDate = formatEmailDate(email.date);
// Create the reply content with attribution line // Create the reply content with attribution line
@ -361,12 +401,19 @@ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all
</div> </div>
`; `;
// Ensure we have clean HTML
const cleanedHtml = DOMPurify.sanitize(htmlContent, {
ADD_TAGS: ['style'],
ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'],
ALLOW_DATA_ATTR: true
});
// Create normalized content with HTML and extracted text // Create normalized content with HTML and extracted text
const content: EmailContent = { const content: EmailContent = {
html: sanitizeHtml(htmlContent), html: cleanedHtml,
text: '', // Will be extracted when composing text: '', // Will be extracted when composing
isHtml: true, isHtml: true,
direction: email.content.direction || 'ltr' direction: email.content?.direction || 'ltr'
}; };
// Extract text from HTML if in browser environment // Extract text from HTML if in browser environment