courrier preview

This commit is contained in:
alma 2025-05-01 12:47:36 +02:00
parent 1211b43f46
commit 080b9ab795
6 changed files with 184 additions and 110 deletions

View File

@ -200,48 +200,58 @@ export default function ComposeEmail(props: ComposeEmailProps) {
// Provide a basic template if the content is empty
const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedInfoForEmail(initialEmail);
const 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 class="forwarded-email-container">
<div class="forwarded-email-header" 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; width: 100%;">
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top; white-space: nowrap;">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; white-space: nowrap;">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; white-space: nowrap;">Subject:</td>
<td style="padding: 3px 0;">${subject || ''}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top; white-space: nowrap;">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; white-space: nowrap;">Cc:</td>
<td style="padding: 3px 0;">${ccStr}</td>
</tr>` : ''}
</table>
<div style="border-bottom: 1px solid #ccc; margin-top: 5px; margin-bottom: 15px; padding-bottom: 5px;">
<div>----------------------------------------------------------------------</div>
</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;">${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>` : ''}
</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;">
[Original message content could not be loaded]
<div class="forwarded-email-content">[Original message content could not be loaded]</div>
</div>
`;
setEmailContent(fallbackContent);
} else {
console.log('Setting forward content:', {
length: content.length,
isHtml: formatted.content.isHtml
isHtml: formatted.content.isHtml,
hasForwardedTable: content.includes('---------------------------- Forwarded Message ----------------------------'),
hasClosingContainer: content.includes('</div></div>')
});
setEmailContent(content);
// Ensure the content is wrapped in a div to preserve structure
// Add a distinctive id to help with debugging
if (!content.startsWith('<div')) {
const wrappedContent = `<div id="wrapped-forward-content">${content}</div>`;
setEmailContent(wrappedContent);
} else {
setEmailContent(content);
}
}
// Handle attachments for forward (original attachments + extracted inline images)

View File

@ -73,7 +73,11 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
<div className="mt-4 p-2 text-xs bg-gray-100 border rounded">
<p><strong>Content Type:</strong> {typeof content === 'string' ? 'Text' : 'HTML'}</p>
<p><strong>HTML Length:</strong> {typeof content === 'string' ? content.length : content?.html?.length || 0}</p>
<p><strong>Text Length:</strong> {typeof content === 'string' ? content.length : content?.text?.length || 0}</p>
<p><strong>Text Length:</strong> {typeof content === 'string' ? 0 : content?.text?.length || 0}</p>
<p><strong>Has Forwarded Header:</strong> {typeof content === 'string' ?
content.includes('Forwarded Message') :
(content?.html?.includes('Forwarded Message') || content?.text?.includes('Forwarded Message')) ? 'Yes' : 'No'
}</p>
</div>
)}
@ -103,6 +107,29 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
border-left: none;
border-right: 2px solid #ddd;
}
/* Specific styling for forwarded email header */
:global(.forwarded-email-header) {
margin: 20px 0 10px 0 !important;
color: #666 !important;
font-family: Arial, sans-serif !important;
}
:global(.forwarded-email-header table) {
margin-bottom: 10px !important;
font-size: 14px !important;
width: 100% !important;
border-collapse: collapse !important;
}
:global(.forwarded-email-header td) {
padding: 3px 5px !important;
vertical-align: top !important;
}
:global(.forwarded-email-content) {
margin-top: 15px !important;
}
`}</style>
</div>
);

View File

