courrier preview
This commit is contained in:
parent
83af1599f9
commit
86581cea72
@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { parseEmail } from '@/lib/server/email-parser';
|
import { parseEmail } from '@/lib/server/email-parser';
|
||||||
import { sanitizeHtml } from '@/lib/utils/email-formatter';
|
import { sanitizeHtml } from '@/lib/utils/dom-purify-config';
|
||||||
|
|
||||||
interface EmailAddress {
|
interface EmailAddress {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Loader2, Paperclip, Download } from 'lucide-react';
|
import { Loader2, Paperclip, Download } from 'lucide-react';
|
||||||
import { sanitizeHtml } from '@/lib/utils/email-formatter';
|
import { sanitizeHtml } from '@/lib/utils/dom-purify-config';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
|
|
||||||
|
|||||||
@ -3,10 +3,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { EmailContent } from '@/types/email';
|
import { EmailContent } from '@/types/email';
|
||||||
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
|
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import { sanitizeHtml, getDOMPurify } from '@/lib/utils/dom-purify-config';
|
||||||
|
|
||||||
// Configure DOMPurify to not interfere with our direction handling
|
|
||||||
DOMPurify.removeHooks('afterSanitizeAttributes');
|
|
||||||
|
|
||||||
interface EmailContentDisplayProps {
|
interface EmailContentDisplayProps {
|
||||||
content: EmailContent | null | undefined;
|
content: EmailContent | null | undefined;
|
||||||
@ -99,15 +96,11 @@ const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
|||||||
|
|
||||||
// Sanitize HTML content and apply proper direction
|
// Sanitize HTML content and apply proper direction
|
||||||
const sanitizedHTML = useMemo(() => {
|
const sanitizedHTML = useMemo(() => {
|
||||||
// Use DOMPurify with explicit config to avoid global configuration issues
|
// First sanitize the HTML with our centralized utility
|
||||||
const clean = DOMPurify.sanitize(processedHTML, {
|
const cleanHtml = sanitizeHtml(processedHTML);
|
||||||
ADD_ATTR: ['dir'], // Ensure dir attributes are preserved
|
|
||||||
ALLOWED_ATTR: ['style', 'class', 'id', 'dir'], // Allow direction attributes
|
|
||||||
FORBID_TAGS: ['script', 'iframe', 'object'] // Only strip dangerous elements
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply text direction consistently using our utility
|
// Then apply text direction consistently
|
||||||
return applyTextDirection(clean, safeContent.text);
|
return applyTextDirection(cleanHtml, safeContent.text);
|
||||||
}, [processedHTML, safeContent.text]);
|
}, [processedHTML, safeContent.text]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
import React, { useState, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||||
import { detectTextDirection } from '@/lib/utils/text-direction';
|
import { detectTextDirection } from '@/lib/utils/text-direction';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import { getDOMPurify } from '@/lib/utils/dom-purify-config';
|
||||||
|
|
||||||
interface RichTextEditorProps {
|
interface RichTextEditorProps {
|
||||||
/** Initial HTML content */
|
/** Initial HTML content */
|
||||||
@ -27,9 +27,8 @@ interface RichTextEditorProps {
|
|||||||
initialDirection?: 'ltr' | 'rtl';
|
initialDirection?: 'ltr' | 'rtl';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before using DOMPurify, configure it to not add dir attributes automatically
|
// Get DOMPurify from our centralized configuration
|
||||||
// This will prevent conflicts with our explicit direction handling
|
const DOMPurify = getDOMPurify();
|
||||||
DOMPurify.removeHooks('afterSanitizeAttributes');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified rich text editor component with proper RTL support
|
* Unified rich text editor component with proper RTL support
|
||||||
@ -55,10 +54,9 @@ const RichTextEditor = forwardRef<HTMLDivElement, RichTextEditorProps>(({
|
|||||||
// Initialize editor with clean content
|
// Initialize editor with clean content
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (internalEditorRef.current) {
|
if (internalEditorRef.current) {
|
||||||
// Clean the initial content but preserve existing dir attributes
|
// Clean the initial content using our centralized config
|
||||||
const cleanContent = DOMPurify.sanitize(initialContent, {
|
const cleanContent = DOMPurify.sanitize(initialContent, {
|
||||||
ADD_ATTR: ['dir'], // Ensure dir attributes are preserved
|
ADD_ATTR: ['dir'] // Ensure dir attributes are preserved
|
||||||
FORBID_ATTR: ['onerror', 'onload', 'onclick'] // Only strip dangerous attributes
|
|
||||||
});
|
});
|
||||||
|
|
||||||
internalEditorRef.current.innerHTML = cleanContent;
|
internalEditorRef.current.innerHTML = cleanContent;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { sanitizeHtml } from '@/lib/utils/email-formatter';
|
import { sanitizeHtml } from '@/lib/utils/dom-purify-config';
|
||||||
import { simpleParser } from 'mailparser';
|
import { simpleParser } from 'mailparser';
|
||||||
|
|
||||||
function getAddressText(addresses: any): string | null {
|
function getAddressText(addresses: any): string | null {
|
||||||
|
|||||||
76
lib/utils/dom-purify-config.ts
Normal file
76
lib/utils/dom-purify-config.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* CENTRALIZED DOMPURIFY CONFIGURATION
|
||||||
|
*
|
||||||
|
* This file provides a consistent, centralized configuration for DOMPurify
|
||||||
|
* used throughout the application. All components that need to sanitize HTML
|
||||||
|
* should import from this file instead of configuring DOMPurify directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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({
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes HTML content with the centralized DOMPurify configuration
|
||||||
|
* @param html HTML content to sanitize
|
||||||
|
* @returns Sanitized HTML
|
||||||
|
*/
|
||||||
|
export function sanitizeHtml(html: string): string {
|
||||||
|
if (!html) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use DOMPurify with our central configuration
|
||||||
|
const clean = DOMPurify.sanitize(html);
|
||||||
|
|
||||||
|
// 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://');
|
||||||
|
|
||||||
|
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:');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configured DOMPurify instance
|
||||||
|
* Use this if you need to perform custom sanitization beyond the standard function
|
||||||
|
*/
|
||||||
|
export function getDOMPurify(): typeof DOMPurify {
|
||||||
|
return DOMPurify;
|
||||||
|
}
|
||||||
@ -1,18 +1,28 @@
|
|||||||
/**
|
/**
|
||||||
* CENTRAL EMAIL FORMATTING UTILITY
|
* DEPRECATED - USE email-utils.ts INSTEAD
|
||||||
*
|
*
|
||||||
* This is the centralized email formatting utility used throughout the application.
|
* This file is maintained for backward compatibility only.
|
||||||
* It provides consistent handling of email content, sanitization, and text direction.
|
* New code should import directly from email-utils.ts, which contains
|
||||||
*
|
* the canonical implementations of these functions.
|
||||||
* All code that needs to format email content should import from this file.
|
|
||||||
* Text direction is preserved based on content language for proper RTL/LTR display.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import { sanitizeHtml } from './dom-purify-config';
|
||||||
import { sanitizeHtml } from './email-utils';
|
import { formatEmailAddresses, formatEmailDate, formatReplyEmail, formatForwardedEmail, formatEmailForReplyOrForward } from './email-utils';
|
||||||
import { applyTextDirection } from './text-direction';
|
import { applyTextDirection } from './text-direction';
|
||||||
// Instead of importing, implement the formatDateRelative function directly
|
import type { EmailMessage, EmailAddress } from '@/types/email';
|
||||||
// import { formatDateRelative } from './date-formatter';
|
|
||||||
|
// Re-export the functions from email-utils for backward compatibility
|
||||||
|
export {
|
||||||
|
formatEmailAddresses,
|
||||||
|
formatEmailDate,
|
||||||
|
formatReplyEmail,
|
||||||
|
formatForwardedEmail,
|
||||||
|
formatEmailForReplyOrForward,
|
||||||
|
sanitizeHtml
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-export types for backward compatibility
|
||||||
|
export type { EmailAddress, EmailMessage };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format a date in a relative format
|
* Format a date in a relative format
|
||||||
@ -35,263 +45,40 @@ function formatDateRelative(date: Date): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset any existing hooks to start clean
|
/**
|
||||||
DOMPurify.removeAllHooks();
|
* Parse text and find URLs to turn into clickable links
|
||||||
|
* This is a utility function used only in this file
|
||||||
// IMPORTANT: We do NOT add any hooks that modify direction attributes
|
*/
|
||||||
// Direction will be handled explicitly by the text-direction.ts utility
|
function parseUrlsToLinks(text: string): string {
|
||||||
|
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||||
// NOTE: Global DOMPurify configuration is now centralized in email-utils.ts
|
return text.replace(
|
||||||
// We don't set config here to avoid conflicts
|
urlRegex,
|
||||||
|
url => `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`
|
||||||
// Note: We ensure proper text direction is applied via the applyTextDirection utility
|
);
|
||||||
// when rendering email content
|
|
||||||
|
|
||||||
// Interface definitions
|
|
||||||
export interface EmailAddress {
|
|
||||||
name: string;
|
|
||||||
address: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EmailMessage {
|
|
||||||
id: string;
|
|
||||||
messageId?: string;
|
|
||||||
subject: string;
|
|
||||||
from: EmailAddress[];
|
|
||||||
to: EmailAddress[];
|
|
||||||
cc?: EmailAddress[];
|
|
||||||
bcc?: EmailAddress[];
|
|
||||||
date: Date | string;
|
|
||||||
flags?: {
|
|
||||||
seen: boolean;
|
|
||||||
flagged: boolean;
|
|
||||||
answered: boolean;
|
|
||||||
deleted: boolean;
|
|
||||||
draft: boolean;
|
|
||||||
};
|
|
||||||
preview?: string;
|
|
||||||
content?: string;
|
|
||||||
html?: string;
|
|
||||||
text?: string;
|
|
||||||
hasAttachments?: boolean;
|
|
||||||
attachments?: any[];
|
|
||||||
folder?: string;
|
|
||||||
size?: number;
|
|
||||||
contentFetched?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EmailContent {
|
|
||||||
html?: string;
|
|
||||||
text?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format email addresses for display
|
* Convert plain text email content to HTML with proper line breaks
|
||||||
|
* This is a utility function used only in this file
|
||||||
*/
|
*/
|
||||||
export function formatEmailAddresses(addresses: EmailAddress[]): string {
|
function textToHtml(text: string): string {
|
||||||
if (!addresses || addresses.length === 0) return '';
|
if (!text) return '';
|
||||||
|
|
||||||
return addresses.map(addr =>
|
// Escape HTML characters first
|
||||||
addr.name && addr.name !== addr.address
|
const escaped = text
|
||||||
? `${addr.name} <${addr.address}>`
|
.replace(/&/g, '&')
|
||||||
: addr.address
|
.replace(/</g, '<')
|
||||||
).join(', ');
|
.replace(/>/g, '>')
|
||||||
}
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
/**
|
// Convert line breaks and wrap in a div
|
||||||
* Format date for display
|
const withLineBreaks = escaped.replace(/\n/g, '<br>');
|
||||||
*/
|
|
||||||
export function formatEmailDate(date: Date | string | undefined): string {
|
|
||||||
if (!date) return '';
|
|
||||||
|
|
||||||
try {
|
// Parse URLs to make them clickable
|
||||||
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
const withLinks = parseUrlsToLinks(withLineBreaks);
|
||||||
return dateObj.toLocaleString('en-US', {
|
|
||||||
weekday: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return typeof date === 'string' ? date : date.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return withLinks;
|
||||||
* Format an email for forwarding - CENTRAL IMPLEMENTATION
|
|
||||||
* All other formatting functions should be deprecated in favor of this one
|
|
||||||
*/
|
|
||||||
export function formatForwardedEmail(email: EmailMessage): {
|
|
||||||
subject: string;
|
|
||||||
content: string;
|
|
||||||
} {
|
|
||||||
// Format subject with Fwd: prefix if needed
|
|
||||||
const subjectBase = email.subject || '(No subject)';
|
|
||||||
const subject = subjectBase.match(/^(Fwd|FW|Forward):/i)
|
|
||||||
? subjectBase
|
|
||||||
: `Fwd: ${subjectBase}`;
|
|
||||||
|
|
||||||
// Get sender and recipient information
|
|
||||||
const fromString = formatEmailAddresses(email.from || []);
|
|
||||||
const toString = formatEmailAddresses(email.to || []);
|
|
||||||
const dateString = formatEmailDate(email.date);
|
|
||||||
|
|
||||||
// Get and sanitize original content (sanitization preserves content direction)
|
|
||||||
const originalContent = sanitizeHtml(email.content || email.html || email.text || '');
|
|
||||||
|
|
||||||
// Check if the content already has a forwarded message header
|
|
||||||
const hasExistingHeader = originalContent.includes('---------- Forwarded message ---------');
|
|
||||||
|
|
||||||
// If there's already a forwarded message header, don't add another one
|
|
||||||
if (hasExistingHeader) {
|
|
||||||
// Just wrap the content without additional formatting
|
|
||||||
const content = `
|
|
||||||
<div style="min-height: 20px;"></div>
|
|
||||||
<div class="email-original-content">
|
|
||||||
${originalContent}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return { subject, content };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create formatted content for forwarded email
|
|
||||||
const content = `
|
|
||||||
<div style="min-height: 20px;">
|
|
||||||
<div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px;">
|
|
||||||
<div style="font-family: Arial, sans-serif; color: #333;">
|
|
||||||
<div style="margin-bottom: 15px;">
|
|
||||||
<div>---------- Forwarded message ---------</div>
|
|
||||||
<div><b>From:</b> ${fromString}</div>
|
|
||||||
<div><b>Date:</b> ${dateString}</div>
|
|
||||||
<div><b>Subject:</b> ${email.subject || ''}</div>
|
|
||||||
<div><b>To:</b> ${toString}</div>
|
|
||||||
</div>
|
|
||||||
<div class="email-original-content">
|
|
||||||
${originalContent}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return { subject, content };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format an email for reply or reply-all - CENTRAL IMPLEMENTATION
|
|
||||||
* All other formatting functions should be deprecated in favor of this one
|
|
||||||
*/
|
|
||||||
export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all'): {
|
|
||||||
to: string;
|
|
||||||
cc?: string;
|
|
||||||
subject: string;
|
|
||||||
content: string;
|
|
||||||
} {
|
|
||||||
// Format subject with Re: prefix if needed
|
|
||||||
const subjectBase = email.subject || '(No subject)';
|
|
||||||
const subject = subjectBase.match(/^Re:/i)
|
|
||||||
? subjectBase
|
|
||||||
: `Re: ${subjectBase}`;
|
|
||||||
|
|
||||||
// Get sender information for quote header
|
|
||||||
const sender = email.from[0];
|
|
||||||
const fromText = sender?.name
|
|
||||||
? `${sender.name} <${sender.address}>`
|
|
||||||
: sender?.address || 'Unknown sender';
|
|
||||||
|
|
||||||
// Format date for quote header
|
|
||||||
const date = typeof email.date === 'string' ? new Date(email.date) : email.date;
|
|
||||||
const formattedDate = date.toLocaleString('en-US', {
|
|
||||||
weekday: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create quote header
|
|
||||||
const quoteHeader = `<div style="font-weight: 400; color: #555; margin: 20px 0 8px 0; font-size: 13px;">On ${formattedDate}, ${fromText} wrote:</div>`;
|
|
||||||
|
|
||||||
// Get and sanitize original content (sanitization preserves content direction)
|
|
||||||
let originalContent = '';
|
|
||||||
if (email.content && typeof email.content === 'object') {
|
|
||||||
const contentObj = email.content as EmailContent;
|
|
||||||
originalContent = contentObj.html || contentObj.text || '';
|
|
||||||
} else if (typeof email.content === 'string') {
|
|
||||||
originalContent = email.content;
|
|
||||||
} else {
|
|
||||||
originalContent = email.html || email.text || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const quotedContent = sanitizeHtml(originalContent);
|
|
||||||
|
|
||||||
// Format recipients
|
|
||||||
let to = formatEmailAddresses(email.from || []);
|
|
||||||
let cc = '';
|
|
||||||
|
|
||||||
if (type === 'reply-all') {
|
|
||||||
// For reply-all, add all original recipients to CC
|
|
||||||
const allRecipients = [
|
|
||||||
...(email.to || []),
|
|
||||||
...(email.cc || [])
|
|
||||||
];
|
|
||||||
|
|
||||||
cc = formatEmailAddresses(allRecipients);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format content for reply with improved styling
|
|
||||||
const content = `
|
|
||||||
<div style="min-height: 20px;"></div>
|
|
||||||
<div class="reply-body" style="font-family: Arial, sans-serif; line-height: 1.5;">
|
|
||||||
${quoteHeader}
|
|
||||||
<blockquote style="margin: 0; padding: 10px 0 10px 15px; border-left: 2px solid #ddd; color: #505050; background-color: #f9f9f9; border-radius: 4px;">
|
|
||||||
<div class="quoted-content" style="font-size: 13px;">
|
|
||||||
${quotedContent}
|
|
||||||
</div>
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
to,
|
|
||||||
cc: cc || undefined,
|
|
||||||
subject,
|
|
||||||
content
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* COMPATIBILITY LAYER: For backward compatibility with the old email-formatter.ts
|
|
||||||
* These functions map to our new implementation but preserve the old interface
|
|
||||||
*/
|
|
||||||
export function formatEmailForReplyOrForward(
|
|
||||||
email: EmailMessage,
|
|
||||||
type: 'reply' | 'reply-all' | 'forward'
|
|
||||||
): {
|
|
||||||
to: string;
|
|
||||||
cc?: string;
|
|
||||||
subject: string;
|
|
||||||
body: string;
|
|
||||||
} {
|
|
||||||
if (type === 'forward') {
|
|
||||||
const { subject, content } = formatForwardedEmail(email);
|
|
||||||
return {
|
|
||||||
to: '',
|
|
||||||
subject,
|
|
||||||
body: content
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const { to, cc, subject, content } = formatReplyEmail(email, type as 'reply' | 'reply-all');
|
|
||||||
return {
|
|
||||||
to,
|
|
||||||
cc,
|
|
||||||
subject,
|
|
||||||
body: content
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -320,7 +107,7 @@ export async function decodeComposeContent(content: string): Promise<{
|
|||||||
|
|
||||||
const parsed = await response.json();
|
const parsed = await response.json();
|
||||||
|
|
||||||
// Apply LTR sanitization to the parsed content
|
// Apply sanitization to the parsed content
|
||||||
return {
|
return {
|
||||||
html: parsed.html ? sanitizeHtml(parsed.html) : null,
|
html: parsed.html ? sanitizeHtml(parsed.html) : null,
|
||||||
text: parsed.text || null
|
text: parsed.text || null
|
||||||
@ -356,65 +143,6 @@ export function encodeComposeContent(content: string): string {
|
|||||||
.join('\n') + '\n\n' + content;
|
.join('\n') + '\n\n' + content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
// Use our own sanitizeHtml function consistently
|
|
||||||
const sanitizedBody = sanitizeHtml(email.content || '');
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// Use our own sanitizeHtml function consistently
|
|
||||||
const sanitizedBody = sanitizeHtml(email.content || '');
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// For reply all, we use the same format as regular reply
|
|
||||||
return formatReplyEmailLegacy(email);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility function to get the reply subject line
|
// Utility function to get the reply subject line
|
||||||
export function getReplySubject(subject: string): string {
|
export function getReplySubject(subject: string): string {
|
||||||
return subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
return subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
||||||
|
|||||||
@ -3,12 +3,12 @@
|
|||||||
*
|
*
|
||||||
* This file contains all email-related utility functions:
|
* This file contains all email-related utility functions:
|
||||||
* - Content normalization
|
* - Content normalization
|
||||||
* - Content sanitization
|
|
||||||
* - Email formatting (replies, forwards)
|
* - Email formatting (replies, forwards)
|
||||||
* - Text direction detection
|
* - Text direction detection
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
// Import from centralized DOMPurify configuration instead of configuring directly
|
||||||
|
import { sanitizeHtml, getDOMPurify } from './dom-purify-config';
|
||||||
import {
|
import {
|
||||||
EmailMessage,
|
EmailMessage,
|
||||||
EmailContent,
|
EmailContent,
|
||||||
@ -19,16 +19,37 @@ import { adaptLegacyEmail } from '@/lib/utils/email-adapters';
|
|||||||
import { decodeInfomaniakEmail, adaptMimeEmail, isMimeFormat } from './email-mime-decoder';
|
import { decodeInfomaniakEmail, adaptMimeEmail, isMimeFormat } from './email-mime-decoder';
|
||||||
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
|
import { detectTextDirection, applyTextDirection } from '@/lib/utils/text-direction';
|
||||||
|
|
||||||
// Reset any existing hooks to start clean
|
// Export the sanitizeHtml function from the centralized config
|
||||||
DOMPurify.removeAllHooks();
|
export { sanitizeHtml };
|
||||||
|
|
||||||
// Remove the hook that adds dir="auto" - we'll handle direction explicitly instead
|
/**
|
||||||
|
* Standard interface for formatted email responses
|
||||||
|
*/
|
||||||
|
export interface FormattedEmail {
|
||||||
|
to: string;
|
||||||
|
cc?: string;
|
||||||
|
subject: string;
|
||||||
|
content: EmailContent;
|
||||||
|
}
|
||||||
|
|
||||||
// Configure DOMPurify to preserve direction attributes
|
/**
|
||||||
DOMPurify.setConfig({
|
* Utility type that combines EmailMessage and LegacyEmailMessage
|
||||||
ADD_ATTR: ['dir'],
|
* to allow access to properties that might exist in either type
|
||||||
ALLOWED_ATTR: ['style', 'class', 'id', 'dir']
|
*/
|
||||||
});
|
type AnyEmailMessage = {
|
||||||
|
id: string;
|
||||||
|
subject: string | undefined;
|
||||||
|
from: any;
|
||||||
|
to: any;
|
||||||
|
cc?: any;
|
||||||
|
date: any;
|
||||||
|
content?: any;
|
||||||
|
html?: string;
|
||||||
|
text?: string;
|
||||||
|
attachments?: any[];
|
||||||
|
flags?: any;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format email addresses for display
|
* Format email addresses for display
|
||||||
@ -75,59 +96,6 @@ 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:');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format plain text for HTML display with proper line breaks
|
* Format plain text for HTML display with proper line breaks
|
||||||
*/
|
*/
|
||||||
@ -152,7 +120,6 @@ export function formatPlainTextToHtml(text: string | null | undefined): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize email content to our standard format regardless of input format
|
* Normalize email content to our standard format regardless of input format
|
||||||
* This is the key function that handles all the different email content formats
|
|
||||||
*/
|
*/
|
||||||
export function normalizeEmailContent(email: any): EmailMessage {
|
export function normalizeEmailContent(email: any): EmailMessage {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
@ -177,8 +144,8 @@ export function normalizeEmailContent(email: any): EmailMessage {
|
|||||||
return email as EmailMessage;
|
return email as EmailMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, adapt from legacy format
|
// Otherwise, adapt from legacy format and cast to EmailMessage
|
||||||
return adaptLegacyEmail(email);
|
return adaptLegacyEmail(email as LegacyEmailMessage) as unknown as EmailMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -191,35 +158,22 @@ export function renderEmailContent(content: EmailContent | null): string {
|
|||||||
|
|
||||||
const safeContent = {
|
const safeContent = {
|
||||||
text: content.text || '',
|
text: content.text || '',
|
||||||
html: content.html,
|
html: content.html || '',
|
||||||
isHtml: content.isHtml,
|
isHtml: !!content.isHtml,
|
||||||
direction: content.direction || 'ltr'
|
direction: content.direction || 'ltr'
|
||||||
};
|
};
|
||||||
|
|
||||||
// If we have HTML content and isHtml flag is true, use it
|
// If we have HTML content and isHtml flag is true, use it
|
||||||
if (safeContent.isHtml && safeContent.html) {
|
if (safeContent.isHtml && safeContent.html) {
|
||||||
// Apply text direction consistently using the utility
|
return sanitizeHtml(safeContent.html);
|
||||||
return applyTextDirection(safeContent.html, safeContent.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, format the text content with basic HTML
|
// Otherwise, convert text to HTML with proper line breaks
|
||||||
const text = safeContent.text;
|
if (safeContent.text) {
|
||||||
const formattedText = text
|
return `<div dir="${safeContent.direction}">${formatPlainTextToHtml(safeContent.text)}</div>`;
|
||||||
.replace(/&/g, '&')
|
}
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/\n/g, '<br>');
|
|
||||||
|
|
||||||
// Apply text direction consistently
|
return '<div class="email-content-empty">No content available</div>';
|
||||||
return applyTextDirection(formattedText, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add interface for email formatting functions
|
|
||||||
interface FormattedEmail {
|
|
||||||
to: string;
|
|
||||||
cc?: string;
|
|
||||||
subject: string;
|
|
||||||
content: EmailContent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -240,83 +194,86 @@ export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessag
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cast to AnyEmailMessage for property access
|
||||||
|
const email = originalEmail as AnyEmailMessage;
|
||||||
|
|
||||||
// Format the recipients
|
// Format the recipients
|
||||||
const to = Array.isArray(originalEmail.from)
|
const to = Array.isArray(email.from)
|
||||||
? originalEmail.from.map((addr: any) => {
|
? email.from.map((addr: any) => {
|
||||||
if (typeof addr === 'string') return addr;
|
if (typeof addr === 'string') return addr;
|
||||||
return addr.address ? addr.address : '';
|
return addr.address ? addr.address : '';
|
||||||
}).filter(Boolean).join(', ')
|
}).filter(Boolean).join(', ')
|
||||||
: typeof originalEmail.from === 'string'
|
: typeof email.from === 'string'
|
||||||
? originalEmail.from
|
? email.from
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// For reply-all, include other recipients in CC
|
// For reply-all, include other recipients in CC
|
||||||
let cc = '';
|
let cc = '';
|
||||||
if (type === 'reply-all') {
|
if (type === 'reply-all') {
|
||||||
const toRecipients = Array.isArray(originalEmail.to)
|
const toRecipients = Array.isArray(email.to)
|
||||||
? originalEmail.to.map((addr: any) => {
|
? email.to.map((addr: any) => {
|
||||||
if (typeof addr === 'string') return addr;
|
if (typeof addr === 'string') return addr;
|
||||||
return addr.address ? addr.address : '';
|
return addr.address ? addr.address : '';
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
: typeof originalEmail.to === 'string'
|
: typeof email.to === 'string'
|
||||||
? [originalEmail.to]
|
? [email.to]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const ccRecipients = Array.isArray(originalEmail.cc)
|
const ccRecipients = Array.isArray(email.cc)
|
||||||
? originalEmail.cc.map((addr: any) => {
|
? email.cc.map((addr: any) => {
|
||||||
if (typeof addr === 'string') return addr;
|
if (typeof addr === 'string') return addr;
|
||||||
return addr.address ? addr.address : '';
|
return addr.address ? addr.address : '';
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
: typeof originalEmail.cc === 'string'
|
: typeof email.cc === 'string'
|
||||||
? [originalEmail.cc]
|
? [email.cc]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
cc = [...toRecipients, ...ccRecipients].join(', ');
|
cc = [...toRecipients, ...ccRecipients].join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format the subject
|
// Format the subject
|
||||||
const subject = originalEmail.subject && !originalEmail.subject.startsWith('Re:')
|
const subject = email.subject && !email.subject.startsWith('Re:')
|
||||||
? `Re: ${originalEmail.subject}`
|
? `Re: ${email.subject}`
|
||||||
: originalEmail.subject || '';
|
: email.subject || '';
|
||||||
|
|
||||||
// Format the content
|
// Format the content
|
||||||
const originalDate = originalEmail.date ? new Date(originalEmail.date) : new Date();
|
const originalDate = email.date ? new Date(email.date) : new Date();
|
||||||
const dateStr = originalDate.toLocaleString();
|
const dateStr = originalDate.toLocaleString();
|
||||||
|
|
||||||
const fromStr = Array.isArray(originalEmail.from)
|
const fromStr = Array.isArray(email.from)
|
||||||
? originalEmail.from.map((addr: any) => {
|
? email.from.map((addr: any) => {
|
||||||
if (typeof addr === 'string') return addr;
|
if (typeof addr === 'string') return addr;
|
||||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||||
}).join(', ')
|
}).join(', ')
|
||||||
: typeof originalEmail.from === 'string'
|
: typeof email.from === 'string'
|
||||||
? originalEmail.from
|
? email.from
|
||||||
: 'Unknown Sender';
|
: 'Unknown Sender';
|
||||||
|
|
||||||
const toStr = Array.isArray(originalEmail.to)
|
const toStr = Array.isArray(email.to)
|
||||||
? originalEmail.to.map((addr: any) => {
|
? email.to.map((addr: any) => {
|
||||||
if (typeof addr === 'string') return addr;
|
if (typeof addr === 'string') return addr;
|
||||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||||
}).join(', ')
|
}).join(', ')
|
||||||
: typeof originalEmail.to === 'string'
|
: typeof email.to === 'string'
|
||||||
? originalEmail.to
|
? email.to
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Extract original content
|
// Extract original content
|
||||||
const originalTextContent =
|
const originalTextContent =
|
||||||
typeof originalEmail?.content === 'object' ? originalEmail.content.text :
|
typeof email.content === 'object' && email.content?.text ? email.content.text :
|
||||||
typeof originalEmail?.content === 'string' ? originalEmail.content :
|
typeof email.content === 'string' ? email.content :
|
||||||
originalEmail?.text || '';
|
email.text || '';
|
||||||
|
|
||||||
const originalHtmlContent =
|
const originalHtmlContent =
|
||||||
typeof originalEmail?.content === 'object' ? originalEmail.content.html :
|
typeof email.content === 'object' && email.content?.html ? email.content.html :
|
||||||
originalEmail?.html ||
|
email.html ||
|
||||||
(typeof originalEmail?.content === 'string' && originalEmail?.content.includes('<')
|
(typeof email.content === 'string' && email.content.includes('<')
|
||||||
? originalEmail.content
|
? email.content
|
||||||
: '');
|
: '');
|
||||||
|
|
||||||
// Get the direction from the original email
|
// Get the direction from the original email
|
||||||
const originalDirection =
|
const originalDirection =
|
||||||
typeof originalEmail?.content === 'object' ? originalEmail.content.direction :
|
typeof email.content === 'object' && email.content?.direction ? email.content.direction :
|
||||||
detectTextDirection(originalTextContent);
|
detectTextDirection(originalTextContent);
|
||||||
|
|
||||||
// Create content with appropriate quote formatting
|
// Create content with appropriate quote formatting
|
||||||
@ -369,57 +326,60 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cast to AnyEmailMessage for property access
|
||||||
|
const email = originalEmail as AnyEmailMessage;
|
||||||
|
|
||||||
// Format the subject
|
// Format the subject
|
||||||
const subject = originalEmail.subject && !originalEmail.subject.startsWith('Fwd:')
|
const subject = email.subject && !email.subject.startsWith('Fwd:')
|
||||||
? `Fwd: ${originalEmail.subject}`
|
? `Fwd: ${email.subject}`
|
||||||
: originalEmail.subject || '';
|
: email.subject || '';
|
||||||
|
|
||||||
// Format from, to, cc for the header
|
// Format from, to, cc for the header
|
||||||
const fromStr = Array.isArray(originalEmail.from)
|
const fromStr = Array.isArray(email.from)
|
||||||
? originalEmail.from.map((addr: any) => {
|
? email.from.map((addr: any) => {
|
||||||
if (typeof addr === 'string') return addr;
|
if (typeof addr === 'string') return addr;
|
||||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||||
}).join(', ')
|
}).join(', ')
|
||||||
: typeof originalEmail.from === 'string'
|
: typeof email.from === 'string'
|
||||||
? originalEmail.from
|
? email.from
|
||||||
: 'Unknown Sender';
|
: 'Unknown Sender';
|
||||||
|
|
||||||
const toStr = Array.isArray(originalEmail.to)
|
const toStr = Array.isArray(email.to)
|
||||||
? originalEmail.to.map((addr: any) => {
|
? email.to.map((addr: any) => {
|
||||||
if (typeof addr === 'string') return addr;
|
if (typeof addr === 'string') return addr;
|
||||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||||
}).join(', ')
|
}).join(', ')
|
||||||
: typeof originalEmail.to === 'string'
|
: typeof email.to === 'string'
|
||||||
? originalEmail.to
|
? email.to
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const ccStr = Array.isArray(originalEmail.cc)
|
const ccStr = Array.isArray(email.cc)
|
||||||
? originalEmail.cc.map((addr: any) => {
|
? email.cc.map((addr: any) => {
|
||||||
if (typeof addr === 'string') return addr;
|
if (typeof addr === 'string') return addr;
|
||||||
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||||
}).join(', ')
|
}).join(', ')
|
||||||
: typeof originalEmail.cc === 'string'
|
: typeof email.cc === 'string'
|
||||||
? originalEmail.cc
|
? email.cc
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const dateStr = originalEmail.date ? new Date(originalEmail.date).toLocaleString() : 'Unknown Date';
|
const dateStr = email.date ? new Date(email.date).toLocaleString() : 'Unknown Date';
|
||||||
|
|
||||||
// Extract original content
|
// Extract original content
|
||||||
const originalTextContent =
|
const originalTextContent =
|
||||||
typeof originalEmail?.content === 'object' ? originalEmail.content.text :
|
typeof email.content === 'object' && email.content?.text ? email.content.text :
|
||||||
typeof originalEmail?.content === 'string' ? originalEmail.content :
|
typeof email.content === 'string' ? email.content :
|
||||||
originalEmail?.text || '';
|
email.text || '';
|
||||||
|
|
||||||
const originalHtmlContent =
|
const originalHtmlContent =
|
||||||
typeof originalEmail?.content === 'object' ? originalEmail.content.html :
|
typeof email.content === 'object' && email.content?.html ? email.content.html :
|
||||||
originalEmail?.html ||
|
email.html ||
|
||||||
(typeof originalEmail?.content === 'string' && originalEmail?.content.includes('<')
|
(typeof email.content === 'string' && email.content.includes('<')
|
||||||
? originalEmail.content
|
? email.content
|
||||||
: '');
|
: '');
|
||||||
|
|
||||||
// Get the direction from the original email
|
// Get the direction from the original email
|
||||||
const originalDirection =
|
const originalDirection =
|
||||||
typeof originalEmail?.content === 'object' ? originalEmail.content.direction :
|
typeof email.content === 'object' && email.content?.direction ? email.content.direction :
|
||||||
detectTextDirection(originalTextContent);
|
detectTextDirection(originalTextContent);
|
||||||
|
|
||||||
// Create forwarded content with header information
|
// Create forwarded content with header information
|
||||||
@ -430,7 +390,7 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
|
|||||||
<p>---------- Forwarded message ---------</p>
|
<p>---------- Forwarded message ---------</p>
|
||||||
<p><strong>From:</strong> ${fromStr}</p>
|
<p><strong>From:</strong> ${fromStr}</p>
|
||||||
<p><strong>Date:</strong> ${dateStr}</p>
|
<p><strong>Date:</strong> ${dateStr}</p>
|
||||||
<p><strong>Subject:</strong> ${originalEmail?.subject || ''}</p>
|
<p><strong>Subject:</strong> ${email.subject || ''}</p>
|
||||||
<p><strong>To:</strong> ${toStr}</p>
|
<p><strong>To:</strong> ${toStr}</p>
|
||||||
${ccStr ? `<p><strong>Cc:</strong> ${ccStr}</p>` : ''}
|
${ccStr ? `<p><strong>Cc:</strong> ${ccStr}</p>` : ''}
|
||||||
<div style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;">
|
<div style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;">
|
||||||
@ -448,7 +408,7 @@ export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMe
|
|||||||
---------- Forwarded message ---------
|
---------- Forwarded message ---------
|
||||||
From: ${fromStr}
|
From: ${fromStr}
|
||||||
Date: ${dateStr}
|
Date: ${dateStr}
|
||||||
Subject: ${originalEmail?.subject || ''}
|
Subject: ${email.subject || ''}
|
||||||
To: ${toStr}
|
To: ${toStr}
|
||||||
${ccStr ? `Cc: ${ccStr}\n` : ''}
|
${ccStr ? `Cc: ${ccStr}\n` : ''}
|
||||||
|
|
||||||
@ -468,32 +428,16 @@ ${originalTextContent}
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format an email for reply or reply-all
|
* Format an email for reply or reply-all - canonical implementation
|
||||||
*/
|
*/
|
||||||
export function formatEmailForReplyOrForward(
|
export function formatEmailForReplyOrForward(
|
||||||
email: EmailMessage,
|
email: EmailMessage | LegacyEmailMessage | null,
|
||||||
type: 'reply' | 'reply-all' | 'forward'
|
type: 'reply' | 'reply-all' | 'forward'
|
||||||
): {
|
): FormattedEmail {
|
||||||
to?: string;
|
// Use our dedicated formatters
|
||||||
cc?: string;
|
|
||||||
subject: string;
|
|
||||||
content: EmailContent;
|
|
||||||
} {
|
|
||||||
// Use our dedicated formatters but ensure the return is properly typed
|
|
||||||
if (type === 'forward') {
|
if (type === 'forward') {
|
||||||
const formatted = formatForwardedEmail(email);
|
return formatForwardedEmail(email);
|
||||||
return {
|
|
||||||
to: formatted.to,
|
|
||||||
subject: formatted.subject,
|
|
||||||
content: formatted.content
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
const formatted = formatReplyEmail(email, type);
|
return formatReplyEmail(email, type as 'reply' | 'reply-all');
|
||||||
return {
|
|
||||||
to: formatted.to,
|
|
||||||
cc: formatted.cc,
|
|
||||||
subject: formatted.subject,
|
|
||||||
content: formatted.content
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user