panel 2 courier api restore
This commit is contained in:
parent
6edcb636c8
commit
b58539aeaa
@ -1,22 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* This route is deprecated. It redirects to the new courrier API endpoint.
|
||||
* @deprecated Use the /api/courrier endpoint instead
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
console.warn('Deprecated: /api/mail route is being used. Update your code to use /api/courrier instead.');
|
||||
|
||||
// Extract query parameters
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Redirect to the new API endpoint
|
||||
const redirectUrl = new URL('/api/courrier', url.origin);
|
||||
|
||||
// Copy all search parameters
|
||||
url.searchParams.forEach((value, key) => {
|
||||
redirectUrl.searchParams.set(key, value);
|
||||
});
|
||||
|
||||
return NextResponse.redirect(redirectUrl.toString());
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
/**
|
||||
* This route is deprecated. It redirects to the new courrier API endpoint.
|
||||
* @deprecated Use the /api/courrier/send endpoint instead
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
console.warn('Deprecated: /api/mail/send route is being used. Update your code to use /api/courrier/send instead.');
|
||||
|
||||
try {
|
||||
// Clone the request body
|
||||
const body = await request.json();
|
||||
|
||||
// Make a new request to the courrier API
|
||||
const newRequest = new Request(new URL('/api/courrier/send', request.url).toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
// Forward the request
|
||||
const response = await fetch(newRequest);
|
||||
const data = await response.json();
|
||||
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
} catch (error) {
|
||||
console.error('Error forwarding to courrier/send:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to send email' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,38 +1,83 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { simpleParser, AddressObject } from 'mailparser';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import * as DOMPurify from 'isomorphic-dompurify';
|
||||
|
||||
function getEmailAddress(address: AddressObject | AddressObject[] | undefined): string | null {
|
||||
if (!address) return null;
|
||||
if (Array.isArray(address)) {
|
||||
return address.map(a => a.text).join(', ');
|
||||
}
|
||||
return address.text;
|
||||
interface EmailAddress {
|
||||
name?: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
// Clean up the HTML to make it safe but preserve styles
|
||||
function processHtml(html: string | null): string | null {
|
||||
if (!html) return null;
|
||||
// Helper to extract email addresses from mailparser Address objects
|
||||
function getEmailAddresses(addresses: any): EmailAddress[] {
|
||||
if (!addresses) return [];
|
||||
|
||||
// Handle various address formats
|
||||
if (Array.isArray(addresses)) {
|
||||
return addresses.map(addr => ({
|
||||
name: addr.name || undefined,
|
||||
address: addr.address
|
||||
}));
|
||||
}
|
||||
|
||||
if (typeof addresses === 'object') {
|
||||
const result: EmailAddress[] = [];
|
||||
// Handle mailparser format with text, html, value properties
|
||||
if (addresses.value) {
|
||||
addresses.value.forEach((addr: any) => {
|
||||
result.push({
|
||||
name: addr.name || undefined,
|
||||
address: addr.address
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// Handle direct object with address property
|
||||
if (addresses.address) {
|
||||
return [{
|
||||
name: addresses.name || undefined,
|
||||
address: addresses.address
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Process HTML to ensure it displays well in our email context
|
||||
function processHtml(html: string): string {
|
||||
if (!html) return '';
|
||||
|
||||
try {
|
||||
// Make the content display well in the email context
|
||||
return html
|
||||
// Fix self-closing tags that might break React
|
||||
.replace(/<(br|hr|img|input|link|meta|area|base|col|embed|keygen|param|source|track|wbr)([^>]*)>/gi, '<$1$2 />')
|
||||
// Keep style tags but ensure they're closed properly
|
||||
.replace(/<style([^>]*)>([\s\S]*?)<\/style>/gi, (match) => {
|
||||
// Just return the matched style tag as-is
|
||||
return match;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing HTML:', error);
|
||||
return html;
|
||||
// Fix self-closing tags that might break in contentEditable
|
||||
html = html.replace(/<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([^>]*)>/gi,
|
||||
(match, tag, attrs) => `<${tag}${attrs}${attrs.endsWith('/') ? '' : '/'}>`)
|
||||
|
||||
// Clean up HTML with DOMPurify - CRITICAL for security
|
||||
// Allow style tags but remove script tags
|
||||
const cleaned = DOMPurify.sanitize(html, {
|
||||
ADD_TAGS: ['style'],
|
||||
FORBID_TAGS: ['script', 'iframe', 'object', 'embed'],
|
||||
WHOLE_DOCUMENT: false
|
||||
});
|
||||
|
||||
// Scope CSS to prevent leakage
|
||||
return cleaned.replace(/<style([^>]*)>([\s\S]*?)<\/style>/gi, (match, attrs, css) => {
|
||||
// Generate a unique class for this email content
|
||||
const uniqueClass = `email-content-${Date.now()}`;
|
||||
|
||||
// Add the unique class to outer container that will be added
|
||||
return `<style${attrs}>.${uniqueClass} {contain: content;} .${uniqueClass} ${css}</style>`;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error processing HTML:', e);
|
||||
return html; // Return original if processing fails
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email } = body;
|
||||
const { email } = await req.json();
|
||||
|
||||
if (!email || typeof email !== 'string') {
|
||||
return NextResponse.json(
|
||||
@ -40,33 +85,42 @@ export async function POST(request: Request) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const parsed = await simpleParser(email);
|
||||
|
||||
// Process the HTML to preserve styling but make it safe
|
||||
// Handle the case where parsed.html could be a boolean
|
||||
const processedHtml = typeof parsed.html === 'string' ? processHtml(parsed.html) : null;
|
||||
// Process the HTML content to make it safe and displayable
|
||||
const html = parsed.html
|
||||
? processHtml(parsed.html.toString())
|
||||
: undefined;
|
||||
|
||||
const text = parsed.text
|
||||
? parsed.text.toString()
|
||||
: undefined;
|
||||
|
||||
// Extract attachments info if available
|
||||
const attachments = parsed.attachments?.map(attachment => ({
|
||||
filename: attachment.filename,
|
||||
contentType: attachment.contentType,
|
||||
contentDisposition: attachment.contentDisposition,
|
||||
size: attachment.size
|
||||
})) || [];
|
||||
|
||||
// Return all parsed email details
|
||||
return NextResponse.json({
|
||||
subject: parsed.subject || null,
|
||||
from: getEmailAddress(parsed.from),
|
||||
to: getEmailAddress(parsed.to),
|
||||
cc: getEmailAddress(parsed.cc),
|
||||
bcc: getEmailAddress(parsed.bcc),
|
||||
date: parsed.date || null,
|
||||
html: processedHtml,
|
||||
text: parsed.textAsHtml || parsed.text || null,
|
||||
attachments: parsed.attachments?.map(att => ({
|
||||
filename: att.filename,
|
||||
contentType: att.contentType,
|
||||
size: att.size
|
||||
})) || [],
|
||||
headers: parsed.headers || {}
|
||||
subject: parsed.subject,
|
||||
from: getEmailAddresses(parsed.from),
|
||||
to: getEmailAddresses(parsed.to),
|
||||
cc: getEmailAddresses(parsed.cc),
|
||||
bcc: getEmailAddresses(parsed.bcc),
|
||||
date: parsed.date,
|
||||
html,
|
||||
text,
|
||||
attachments
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing email:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to parse email' },
|
||||
{ error: 'Failed to parse email content' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
/**
|
||||
* This is a debugging component that provides troubleshooting tools
|
||||
* for the email loading process in the Courrier application.
|
||||
*
|
||||
* NOTE: This component should only be used during development for debugging purposes.
|
||||
* It's kept in the codebase for future reference but won't render in production.
|
||||
*/
|
||||
'use client';
|
||||
|
||||
@ -26,6 +29,11 @@ export function LoadingFix({
|
||||
loadEmails,
|
||||
emails
|
||||
}: LoadingFixProps) {
|
||||
// Don't render anything in production mode
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const forceResetLoadingStates = () => {
|
||||
console.log('[DEBUG] Force resetting loading states to false');
|
||||
// Force both loading states to false
|
||||
|
||||
@ -95,6 +95,10 @@ interface ParsedEmailMetadata {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This function is deprecated and will be removed in future versions.
|
||||
* Email parsing has been centralized in lib/mail-parser-wrapper.ts and the API endpoint.
|
||||
*/
|
||||
function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: string } {
|
||||
const [headers, ...bodyParts] = emailBody.split('\r\n\r\n');
|
||||
return {
|
||||
@ -322,6 +326,15 @@ function formatDate(date: Date | null): string {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated This function is deprecated and will be removed in future versions.
|
||||
* Use the ReplyContent component directly instead.
|
||||
*/
|
||||
function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') {
|
||||
console.warn('getReplyBody is deprecated, use <ReplyContent email={email} type={type} /> instead');
|
||||
return <ReplyContent email={email} type={type} />;
|
||||
}
|
||||
|
||||
function ReplyContent({ email, type }: { email: Email; type: 'reply' | 'reply-all' | 'forward' }) {
|
||||
const [content, setContent] = useState<string>('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -390,11 +403,6 @@ function ReplyContent({ email, type }: { email: Email; type: 'reply' | 'reply-al
|
||||
return <div dangerouslySetInnerHTML={{ __html: content }} />;
|
||||
}
|
||||
|
||||
// Update the getReplyBody function to use the new component
|
||||
function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'reply') {
|
||||
return <ReplyContent email={email} type={type} />;
|
||||
}
|
||||
|
||||
function EmailPreview({ email }: { email: Email }) {
|
||||
const [preview, setPreview] = useState<string>('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@ -472,6 +480,9 @@ function EmailPreview({ email }: { email: Email }) {
|
||||
|
||||
// Update the generateEmailPreview function to use the new component
|
||||
function generateEmailPreview(email: Email) {
|
||||
// @deprecated - This function is deprecated and will be removed in future versions.
|
||||
// Use the EmailPreview component directly instead.
|
||||
console.warn('generateEmailPreview is deprecated, use <EmailPreview email={email} /> instead');
|
||||
return <EmailPreview email={email} />;
|
||||
}
|
||||
|
||||
|
||||
@ -1,587 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Paperclip, X } from 'lucide-react';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { decodeComposeContent, encodeComposeContent } from '@/lib/compose-mime-decoder';
|
||||
import { Email } from '@/app/courrier/page';
|
||||
import mime from 'mime';
|
||||
import { simpleParser } from 'mailparser';
|
||||
import { decodeEmail } from '@/lib/mail-parser-wrapper';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
interface ComposeEmailProps {
|
||||
showCompose: boolean;
|
||||
setShowCompose: (show: boolean) => void;
|
||||
composeTo: string;
|
||||
setComposeTo: (to: string) => void;
|
||||
composeCc: string;
|
||||
setComposeCc: (cc: string) => void;
|
||||
composeBcc: string;
|
||||
setComposeBcc: (bcc: string) => void;
|
||||
composeSubject: string;
|
||||
setComposeSubject: (subject: string) => void;
|
||||
composeBody: string;
|
||||
setComposeBody: (body: string) => void;
|
||||
showCc: boolean;
|
||||
setShowCc: (show: boolean) => void;
|
||||
showBcc: boolean;
|
||||
setShowBcc: (show: boolean) => void;
|
||||
attachments: any[];
|
||||
setAttachments: (attachments: any[]) => void;
|
||||
handleSend: () => Promise<void>;
|
||||
originalEmail?: {
|
||||
content: string;
|
||||
type: 'reply' | 'reply-all' | 'forward';
|
||||
};
|
||||
onSend: (email: Email) => void;
|
||||
onCancel: () => void;
|
||||
onBodyChange?: (body: string) => void;
|
||||
initialTo?: string;
|
||||
initialSubject?: string;
|
||||
initialBody?: string;
|
||||
initialCc?: string;
|
||||
initialBcc?: string;
|
||||
replyTo?: Email | null;
|
||||
forwardFrom?: Email | null;
|
||||
}
|
||||
|
||||
export default function ComposeEmail({
|
||||
showCompose,
|
||||
setShowCompose,
|
||||
composeTo,
|
||||
setComposeTo,
|
||||
composeCc,
|
||||
setComposeCc,
|
||||
composeBcc,
|
||||
setComposeBcc,
|
||||
composeSubject,
|
||||
setComposeSubject,
|
||||
composeBody,
|
||||
setComposeBody,
|
||||
showCc,
|
||||
setShowCc,
|
||||
showBcc,
|
||||
setShowBcc,
|
||||
attachments,
|
||||
setAttachments,
|
||||
handleSend,
|
||||
originalEmail,
|
||||
onSend,
|
||||
onCancel,
|
||||
onBodyChange,
|
||||
initialTo,
|
||||
initialSubject,
|
||||
initialBody,
|
||||
initialCc,
|
||||
initialBcc,
|
||||
replyTo,
|
||||
forwardFrom
|
||||
}: ComposeEmailProps) {
|
||||
const composeBodyRef = useRef<HTMLDivElement>(null);
|
||||
const [localContent, setLocalContent] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (replyTo || forwardFrom) {
|
||||
const initializeContent = async () => {
|
||||
if (!composeBodyRef.current) return;
|
||||
|
||||
try {
|
||||
const emailToProcess = replyTo || forwardFrom;
|
||||
console.log('[DEBUG] Initializing compose content with email:',
|
||||
emailToProcess ? {
|
||||
id: emailToProcess.id,
|
||||
subject: emailToProcess.subject,
|
||||
hasContent: !!emailToProcess.content,
|
||||
contentLength: emailToProcess.content ? emailToProcess.content.length : 0,
|
||||
preview: emailToProcess.preview
|
||||
} : 'null'
|
||||
);
|
||||
|
||||
// Set initial loading state
|
||||
composeBodyRef.current.innerHTML = `
|
||||
<div class="compose-area" contenteditable="true">
|
||||
<br/>
|
||||
<div class="text-gray-500">Loading original message...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
// Check if email object exists
|
||||
if (!emailToProcess) {
|
||||
console.error('[DEBUG] No email to process for reply/forward');
|
||||
composeBodyRef.current.innerHTML = `
|
||||
<div class="compose-area" contenteditable="true">
|
||||
<br/>
|
||||
<div style="color: #ef4444;">No email selected for reply/forward.</div>
|
||||
</div>
|
||||
`;
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we need to fetch full content first
|
||||
if (!emailToProcess.content || emailToProcess.content.length === 0) {
|
||||
console.log('[DEBUG] Need to fetch content before composing reply/forward');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/courrier/${emailToProcess.id}?folder=${encodeURIComponent(emailToProcess.folder || 'INBOX')}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch email content: ${response.status}`);
|
||||
}
|
||||
|
||||
const fullContent = await response.json();
|
||||
|
||||
// Update the email content with the fetched full content
|
||||
emailToProcess.content = fullContent.content;
|
||||
emailToProcess.contentFetched = true;
|
||||
|
||||
console.log('[DEBUG] Successfully fetched content for reply/forward');
|
||||
} catch (error) {
|
||||
console.error('[DEBUG] Error fetching content for reply:', error);
|
||||
composeBodyRef.current.innerHTML = `
|
||||
<div class="compose-area" contenteditable="true">
|
||||
<br/>
|
||||
<div style="color: #ef4444;">Failed to load email content. Please try again.</div>
|
||||
</div>
|
||||
`;
|
||||
setIsLoading(false);
|
||||
return; // Exit if we couldn't get the content
|
||||
}
|
||||
}
|
||||
|
||||
// Use the exact same implementation as Panel 3's ReplyContent
|
||||
try {
|
||||
const decoded = await decodeEmail(emailToProcess.content);
|
||||
|
||||
let formattedContent = '';
|
||||
|
||||
if (forwardFrom) {
|
||||
// Create a clean header for the forwarded email
|
||||
const headerHtml = `
|
||||
<div style="border-bottom: 1px solid #e2e2e2; margin-bottom: 15px; padding-bottom: 15px; font-family: Arial, sans-serif;">
|
||||
<p style="margin: 4px 0;">---------- Forwarded message ---------</p>
|
||||
<p style="margin: 4px 0;"><b>From:</b> ${decoded.from || ''}</p>
|
||||
<p style="margin: 4px 0;"><b>Date:</b> ${formatDate(decoded.date)}</p>
|
||||
<p style="margin: 4px 0;"><b>Subject:</b> ${decoded.subject || ''}</p>
|
||||
<p style="margin: 4px 0;"><b>To:</b> ${decoded.to || ''}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Use the original HTML as-is without DOMPurify or any modification
|
||||
formattedContent = `
|
||||
${headerHtml}
|
||||
${decoded.html || decoded.text || 'No content available'}
|
||||
`;
|
||||
} else {
|
||||
formattedContent = `
|
||||
<div class="quoted-message">
|
||||
<p>On ${formatDate(decoded.date)}, ${decoded.from || ''} wrote:</p>
|
||||
<blockquote>
|
||||
<div class="email-content prose prose-sm max-w-none dark:prose-invert">
|
||||
${decoded.html || `<pre>${decoded.text || ''}</pre>`}
|
||||
</div>
|
||||
</blockquote>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Set the content in the compose area with proper structure
|
||||
const wrappedContent = `
|
||||
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px;">
|
||||
<div style="min-height: 20px;"><br/></div>
|
||||
${formattedContent}
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (composeBodyRef.current) {
|
||||
composeBodyRef.current.innerHTML = wrappedContent;
|
||||
|
||||
// Place cursor at the beginning before the quoted content
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
const firstDiv = composeBodyRef.current.querySelector('div[style*="min-height: 20px;"]');
|
||||
if (firstDiv) {
|
||||
range.setStart(firstDiv, 0);
|
||||
range.collapse(true);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
(firstDiv as HTMLElement).focus();
|
||||
}
|
||||
|
||||
// Update compose state
|
||||
setComposeBody(wrappedContent);
|
||||
setLocalContent(wrappedContent);
|
||||
console.log('[DEBUG] Successfully set compose content');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DEBUG] Error parsing email for compose:', error);
|
||||
|
||||
// Fallback to basic content display
|
||||
const errorContent = `
|
||||
<div class="compose-area" contenteditable="true">
|
||||
<br/>
|
||||
<div style="color: #64748b;">
|
||||
---------- Original Message ---------<br/>
|
||||
${emailToProcess.subject ? `Subject: ${emailToProcess.subject}<br/>` : ''}
|
||||
${emailToProcess.from ? `From: ${emailToProcess.from}<br/>` : ''}
|
||||
${emailToProcess.date ? `Date: ${new Date(emailToProcess.date).toLocaleString()}<br/>` : ''}
|
||||
</div>
|
||||
<div style="color: #64748b; border-left: 2px solid #e5e7eb; padding-left: 10px; margin: 10px 0;">
|
||||
${emailToProcess.preview || 'No content available'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (composeBodyRef.current) {
|
||||
composeBodyRef.current.innerHTML = errorContent;
|
||||
setComposeBody(errorContent);
|
||||
setLocalContent(errorContent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DEBUG] Error initializing compose content:', error);
|
||||
if (composeBodyRef.current) {
|
||||
const errorContent = `
|
||||
<div class="compose-area" contenteditable="true">
|
||||
<br/>
|
||||
<div style="color: #ef4444;">Error loading original message.</div>
|
||||
<div style="color: #64748b; font-size: 0.875rem; margin-top: 0.5rem;">
|
||||
Technical details: ${error instanceof Error ? error.message : 'Unknown error'}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
composeBodyRef.current.innerHTML = errorContent;
|
||||
setComposeBody(errorContent);
|
||||
setLocalContent(errorContent);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initializeContent();
|
||||
}
|
||||
}, [replyTo, forwardFrom, setComposeBody]);
|
||||
|
||||
const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
|
||||
if (!e.currentTarget) return;
|
||||
const content = e.currentTarget.innerHTML;
|
||||
if (!content.trim()) {
|
||||
setLocalContent('');
|
||||
setComposeBody('');
|
||||
} else {
|
||||
setLocalContent(content);
|
||||
setComposeBody(content);
|
||||
}
|
||||
|
||||
if (onBodyChange) {
|
||||
onBodyChange(content);
|
||||
}
|
||||
|
||||
// Ensure scrolling and cursor behavior works after edits
|
||||
const messageContentDivs = e.currentTarget.querySelectorAll('.message-content');
|
||||
messageContentDivs.forEach(div => {
|
||||
// Make sure the div remains scrollable after input events
|
||||
(div as HTMLElement).style.maxHeight = '300px';
|
||||
(div as HTMLElement).style.overflowY = 'auto';
|
||||
(div as HTMLElement).style.border = '1px solid #e5e7eb';
|
||||
(div as HTMLElement).style.borderRadius = '4px';
|
||||
(div as HTMLElement).style.padding = '10px';
|
||||
|
||||
// Ensure wheel events are properly handled
|
||||
if (!(div as HTMLElement).hasAttribute('data-scroll-handler-attached')) {
|
||||
div.addEventListener('wheel', function(this: HTMLElement, ev: Event) {
|
||||
const e = ev as WheelEvent;
|
||||
const target = this;
|
||||
|
||||
// Check if we're at the boundary of the scrollable area
|
||||
const isAtBottom = target.scrollHeight - target.scrollTop <= target.clientHeight + 1;
|
||||
const isAtTop = target.scrollTop <= 0;
|
||||
|
||||
// Only prevent default if we're not at the boundaries in the direction of scrolling
|
||||
if ((e.deltaY > 0 && !isAtBottom) || (e.deltaY < 0 && !isAtTop)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // Prevent the parent container from scrolling
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// Mark this element as having a scroll handler attached
|
||||
(div as HTMLElement).setAttribute('data-scroll-handler-attached', 'true');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
if (!composeBodyRef.current) return;
|
||||
|
||||
const composeArea = composeBodyRef.current.querySelector('.compose-area');
|
||||
if (!composeArea) return;
|
||||
|
||||
const content = composeArea.innerHTML;
|
||||
if (!content.trim()) {
|
||||
console.error('Email content is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const encodedContent = await encodeComposeContent(content);
|
||||
setComposeBody(encodedContent);
|
||||
await handleSend();
|
||||
setShowCompose(false);
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileAttachment = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!e.target.files) return;
|
||||
|
||||
const newAttachments: any[] = [];
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB in bytes
|
||||
const oversizedFiles: string[] = [];
|
||||
|
||||
for (const file of e.target.files) {
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
oversizedFiles.push(file.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read file as base64
|
||||
const base64Content = await new Promise<string>((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64 = reader.result as string;
|
||||
resolve(base64.split(',')[1]); // Remove data URL prefix
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
newAttachments.push({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
content: base64Content,
|
||||
encoding: 'base64'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing attachment:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (oversizedFiles.length > 0) {
|
||||
alert(`The following files exceed the 10MB size limit and were not attached:\n${oversizedFiles.join('\n')}`);
|
||||
}
|
||||
|
||||
if (newAttachments.length > 0) {
|
||||
setAttachments([...attachments, ...newAttachments]);
|
||||
}
|
||||
};
|
||||
|
||||
// Add focus handling for better UX
|
||||
const handleComposeAreaClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
// If the click is directly on the compose area and not on any child element
|
||||
if (e.target === e.currentTarget) {
|
||||
// Find the cursor position element
|
||||
const cursorPosition = e.currentTarget.querySelector('.cursor-position');
|
||||
if (cursorPosition) {
|
||||
// Focus the cursor position element
|
||||
(cursorPosition as HTMLElement).focus();
|
||||
|
||||
// Set cursor at the beginning
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
range.setStart(cursorPosition, 0);
|
||||
range.collapse(true);
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add formatDate function to match Panel 3 implementation
|
||||
function formatDate(date: Date | null): string {
|
||||
if (!date) return '';
|
||||
return new Intl.DateTimeFormat('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
if (!showCompose) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-gray-600/30 backdrop-blur-sm z-50 flex items-center justify-center">
|
||||
<div className="w-full max-w-2xl h-[90vh] bg-white rounded-xl shadow-xl flex flex-col mx-4">
|
||||
{/* Modal Header */}
|
||||
<div className="flex-none flex items-center justify-between px-6 py-3 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{replyTo ? 'Reply' : forwardFrom ? 'Forward' : 'New Message'}
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hover:bg-gray-100 rounded-full"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<X className="h-5 w-5 text-gray-500" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full flex flex-col p-6 space-y-4 overflow-y-auto">
|
||||
{/* To Field */}
|
||||
<div className="flex-none">
|
||||
<Label htmlFor="to" className="block text-sm font-medium text-gray-700">To</Label>
|
||||
<Input
|
||||
id="to"
|
||||
value={composeTo}
|
||||
onChange={(e) => setComposeTo(e.target.value)}
|
||||
placeholder="recipient@example.com"
|
||||
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* CC/BCC Toggle Buttons */}
|
||||
<div className="flex-none flex items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
|
||||
onClick={() => setShowCc(!showCc)}
|
||||
>
|
||||
{showCc ? 'Hide Cc' : 'Add Cc'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
|
||||
onClick={() => setShowBcc(!showBcc)}
|
||||
>
|
||||
{showBcc ? 'Hide Bcc' : 'Add Bcc'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* CC Field */}
|
||||
{showCc && (
|
||||
<div className="flex-none">
|
||||
<Label htmlFor="cc" className="block text-sm font-medium text-gray-700">Cc</Label>
|
||||
<Input
|
||||
id="cc"
|
||||
value={composeCc}
|
||||
onChange={(e) => setComposeCc(e.target.value)}
|
||||
placeholder="cc@example.com"
|
||||
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* BCC Field */}
|
||||
{showBcc && (
|
||||
<div className="flex-none">
|
||||
<Label htmlFor="bcc" className="block text-sm font-medium text-gray-700">Bcc</Label>
|
||||
<Input
|
||||
id="bcc"
|
||||
value={composeBcc}
|
||||
onChange={(e) => setComposeBcc(e.target.value)}
|
||||
placeholder="bcc@example.com"
|
||||
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Subject Field */}
|
||||
<div className="flex-none">
|
||||
<Label htmlFor="subject" className="block text-sm font-medium text-gray-700">Subject</Label>
|
||||
<Input
|
||||
id="subject"
|
||||
value={composeSubject}
|
||||
onChange={(e) => setComposeSubject(e.target.value)}
|
||||
placeholder="Enter subject"
|
||||
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Message Body */}
|
||||
<div className="flex-1 min-h-[200px] flex flex-col">
|
||||
<Label htmlFor="message" className="flex-none block text-sm font-medium text-gray-700 mb-2">Message</Label>
|
||||
<div
|
||||
ref={composeBodyRef}
|
||||
contentEditable="true"
|
||||
onInput={handleInput}
|
||||
onClick={handleComposeAreaClick}
|
||||
className="flex-1 w-full bg-white border border-gray-300 rounded-md p-4 text-black overflow-y-auto focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
style={{
|
||||
direction: 'ltr',
|
||||
maxHeight: 'calc(100vh - 400px)',
|
||||
minHeight: '200px',
|
||||
overflowY: 'auto',
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: '#cbd5e0 #f3f4f6',
|
||||
cursor: 'text'
|
||||
}}
|
||||
dir="ltr"
|
||||
spellCheck="true"
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
tabIndex={0}
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="flex-none flex items-center justify-between px-6 py-3 border-t border-gray-200 bg-white">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* File Input for Attachments */}
|
||||
<input
|
||||
type="file"
|
||||
id="file-attachment"
|
||||
className="hidden"
|
||||
multiple
|
||||
onChange={handleFileAttachment}
|
||||
/>
|
||||
<label htmlFor="file-attachment">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="rounded-full bg-white hover:bg-gray-100 border-gray-300"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('file-attachment')?.click();
|
||||
}}
|
||||
>
|
||||
<Paperclip className="h-4 w-4 text-gray-600" />
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-gray-600 hover:text-gray-700 hover:bg-gray-100"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-600 text-white hover:bg-blue-700"
|
||||
onClick={handleSendEmail}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -179,8 +179,7 @@ export default function ComposeEmail({
|
||||
: initialEmail.date.toLocaleString()
|
||||
: new Date().toLocaleString();
|
||||
|
||||
// Create a clean wrapper that won't interfere with the original email's styling
|
||||
// Use inline styles for the header to avoid CSS conflicts
|
||||
// Create a clean header with inline styles only - no external CSS
|
||||
const headerHtml = `
|
||||
<div style="border-top: 1px solid #e1e1e1; margin-top: 20px; padding-top: 15px; font-family: Arial, sans-serif; color: #333;">
|
||||
<div style="margin-bottom: 15px;">
|
||||
@ -193,91 +192,83 @@ export default function ComposeEmail({
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Process the original content
|
||||
let originalContent = '';
|
||||
// Default content is a clear "no content" message
|
||||
let contentHtml = '<div style="color: #666; font-style: italic; padding: 15px; font-size: 14px; border: 1px dashed #ccc; margin: 15px 0; text-align: center; background-color: #f9f9f9; border-radius: 4px;">No content available in original email</div>';
|
||||
|
||||
// First try to use the API to parse and sanitize the email content
|
||||
try {
|
||||
// Use server-side parsing via fetch API to properly handle complex emails
|
||||
const response = await fetch('/api/parse-email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: initialEmail.content || initialEmail.html || initialEmail.text || ''
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsedEmail = await response.json();
|
||||
|
||||
if (parsedEmail.html && parsedEmail.html.trim()) {
|
||||
console.log('Using parsed HTML content for forward');
|
||||
|
||||
// Create an iframe-like containment for the email content
|
||||
// This prevents CSS from the original email leaking into our compose view
|
||||
originalContent = `
|
||||
<div class="email-content-container">
|
||||
${parsedEmail.html}
|
||||
</div>
|
||||
`;
|
||||
} else if (parsedEmail.text && parsedEmail.text.trim()) {
|
||||
console.log('Using parsed text content for forward');
|
||||
originalContent = `<div style="white-space: pre-wrap; font-family: monospace;">${parsedEmail.text}</div>`;
|
||||
} else {
|
||||
console.log('No content available from parser');
|
||||
originalContent = '<div style="color: #666; font-style: italic; padding: 10px; font-size: 14px; border: 1px dashed #ccc; margin: 10px 0; text-align: center; background-color: #f9f9f9;">No content available</div>';
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to parse email content');
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing email content:', parseError);
|
||||
|
||||
// Fall back to direct content handling if API parsing fails
|
||||
if (initialEmail.html && initialEmail.html.trim()) {
|
||||
console.log('Falling back to HTML content for forward');
|
||||
// Use DOMPurify to sanitize HTML and remove dangerous elements
|
||||
originalContent = DOMPurify.sanitize(initialEmail.html, {
|
||||
ADD_TAGS: ['style', 'div', 'span', 'p', 'br', 'hr', 'h1', 'h2', 'h3', 'img', 'table', 'tr', 'td', 'th'],
|
||||
ADD_ATTR: ['style', 'class', 'id', 'src', 'alt', 'href', 'target'],
|
||||
FORBID_TAGS: ['script', 'iframe', 'object', 'embed'],
|
||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover']
|
||||
// Check if we have content to forward
|
||||
if (initialEmail.content || initialEmail.html || initialEmail.text) {
|
||||
try {
|
||||
// Use the parse-email API endpoint which centralizes our email parsing logic
|
||||
const response = await fetch('/api/parse-email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: initialEmail.content || initialEmail.html || initialEmail.text || ''
|
||||
}),
|
||||
});
|
||||
} else if (initialEmail.content && initialEmail.content.trim()) {
|
||||
console.log('Falling back to content field for forward');
|
||||
originalContent = DOMPurify.sanitize(initialEmail.content);
|
||||
} else if (initialEmail.text && initialEmail.text.trim()) {
|
||||
console.log('Falling back to text content for forward');
|
||||
originalContent = `<div style="white-space: pre-wrap; font-family: monospace;">${initialEmail.text}</div>`;
|
||||
} else {
|
||||
console.log('No content available for forward');
|
||||
originalContent = '<div style="color: #666; font-style: italic; padding: 10px; font-size: 14px; border: 1px dashed #ccc; margin: 10px 0; text-align: center; background-color: #f9f9f9;">No content available</div>';
|
||||
|
||||
if (response.ok) {
|
||||
const parsedEmail = await response.json();
|
||||
|
||||
// Use the parsed HTML content if available
|
||||
if (parsedEmail.html) {
|
||||
contentHtml = parsedEmail.html;
|
||||
console.log('Successfully parsed HTML content');
|
||||
} else if (parsedEmail.text) {
|
||||
// Text-only content is wrapped in pre-formatted styling
|
||||
contentHtml = `<div style="white-space: pre-wrap; font-family: monospace; padding: 10px; background-color: #f9f9f9; border-radius: 4px;">${parsedEmail.text}</div>`;
|
||||
console.log('Using text content');
|
||||
} else {
|
||||
console.warn('API returned success but no content');
|
||||
}
|
||||
} else {
|
||||
console.error('API returned error:', await response.text());
|
||||
throw new Error('API call failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing email:', error);
|
||||
|
||||
// Fallback processing - using our cleanHtml utility directly
|
||||
if (initialEmail.html) {
|
||||
// Import the cleanHtml function dynamically if needed
|
||||
const { cleanHtml } = await import('@/lib/mail-parser-wrapper');
|
||||
contentHtml = cleanHtml(initialEmail.html, {
|
||||
preserveStyles: true,
|
||||
scopeStyles: true,
|
||||
addWrapper: true
|
||||
});
|
||||
console.log('Using direct HTML cleaning fallback');
|
||||
} else if (initialEmail.content) {
|
||||
contentHtml = DOMPurify.sanitize(initialEmail.content, {
|
||||
ADD_TAGS: ['style'],
|
||||
FORBID_TAGS: ['script', 'iframe']
|
||||
});
|
||||
console.log('Using DOMPurify sanitized content');
|
||||
} else if (initialEmail.text) {
|
||||
contentHtml = `<div style="white-space: pre-wrap; font-family: monospace; padding: 10px; background-color: #f9f9f9; border-radius: 4px;">${initialEmail.text}</div>`;
|
||||
console.log('Using plain text fallback');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn('No email content available for forwarding');
|
||||
}
|
||||
|
||||
// Preserve all original structure by wrapping, not modifying the original content
|
||||
// Important: We add a style scope to prevent CSS leakage
|
||||
// Combine the header and content - using containment
|
||||
const forwardedContent = `
|
||||
${headerHtml}
|
||||
<!-- Start original email content - DO NOT MODIFY THIS CONTENT -->
|
||||
<div class="original-email-content" style="margin-top: 10px; border-left: 2px solid #e1e1e1; padding-left: 15px;">
|
||||
<!-- Email content styling isolation container -->
|
||||
<div style="position: relative; overflow: auto;">
|
||||
${originalContent}
|
||||
<div style="margin-top: 10px; border-left: 2px solid #e1e1e1; padding-left: 15px;">
|
||||
<div style="isolation: isolate; contain: content; overflow: auto;">
|
||||
${contentHtml}
|
||||
</div>
|
||||
</div>
|
||||
<!-- End original email content -->
|
||||
`;
|
||||
|
||||
console.log('Setting body with forwarded content');
|
||||
setBody(forwardedContent);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error initializing forwarded email:', error);
|
||||
|
||||
// Even in error case, provide a usable template with empty values
|
||||
// Provide a minimal template even in error case
|
||||
setBody(`
|
||||
<div style="border-top: 1px solid #e1e1e1; margin-top: 20px; padding-top: 15px; font-family: Arial, sans-serif; color: #333;">
|
||||
<div style="margin-bottom: 15px;">
|
||||
@ -288,7 +279,7 @@ export default function ComposeEmail({
|
||||
<div><b>To:</b> ${initialEmail.to ? formatRecipients(initialEmail.to) : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 10px; padding: 10px; color: #d32f2f; font-style: italic; border: 1px dashed #d32f2f; margin: 10px 0; text-align: center; background-color: #fff8f8;">
|
||||
<div style="margin-top: 10px; padding: 15px; color: #d32f2f; font-style: italic; border: 1px dashed #d32f2f; margin: 15px 0; text-align: center; background-color: #fff8f8; border-radius: 4px;">
|
||||
Error loading original message content. The original message may still be viewable in your inbox.
|
||||
</div>
|
||||
`);
|
||||
|
||||
@ -101,23 +101,107 @@ export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanHtml(html: string): string {
|
||||
/**
|
||||
* Cleans HTML content by removing potentially harmful elements while preserving styling.
|
||||
* This is the centralized HTML sanitization function to be used across the application.
|
||||
* @param html HTML content to sanitize
|
||||
* @param options Optional configuration
|
||||
* @returns Sanitized HTML
|
||||
*/
|
||||
export function cleanHtml(html: string, options: {
|
||||
preserveStyles?: boolean;
|
||||
scopeStyles?: boolean;
|
||||
addWrapper?: boolean;
|
||||
} = {}): string {
|
||||
if (!html) return '';
|
||||
|
||||
try {
|
||||
// Enhanced configuration to preserve more HTML elements for complex emails
|
||||
return DOMPurify.sanitize(html, {
|
||||
ADD_TAGS: ['style', 'meta', 'link', 'table', 'thead', 'tbody', 'tr', 'td', 'th', 'hr', 'font', 'div', 'span', 'a', 'img', 'b', 'strong', 'i', 'em', 'u', 'br', 'p', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code', 'center', 'section', 'header', 'footer', 'article', 'nav', 'keyframes'],
|
||||
ADD_ATTR: ['*', 'colspan', 'rowspan', 'cellpadding', 'cellspacing', 'border', 'bgcolor', 'width', 'height', 'align', 'valign', 'class', 'id', 'style', 'color', 'face', 'size', 'background', 'src', 'href', 'target', 'rel', 'alt', 'title', 'name', 'animation', 'animation-name', 'animation-duration', 'animation-fill-mode'],
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
WHOLE_DOCUMENT: true,
|
||||
KEEP_CONTENT: true,
|
||||
RETURN_DOM: false,
|
||||
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'select', 'option', 'textarea', 'canvas', 'video', 'audio'],
|
||||
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout', 'onchange', 'onsubmit'],
|
||||
USE_PROFILES: { html: true, svg: false, svgFilters: false, mathMl: false },
|
||||
FORCE_BODY: true
|
||||
});
|
||||
const defaultOptions = {
|
||||
preserveStyles: true,
|
||||
scopeStyles: true,
|
||||
addWrapper: true,
|
||||
...options
|
||||
};
|
||||
|
||||
// Extract style tags if we're preserving them
|
||||
const styleTagsContent: string[] = [];
|
||||
let processedHtml = html;
|
||||
|
||||
if (defaultOptions.preserveStyles) {
|
||||
processedHtml = html.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (match, styleContent) => {
|
||||
styleTagsContent.push(styleContent);
|
||||
return ''; // Remove style tags temporarily
|
||||
});
|
||||
}
|
||||
|
||||
// Process the HTML content
|
||||
processedHtml = processedHtml
|
||||
// Remove potentially harmful elements and attributes
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
||||
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
|
||||
.replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, '')
|
||||
.replace(/<form\b[^<]*(?:(?!<\/form>)<[^<]*)*<\/form>/gi, '')
|
||||
.replace(/on\w+="[^"]*"/gi, '') // Remove inline event handlers (onclick, onload, etc.)
|
||||
.replace(/on\w+='[^']*'/gi, '')
|
||||
// Fix self-closing tags that might break React
|
||||
.replace(/<(br|hr|img|input|link|meta|area|base|col|embed|keygen|param|source|track|wbr)([^>]*)>/gi, '<$1$2 />');
|
||||
|
||||
// If we're scoping styles, prefix classes
|
||||
if (defaultOptions.scopeStyles) {
|
||||
processedHtml = processedHtml.replace(/class=["']([^"']*)["']/gi, (match, classContent) => {
|
||||
const classes = classContent.split(/\s+/).map((cls: string) => `email-forwarded-${cls}`).join(' ');
|
||||
return `class="${classes}"`;
|
||||
});
|
||||
}
|
||||
|
||||
// Add scoped styles if needed
|
||||
if (defaultOptions.preserveStyles && styleTagsContent.length > 0 && defaultOptions.scopeStyles) {
|
||||
// Create a modified version of the styles that scope them to our container
|
||||
const scopedStyles = styleTagsContent.map(style => {
|
||||
// Replace CSS selectors to be scoped to our container
|
||||
return style
|
||||
// Add scope to class selectors
|
||||
.replace(/(\.[a-zA-Z0-9_-]+)/g, '.email-forwarded-$1')
|
||||
// Add scope to ID selectors
|
||||
.replace(/(#[a-zA-Z0-9_-]+)/g, '#email-forwarded-$1')
|
||||
// Fix any CSS that might break out
|
||||
.replace(/@import/g, '/* @import */')
|
||||
.replace(/@media/g, '/* @media */');
|
||||
}).join('\n');
|
||||
|
||||
// Add the styles back in a scoped way
|
||||
if (defaultOptions.addWrapper) {
|
||||
return `
|
||||
<div class="email-forwarded-content" style="position: relative; overflow: auto; max-width: 100%;">
|
||||
<style type="text/css">
|
||||
/* Scoped styles for forwarded email */
|
||||
.email-forwarded-content {
|
||||
/* Base containment */
|
||||
font-family: Arial, sans-serif;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
/* Original email styles (scoped) */
|
||||
${scopedStyles}
|
||||
</style>
|
||||
${processedHtml}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `<style type="text/css">${scopedStyles}</style>${processedHtml}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Just wrap the content if needed
|
||||
if (defaultOptions.addWrapper) {
|
||||
return `<div class="email-forwarded-content" style="position: relative; overflow: auto; max-width: 100%;">${processedHtml}</div>`;
|
||||
}
|
||||
|
||||
return processedHtml;
|
||||
} catch (error) {
|
||||
console.error('Error cleaning HTML:', error);
|
||||
return html;
|
||||
// Return something safe in case of error
|
||||
return `<div style="color: #666; font-style: italic;">Error processing HTML content</div>`;
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,10 @@
|
||||
import { simpleParser } from 'mailparser';
|
||||
import { cleanHtml as cleanHtmlCentralized } from '@/lib/mail-parser-wrapper';
|
||||
|
||||
// This function is now deprecated in favor of the centralized cleanHtml in mail-parser-wrapper.ts
|
||||
// It's kept here temporarily for backward compatibility
|
||||
export function cleanHtml(html: string): string {
|
||||
try {
|
||||
// More permissive cleaning that preserves styling but removes potentially harmful elements
|
||||
return html
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
|
||||
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
|
||||
.replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, '')
|
||||
.replace(/<form\b[^<]*(?:(?!<\/form>)<[^<]*)*<\/form>/gi, '')
|
||||
.replace(/on\w+="[^"]*"/gi, '') // Remove inline event handlers (onclick, onload, etc.)
|
||||
.replace(/on\w+='[^']*'/gi, '');
|
||||
} catch (error) {
|
||||
console.error('Error cleaning HTML:', error);
|
||||
return html;
|
||||
}
|
||||
return cleanHtmlCentralized(html, { preserveStyles: true, scopeStyles: false });
|
||||
}
|
||||
|
||||
function getAddressText(address: any): string | null {
|
||||
@ -36,7 +26,7 @@ export async function parseEmail(emailContent: string) {
|
||||
cc: getAddressText(parsed.cc),
|
||||
bcc: getAddressText(parsed.bcc),
|
||||
date: parsed.date || null,
|
||||
html: parsed.html ? cleanHtml(parsed.html) : null,
|
||||
html: parsed.html ? cleanHtml(parsed.html as string) : null,
|
||||
text: parsed.text || null,
|
||||
attachments: parsed.attachments || [],
|
||||
headers: Object.fromEntries(parsed.headers)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user