courrier preview
This commit is contained in:
parent
d88fc133d2
commit
975dc1e1f8
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
83
lib/utils/dom-sanitizer.ts
Normal file
83
lib/utils/dom-sanitizer.ts
Normal 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:');
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
/**
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user