compose mime
This commit is contained in:
parent
48403d2e67
commit
c70ba386ae
@ -1,48 +1,29 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { simpleParser, AddressObject } from 'mailparser';
|
import { parseEmail } from '@/lib/server/email-parser';
|
||||||
|
|
||||||
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();
|
||||||
const { email } = body;
|
console.log('Received request body:', body);
|
||||||
|
|
||||||
if (!email || typeof email !== 'string') {
|
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 });
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Invalid email content' },
|
{ error: 'Invalid email content. Expected a string.', received: { type: typeof emailContent, length: emailContent?.length } },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = await simpleParser(email);
|
const parsed = await parseEmail(emailContent);
|
||||||
|
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' },
|
{ error: 'Failed to parse email', details: error instanceof Error ? error.message : 'Unknown error' },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,30 +103,20 @@ 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) {
|
if (mounted) setContent(null);
|
||||||
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) {
|
if (mounted) setContent(null);
|
||||||
setContent(<div className="text-gray-500">No content available</div>);
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +127,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: DOMPurify.sanitize(parsedEmail.html) }}
|
dangerouslySetInnerHTML={{ __html: parsedEmail.html }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (parsedEmail.text) {
|
} else if (parsedEmail.text) {
|
||||||
@ -147,17 +137,15 @@ function EmailContent({ email }: { email: Email }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setContent(<div className="text-gray-500">No content available</div>);
|
setContent(null);
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,21 +155,13 @@ 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 || <div className="text-gray-500">No content available</div>;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEmailContent(email: Email) {
|
function renderEmailContent(email: Email) {
|
||||||
@ -330,29 +310,15 @@ 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) {
|
||||||
if (decoded.text) {
|
setPreview(decoded.text || cleanHtml(decoded.html || ''));
|
||||||
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) {
|
||||||
@ -361,8 +327,6 @@ function EmailPreview({ email }: { email: Email }) {
|
|||||||
setError('Error generating preview');
|
setError('Error generating preview');
|
||||||
setPreview('');
|
setPreview('');
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
if (mounted) setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,11 +335,7 @@ 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>;
|
||||||
@ -547,55 +507,38 @@ export default function CourrierPage() {
|
|||||||
setAvailableFolders(data.folders);
|
setAvailableFolders(data.folders);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process emails keeping exact folder names and sort by date
|
// Process emails keeping exact folder names
|
||||||
const processedEmails = (data.emails || [])
|
const processedEmails = (data.emails || []).map((email: any) => ({
|
||||||
.map((email: any) => ({
|
id: Number(email.id),
|
||||||
id: Number(email.id),
|
accountId: 1,
|
||||||
accountId: 1,
|
from: email.from || '',
|
||||||
from: email.from || '',
|
fromName: email.fromName || email.from?.split('@')[0] || '',
|
||||||
fromName: email.fromName || email.from?.split('@')[0] || '',
|
to: email.to || '',
|
||||||
to: email.to || '',
|
subject: email.subject || '(No subject)',
|
||||||
subject: email.subject || '(No subject)',
|
body: email.body || '',
|
||||||
body: email.body || '',
|
date: email.date || new Date().toISOString(),
|
||||||
date: email.date || new Date().toISOString(),
|
read: email.read || false,
|
||||||
read: email.read || false,
|
starred: email.starred || false,
|
||||||
starred: email.starred || false,
|
folder: email.folder || currentView,
|
||||||
folder: email.folder || currentView,
|
cc: email.cc,
|
||||||
cc: email.cc,
|
bcc: email.bcc,
|
||||||
bcc: email.bcc,
|
flags: email.flags || [],
|
||||||
flags: email.flags || [],
|
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 = sortedEmails.filter(
|
const unreadInboxEmails = processedEmails.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) {
|
||||||
// When loading more, merge with existing emails and re-sort
|
setEmails(prev => [...prev, ...processedEmails]);
|
||||||
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 {
|
||||||
// For initial load or refresh, just use the sorted emails
|
setEmails(processedEmails);
|
||||||
setEmails(sortedEmails);
|
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,60 +572,53 @@ export default function CourrierPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the selected email first to show preview immediately
|
||||||
|
setSelectedEmail(email);
|
||||||
|
|
||||||
|
// Fetch the full email content
|
||||||
|
const response = await fetch(`/api/mail/${emailId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch full email content');
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullEmail = await response.json();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
));
|
||||||
|
|
||||||
|
setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body } : prev);
|
||||||
|
|
||||||
|
// Try to mark as read in the background
|
||||||
try {
|
try {
|
||||||
// Set the selected email first to show preview immediately
|
const markReadResponse = await fetch(`/api/mail/mark-read`, {
|
||||||
setSelectedEmail(email);
|
method: 'POST',
|
||||||
setContentLoading(true);
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
// Fetch the full email content
|
},
|
||||||
const response = await fetch(`/api/mail/${emailId}`);
|
body: JSON.stringify({
|
||||||
if (!response.ok) {
|
emailId,
|
||||||
throw new Error('Failed to fetch full email content');
|
isRead: true,
|
||||||
}
|
}),
|
||||||
|
});
|
||||||
const fullEmail = await response.json();
|
|
||||||
|
|
||||||
// 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 }
|
|
||||||
: email
|
|
||||||
));
|
|
||||||
|
|
||||||
setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body || prev.body } : prev);
|
|
||||||
setContentLoading(false);
|
|
||||||
|
|
||||||
// Try to mark as read in the background
|
if (markReadResponse.ok) {
|
||||||
try {
|
// Only update the emails list if the API call was successful
|
||||||
const markReadResponse = await fetch(`/api/mail/mark-read`, {
|
setEmails((prevEmails: Email[]) =>
|
||||||
method: 'POST',
|
prevEmails.map((email: Email): Email =>
|
||||||
headers: {
|
email.id === emailId
|
||||||
'Content-Type': 'application/json',
|
? { ...email, read: true }
|
||||||
},
|
: email
|
||||||
body: JSON.stringify({
|
)
|
||||||
emailId,
|
);
|
||||||
isRead: true,
|
} else {
|
||||||
}),
|
console.error('Failed to mark email as read:', await markReadResponse.text());
|
||||||
});
|
|
||||||
|
|
||||||
if (markReadResponse.ok) {
|
|
||||||
// Only update the emails list if the API call was successful
|
|
||||||
setEmails((prevEmails: Email[]) =>
|
|
||||||
prevEmails.map((email: Email): Email =>
|
|
||||||
email.id === emailId
|
|
||||||
? { ...email, read: true }
|
|
||||||
: email
|
|
||||||
)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error('Failed to mark email as read:', await markReadResponse.text());
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error marking email as read:', error);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching email content:', error);
|
console.error('Error marking email as read:', error);
|
||||||
setContentLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1217,7 +1153,7 @@ export default function CourrierPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Add back the handleReply function
|
// Add back the handleReply function
|
||||||
const handleReply = async (type: 'reply' | 'reply-all' | 'forward') => {
|
const handleReply = (type: 'reply' | 'reply-all' | 'forward') => {
|
||||||
if (!selectedEmail) return;
|
if (!selectedEmail) return;
|
||||||
|
|
||||||
const getReplyTo = () => {
|
const getReplyTo = () => {
|
||||||
@ -1238,51 +1174,45 @@ export default function CourrierPage() {
|
|||||||
return subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
return subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
// Get the formatted original email content
|
||||||
// Get the decoded content first
|
const originalContent = getReplyBody(selectedEmail, type);
|
||||||
const decoded = await decodeEmail(selectedEmail.body);
|
|
||||||
|
// Create a clean structure with clear separation
|
||||||
// Create a clean structure with clear separation
|
const formattedContent = `
|
||||||
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="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;">
|
<div class="quoted-content" contenteditable="false" style="color: #6b7280; font-size: 0.875rem;">
|
||||||
<br/>
|
${type === 'forward' ? `
|
||||||
${type === 'forward' ? `
|
<div style="margin-bottom: 10px;">
|
||||||
<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: ${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/>` : ''}
|
</div>
|
||||||
<br/>
|
` : `
|
||||||
${decoded.html || decoded.text || ''}
|
<div style="margin-bottom: 10px;">
|
||||||
</div>
|
On ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote:
|
||||||
` : `
|
</div>
|
||||||
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
|
`}
|
||||||
On ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote:
|
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb;">
|
||||||
</div>
|
${originalContent}
|
||||||
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #6b7280;">
|
</blockquote>
|
||||||
${decoded.html || decoded.text || ''}
|
</div>
|
||||||
</blockquote>
|
`;
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Update the compose form
|
// Update the compose form
|
||||||
setComposeTo(getReplyTo());
|
setComposeTo(getReplyTo());
|
||||||
setComposeCc(getReplyCc());
|
setComposeCc(getReplyCc());
|
||||||
setComposeSubject(getReplySubject());
|
setComposeSubject(getReplySubject());
|
||||||
setComposeBody(formattedContent);
|
setComposeBody(formattedContent);
|
||||||
setComposeBcc('');
|
setComposeBcc('');
|
||||||
|
|
||||||
// Show the compose form and CC field for Reply All
|
// 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
|
||||||
|
|||||||
@ -81,145 +81,157 @@ 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 [isLoading, setIsLoading] = useState(false);
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (replyTo || forwardFrom) {
|
if (composeBodyRef.current && !isInitialized) {
|
||||||
const initializeContent = async () => {
|
let content = '';
|
||||||
if (!composeBodyRef.current) return;
|
|
||||||
|
if (replyTo || forwardFrom) {
|
||||||
|
const originalContent = replyTo?.body || forwardFrom?.body || '';
|
||||||
|
|
||||||
try {
|
fetch('/api/parse-email', {
|
||||||
const emailToProcess = replyTo || forwardFrom;
|
method: 'POST',
|
||||||
if (!emailToProcess?.body) {
|
headers: {
|
||||||
console.error('No email body found to process');
|
'Content-Type': 'application/json',
|
||||||
return;
|
},
|
||||||
}
|
body: JSON.stringify({ emailContent: originalContent }),
|
||||||
|
})
|
||||||
// Set initial loading state
|
.then(response => response.json())
|
||||||
composeBodyRef.current.innerHTML = `
|
.then(parsed => {
|
||||||
<div class="compose-area" contenteditable="true">
|
content = `
|
||||||
<br/>
|
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;">
|
||||||
<div class="text-gray-500">Loading original message...</div>
|
<br/><br/><br/>
|
||||||
</div>
|
${forwardFrom ? `
|
||||||
`;
|
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
|
||||||
|
---------- Forwarded message ---------<br/>
|
||||||
// Parse the original email using the API
|
From: ${forwardFrom.from}<br/>
|
||||||
const response = await fetch('/api/parse-email', {
|
Date: ${new Date(forwardFrom.date).toLocaleString()}<br/>
|
||||||
method: 'POST',
|
Subject: ${forwardFrom.subject}<br/>
|
||||||
headers: {
|
To: ${forwardFrom.to}<br/>
|
||||||
'Content-Type': 'application/json',
|
${forwardFrom.cc ? `Cc: ${forwardFrom.cc}<br/>` : ''}
|
||||||
},
|
<br/>
|
||||||
body: JSON.stringify({ email: emailToProcess.body }),
|
${parsed.html || parsed.text}
|
||||||
});
|
</div>
|
||||||
|
` : `
|
||||||
const data = await response.json();
|
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
|
||||||
if (!response.ok) {
|
On ${new Date(replyTo?.date || '').toLocaleString()}, ${replyTo?.from} wrote:
|
||||||
throw new Error(data.error || 'Failed to parse email');
|
</div>
|
||||||
}
|
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #6b7280;">
|
||||||
|
${parsed.html || parsed.text}
|
||||||
const emailContent = data.html || data.text || '';
|
</blockquote>
|
||||||
|
`}
|
||||||
// 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: ${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(emailToProcess.date).toLocaleString()}, ${emailToProcess.from} wrote:
|
|
||||||
</div>
|
|
||||||
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (composeBodyRef.current) {
|
if (composeBodyRef.current) {
|
||||||
composeBodyRef.current.innerHTML = formattedContent;
|
composeBodyRef.current.innerHTML = content;
|
||||||
|
setIsInitialized(true);
|
||||||
// Place cursor at the beginning before the quoted content
|
|
||||||
const selection = window.getSelection();
|
// Place cursor at the beginning of the compose area
|
||||||
const range = document.createRange();
|
const composeArea = composeBodyRef.current.querySelector('.compose-area');
|
||||||
const firstDiv = composeBodyRef.current.querySelector('div[style*="min-height: 20px;"]');
|
if (composeArea) {
|
||||||
if (firstDiv) {
|
const range = document.createRange();
|
||||||
range.setStart(firstDiv, 0);
|
const sel = window.getSelection();
|
||||||
|
range.setStart(composeArea, 0);
|
||||||
range.collapse(true);
|
range.collapse(true);
|
||||||
selection?.removeAllRanges();
|
sel?.removeAllRanges();
|
||||||
selection?.addRange(range);
|
sel?.addRange(range);
|
||||||
(firstDiv as HTMLElement).focus();
|
(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);
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.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();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
initializeContent();
|
|
||||||
}
|
}
|
||||||
}, [replyTo, forwardFrom]);
|
}, [composeBody, replyTo, forwardFrom, isInitialized]);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
const content = composeBodyRef.current.innerHTML;
|
// Get the compose area content
|
||||||
|
const composeArea = composeBodyRef.current.querySelector('.compose-area');
|
||||||
|
if (!composeArea) return;
|
||||||
|
|
||||||
|
const content = composeArea.innerHTML;
|
||||||
|
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
setLocalContent('');
|
console.warn('Email content is empty');
|
||||||
setComposeBody('');
|
return;
|
||||||
} 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(content);
|
onBodyChange(mimeContent);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendEmail = async () => {
|
const handleSendEmail = async () => {
|
||||||
if (!composeBodyRef.current) return;
|
// Ensure we have content before sending
|
||||||
|
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) return;
|
if (!composeArea) {
|
||||||
|
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,58 +3,73 @@
|
|||||||
* Handles basic email content without creating nested structures
|
* Handles basic email content without creating nested structures
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface ParsedContent {
|
export function decodeComposeContent(content: string): string {
|
||||||
html: string | null;
|
if (!content) return '';
|
||||||
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 async function decodeComposeContent(content: string): Promise<ParsedContent> {
|
export function encodeComposeContent(content: string): string {
|
||||||
if (!content.trim()) {
|
if (!content) return '';
|
||||||
return { html: null, text: null };
|
|
||||||
}
|
// Basic HTML encoding without adding structure
|
||||||
|
const encoded = content
|
||||||
try {
|
.replace(/&/g, '&')
|
||||||
const response = await fetch('/api/parse-email', {
|
.replace(/</g, '<')
|
||||||
method: 'POST',
|
.replace(/>/g, '>')
|
||||||
headers: {
|
.replace(/"/g, '"')
|
||||||
'Content-Type': 'application/json',
|
.replace(/'/g, ''')
|
||||||
},
|
.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,20 +22,9 @@ 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) {
|
||||||
return {
|
throw new Error('Email content is empty');
|
||||||
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', {
|
||||||
@ -43,61 +32,22 @@ export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ email: formattedContent }),
|
body: JSON.stringify({ emailContent: formattedContent }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('API Error:', data);
|
const errorData = await response.json();
|
||||||
return {
|
throw new Error(errorData.error || 'Failed to parse email');
|
||||||
subject: null,
|
|
||||||
from: null,
|
|
||||||
to: null,
|
|
||||||
cc: null,
|
|
||||||
bcc: null,
|
|
||||||
date: null,
|
|
||||||
html: null,
|
|
||||||
text: data.error || 'Failed to parse email',
|
|
||||||
attachments: [],
|
|
||||||
headers: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a successful response but no content
|
|
||||||
if (!data.html && !data.text) {
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
date: data.date ? new Date(data.date) : null,
|
|
||||||
html: null,
|
|
||||||
text: 'No content available',
|
|
||||||
attachments: data.attachments || [],
|
|
||||||
headers: data.headers || {}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
date: data.date ? new Date(data.date) : null,
|
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);
|
||||||
return {
|
throw error;
|
||||||
subject: null,
|
|
||||||
from: null,
|
|
||||||
to: null,
|
|
||||||
cc: null,
|
|
||||||
bcc: null,
|
|
||||||
date: null,
|
|
||||||
html: null,
|
|
||||||
text: 'Error parsing email content',
|
|
||||||
attachments: [],
|
|
||||||
headers: {}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
/** @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,
|
||||||
buffer: require.resolve('buffer/'),
|
net: false,
|
||||||
stream: require.resolve('stream-browserify'),
|
tls: false,
|
||||||
util: require.resolve('util/'),
|
fs: false,
|
||||||
|
dns: false,
|
||||||
|
child_process: false,
|
||||||
|
http2: false,
|
||||||
|
module: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user