courrier preview

This commit is contained in:
alma 2025-05-01 09:43:25 +02:00
parent 193a265109
commit d88fc133d2
5 changed files with 130 additions and 157 deletions

View File

@ -352,7 +352,13 @@ export default function ComposeEmail(props: ComposeEmailProps) {
ref={editorRef}
initialContent={emailContent}
initialDirection={detectTextDirection(emailContent)}
onChange={setEmailContent}
onChange={(html) => {
// Store the content
setEmailContent(html);
// But don't update direction on every keystroke
// The RichTextEditor will handle direction changes internally
}}
className="min-h-[320px] border rounded-md bg-white text-gray-800 flex-1"
placeholder="Write your message here..."
/>

View File

@ -2,7 +2,7 @@
import React, { useMemo } from 'react';
import { EmailContent } from '@/types/email';
import { detectTextDirection } from '@/lib/utils/text-direction';
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
import DOMPurify from 'isomorphic-dompurify';
interface EmailContentDisplayProps {
@ -94,13 +94,16 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
return htmlToDisplay;
}, [htmlToDisplay, showQuotedText]);
// Sanitize HTML content before rendering
// Sanitize HTML content and apply proper direction
const sanitizedHTML = useMemo(() => {
return DOMPurify.sanitize(processedHTML);
}, [processedHTML]);
const clean = DOMPurify.sanitize(processedHTML);
// Apply text direction consistently using our utility
return applyTextDirection(clean, safeContent.text);
}, [processedHTML, safeContent.text]);
return (
<div className={`email-content-display ${className}`} dir={safeContent.direction}>
<div className={`email-content-display ${className}`}>
<div
className="email-content-inner"
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
@ -121,10 +124,6 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
width: 100%;
}
.email-content-display[dir="rtl"] {
text-align: right;
}
.email-content-inner img {
max-width: 100%;
height: auto;
@ -139,7 +138,8 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
border-radius: 4px;
}
.email-content-display[dir="rtl"] .email-content-inner blockquote {
/* RTL blockquote styling will be handled by the direction attribute now */
[dir="rtl"] blockquote {
padding-left: 0;
padding-right: 15px;
border-left: none;

View File

@ -1,5 +1,6 @@
import { EmailMessage, EmailContent, EmailAddress, LegacyEmailMessage } from '@/types/email';
import { sanitizeHtml } from './email-utils';
import { detectTextDirection } from './text-direction';
/**
* Adapts a legacy email format to the standardized EmailMessage format
@ -224,17 +225,7 @@ function normalizeContent(email: LegacyEmailMessage): EmailContent {
}
/**
* Detects the text direction (LTR or RTL) based on the content
*/
function detectTextDirection(text: string): 'ltr' | 'rtl' {
// Simple RTL detection for common RTL languages
// This is a basic implementation and can be enhanced
const rtlChars = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/;
return rtlChars.test(text) ? 'rtl' : 'ltr';
}
/**
* Normalizes email addresses to the EmailAddress format
* Normalizes addresses to EmailAddress objects
*/
function normalizeAddresses(addresses: string | EmailAddress[] | undefined): EmailAddress[] {
if (!addresses) {

View File

@ -9,6 +9,8 @@
*/
import DOMPurify from 'isomorphic-dompurify';
import { sanitizeHtml } from './email-utils';
import { applyTextDirection } from './text-direction';
// Instead of importing, implement the formatDateRelative function directly
// import { formatDateRelative } from './date-formatter';
@ -133,59 +135,6 @@ export function formatEmailDate(date: Date | string | undefined): string {
}
}
/**
* Sanitize HTML content before processing or displaying
* Implements email industry standards for proper, consistent, and secure rendering
*
* @param html HTML content to sanitize
* @returns Sanitized HTML with preserved styling and structure
*/
export function sanitizeHtml(html: string): string {
if (!html) return '';
try {
// Use DOMPurify with comprehensive email HTML standards
const clean = DOMPurify.sanitize(html, {
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'
],
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-*'
],
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
});
// Fix common email rendering issues
return 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://');
} catch (e) {
console.error('Error sanitizing HTML:', e);
// Fall back to a basic sanitization approach
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+="[^"]*"/g, '')
.replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:');
}
}
/**
* Format an email for forwarding - CENTRAL IMPLEMENTATION
* All other formatting functions should be deprecated in favor of this one
@ -423,46 +372,58 @@ export function encodeComposeContent(content: string): string {
.join('\n') + '\n\n' + content;
}
// Legacy email formatter functions - renamed to avoid conflicts
/**
* COMPATIBILITY LAYER: For backward compatibility with the old email-formatter.ts
* Use the newer formatReplyEmail function instead when possible.
*
* @deprecated Use formatReplyEmail instead
*/
export function formatReplyEmailLegacy(email: any): string {
const originalSender = email.sender?.name || email.sender?.email || 'Unknown Sender';
const originalDate = formatDateRelative(new Date(email.date));
// Use our own sanitizeHtml function consistently
const sanitizedBody = sanitizeHtml(email.content || '');
return `
<p></p>
<p>On ${originalDate}, ${originalSender} wrote:</p>
<blockquote class="quoted-content">
${sanitizedBody}
</blockquote>
`.trim();
// Format the reply with consistent direction handling
const replyContent = `
<br/>
<br/>
<blockquote>
On ${email.date}, ${email.from} wrote:
<br/>
${sanitizedBody}
</blockquote>
`;
// Apply consistent text direction
return applyTextDirection(replyContent);
}
/**
* COMPATIBILITY LAYER: For backward compatibility with the old email-formatter.ts
* Use the newer formatForwardedEmail function instead when possible.
*
* @deprecated Use formatForwardedEmail instead
*/
export function formatForwardedEmailLegacy(email: any): string {
const originalSender = email.sender?.name || email.sender?.email || 'Unknown Sender';
const originalRecipients = email.to?.map((recipient: any) =>
recipient.name || recipient.email
).join(', ') || 'Unknown Recipients';
const originalDate = formatDateRelative(new Date(email.date));
const originalSubject = email.subject || 'No Subject';
// Use our own sanitizeHtml function consistently
const sanitizedBody = sanitizeHtml(email.content || '');
return `
<p></p>
<p>---------- Forwarded message ---------</p>
<p><strong>From:</strong> ${originalSender}</p>
<p><strong>Date:</strong> ${originalDate}</p>
<p><strong>Subject:</strong> ${originalSubject}</p>
<p><strong>To:</strong> ${originalRecipients}</p>
<br>
<div class="email-original-content">
${sanitizedBody}
</div>
`.trim();
// Format the forwarded content with consistent direction handling
const forwardedContent = `
<br/>
<br/>
<div>
---------- Forwarded message ---------<br/>
From: ${email.from}<br/>
Date: ${email.date}<br/>
Subject: ${email.subject}<br/>
To: ${email.to}<br/>
<br/>
${sanitizedBody}
</div>
`;
// Apply consistent text direction
return applyTextDirection(forwardedContent);
}
export function formatReplyToAllEmail(email: any): string {

View File

@ -17,21 +17,12 @@ import {
} from '@/types/email';
import { adaptLegacyEmail } from '@/lib/utils/email-adapters';
import { decodeInfomaniakEmail, adaptMimeEmail, isMimeFormat } from './email-mime-decoder';
import { detectTextDirection } from '@/lib/utils/text-direction';
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
// Reset any existing hooks to start clean
DOMPurify.removeAllHooks();
// Configure DOMPurify for auto text direction
DOMPurify.addHook('afterSanitizeAttributes', function(node) {
if (node instanceof HTMLElement) {
// Only set direction if not already specified
if (!node.hasAttribute('dir')) {
// Add dir attribute only if not present
node.setAttribute('dir', 'auto');
}
}
});
// Remove the hook that adds dir="auto" - we'll handle direction explicitly instead
// Configure DOMPurify to preserve direction attributes
DOMPurify.setConfig({
@ -119,11 +110,14 @@ export function sanitizeHtml(html: string): string {
});
// Fix common email rendering issues
return clean
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://');
// We don't manually add direction here anymore - applyTextDirection will handle it
return fixedHtml;
} catch (e) {
console.error('Error sanitizing HTML:', e);
// Fall back to a basic sanitization approach
@ -190,30 +184,34 @@ export function normalizeEmailContent(email: any): EmailMessage {
/**
* Render normalized email content into HTML for display
*/
export function renderEmailContent(content: EmailContent): string {
console.log('renderEmailContent received:', JSON.stringify(content, null, 2));
export function renderEmailContent(content: EmailContent | null): string {
if (!content) {
console.log('No content provided to renderEmailContent');
return '<div class="email-content-empty">No content available</div>';
}
const safeContent = {
text: content.text || '',
html: content.html,
isHtml: content.isHtml,
direction: content.direction || 'ltr'
};
// If we have HTML content and isHtml flag is true, use it
if (content.isHtml && content.html) {
console.log('Rendering HTML content, length:', content.html.length);
return `<div class="email-content" dir="${content.direction || 'ltr'}">${content.html}</div>`;
if (safeContent.isHtml && safeContent.html) {
// Apply text direction consistently using the utility
return applyTextDirection(safeContent.html, safeContent.text);
}
// Otherwise, format the text content with basic HTML
const text = content.text || '';
console.log('Rendering text content, length:', text.length);
const text = safeContent.text;
const formattedText = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br>');
return `<div class="email-content plain-text" dir="${content.direction || 'ltr'}">${formattedText}</div>`;
// Apply text direction consistently
return applyTextDirection(formattedText, text);
}
// Add interface for email formatting functions
@ -305,33 +303,35 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
// Extract original content
const originalTextContent =
originalEmail.content?.text ||
(typeof originalEmail.content === 'string' ? originalEmail.content : '');
typeof originalEmail?.content === 'object' ? originalEmail.content.text :
typeof originalEmail?.content === 'string' ? originalEmail.content :
originalEmail?.text || '';
const originalHtmlContent =
originalEmail.content?.html ||
originalEmail.html ||
(typeof originalEmail.content === 'string' && originalEmail.content.includes('<')
typeof originalEmail?.content === 'object' ? originalEmail.content.html :
originalEmail?.html ||
(typeof originalEmail?.content === 'string' && originalEmail?.content.includes('<')
? originalEmail.content
: '');
// Get the direction from the original email
const originalDirection =
originalEmail.content?.direction ||
(originalTextContent ? detectTextDirection(originalTextContent) : 'ltr');
typeof originalEmail?.content === 'object' ? originalEmail.content.direction :
detectTextDirection(originalTextContent);
// Create HTML content that preserves the directionality
const htmlContent = `
// Create content with appropriate quote formatting
const replyBody = `
<br/>
<br/>
<div class="email-original-content" dir="${originalDirection}">
<blockquote style="border-left: 2px solid #ddd; padding-left: 10px; margin: 10px 0; color: #505050;">
<p>On ${dateStr}, ${fromStr} wrote:</p>
${originalHtmlContent || originalTextContent.replace(/\n/g, '<br>')}
</blockquote>
</div>
<blockquote style="border-left: 2px solid #ddd; padding-left: 10px; margin: 10px 0; color: #505050;">
<p>On ${dateStr}, ${fromStr} wrote:</p>
${originalHtmlContent || originalTextContent.replace(/\n/g, '<br>')}
</blockquote>
`;
// Apply consistent text direction
const htmlContent = applyTextDirection(replyBody);
// Create plain text content
const textContent = `
@ -406,45 +406,49 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
// Extract original content
const originalTextContent =
originalEmail.content?.text ||
(typeof originalEmail.content === 'string' ? originalEmail.content : '');
typeof originalEmail?.content === 'object' ? originalEmail.content.text :
typeof originalEmail?.content === 'string' ? originalEmail.content :
originalEmail?.text || '';
const originalHtmlContent =
originalEmail.content?.html ||
originalEmail.html ||
(typeof originalEmail.content === 'string' && originalEmail.content.includes('<')
typeof originalEmail?.content === 'object' ? originalEmail.content.html :
originalEmail?.html ||
(typeof originalEmail?.content === 'string' && originalEmail?.content.includes('<')
? originalEmail.content
: '');
// Get the direction from the original email
const originalDirection =
originalEmail.content?.direction ||
(originalTextContent ? detectTextDirection(originalTextContent) : 'ltr');
typeof originalEmail?.content === 'object' ? originalEmail.content.direction :
detectTextDirection(originalTextContent);
// Create HTML content that preserves the directionality
const htmlContent = `
// Create forwarded content with header information
const forwardBody = `
<br/>
<br/>
<div class="email-forwarded-content">
<p>---------- Forwarded message ---------</p>
<p><strong>From:</strong> ${fromStr}</p>
<p><strong>Date:</strong> ${dateStr}</p>
<p><strong>Subject:</strong> ${originalEmail.subject || ''}</p>
<p><strong>Subject:</strong> ${originalEmail?.subject || ''}</p>
<p><strong>To:</strong> ${toStr}</p>
${ccStr ? `<p><strong>Cc:</strong> ${ccStr}</p>` : ''}
<div style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;" dir="${originalDirection}">
<div style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;">
${originalHtmlContent || originalTextContent.replace(/\n/g, '<br>')}
</div>
</div>
`;
// Apply consistent text direction
const htmlContent = applyTextDirection(forwardBody);
// Create plain text content
const textContent = `
---------- Forwarded message ---------
From: ${fromStr}
Date: ${dateStr}
Subject: ${originalEmail.subject || ''}
Subject: ${originalEmail?.subject || ''}
To: ${toStr}
${ccStr ? `Cc: ${ccStr}\n` : ''}
@ -475,10 +479,21 @@ export function formatEmailForReplyOrForward(
subject: string;
content: EmailContent;
} {
// Use our dedicated formatters but ensure the return is properly typed
if (type === 'forward') {
const { subject, content } = formatForwardedEmail(email);
return { subject, content };
const formatted = formatForwardedEmail(email);
return {
to: formatted.to,
subject: formatted.subject,
content: formatted.content
};
} else {
return formatReplyEmail(email, type);
const formatted = formatReplyEmail(email, type);
return {
to: formatted.to,
cc: formatted.cc,
subject: formatted.subject,
content: formatted.content
};
}
}