compose mime

This commit is contained in:
alma 2025-04-25 09:25:04 +02:00
parent c70ba386ae
commit 9a762927fc
6 changed files with 457 additions and 343 deletions

View File

@ -1,29 +1,48 @@
import { NextResponse } from 'next/server'; 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) { export async function POST(request: Request) {
try { try {
const body = await request.json(); const body = await request.json();
console.log('Received request body:', body); const { email } = body;
const { emailContent } = body; if (!email || typeof email !== 'string') {
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 });
return NextResponse.json( return NextResponse.json(
{ error: 'Invalid email content. Expected a string.', received: { type: typeof emailContent, length: emailContent?.length } }, { error: 'Invalid email content' },
{ status: 400 } { status: 400 }
); );
} }
const parsed = await parseEmail(emailContent); const parsed = await simpleParser(email);
return NextResponse.json(parsed);
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) { } catch (error) {
console.error('Error parsing email:', error); console.error('Error parsing email:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to parse email', details: error instanceof Error ? error.message : 'Unknown error' }, { error: 'Failed to parse email' },
{ status: 500 } { status: 500 }
); );
} }

View File

@ -103,20 +103,30 @@ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: s
function EmailContent({ email }: { email: Email }) { function EmailContent({ email }: { email: Email }) {
const [content, setContent] = useState<React.ReactNode>(null); const [content, setContent] = useState<React.ReactNode>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
async function loadContent() { async function loadContent() {
if (!email) return;
setIsLoading(true);
try { try {
if (!email.body) { if (!email.body) {
if (mounted) setContent(null); if (mounted) {
setContent(<div className="text-gray-500">No content available</div>);
setIsLoading(false);
}
return; return;
} }
const formattedEmail = email.body.trim(); const formattedEmail = email.body.trim();
if (!formattedEmail) { if (!formattedEmail) {
if (mounted) setContent(null); if (mounted) {
setContent(<div className="text-gray-500">No content available</div>);
setIsLoading(false);
}
return; return;
} }
@ -127,7 +137,7 @@ function EmailContent({ email }: { email: Email }) {
setContent( setContent(
<div <div
className="email-content prose prose-sm max-w-none dark:prose-invert" className="email-content prose prose-sm max-w-none dark:prose-invert"
dangerouslySetInnerHTML={{ __html: parsedEmail.html }} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(parsedEmail.html) }}
/> />
); );
} else if (parsedEmail.text) { } else if (parsedEmail.text) {
@ -137,15 +147,17 @@ function EmailContent({ email }: { email: Email }) {
</div> </div>
); );
} else { } else {
setContent(null); setContent(<div className="text-gray-500">No content available</div>);
} }
setError(null); setError(null);
setIsLoading(false);
} }
} catch (err) { } catch (err) {
console.error('Error rendering email content:', err); console.error('Error rendering email content:', err);
if (mounted) { if (mounted) {
setError('Error rendering email content. Please try again.'); setError('Error rendering email content. Please try again.');
setContent(null); setContent(null);
setIsLoading(false);
} }
} }
} }
@ -155,13 +167,21 @@ function EmailContent({ email }: { email: Email }) {
return () => { return () => {
mounted = false; 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) { if (error) {
return <div className="text-red-500">{error}</div>; 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) { function renderEmailContent(email: Email) {
@ -310,15 +330,29 @@ function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'r
function EmailPreview({ email }: { email: Email }) { function EmailPreview({ email }: { email: Email }) {
const [preview, setPreview] = useState<string>(''); const [preview, setPreview] = useState<string>('');
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
async function loadPreview() { async function loadPreview() {
if (!email?.body) {
if (mounted) setPreview('No content available');
return;
}
setIsLoading(true);
try { try {
const decoded = await decodeEmail(email.body); const decoded = await decodeEmail(email.body);
if (mounted) { 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); setError(null);
} }
} catch (err) { } catch (err) {
@ -327,6 +361,8 @@ function EmailPreview({ email }: { email: Email }) {
setError('Error generating preview'); setError('Error generating preview');
setPreview(''); setPreview('');
} }
} finally {
if (mounted) setIsLoading(false);
} }
} }
@ -335,7 +371,11 @@ function EmailPreview({ email }: { email: Email }) {
return () => { return () => {
mounted = false; mounted = false;
}; };
}, [email.body]); }, [email?.body]);
if (isLoading) {
return <span className="text-gray-400">Loading preview...</span>;
}
if (error) { if (error) {
return <span className="text-red-500 text-xs">{error}</span>; return <span className="text-red-500 text-xs">{error}</span>;
@ -507,8 +547,9 @@ export default function CourrierPage() {
setAvailableFolders(data.folders); setAvailableFolders(data.folders);
} }
// Process emails keeping exact folder names // Process emails keeping exact folder names and sort by date
const processedEmails = (data.emails || []).map((email: any) => ({ const processedEmails = (data.emails || [])
.map((email: any) => ({
id: Number(email.id), id: Number(email.id),
accountId: 1, accountId: 1,
from: email.from || '', from: email.from || '',
@ -526,19 +567,35 @@ export default function CourrierPage() {
raw: email.body || '' 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 // Only update unread count if we're in the Inbox folder
if (currentView === 'INBOX') { if (currentView === 'INBOX') {
const unreadInboxEmails = processedEmails.filter( const unreadInboxEmails = sortedEmails.filter(
(email: Email) => !email.read && email.folder === 'INBOX' (email: Email) => !email.read && email.folder === 'INBOX'
).length; ).length;
setUnreadCount(unreadInboxEmails); setUnreadCount(unreadInboxEmails);
} }
if (isLoadMore) { 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); setPage(prev => prev + 1);
} else { } else {
setEmails(processedEmails); // For initial load or refresh, just use the sorted emails
setEmails(sortedEmails);
setPage(1); setPage(1);
} }
@ -572,8 +629,10 @@ export default function CourrierPage() {
return; return;
} }
try {
// Set the selected email first to show preview immediately // Set the selected email first to show preview immediately
setSelectedEmail(email); setSelectedEmail(email);
setContentLoading(true);
// Fetch the full email content // Fetch the full email content
const response = await fetch(`/api/mail/${emailId}`); 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 // Update the email in the list and selected email with full content
setEmails(prevEmails => prevEmails.map(email => setEmails(prevEmails => prevEmails.map(email =>
email.id === emailId email.id === emailId
? { ...email, body: fullEmail.body } ? { ...email, body: fullEmail.body || email.body }
: email : 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 to mark as read in the background
try { try {
@ -620,6 +680,10 @@ export default function CourrierPage() {
} catch (error) { } catch (error) {
console.error('Error marking email as read:', error); console.error('Error marking email as read:', error);
} }
} catch (error) {
console.error('Error fetching email content:', error);
setContentLoading(false);
}
}; };
// Add these improved handlers // Add these improved handlers
@ -1153,9 +1217,14 @@ export default function CourrierPage() {
); );
// Add back the handleReply function // Add back the handleReply function
const handleReply = (type: 'reply' | 'reply-all' | 'forward') => { const handleReply = async (type: 'reply' | 'reply-all' | 'forward') => {
if (!selectedEmail) return; if (!selectedEmail) return;
try {
// Get the decoded content first
const decoded = await decodeEmail(selectedEmail.body);
// Set up the reply details
const getReplyTo = () => { const getReplyTo = () => {
if (type === 'forward') return ''; if (type === 'forward') return '';
return selectedEmail.from; return selectedEmail.from;
@ -1174,32 +1243,39 @@ export default function CourrierPage() {
return subject.startsWith('Re:') ? subject : `Re: ${subject}`; return subject.startsWith('Re:') ? subject : `Re: ${subject}`;
}; };
// Get the formatted original email content // Create the appropriate email content based on type
const originalContent = getReplyBody(selectedEmail, type); let formattedContent = '';
if (type === 'forward') {
// Create a clean structure with clear separation formattedContent = `
const formattedContent = ` <div style="min-height: 100px;">
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; border: 1px solid #e5e7eb; border-radius: 4px; margin-bottom: 20px;"></div> <br/>
<div class="quoted-content" contenteditable="false" style="color: #6b7280; font-size: 0.875rem;"> <div style="border-top: 1px solid #e5e7eb; margin-top: 20px; padding-top: 10px; color: #666;">
${type === 'forward' ? `
<div style="margin-bottom: 10px;">
---------- Forwarded message ---------<br/> ---------- Forwarded message ---------<br/>
From: ${selectedEmail.from}<br/> From: ${selectedEmail.from}<br/>
Date: ${new Date(selectedEmail.date).toLocaleString()}<br/> Date: ${new Date(selectedEmail.date).toLocaleString()}<br/>
Subject: ${selectedEmail.subject}<br/> Subject: ${selectedEmail.subject}<br/>
To: ${selectedEmail.to}<br/> To: ${selectedEmail.to}<br/>
${selectedEmail.cc ? `Cc: ${selectedEmail.cc}<br/>` : ''} ${selectedEmail.cc ? `Cc: ${selectedEmail.cc}<br/>` : ''}
<br/>
<div style="margin-top: 10px;">
${decoded.html || decoded.text || ''}
</div> </div>
` : `
<div style="margin-bottom: 10px;">
On ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote:
</div> </div>
`}
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb;">
${originalContent}
</blockquote>
</div> </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 // Update the compose form
setComposeTo(getReplyTo()); setComposeTo(getReplyTo());
@ -1207,12 +1283,14 @@ export default function CourrierPage() {
setComposeSubject(getReplySubject()); setComposeSubject(getReplySubject());
setComposeBody(formattedContent); setComposeBody(formattedContent);
setComposeBcc(''); setComposeBcc('');
// Show the compose form and CC field for Reply All
setShowCompose(true); setShowCompose(true);
setShowCc(type === 'reply-all'); setShowCc(type === 'reply-all');
setShowBcc(false); setShowBcc(false);
setAttachments([]); setAttachments([]);
} catch (error) {
console.error('Error preparing reply:', error);
}
}; };
// Add back the toggleStarred function // Add back the toggleStarred function
@ -1445,10 +1523,7 @@ export default function CourrierPage() {
attachments={attachments} attachments={attachments}
setAttachments={setAttachments} setAttachments={setAttachments}
handleSend={handleSend} handleSend={handleSend}
replyTo={selectedEmail || undefined}
forwardFrom={selectedEmail || undefined}
onSend={(email) => { onSend={(email) => {
// Handle the sent email
console.log('Email sent:', email); console.log('Email sent:', email);
setShowCompose(false); setShowCompose(false);
}} }}

View File

@ -81,157 +81,145 @@ export default function ComposeEmail({
}: ComposeEmailProps) { }: ComposeEmailProps) {
const composeBodyRef = useRef<HTMLDivElement>(null); const composeBodyRef = useRef<HTMLDivElement>(null);
const [localContent, setLocalContent] = useState(''); const [localContent, setLocalContent] = useState('');
const [isInitialized, setIsInitialized] = useState(false); const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
if (composeBodyRef.current && !isInitialized) {
let content = '';
if (replyTo || forwardFrom) { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ emailContent: originalContent }), body: JSON.stringify({ email: emailToProcess.body }),
}) });
.then(response => response.json())
.then(parsed => { const data = await response.json();
content = ` if (!response.ok) {
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;"> throw new Error(data.error || 'Failed to parse email');
<br/><br/><br/> }
${forwardFrom ? `
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;"> <div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
---------- Forwarded message ---------<br/> ---------- Forwarded message ---------<br/>
From: ${forwardFrom.from}<br/> From: ${emailToProcess.from}<br/>
Date: ${new Date(forwardFrom.date).toLocaleString()}<br/> Date: ${new Date(emailToProcess.date).toLocaleString()}<br/>
Subject: ${forwardFrom.subject}<br/> Subject: ${emailToProcess.subject}<br/>
To: ${forwardFrom.to}<br/> To: ${emailToProcess.to}<br/>
${forwardFrom.cc ? `Cc: ${forwardFrom.cc}<br/>` : ''} ${emailToProcess.cc ? `Cc: ${emailToProcess.cc}<br/>` : ''}
<br/> </div>
${parsed.html || parsed.text} <div style="margin-top: 10px; color: #374151;">
${emailContent}
</div> </div>
` : ` ` : `
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;"> <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> </div>
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #6b7280;"> <blockquote style="margin: 10px 0 0 10px; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #374151;">
${parsed.html || parsed.text} ${emailContent}
</blockquote> </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> </div>
`; `;
if (composeBodyRef.current) { if (composeBodyRef.current) {
composeBodyRef.current.innerHTML = content; composeBodyRef.current.innerHTML = formattedContent;
setIsInitialized(true);
// Place cursor at the beginning of the compose area // Place cursor at the beginning before the quoted content
const composeArea = composeBodyRef.current.querySelector('.compose-area'); const selection = window.getSelection();
if (composeArea) {
const range = document.createRange(); const range = document.createRange();
const sel = window.getSelection(); const firstDiv = composeBodyRef.current.querySelector('div[style*="min-height: 20px;"]');
range.setStart(composeArea, 0); if (firstDiv) {
range.setStart(firstDiv, 0);
range.collapse(true); range.collapse(true);
sel?.removeAllRanges(); selection?.removeAllRanges();
sel?.addRange(range); selection?.addRange(range);
(composeArea as HTMLElement).focus(); (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'); // Update compose state
if (composeArea) { setComposeBody(formattedContent);
const range = document.createRange(); setLocalContent(formattedContent);
const sel = window.getSelection(); }
range.setStart(composeArea, 0); } catch (error) {
range.collapse(true); console.error('Error initializing compose content:', error);
sel?.removeAllRanges(); if (composeBodyRef.current) {
sel?.addRange(range); const errorContent = `
(composeArea as HTMLElement).focus(); <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>) => { const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
if (!composeBodyRef.current) return; if (!composeBodyRef.current) return;
// Get the compose area content const content = composeBodyRef.current.innerHTML;
const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (!composeArea) return;
const content = composeArea.innerHTML;
if (!content.trim()) { if (!content.trim()) {
console.warn('Email content is empty'); setLocalContent('');
return; 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) { if (onBodyChange) {
onBodyChange(mimeContent); onBodyChange(content);
} }
}; };
const handleSendEmail = async () => { const handleSendEmail = async () => {
// Ensure we have content before sending if (!composeBodyRef.current) return;
if (!composeBodyRef.current) {
console.error('Compose body ref is not available');
return;
}
const composeArea = composeBodyRef.current.querySelector('.compose-area'); const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (!composeArea) { if (!composeArea) return;
console.error('Compose area not found');
return;
}
// Get the current content
const content = composeArea.innerHTML; const content = composeArea.innerHTML;
if (!content.trim()) { if (!content.trim()) {
console.error('Email content is empty'); console.error('Email content is empty');
return; 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 { try {
const encodedContent = await encodeComposeContent(content);
setComposeBody(encodedContent);
await handleSend(); await handleSend();
setShowCompose(false); setShowCompose(false);
} catch (error) { } catch (error) {

View File

@ -3,73 +3,58 @@
* Handles basic email content without creating nested structures * Handles basic email content without creating nested structures
*/ */
export function decodeComposeContent(content: string): string { interface ParsedContent {
if (!content) return ''; html: string | null;
text: string | null;
// 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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
// Clean up whitespace
.replace(/\s+/g, ' ')
.trim();
// Do NOT wrap in additional divs
return cleaned;
} }
export function encodeComposeContent(content: string): string { export async function decodeComposeContent(content: string): Promise<ParsedContent> {
if (!content) return ''; if (!content.trim()) {
return { html: null, text: null };
}
// Basic HTML encoding without adding structure try {
const encoded = content const response = await fetch('/api/parse-email', {
.replace(/&/g, '&amp;') method: 'POST',
.replace(/</g, '&lt;') headers: {
.replace(/>/g, '&gt;') 'Content-Type': 'application/json',
.replace(/"/g, '&quot;') },
.replace(/'/g, '&#39;') body: JSON.stringify({ emailContent: content }),
.replace(/\n/g, '<br>'); });
return encoded; 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;
} }

View File

@ -22,9 +22,20 @@ export interface ParsedEmail {
export async function decodeEmail(emailContent: string): Promise<ParsedEmail> { export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
try { try {
// Ensure the email content is properly formatted // Ensure the email content is properly formatted
const formattedContent = emailContent.trim(); const formattedContent = emailContent?.trim();
if (!formattedContent) { 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', { const response = await fetch('/api/parse-email', {
@ -32,22 +43,61 @@ export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ emailContent: formattedContent }), body: JSON.stringify({ email: formattedContent }),
}); });
const data = await response.json();
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); console.error('API Error:', data);
throw new Error(errorData.error || 'Failed to parse email'); 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 { return {
...data, ...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) { } catch (error) {
console.error('Error parsing email:', 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: {}
};
} }
} }

View File

@ -1,16 +1,13 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
webpack: (config, { isServer }) => { webpack: (config, { isServer }) => {
// Handle node: protocol imports
if (!isServer) { if (!isServer) {
config.resolve.fallback = { config.resolve.fallback = {
...config.resolve.fallback, ...config.resolve.fallback,
net: false, buffer: require.resolve('buffer/'),
tls: false, stream: require.resolve('stream-browserify'),
fs: false, util: require.resolve('util/'),
dns: false,
child_process: false,
http2: false,
module: false,
}; };
} }
return config; return config;