compose mime
This commit is contained in:
parent
c70ba386ae
commit
9a762927fc
@ -1,29 +1,48 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { parseEmail } from '@/lib/server/email-parser';
|
||||
import { simpleParser, AddressObject } from 'mailparser';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
console.log('Received request body:', body);
|
||||
const { email } = body;
|
||||
|
||||
const { emailContent } = body;
|
||||
console.log('Email content type:', typeof emailContent);
|
||||
console.log('Email content length:', emailContent?.length);
|
||||
|
||||
if (!emailContent || typeof emailContent !== 'string') {
|
||||
console.log('Invalid email content:', { emailContent, type: typeof emailContent });
|
||||
if (!email || typeof email !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid email content. Expected a string.', received: { type: typeof emailContent, length: emailContent?.length } },
|
||||
{ error: 'Invalid email content' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = await parseEmail(emailContent);
|
||||
return NextResponse.json(parsed);
|
||||
const parsed = await simpleParser(email);
|
||||
|
||||
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: parsed.html || null,
|
||||
text: parsed.textAsHtml || parsed.text || null,
|
||||
attachments: parsed.attachments?.map(att => ({
|
||||
filename: att.filename,
|
||||
contentType: att.contentType,
|
||||
size: att.size
|
||||
})) || [],
|
||||
headers: parsed.headers || {}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error parsing email:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to parse email', details: error instanceof Error ? error.message : 'Unknown error' },
|
||||
{ error: 'Failed to parse email' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -103,20 +103,30 @@ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: s
|
||||
function EmailContent({ email }: { email: Email }) {
|
||||
const [content, setContent] = useState<React.ReactNode>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
async function loadContent() {
|
||||
if (!email) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (!email.body) {
|
||||
if (mounted) setContent(null);
|
||||
if (mounted) {
|
||||
setContent(<div className="text-gray-500">No content available</div>);
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedEmail = email.body.trim();
|
||||
if (!formattedEmail) {
|
||||
if (mounted) setContent(null);
|
||||
if (mounted) {
|
||||
setContent(<div className="text-gray-500">No content available</div>);
|
||||
setIsLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -127,7 +137,7 @@ function EmailContent({ email }: { email: Email }) {
|
||||
setContent(
|
||||
<div
|
||||
className="email-content prose prose-sm max-w-none dark:prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: parsedEmail.html }}
|
||||
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(parsedEmail.html) }}
|
||||
/>
|
||||
);
|
||||
} else if (parsedEmail.text) {
|
||||
@ -137,15 +147,17 @@ function EmailContent({ email }: { email: Email }) {
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
setContent(null);
|
||||
setContent(<div className="text-gray-500">No content available</div>);
|
||||
}
|
||||
setError(null);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error rendering email content:', err);
|
||||
if (mounted) {
|
||||
setError('Error rendering email content. Please try again.');
|
||||
setContent(null);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -155,13 +167,21 @@ function EmailContent({ email }: { email: Email }) {
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [email.body]);
|
||||
}, [email?.body]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">{error}</div>;
|
||||
}
|
||||
|
||||
return content;
|
||||
return content || <div className="text-gray-500">No content available</div>;
|
||||
}
|
||||
|
||||
function renderEmailContent(email: Email) {
|
||||
@ -310,15 +330,29 @@ function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'r
|
||||
function EmailPreview({ email }: { email: Email }) {
|
||||
const [preview, setPreview] = useState<string>('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
async function loadPreview() {
|
||||
if (!email?.body) {
|
||||
if (mounted) setPreview('No content available');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const decoded = await decodeEmail(email.body);
|
||||
if (mounted) {
|
||||
setPreview(decoded.text || cleanHtml(decoded.html || ''));
|
||||
if (decoded.text) {
|
||||
setPreview(decoded.text.substring(0, 150) + '...');
|
||||
} else if (decoded.html) {
|
||||
const cleanText = decoded.html.replace(/<[^>]*>/g, ' ').trim();
|
||||
setPreview(cleanText.substring(0, 150) + '...');
|
||||
} else {
|
||||
setPreview('No preview available');
|
||||
}
|
||||
setError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
@ -327,6 +361,8 @@ function EmailPreview({ email }: { email: Email }) {
|
||||
setError('Error generating preview');
|
||||
setPreview('');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,7 +371,11 @@ function EmailPreview({ email }: { email: Email }) {
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [email.body]);
|
||||
}, [email?.body]);
|
||||
|
||||
if (isLoading) {
|
||||
return <span className="text-gray-400">Loading preview...</span>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <span className="text-red-500 text-xs">{error}</span>;
|
||||
@ -507,8 +547,9 @@ export default function CourrierPage() {
|
||||
setAvailableFolders(data.folders);
|
||||
}
|
||||
|
||||
// Process emails keeping exact folder names
|
||||
const processedEmails = (data.emails || []).map((email: any) => ({
|
||||
// Process emails keeping exact folder names and sort by date
|
||||
const processedEmails = (data.emails || [])
|
||||
.map((email: any) => ({
|
||||
id: Number(email.id),
|
||||
accountId: 1,
|
||||
from: email.from || '',
|
||||
@ -526,19 +567,35 @@ export default function CourrierPage() {
|
||||
raw: email.body || ''
|
||||
}));
|
||||
|
||||
// Sort emails by date, ensuring most recent first
|
||||
const sortedEmails = processedEmails.sort((a: Email, b: Email) => {
|
||||
const dateA = new Date(a.date).getTime();
|
||||
const dateB = new Date(b.date).getTime();
|
||||
return dateB - dateA; // Most recent first
|
||||
});
|
||||
|
||||
// Only update unread count if we're in the Inbox folder
|
||||
if (currentView === 'INBOX') {
|
||||
const unreadInboxEmails = processedEmails.filter(
|
||||
const unreadInboxEmails = sortedEmails.filter(
|
||||
(email: Email) => !email.read && email.folder === 'INBOX'
|
||||
).length;
|
||||
setUnreadCount(unreadInboxEmails);
|
||||
}
|
||||
|
||||
if (isLoadMore) {
|
||||
setEmails(prev => [...prev, ...processedEmails]);
|
||||
// When loading more, merge with existing emails and re-sort
|
||||
setEmails(prev => {
|
||||
const combined = [...prev, ...sortedEmails];
|
||||
return combined.sort((a: Email, b: Email) => {
|
||||
const dateA = new Date(a.date).getTime();
|
||||
const dateB = new Date(b.date).getTime();
|
||||
return dateB - dateA; // Most recent first
|
||||
});
|
||||
});
|
||||
setPage(prev => prev + 1);
|
||||
} else {
|
||||
setEmails(processedEmails);
|
||||
// For initial load or refresh, just use the sorted emails
|
||||
setEmails(sortedEmails);
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
@ -572,8 +629,10 @@ export default function CourrierPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Set the selected email first to show preview immediately
|
||||
setSelectedEmail(email);
|
||||
setContentLoading(true);
|
||||
|
||||
// Fetch the full email content
|
||||
const response = await fetch(`/api/mail/${emailId}`);
|
||||
@ -586,11 +645,12 @@ export default function CourrierPage() {
|
||||
// Update the email in the list and selected email with full content
|
||||
setEmails(prevEmails => prevEmails.map(email =>
|
||||
email.id === emailId
|
||||
? { ...email, body: fullEmail.body }
|
||||
? { ...email, body: fullEmail.body || email.body }
|
||||
: email
|
||||
));
|
||||
|
||||
setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body } : prev);
|
||||
setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body || prev.body } : prev);
|
||||
setContentLoading(false);
|
||||
|
||||
// Try to mark as read in the background
|
||||
try {
|
||||
@ -620,6 +680,10 @@ export default function CourrierPage() {
|
||||
} catch (error) {
|
||||
console.error('Error marking email as read:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching email content:', error);
|
||||
setContentLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Add these improved handlers
|
||||
@ -1153,9 +1217,14 @@ export default function CourrierPage() {
|
||||
);
|
||||
|
||||
// Add back the handleReply function
|
||||
const handleReply = (type: 'reply' | 'reply-all' | 'forward') => {
|
||||
const handleReply = async (type: 'reply' | 'reply-all' | 'forward') => {
|
||||
if (!selectedEmail) return;
|
||||
|
||||
try {
|
||||
// Get the decoded content first
|
||||
const decoded = await decodeEmail(selectedEmail.body);
|
||||
|
||||
// Set up the reply details
|
||||
const getReplyTo = () => {
|
||||
if (type === 'forward') return '';
|
||||
return selectedEmail.from;
|
||||
@ -1174,32 +1243,39 @@ export default function CourrierPage() {
|
||||
return subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
||||
};
|
||||
|
||||
// Get the formatted original email content
|
||||
const originalContent = getReplyBody(selectedEmail, type);
|
||||
|
||||
// Create a clean structure with clear separation
|
||||
const formattedContent = `
|
||||
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; border: 1px solid #e5e7eb; border-radius: 4px; margin-bottom: 20px;"></div>
|
||||
<div class="quoted-content" contenteditable="false" style="color: #6b7280; font-size: 0.875rem;">
|
||||
${type === 'forward' ? `
|
||||
<div style="margin-bottom: 10px;">
|
||||
// Create the appropriate email content based on type
|
||||
let formattedContent = '';
|
||||
if (type === 'forward') {
|
||||
formattedContent = `
|
||||
<div style="min-height: 100px;">
|
||||
<br/>
|
||||
<div style="border-top: 1px solid #e5e7eb; margin-top: 20px; padding-top: 10px; color: #666;">
|
||||
---------- Forwarded message ---------<br/>
|
||||
From: ${selectedEmail.from}<br/>
|
||||
Date: ${new Date(selectedEmail.date).toLocaleString()}<br/>
|
||||
Subject: ${selectedEmail.subject}<br/>
|
||||
To: ${selectedEmail.to}<br/>
|
||||
${selectedEmail.cc ? `Cc: ${selectedEmail.cc}<br/>` : ''}
|
||||
<br/>
|
||||
<div style="margin-top: 10px;">
|
||||
${decoded.html || decoded.text || ''}
|
||||
</div>
|
||||
` : `
|
||||
<div style="margin-bottom: 10px;">
|
||||
On ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote:
|
||||
</div>
|
||||
`}
|
||||
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb;">
|
||||
${originalContent}
|
||||
</blockquote>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// For reply and reply-all
|
||||
formattedContent = `
|
||||
<div style="min-height: 100px;">
|
||||
<br/>
|
||||
<div style="border-left: 2px solid #e5e7eb; margin: 10px 0; padding-left: 10px; color: #666;">
|
||||
On ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote:<br/>
|
||||
<br/>
|
||||
${decoded.html || decoded.text || ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Update the compose form
|
||||
setComposeTo(getReplyTo());
|
||||
@ -1207,12 +1283,14 @@ export default function CourrierPage() {
|
||||
setComposeSubject(getReplySubject());
|
||||
setComposeBody(formattedContent);
|
||||
setComposeBcc('');
|
||||
|
||||
// Show the compose form and CC field for Reply All
|
||||
setShowCompose(true);
|
||||
setShowCc(type === 'reply-all');
|
||||
setShowBcc(false);
|
||||
setAttachments([]);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error preparing reply:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Add back the toggleStarred function
|
||||
@ -1445,10 +1523,7 @@ export default function CourrierPage() {
|
||||
attachments={attachments}
|
||||
setAttachments={setAttachments}
|
||||
handleSend={handleSend}
|
||||
replyTo={selectedEmail || undefined}
|
||||
forwardFrom={selectedEmail || undefined}
|
||||
onSend={(email) => {
|
||||
// Handle the sent email
|
||||
console.log('Email sent:', email);
|
||||
setShowCompose(false);
|
||||
}}
|
||||
|
||||
@ -81,157 +81,145 @@ export default function ComposeEmail({
|
||||
}: ComposeEmailProps) {
|
||||
const composeBodyRef = useRef<HTMLDivElement>(null);
|
||||
const [localContent, setLocalContent] = useState('');
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (composeBodyRef.current && !isInitialized) {
|
||||
let content = '';
|
||||
|
||||
if (replyTo || forwardFrom) {
|
||||
const originalContent = replyTo?.body || forwardFrom?.body || '';
|
||||
const initializeContent = async () => {
|
||||
if (!composeBodyRef.current) return;
|
||||
|
||||
fetch('/api/parse-email', {
|
||||
try {
|
||||
const emailToProcess = replyTo || forwardFrom;
|
||||
if (!emailToProcess?.body) {
|
||||
console.error('No email body found to process');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set initial loading state
|
||||
composeBodyRef.current.innerHTML = `
|
||||
<div class="compose-area" contenteditable="true">
|
||||
<br/>
|
||||
<div class="text-gray-500">Loading original message...</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Parse the original email using the API
|
||||
const response = await fetch('/api/parse-email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ emailContent: originalContent }),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(parsed => {
|
||||
content = `
|
||||
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;">
|
||||
<br/><br/><br/>
|
||||
${forwardFrom ? `
|
||||
body: JSON.stringify({ email: emailToProcess.body }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to parse email');
|
||||
}
|
||||
|
||||
const emailContent = data.html || data.text || '';
|
||||
|
||||
// Format the reply/forward content
|
||||
const quotedContent = forwardFrom ? `
|
||||
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
|
||||
---------- Forwarded message ---------<br/>
|
||||
From: ${forwardFrom.from}<br/>
|
||||
Date: ${new Date(forwardFrom.date).toLocaleString()}<br/>
|
||||
Subject: ${forwardFrom.subject}<br/>
|
||||
To: ${forwardFrom.to}<br/>
|
||||
${forwardFrom.cc ? `Cc: ${forwardFrom.cc}<br/>` : ''}
|
||||
<br/>
|
||||
${parsed.html || parsed.text}
|
||||
From: ${emailToProcess.from}<br/>
|
||||
Date: ${new Date(emailToProcess.date).toLocaleString()}<br/>
|
||||
Subject: ${emailToProcess.subject}<br/>
|
||||
To: ${emailToProcess.to}<br/>
|
||||
${emailToProcess.cc ? `Cc: ${emailToProcess.cc}<br/>` : ''}
|
||||
</div>
|
||||
<div style="margin-top: 10px; color: #374151;">
|
||||
${emailContent}
|
||||
</div>
|
||||
` : `
|
||||
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
|
||||
On ${new Date(replyTo?.date || '').toLocaleString()}, ${replyTo?.from} wrote:
|
||||
On ${new Date(emailToProcess.date).toLocaleString()}, ${emailToProcess.from} wrote:
|
||||
</div>
|
||||
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #6b7280;">
|
||||
${parsed.html || parsed.text}
|
||||
<blockquote style="margin: 10px 0 0 10px; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #374151;">
|
||||
${emailContent}
|
||||
</blockquote>
|
||||
`}
|
||||
`;
|
||||
|
||||
// Set the content in the compose area with proper structure
|
||||
const formattedContent = `
|
||||
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px;">
|
||||
<div style="min-height: 20px;"><br/></div>
|
||||
${quotedContent}
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (composeBodyRef.current) {
|
||||
composeBodyRef.current.innerHTML = content;
|
||||
setIsInitialized(true);
|
||||
composeBodyRef.current.innerHTML = formattedContent;
|
||||
|
||||
// Place cursor at the beginning of the compose area
|
||||
const composeArea = composeBodyRef.current.querySelector('.compose-area');
|
||||
if (composeArea) {
|
||||
// Place cursor at the beginning before the quoted content
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
const sel = window.getSelection();
|
||||
range.setStart(composeArea, 0);
|
||||
const firstDiv = composeBodyRef.current.querySelector('div[style*="min-height: 20px;"]');
|
||||
if (firstDiv) {
|
||||
range.setStart(firstDiv, 0);
|
||||
range.collapse(true);
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
(composeArea as HTMLElement).focus();
|
||||
selection?.removeAllRanges();
|
||||
selection?.addRange(range);
|
||||
(firstDiv as HTMLElement).focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error parsing email:', error);
|
||||
});
|
||||
} else {
|
||||
content = `<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;"></div>`;
|
||||
composeBodyRef.current.innerHTML = content;
|
||||
setIsInitialized(true);
|
||||
|
||||
const composeArea = composeBodyRef.current.querySelector('.compose-area');
|
||||
if (composeArea) {
|
||||
const range = document.createRange();
|
||||
const sel = window.getSelection();
|
||||
range.setStart(composeArea, 0);
|
||||
range.collapse(true);
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
(composeArea as HTMLElement).focus();
|
||||
// Update compose state
|
||||
setComposeBody(formattedContent);
|
||||
setLocalContent(formattedContent);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('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>
|
||||
`;
|
||||
composeBodyRef.current.innerHTML = errorContent;
|
||||
setComposeBody(errorContent);
|
||||
setLocalContent(errorContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [composeBody, replyTo, forwardFrom, isInitialized]);
|
||||
};
|
||||
|
||||
initializeContent();
|
||||
}
|
||||
}, [replyTo, forwardFrom]);
|
||||
|
||||
// Modified input handler to work with the single contentEditable area
|
||||
const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
|
||||
if (!composeBodyRef.current) return;
|
||||
|
||||
// Get the compose area content
|
||||
const composeArea = composeBodyRef.current.querySelector('.compose-area');
|
||||
if (!composeArea) return;
|
||||
|
||||
const content = composeArea.innerHTML;
|
||||
|
||||
const content = composeBodyRef.current.innerHTML;
|
||||
if (!content.trim()) {
|
||||
console.warn('Email content is empty');
|
||||
return;
|
||||
setLocalContent('');
|
||||
setComposeBody('');
|
||||
} else {
|
||||
setLocalContent(content);
|
||||
setComposeBody(content);
|
||||
}
|
||||
|
||||
// Create MIME headers
|
||||
const mimeHeaders = {
|
||||
'MIME-Version': '1.0',
|
||||
'Content-Type': 'text/html; charset="utf-8"',
|
||||
'Content-Transfer-Encoding': 'quoted-printable'
|
||||
};
|
||||
|
||||
// Combine headers and content
|
||||
const mimeContent = Object.entries(mimeHeaders)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n') + '\n\n' + content;
|
||||
|
||||
setComposeBody(mimeContent);
|
||||
|
||||
if (onBodyChange) {
|
||||
onBodyChange(mimeContent);
|
||||
onBodyChange(content);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
// Ensure we have content before sending
|
||||
if (!composeBodyRef.current) {
|
||||
console.error('Compose body ref is not available');
|
||||
return;
|
||||
}
|
||||
if (!composeBodyRef.current) return;
|
||||
|
||||
const composeArea = composeBodyRef.current.querySelector('.compose-area');
|
||||
if (!composeArea) {
|
||||
console.error('Compose area not found');
|
||||
return;
|
||||
}
|
||||
if (!composeArea) return;
|
||||
|
||||
// Get the current content
|
||||
const content = composeArea.innerHTML;
|
||||
if (!content.trim()) {
|
||||
console.error('Email content is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create MIME headers
|
||||
const mimeHeaders = {
|
||||
'MIME-Version': '1.0',
|
||||
'Content-Type': 'text/html; charset="utf-8"',
|
||||
'Content-Transfer-Encoding': 'quoted-printable'
|
||||
};
|
||||
|
||||
// Combine headers and content
|
||||
const mimeContent = Object.entries(mimeHeaders)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n') + '\n\n' + content;
|
||||
|
||||
setComposeBody(mimeContent);
|
||||
|
||||
try {
|
||||
const encodedContent = await encodeComposeContent(content);
|
||||
setComposeBody(encodedContent);
|
||||
await handleSend();
|
||||
setShowCompose(false);
|
||||
} catch (error) {
|
||||
|
||||
@ -3,73 +3,58 @@
|
||||
* Handles basic email content without creating nested structures
|
||||
*/
|
||||
|
||||
export function decodeComposeContent(content: string): string {
|
||||
if (!content) return '';
|
||||
|
||||
// Basic HTML cleaning without creating nested structures
|
||||
let cleaned = content
|
||||
// Remove script and style tags
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
// Remove meta tags
|
||||
.replace(/<meta[^>]*>/gi, '')
|
||||
// Remove head and title
|
||||
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
||||
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
|
||||
// Remove body tags
|
||||
.replace(/<body[^>]*>/gi, '')
|
||||
.replace(/<\/body>/gi, '')
|
||||
// Remove html tags
|
||||
.replace(/<html[^>]*>/gi, '')
|
||||
.replace(/<\/html>/gi, '')
|
||||
// Handle basic formatting
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<p[^>]*>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n')
|
||||
// Handle lists
|
||||
.replace(/<ul[^>]*>/gi, '\n')
|
||||
.replace(/<\/ul>/gi, '\n')
|
||||
.replace(/<ol[^>]*>/gi, '\n')
|
||||
.replace(/<\/ol>/gi, '\n')
|
||||
.replace(/<li[^>]*>/gi, '• ')
|
||||
.replace(/<\/li>/gi, '\n')
|
||||
// Handle basic text formatting
|
||||
.replace(/<strong[^>]*>/gi, '**')
|
||||
.replace(/<\/strong>/gi, '**')
|
||||
.replace(/<b[^>]*>/gi, '**')
|
||||
.replace(/<\/b>/gi, '**')
|
||||
.replace(/<em[^>]*>/gi, '*')
|
||||
.replace(/<\/em>/gi, '*')
|
||||
.replace(/<i[^>]*>/gi, '*')
|
||||
.replace(/<\/i>/gi, '*')
|
||||
// Handle links
|
||||
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '$2 ($1)')
|
||||
// Handle basic entities
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
// Clean up whitespace
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
// Do NOT wrap in additional divs
|
||||
return cleaned;
|
||||
interface ParsedContent {
|
||||
html: string | null;
|
||||
text: string | null;
|
||||
}
|
||||
|
||||
export function encodeComposeContent(content: string): string {
|
||||
if (!content) return '';
|
||||
|
||||
// Basic HTML encoding without adding structure
|
||||
const encoded = content
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
return encoded;
|
||||
export async function decodeComposeContent(content: string): Promise<ParsedContent> {
|
||||
if (!content.trim()) {
|
||||
return { html: null, text: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/parse-email', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ emailContent: content }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to parse email');
|
||||
}
|
||||
|
||||
const parsed = await response.json();
|
||||
return {
|
||||
html: parsed.html || null,
|
||||
text: parsed.text || null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing email content:', error);
|
||||
// Fallback to basic content handling
|
||||
return {
|
||||
html: content,
|
||||
text: content
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function encodeComposeContent(content: string): Promise<string> {
|
||||
if (!content.trim()) {
|
||||
throw new Error('Email content is empty');
|
||||
}
|
||||
|
||||
// Create MIME headers
|
||||
const mimeHeaders = {
|
||||
'MIME-Version': '1.0',
|
||||
'Content-Type': 'text/html; charset="utf-8"',
|
||||
'Content-Transfer-Encoding': 'quoted-printable'
|
||||
};
|
||||
|
||||
// Combine headers and content
|
||||
return Object.entries(mimeHeaders)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join('\n') + '\n\n' + content;
|
||||
}
|
||||
@ -22,9 +22,20 @@ export interface ParsedEmail {
|
||||
export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
|
||||
try {
|
||||
// Ensure the email content is properly formatted
|
||||
const formattedContent = emailContent.trim();
|
||||
const formattedContent = emailContent?.trim();
|
||||
if (!formattedContent) {
|
||||
throw new Error('Email content is empty');
|
||||
return {
|
||||
subject: null,
|
||||
from: null,
|
||||
to: null,
|
||||
cc: null,
|
||||
bcc: null,
|
||||
date: null,
|
||||
html: null,
|
||||
text: 'No content available',
|
||||
attachments: [],
|
||||
headers: {}
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch('/api/parse-email', {
|
||||
@ -32,22 +43,61 @@ export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ emailContent: formattedContent }),
|
||||
body: JSON.stringify({ email: formattedContent }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Failed to parse email');
|
||||
console.error('API Error:', data);
|
||||
return {
|
||||
subject: null,
|
||||
from: null,
|
||||
to: null,
|
||||
cc: null,
|
||||
bcc: null,
|
||||
date: null,
|
||||
html: null,
|
||||
text: data.error || 'Failed to parse email',
|
||||
attachments: [],
|
||||
headers: {}
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// If we have a successful response but no content
|
||||
if (!data.html && !data.text) {
|
||||
return {
|
||||
...data,
|
||||
date: data.date ? new Date(data.date) : null
|
||||
date: data.date ? new Date(data.date) : null,
|
||||
html: null,
|
||||
text: 'No content available',
|
||||
attachments: data.attachments || [],
|
||||
headers: data.headers || {}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
date: data.date ? new Date(data.date) : null,
|
||||
text: data.text || null,
|
||||
html: data.html || null,
|
||||
attachments: data.attachments || [],
|
||||
headers: data.headers || {}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error parsing email:', error);
|
||||
throw error;
|
||||
return {
|
||||
subject: null,
|
||||
from: null,
|
||||
to: null,
|
||||
cc: null,
|
||||
bcc: null,
|
||||
date: null,
|
||||
html: null,
|
||||
text: 'Error parsing email content',
|
||||
attachments: [],
|
||||
headers: {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
webpack: (config, { isServer }) => {
|
||||
// Handle node: protocol imports
|
||||
if (!isServer) {
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
net: false,
|
||||
tls: false,
|
||||
fs: false,
|
||||
dns: false,
|
||||
child_process: false,
|
||||
http2: false,
|
||||
module: false,
|
||||
buffer: require.resolve('buffer/'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
util: require.resolve('util/'),
|
||||
};
|
||||
}
|
||||
return config;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user