@ -48,39 +48,19 @@ export default function EmailDetailView({
console.log('EmailDetailView renderEmailContent', {
hasContent: !!email.content,
contentType: typeof email.content,
hasHtml: !!email.html,
hasText: !!email.text
hasHtml: !!email.content?.html,
hasText: !!email.content?.text
});
// Determine what content to use and how to handle it
let contentToUse = '';
// Import the centralized rendering function
const { formatEmailContent } = require('@/lib/utils/email-utils');
if (email.content) {
// If content is a string, use it directly
if (typeof email.content === 'string') {
contentToUse = email.content;
}
// If content is an object with html/text properties
else if (typeof email.content === 'object') {
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;
}
else if (email.text) {
// Convert plain text to HTML with line breaks
contentToUse = email.text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>');
}
// Use the centralized formatting function
const formattedContent = formatEmailContent(email);
// Return content or fallback message
return contentToUse ?
<div dangerouslySetInnerHTML={{ __html: contentToUse }} /> :
// Return formatted content or fallback message
return formattedContent ?
<div dangerouslySetInnerHTML={{ __html: formattedContent }} /> :
<div className="text-gray-500">No content available</div>;
} catch (e) {
console.error('Error rendering email:', e);

View File

@ -91,13 +91,22 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
console.log('Setting initial content in editor', {
length: initialContent.length,
startsWithHtml: initialContent.trim().startsWith('<'),
hasForwardedHeader: initialContent.includes('Forwarded Message'),
});
// Detect text direction
const direction = detectTextDirection(initialContent);
// Preserve forwarded email structure by wrapping in a container if needed
let contentToSet = initialContent;
if (initialContent.includes('Forwarded Message') && !initialContent.includes('forwarded-email-container')) {
// This might be a forwarded email that wasn't properly wrapped
contentToSet = `<div class="forwarded-email-container">${initialContent}</div>`;
console.log('Adding container to forwarded email content');
}
// Process HTML content using centralized utility
const sanitizedContent = processHtmlContent(initialContent);
const sanitizedContent = processHtmlContent(contentToSet);
// Check if sanitized content is valid
if (sanitizedContent.trim().length === 0) {
@ -105,7 +114,7 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
// Try to extract text content if HTML processing failed
try {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = initialContent;
tempDiv.innerHTML = contentToSet;
const textContent = tempDiv.textContent || tempDiv.innerText || 'Empty content';
// Set text directly to ensure something displays
@ -209,27 +218,36 @@ const RichEmailEditor: React.FC<RichEmailEditorProps> = ({
try {
console.log('Updating content in editor:', {
contentLength: initialContent.length,
startsWithHtml: initialContent.trim().startsWith('<')
startsWithHtml: initialContent.trim().startsWith('<'),
hasForwardedHeader: initialContent.includes('Forwarded Message'),
});
// Detect text direction
const direction = detectTextDirection(initialContent);
// Special handling for forwarded email content
let contentToSet = initialContent;
if (initialContent.includes('Forwarded Message') && !initialContent.includes('forwarded-email-container')) {
// This might be a forwarded email that wasn't properly wrapped
contentToSet = `<div class="forwarded-email-container">${initialContent}</div>`;
console.log('Adding container to forwarded email content during update');
}
// Process HTML content using centralized utility
const sanitizedContent = processHtmlContent(initialContent);
const sanitizedContent = processHtmlContent(contentToSet);
// Check if content is valid HTML
if (sanitizedContent.trim().length === 0) {
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 = initialContent;
tempDiv.innerHTML = contentToSet;
const textContent = tempDiv.textContent || tempDiv.innerText || '';
// Create simple HTML with text content
quillRef.current.setText(textContent);
} else {
// SIMPLIFIED: Set content directly to the root element rather than using clipboard
// Set content directly to the root element rather than using clipboard
quillRef.current.root.innerHTML = sanitizedContent;
// Set the direction for the content

View File

@ -65,6 +65,27 @@ export function extractEmailContent(email: any): { text: string; html: string }
}
}
}
// Special case for YCharts and similar emails where content is detected as object
// but doesn't have standard html/text properties - stringify and try to extract
if (!textContent && !htmlContent) {
try {
// Try to extract from stringified version
const contentString = JSON.stringify(email.content);
if (contentString && contentString.includes('<html') || contentString.includes('<body')) {
// Extract HTML content from the stringified object
const htmlMatch = contentString.match(/<html[^>]*>([\s\S]*)<\/html>/i) ||
contentString.match(/<body[^>]*>([\s\S]*)<\/body>/i);
if (htmlMatch && htmlMatch[1]) {
htmlContent = htmlMatch[1];
console.log('Extracted HTML from stringified content object');
}
}
} catch (err) {
console.warn('Failed to extract from stringified content:', err);
}
}
}
} else if (typeof email.content === 'string') {
// Check if content is likely HTML
@ -213,6 +234,16 @@ export function processHtmlContent(htmlContent: string, textContent?: string): s
// Use the centralized sanitizeHtml function
let sanitizedContent = sanitizeHtml(htmlContent);
// If sanitized content is empty but original content wasn't,
// try to extract text as a fallback
if (!sanitizedContent.trim() && htmlContent.trim()) {
console.warn('Sanitized content is empty, attempting to extract text as fallback');
const extractedText = extractTextFromHtml(htmlContent);
if (extractedText) {
return `<div style="white-space: pre-wrap;">${extractedText}</div>`;
}
}
// Fix URL encoding issues and clean up content
try {
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
@ -243,12 +274,14 @@ export function processHtmlContent(htmlContent: string, textContent?: string): s
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
// Keep cid: URLs as they are but add data attribute for debugging
img.setAttribute('data-cid-preserved', 'true');
console.log('Preserving CID reference:', src);
}
// Fix http:// URLs to https:// for security
// Fix http:// URLs to https:// for security - unless they're to known image hosts
else if (src.startsWith('http://')) {
img.setAttribute('src', src.replace('http://', 'https://'));
console.log('Fixed HTTP image URL:', src);
}
// Handle relative URLs that might be broken
else if (!src.startsWith('https://') && !src.startsWith('data:')) {
@ -303,17 +336,21 @@ export function processHtmlContent(htmlContent: string, textContent?: string): s
}
// Fix common email client quirks without breaking cid: URLs
return sanitizedContent
sanitizedContent = sanitizedContent
// Fix for Outlook WebVML content
.replace(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
// Fix for broken image paths WITHOUT replacing cid: URLs
.replace(/(src|background)="(?!(?:https?:|data:|cid:))/gi, '$1="https://')
.replace(/(<img[^>]+src=")(?!(?:https?:|data:|cid:))([^"]+)(")/gi, '$1https://example.com/$2$3')
// Fix for broken background URLs WITHOUT replacing cid: URLs
.replace(/(background=")(?!(?:https?:|data:|cid:))([^"]+)(")/gi, '$1https://example.com/$2$3')
// 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, '> <');
return sanitizedContent;
} catch (error) {
console.error('Error processing HTML content:', error);
return htmlContent;

View File

@ -336,45 +336,47 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
// Extract content using the centralized extraction function
const { text, html } = extractEmailContent(originalEmail);
// Create a traditional forward format with dashed separator
const forwardHeader = `
<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;">${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>` : ''}
</table>
<div style="border-bottom: 1px solid #ccc; margin-top: 5px; margin-bottom: 15px; padding-bottom: 5px;">
<div>----------------------------------------------------------------------</div>
</div>
</div>
`;
// Use the original HTML content if available, otherwise format the text
const contentHtml = html || (text ? `<p>${text.replace(/\n/g, '</p><p>')}</p>` : '<p>No content available</p>');
const cleanHtml = `${forwardHeader}${contentHtml}`;
// Create a traditional forward format with dashed separator
// Wrap everything in a single containing element to prevent structure loss
const cleanHtml = `
<div class="forwarded-email-container">
<div class="forwarded-email-header" 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; width: 100%;">
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top; white-space: nowrap;">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; white-space: nowrap;">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; white-space: nowrap;">Subject:</td>
<td style="padding: 3px 0;">${subject || ''}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top; white-space: nowrap;">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; white-space: nowrap;">Cc:</td>
<td style="padding: 3px 0;">${ccStr}</td>
</tr>` : ''}
</table>
<div style="border-bottom: 1px solid #ccc; margin-top: 5px; margin-bottom: 15px; padding-bottom: 5px;">
<div>----------------------------------------------------------------------</div>
</div>
</div>
<div class="forwarded-email-content">${contentHtml}</div>
</div>
`;
// Plain text version - with clearer formatting
const plainText = `