courrier preview

This commit is contained in:
alma 2025-05-01 09:54:17 +02:00
parent d88fc133d2
commit 975dc1e1f8
13 changed files with 123 additions and 169 deletions

View File

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { parseEmail } from '@/lib/server/email-parser';
import { sanitizeHtml } from '@/lib/utils/email-formatter';
import { sanitizeHtml } from '@/lib/utils/dom-sanitizer';
interface EmailAddress {
name?: string;

View File

@ -6,7 +6,7 @@ import {
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import DOMPurify from 'isomorphic-dompurify';
import { sanitizeHtml } from '@/lib/utils/dom-sanitizer';
import {
DropdownMenu,
DropdownMenuContent,

View File

@ -2,7 +2,7 @@
import React, { useState } from 'react';
import { Loader2, Paperclip, Download } from 'lucide-react';
import { sanitizeHtml } from '@/lib/utils/email-formatter';
import { sanitizeHtml } from '@/lib/utils/dom-sanitizer';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';

View File

@ -3,7 +3,7 @@
import React, { useMemo } from 'react';
import { EmailContent } from '@/types/email';
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
import DOMPurify from 'isomorphic-dompurify';
import { sanitizeHtml } from '@/lib/utils/dom-sanitizer';
interface EmailContentDisplayProps {
content: EmailContent | null | undefined;
@ -96,7 +96,7 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
// Sanitize HTML content and apply proper direction
const sanitizedHTML = useMemo(() => {
const clean = DOMPurify.sanitize(processedHTML);
const clean = sanitizeHtml(processedHTML);
// Apply text direction consistently using our utility
return applyTextDirection(clean, safeContent.text);
@ -124,11 +124,18 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
width: 100%;
}
/* Enhanced RTL styling - handle direction at every level */
[dir="rtl"] {
text-align: right;
}
/* Ensure images don't overflow their containers */
.email-content-inner img {
max-width: 100%;
height: auto;
}
/* Blockquote styling for LTR content */
.email-content-inner blockquote {
margin: 10px 0;
padding-left: 15px;
@ -138,7 +145,7 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
border-radius: 4px;
}
/* RTL blockquote styling will be handled by the direction attribute now */
/* Special styling for RTL blockquotes - handled via attribute selector */
[dir="rtl"] blockquote {
padding-left: 0;
padding-right: 15px;

View File

@ -2,7 +2,7 @@
import React, { useEffect, useRef, useState } from 'react';
import 'quill/dist/quill.snow.css';
import { sanitizeHtml } from '@/lib/utils/email-utils';
import { sanitizeHtml } from '@/lib/utils/dom-sanitizer';
interface RichEmailEditorProps {
initialContent: string;

View File

@ -2,7 +2,7 @@
import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
import { detectTextDirection } from '@/lib/utils/text-direction';
import DOMPurify from 'isomorphic-dompurify';
import { sanitizeHtml } from '@/lib/utils/dom-sanitizer';
interface RichTextEditorProps {
/** Initial HTML content */
@ -52,7 +52,7 @@ const RichTextEditor = forwardRef<HTMLDivElement, RichTextEditorProps>(({
useEffect(() => {
if (internalEditorRef.current) {
// Clean the initial content
const cleanContent = DOMPurify.sanitize(initialContent);
const cleanContent = sanitizeHtml(initialContent);
internalEditorRef.current.innerHTML = cleanContent;
// Set initial direction

View File

@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react';
import { useToast } from './use-toast';
import { EmailMessage, EmailContent } from '@/types/email';
import { detectTextDirection } from '@/lib/utils/text-direction';
import { sanitizeHtml } from '@/lib/utils/email-utils';
import { sanitizeHtml } from '@/lib/utils/dom-sanitizer';
interface EmailFetchState {
email: EmailMessage | null;

View File

@ -1,4 +1,4 @@
import { sanitizeHtml } from '@/lib/utils/email-formatter';
import { sanitizeHtml } from '@/lib/utils/dom-sanitizer';
import { simpleParser } from 'mailparser';
function getAddressText(addresses: any): string | null {

View File

@ -0,0 +1,83 @@
/**
* DOM Sanitizer
*
* Centralized DOMPurify configuration for consistent HTML sanitization
* throughout the application. This ensures all sanitized content follows
* the same rules for security and presentation.
*/
import DOMPurify from 'isomorphic-dompurify';
// Reset any existing hooks to start with a clean slate
DOMPurify.removeAllHooks();
// Configure DOMPurify with settings appropriate for email content
DOMPurify.setConfig({
// Allow these attributes on all elements
ADD_ATTR: [
'dir', // For text direction
'lang', // For language specification
'style', // For inline styles (carefully sanitized)
'class', 'id', // For CSS targeting
'title', // For tooltips
'target', 'rel', // For links
'colspan', 'rowspan', // For tables
'width', 'height', 'align', 'valign', // Basic layout
'alt', 'src', // For images
'href', // For links
'data-*' // For custom data attributes
],
// Allow these HTML tags
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'
],
// Explicitly forbid these dangerous tags
FORBID_TAGS: [
'script', 'iframe', 'object', 'embed', 'form',
'input', 'button', 'select', 'textarea'
],
// Explicitly forbid these dangerous attributes
FORBID_ATTR: [
'onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout',
'onkeydown', 'onkeypress', 'onkeyup', 'onchange'
],
// Other configuration options
KEEP_CONTENT: true, // Keep content of removed tags
WHOLE_DOCUMENT: false, // Don't require a full HTML document
ALLOW_DATA_ATTR: true, // Allow data-* attributes
ALLOW_UNKNOWN_PROTOCOLS: true, // Allow protocols like cid: for email images
FORCE_BODY: false // Don't force content to be wrapped in <body>
});
// Export a wrapped sanitizeHtml function that handles email-specific fixes
export function sanitizeHtml(html: string): string {
if (!html) return '';
try {
// Use DOMPurify with our configured settings
const clean = DOMPurify.sanitize(html);
// 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:');
}
}

View File

@ -1,5 +1,5 @@
import { EmailMessage, EmailContent, EmailAddress, LegacyEmailMessage } from '@/types/email';
import { sanitizeHtml } from './email-utils';
import { sanitizeHtml } from './dom-sanitizer';
import { detectTextDirection } from './text-direction';
/**

View File

@ -1,4 +1,4 @@
import DOMPurify from 'dompurify';
import { sanitizeHtml } from './dom-sanitizer';
/**
* Format and standardize email content for display following email industry standards.
@ -51,74 +51,20 @@ export function formatEmailContent(email: any): string {
// If we have HTML content, sanitize and standardize it
if (isHtml && content) {
// CRITICAL FIX: Check for browser environment since DOMParser is browser-only
const hasHtmlTag = content.includes('<html');
const hasBodyTag = content.includes('<body');
// 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(content, 'text/html');
const bodyContent = doc.body.innerHTML;
if (bodyContent) {
content = bodyContent;
}
} catch (error) {
// If extraction fails, continue with the original content
console.error('Error extracting body content:', error);
// Check if we have a full HTML document or just a fragment
if (content.includes('<html') && content.includes('</html>')) {
// Extract the body content from a complete HTML document
const bodyMatch = content.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
if (bodyMatch && bodyMatch[1]) {
// Sanitize just the body content
const sanitizedContent = sanitizeHtml(bodyMatch[1].trim());
return sanitizedContent;
}
}
// CRITICAL FIX: Configure DOMPurify for maximum compatibility with email content
// This is a more permissive configuration that preserves common email HTML
const sanitizedContent = DOMPurify.sanitize(content, {
ADD_TAGS: [
'style', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
'caption', 'col', 'colgroup', 'div', 'span', 'img', 'br', 'hr',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre',
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a', 'b', 'i', 'u', 'em',
'strong', 'del', 'ins', 'sub', 'sup', 'small', 'mark', 'q',
'section', 'article', 'header', 'footer', 'aside', 'nav', 'figure',
'figcaption', 'address', 'main', 'center', 'font'
],
ADD_ATTR: [
'style', 'class', 'id', 'href', 'src', 'alt', 'title', 'width', 'height',
'border', 'cellspacing', 'cellpadding', 'bgcolor', 'color', 'dir', 'lang',
'align', 'valign', 'span', 'colspan', 'rowspan', 'target', 'rel',
'background', 'data-*', 'face', 'size', 'hspace', 'vspace',
'marginheight', 'marginwidth', 'frameborder'
],
ALLOW_DATA_ATTR: true,
ALLOW_UNKNOWN_PROTOCOLS: true, // Allow cid: and other protocols
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|cid|mailto|tel|callto|sms|bitcoin|data):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
WHOLE_DOCUMENT: false,
RETURN_DOM: false,
USE_PROFILES: { html: true, svg: false, svgFilters: false }, // Allow standard HTML
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout']
});
// Fix common email client quirks
let fixedContent = sanitizedContent
// Fix for Outlook WebVML content
.replace(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
// Fix for broken image paths that might be relative
.replace(/(src|background)="(?!(?: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, '')}"`;
});
// Check for RTL content and set appropriate direction
const rtlLangPattern = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
const containsRtlText = rtlLangPattern.test(textContent);
const dirAttribute = containsRtlText ? 'dir="rtl"' : 'dir="ltr"';
// CRITICAL FIX: Use a single wrapper with all necessary styles for better email client compatibility
return `<div class="email-content" ${dirAttribute} style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-x: auto; overflow-wrap: break-word; word-wrap: break-word;">${fixedContent}</div>`;
// Otherwise sanitize the entire content as a fragment
const sanitizedContent = sanitizeHtml(content);
return sanitizedContent;
}
// If we only have text content, format it properly
else if (textContent) {

View File

@ -8,8 +8,8 @@
* Text direction is preserved based on content language for proper RTL/LTR display.
*/
import DOMPurify from 'isomorphic-dompurify';
import { sanitizeHtml } from './email-utils';
// Remove DOMPurify import and use centralized sanitizeHtml
import { sanitizeHtml } from './dom-sanitizer';
import { applyTextDirection } from './text-direction';
// Instead of importing, implement the formatDateRelative function directly
// import { formatDateRelative } from './date-formatter';
@ -35,33 +35,10 @@ function formatDateRelative(date: Date): string {
}
}
// Reset any existing hooks to start clean
DOMPurify.removeAllHooks();
// Remove local DOMPurify configuration - now using centralized version
// Configure DOMPurify for English-only content (always LTR)
DOMPurify.addHook('afterSanitizeAttributes', function(node) {
// We no longer force LTR direction on all elements
// This allows the natural text direction to be preserved
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');
}
// Don't forcibly modify text alignment or direction in style attributes
// This allows the component to control text direction instead
}
});
// Configure DOMPurify to preserve direction attributes
DOMPurify.setConfig({
ADD_ATTR: ['dir'],
ALLOWED_ATTR: ['style', 'class', 'id', 'dir']
});
// Note: We ensure LTR text direction is applied in the component level
// when rendering email content
// Note: We ensure proper text direction using applyTextDirection or explicit dir attributes
// in the component level when rendering email content
// Interface definitions
export interface EmailAddress {

View File

@ -8,7 +8,6 @@
* - Text direction detection
*/
import DOMPurify from 'isomorphic-dompurify';
import {
EmailMessage,
EmailContent,
@ -18,17 +17,9 @@ import {
import { adaptLegacyEmail } from '@/lib/utils/email-adapters';
import { decodeInfomaniakEmail, adaptMimeEmail, isMimeFormat } from './email-mime-decoder';
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
import { sanitizeHtml } from '@/lib/utils/dom-sanitizer';
// Reset any existing hooks to start clean
DOMPurify.removeAllHooks();
// Remove the hook that adds dir="auto" - we'll handle direction explicitly instead
// Configure DOMPurify to preserve direction attributes
DOMPurify.setConfig({
ADD_ATTR: ['dir'],
ALLOWED_ATTR: ['style', 'class', 'id', 'dir']
});
// Remove all local DOMPurify configuration - now using centralized version
/**
* Format email addresses for display
@ -75,58 +66,8 @@ export function formatEmailDate(date: Date | string | undefined): string {
}
}
/**
* Sanitize HTML content before processing or displaying
* Uses email industry standards for proper, consistent, and secure rendering
*/
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
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
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/on\w+="[^"]*"/g, '')
.replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:');
}
}
// Re-export sanitizeHtml for convenience
export { sanitizeHtml };
/**
* Format plain text for HTML display with proper line breaks