compose mime
This commit is contained in:
parent
c70ba386ae
commit
9a762927fc
@ -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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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(/ /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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
}
|
||||||
const encoded = content
|
|
||||||
.replace(/&/g, '&')
|
try {
|
||||||
.replace(/</g, '<')
|
const response = await fetch('/api/parse-email', {
|
||||||
.replace(/>/g, '>')
|
method: 'POST',
|
||||||
.replace(/"/g, '"')
|
headers: {
|
||||||
.replace(/'/g, ''')
|
'Content-Type': 'application/json',
|
||||||
.replace(/\n/g, '<br>');
|
},
|
||||||
|
body: JSON.stringify({ emailContent: content }),
|
||||||
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;
|
||||||
}
|
}
|
||||||
@ -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: {}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user