courrier clean 2$
This commit is contained in:
parent
0bb4fac9f9
commit
5686b9fb7d
@ -59,7 +59,7 @@ import {
|
|||||||
formatReplyEmail,
|
formatReplyEmail,
|
||||||
formatEmailForReplyOrForward,
|
formatEmailForReplyOrForward,
|
||||||
EmailMessage as FormatterEmailMessage,
|
EmailMessage as FormatterEmailMessage,
|
||||||
cleanHtmlContent
|
sanitizeHtml
|
||||||
} from '@/lib/utils/email-formatter';
|
} from '@/lib/utils/email-formatter';
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
@ -168,7 +168,7 @@ function EmailContent({ email }: { email: Email }) {
|
|||||||
email.content = fullContent.content;
|
email.content = fullContent.content;
|
||||||
|
|
||||||
// Render the content using the centralized cleaner
|
// Render the content using the centralized cleaner
|
||||||
const sanitizedHtml = cleanHtmlContent(fullContent.content);
|
const sanitizedHtml = sanitizeHtml(fullContent.content);
|
||||||
setContent(
|
setContent(
|
||||||
<div
|
<div
|
||||||
className="email-content prose prose-sm max-w-none dark:prose-invert"
|
className="email-content prose prose-sm max-w-none dark:prose-invert"
|
||||||
@ -199,7 +199,7 @@ function EmailContent({ email }: { email: Email }) {
|
|||||||
// Check if content is already HTML
|
// Check if content is already HTML
|
||||||
if (formattedEmail.startsWith('<') && formattedEmail.endsWith('>')) {
|
if (formattedEmail.startsWith('<') && formattedEmail.endsWith('>')) {
|
||||||
// Content is likely HTML, sanitize using the centralized cleaner
|
// Content is likely HTML, sanitize using the centralized cleaner
|
||||||
const sanitizedHtml = cleanHtmlContent(formattedEmail);
|
const sanitizedHtml = sanitizeHtml(formattedEmail);
|
||||||
setContent(
|
setContent(
|
||||||
<div
|
<div
|
||||||
className="email-content prose prose-sm max-w-none dark:prose-invert"
|
className="email-content prose prose-sm max-w-none dark:prose-invert"
|
||||||
@ -209,7 +209,7 @@ function EmailContent({ email }: { email: Email }) {
|
|||||||
setDebugInfo('Rendered existing HTML content with centralized formatter');
|
setDebugInfo('Rendered existing HTML content with centralized formatter');
|
||||||
} else {
|
} else {
|
||||||
// For plain text or complex formats, use the centralized formatter
|
// For plain text or complex formats, use the centralized formatter
|
||||||
const cleanedContent = cleanHtmlContent(formattedEmail);
|
const cleanedContent = sanitizeHtml(formattedEmail);
|
||||||
|
|
||||||
// If it looks like HTML, render it as HTML
|
// If it looks like HTML, render it as HTML
|
||||||
if (cleanedContent.includes('<') && cleanedContent.includes('>')) {
|
if (cleanedContent.includes('<') && cleanedContent.includes('>')) {
|
||||||
@ -453,15 +453,15 @@ function EmailPreview({ email }: { email: Email }) {
|
|||||||
try {
|
try {
|
||||||
// If we have the content already, extract preview from it
|
// If we have the content already, extract preview from it
|
||||||
if (email.content) {
|
if (email.content) {
|
||||||
// Use cleanHtmlContent to safely extract text from HTML
|
// Use sanitizeHtml to safely extract text from HTML
|
||||||
const cleanContent = cleanHtmlContent(email.content);
|
const cleanContent = sanitizeHtml(email.content);
|
||||||
const plainText = cleanContent.replace(/<[^>]*>/g, ' ').trim();
|
const plainText = cleanContent.replace(/<[^>]*>/g, ' ').trim();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setPreview(plainText.substring(0, 150) + '...');
|
setPreview(plainText.substring(0, 150) + '...');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use the centralized cleaner instead of decodeEmail
|
// Use the centralized cleaner instead of decodeEmail
|
||||||
const cleanContent = cleanHtmlContent(email.content || '');
|
const cleanContent = sanitizeHtml(email.content || '');
|
||||||
const plainText = cleanContent.replace(/<[^>]*>/g, ' ').trim();
|
const plainText = cleanContent.replace(/<[^>]*>/g, ' ').trim();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@ -88,7 +88,15 @@
|
|||||||
.email-content .header { margin-bottom: 1em; }
|
.email-content .header { margin-bottom: 1em; }
|
||||||
.email-content .footer { font-size: 0.875rem; color: #6b7280; margin-top: 1em; }
|
.email-content .footer { font-size: 0.875rem; color: #6b7280; margin-top: 1em; }
|
||||||
|
|
||||||
/* Force email content direction */
|
/* Email editor styles */
|
||||||
|
.email-editor {
|
||||||
|
/* Allow text direction to be controlled by component */
|
||||||
|
direction: inherit;
|
||||||
|
unicode-bidi: isolate;
|
||||||
|
text-align: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Email content wrapper should still force LTR for quoted content */
|
||||||
.email-content-wrapper {
|
.email-content-wrapper {
|
||||||
direction: ltr !important;
|
direction: ltr !important;
|
||||||
unicode-bidi: isolate !important;
|
unicode-bidi: isolate !important;
|
||||||
@ -101,10 +109,3 @@
|
|||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Email editor styles */
|
|
||||||
.email-editor {
|
|
||||||
direction: ltr !important;
|
|
||||||
unicode-bidi: isolate !important;
|
|
||||||
text-align: left !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,8 @@ import {
|
|||||||
formatForwardedEmail,
|
formatForwardedEmail,
|
||||||
formatReplyEmail,
|
formatReplyEmail,
|
||||||
formatEmailForReplyOrForward,
|
formatEmailForReplyOrForward,
|
||||||
EmailMessage as FormatterEmailMessage
|
EmailMessage as FormatterEmailMessage,
|
||||||
|
sanitizeHtml
|
||||||
} from '@/lib/utils/email-formatter';
|
} from '@/lib/utils/email-formatter';
|
||||||
|
|
||||||
// Define EmailMessage interface locally instead of importing from server-only file
|
// Define EmailMessage interface locally instead of importing from server-only file
|
||||||
@ -87,6 +88,7 @@ interface LegacyComposeEmailProps {
|
|||||||
interface ComposeEmailProps {
|
interface ComposeEmailProps {
|
||||||
initialEmail?: EmailMessage | null;
|
initialEmail?: EmailMessage | null;
|
||||||
type?: 'new' | 'reply' | 'reply-all' | 'forward';
|
type?: 'new' | 'reply' | 'reply-all' | 'forward';
|
||||||
|
initialRTL?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSend: (emailData: {
|
onSend: (emailData: {
|
||||||
to: string;
|
to: string;
|
||||||
@ -110,14 +112,6 @@ function isLegacyProps(props: ComposeEmailAllProps): props is LegacyComposeEmail
|
|||||||
return 'showCompose' in props && 'setShowCompose' in props;
|
return 'showCompose' in props && 'setShowCompose' in props;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure DOMPurify to preserve certain attributes
|
|
||||||
DOMPurify.addHook('afterSanitizeAttributes', function(node) {
|
|
||||||
// Preserve direction attributes
|
|
||||||
if (node.hasAttribute('dir')) {
|
|
||||||
node.setAttribute('dir', node.getAttribute('dir') || 'ltr');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function ComposeEmail(props: ComposeEmailAllProps) {
|
export default function ComposeEmail(props: ComposeEmailAllProps) {
|
||||||
// Handle legacy props by adapting them to new component
|
// Handle legacy props by adapting them to new component
|
||||||
if (isLegacyProps(props)) {
|
if (isLegacyProps(props)) {
|
||||||
@ -125,7 +119,7 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Continue with modern implementation for new props
|
// Continue with modern implementation for new props
|
||||||
const { initialEmail, type = 'new', onClose, onSend } = props;
|
const { initialEmail, type = 'new', initialRTL, onClose, onSend } = props;
|
||||||
|
|
||||||
// Email form state
|
// Email form state
|
||||||
const [to, setTo] = useState<string>('');
|
const [to, setTo] = useState<string>('');
|
||||||
@ -136,7 +130,7 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
const [showCc, setShowCc] = useState<boolean>(false);
|
const [showCc, setShowCc] = useState<boolean>(false);
|
||||||
const [showBcc, setShowBcc] = useState<boolean>(false);
|
const [showBcc, setShowBcc] = useState<boolean>(false);
|
||||||
const [sending, setSending] = useState<boolean>(false);
|
const [sending, setSending] = useState<boolean>(false);
|
||||||
const [isRTL, setIsRTL] = useState<boolean>(false);
|
const [isRTL, setIsRTL] = useState<boolean>(initialRTL || false);
|
||||||
const [attachments, setAttachments] = useState<Array<{
|
const [attachments, setAttachments] = useState<Array<{
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
@ -147,6 +141,13 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Initialize RTL state from prop if provided
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialRTL !== undefined) {
|
||||||
|
setIsRTL(initialRTL);
|
||||||
|
}
|
||||||
|
}, [initialRTL]);
|
||||||
|
|
||||||
// Initialize the form when replying to or forwarding an email
|
// Initialize the form when replying to or forwarding an email
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialEmail && type !== 'new') {
|
if (initialEmail && type !== 'new') {
|
||||||
@ -264,7 +265,19 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
|
|
||||||
// Toggle text direction for the entire editor
|
// Toggle text direction for the entire editor
|
||||||
const toggleTextDirection = () => {
|
const toggleTextDirection = () => {
|
||||||
setIsRTL(!isRTL);
|
// Toggle the RTL state
|
||||||
|
const newRTL = !isRTL;
|
||||||
|
setIsRTL(newRTL);
|
||||||
|
|
||||||
|
// Apply the direction to the editor content immediately
|
||||||
|
if (editorRef.current) {
|
||||||
|
// Preserve the content but update the direction attributes
|
||||||
|
editorRef.current.dir = newRTL ? 'rtl' : 'ltr';
|
||||||
|
editorRef.current.style.textAlign = newRTL ? 'right' : 'left';
|
||||||
|
editorRef.current.style.direction = newRTL ? 'rtl' : 'ltr';
|
||||||
|
|
||||||
|
// No need to modify the content, just let the user edit with proper directionality
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send email without modifying pre-formatted content
|
// Send email without modifying pre-formatted content
|
||||||
@ -418,7 +431,9 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
dangerouslySetInnerHTML={{ __html: emailContent }}
|
dangerouslySetInnerHTML={{ __html: emailContent }}
|
||||||
dir={isRTL ? 'rtl' : 'ltr'}
|
dir={isRTL ? 'rtl' : 'ltr'}
|
||||||
style={{
|
style={{
|
||||||
textAlign: isRTL ? 'right' : 'left'
|
textAlign: isRTL ? 'right' : 'left',
|
||||||
|
direction: isRTL ? 'rtl' : 'ltr',
|
||||||
|
unicodeBidi: 'isolate'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -514,6 +529,23 @@ function LegacyAdapter({
|
|||||||
replyTo,
|
replyTo,
|
||||||
forwardFrom
|
forwardFrom
|
||||||
}: LegacyComposeEmailProps) {
|
}: LegacyComposeEmailProps) {
|
||||||
|
// Check if the user has RTL preference
|
||||||
|
const [preferRTL, setPreferRTL] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Detect RTL content in body on initialization
|
||||||
|
useEffect(() => {
|
||||||
|
if (!composeBody) return;
|
||||||
|
|
||||||
|
// Better RTL detection based on common RTL languages (Arabic, Hebrew, Persian, Urdu, etc.)
|
||||||
|
const rtlRegex = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||||
|
|
||||||
|
// If there are RTL characters, set preferRTL to true
|
||||||
|
if (rtlRegex.test(composeBody)) {
|
||||||
|
setPreferRTL(true);
|
||||||
|
console.log('RTL text detected - setting editor to RTL mode');
|
||||||
|
}
|
||||||
|
}, [composeBody]);
|
||||||
|
|
||||||
// Determine the type from the original email or subject
|
// Determine the type from the original email or subject
|
||||||
const determineType = (): 'new' | 'reply' | 'reply-all' | 'forward' => {
|
const determineType = (): 'new' | 'reply' | 'reply-all' | 'forward' => {
|
||||||
if (originalEmail) {
|
if (originalEmail) {
|
||||||
@ -581,6 +613,7 @@ function LegacyAdapter({
|
|||||||
<ComposeEmail
|
<ComposeEmail
|
||||||
initialEmail={emailForCompose}
|
initialEmail={emailForCompose}
|
||||||
type={type}
|
type={type}
|
||||||
|
initialRTL={preferRTL}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
onCancel?.();
|
onCancel?.();
|
||||||
setShowCompose(false);
|
setShowCompose(false);
|
||||||
|
|||||||
@ -93,25 +93,15 @@ export function formatEmailDate(date: Date | string | undefined): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean HTML content to prevent RTL/LTR issues
|
* Sanitize HTML content before processing or displaying
|
||||||
* This is the ONLY function that should be used for cleaning HTML content
|
* @param content HTML content to sanitize
|
||||||
|
* @returns Sanitized HTML
|
||||||
*/
|
*/
|
||||||
export function cleanHtmlContent(content: string): string {
|
export function sanitizeHtml(content: string): string {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
// First sanitize the HTML with our configured DOMPurify
|
// Sanitize the HTML using our configured DOMPurify
|
||||||
const sanitized = DOMPurify.sanitize(content);
|
return DOMPurify.sanitize(content);
|
||||||
|
|
||||||
// Process content to ensure consistent direction
|
|
||||||
let processed = sanitized;
|
|
||||||
|
|
||||||
// Replace RTL attributes with LTR if needed
|
|
||||||
// We're now more careful to only modify direction attributes if needed
|
|
||||||
processed = processed.replace(/dir\s*=\s*["']rtl["']/gi, 'dir="ltr"');
|
|
||||||
processed = processed.replace(/style\s*=\s*["']([^"']*)direction\s*:\s*rtl;?([^"']*)["']/gi,
|
|
||||||
(match, before, after) => `style="${before}direction: ltr;${after}"`);
|
|
||||||
|
|
||||||
return processed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,8 +123,8 @@ 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 and clean original content
|
// Get and sanitize original content
|
||||||
const originalContent = cleanHtmlContent(email.content || email.html || email.text || '');
|
const originalContent = sanitizeHtml(email.content || email.html || email.text || '');
|
||||||
|
|
||||||
// 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 ---------');
|
||||||
@ -209,8 +199,8 @@ export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all
|
|||||||
// Create quote header
|
// Create quote header
|
||||||
const quoteHeader = `<div style="font-weight: 500; direction: ltr; text-align: left;" dir="ltr">On ${formattedDate}, ${fromText} wrote:</div>`;
|
const quoteHeader = `<div style="font-weight: 500; direction: ltr; text-align: left;" dir="ltr">On ${formattedDate}, ${fromText} wrote:</div>`;
|
||||||
|
|
||||||
// Get and clean original content
|
// Get and sanitize original content
|
||||||
const quotedContent = cleanHtmlContent(email.html || email.content || email.text || '');
|
const quotedContent = sanitizeHtml(email.html || email.content || email.text || '');
|
||||||
|
|
||||||
// Format recipients
|
// Format recipients
|
||||||
let to = formatEmailAddresses(email.from || []);
|
let to = formatEmailAddresses(email.from || []);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user