diff --git a/app/mail/login/page.tsx b/app/mail/login/page.tsx index 6179d145..3782f9bd 100644 --- a/app/mail/login/page.tsx +++ b/app/mail/login/page.tsx @@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -export default function LoginPage() { +export default function MailLoginPage() { const router = useRouter(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -22,8 +22,7 @@ export default function LoginPage() { setLoading(true); try { - // Test the connection first - const testResponse = await fetch('/api/mail/test-connection', { + const response = await fetch('/api/mail/login', { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -36,30 +35,10 @@ export default function LoginPage() { }), }); - const testData = await testResponse.json(); + const data = await response.json(); - if (!testResponse.ok) { - throw new Error(testData.error || 'Failed to connect to email server'); - } - - // Store credentials using the API endpoint - const loginResponse = await fetch('/api/mail/login', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email, - password, - host, - port, - }), - }); - - const loginData = await loginResponse.json(); - - if (!loginResponse.ok) { - throw new Error(loginData.error || 'Failed to store credentials'); + if (!response.ok) { + throw new Error(data.error || 'Failed to connect to email server'); } // Redirect to mail page @@ -75,7 +54,7 @@ export default function LoginPage() {
- Email Login + Email Configuration
diff --git a/app/mail/page.tsx b/app/mail/page.tsx index c67f2475..236b9fc0 100644 --- a/app/mail/page.tsx +++ b/app/mail/page.tsx @@ -27,6 +27,9 @@ import { AlertOctagon, Archive, RefreshCw } from 'lucide-react'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { MailList } from "@/components/mail/mail-list"; +import { MailToolbar } from "@/components/mail/mail-toolbar"; +import { useMail } from "@/hooks/use-mail"; interface Account { id: number; @@ -472,6 +475,7 @@ const initialSidebarItems = [ export default function MailPage() { const router = useRouter(); + const { mails, isLoading, error, fetchMails } = useMail(); const [loading, setLoading] = useState(true); const [accounts, setAccounts] = useState([ { id: 0, name: 'All', email: '', color: 'bg-gray-500' }, @@ -485,7 +489,6 @@ export default function MailPage() { const [showBulkActions, setShowBulkActions] = useState(false); const [showBcc, setShowBcc] = useState(false); const [emails, setEmails] = useState([]); - const [error, setError] = useState(null); const [composeSubject, setComposeSubject] = useState(''); const [composeTo, setComposeTo] = useState(''); const [composeCc, setComposeCc] = useState(''); @@ -516,1364 +519,39 @@ export default function MailPage() { const [isLoadingMore, setIsLoadingMore] = useState(false); const emailsPerPage = 24; - // Debug logging for email distribution - useEffect(() => { - const emailsByFolder = emails.reduce((acc, email) => { - acc[email.folder] = (acc[email.folder] || 0) + 1; - return acc; - }, {} as Record); - - console.log('Emails by folder:', emailsByFolder); - console.log('Current view:', currentView); - }, [emails, currentView]); - - // Move getSelectedEmail inside the component - const getSelectedEmail = () => { - return emails.find(email => email.id === selectedEmail?.id); - }; - - // Check for stored credentials useEffect(() => { + // Check if we have mail credentials const checkCredentials = async () => { try { - console.log('Checking for stored credentials...'); - const response = await fetch('/api/mail'); + const response = await fetch("/api/mail/login"); if (!response.ok) { - const errorData = await response.json(); - console.log('API response error:', errorData); - if (errorData.error === 'No stored credentials found') { - console.log('No credentials found, redirecting to login...'); - router.push('/mail/login'); - return; - } - throw new Error(errorData.error || 'Failed to check credentials'); + // No credentials found, redirect to login + router.push("/mail/login"); + return; } - console.log('Credentials verified, loading emails...'); - setLoading(false); - loadEmails(); + // We have credentials, fetch mails + fetchMails(); } catch (err) { - console.error('Error checking credentials:', err); - setError(err instanceof Error ? err.message : 'Failed to check credentials'); - setLoading(false); + console.error("Error checking credentials:", err); + router.push("/mail/login"); } }; checkCredentials(); - }, [router]); + }, [router, fetchMails]); - // Update the loadEmails function - const loadEmails = async (isLoadMore = false) => { - try { - if (isLoadMore) { - setIsLoadingMore(true); - } else { - setLoading(true); - } - setError(null); - - const response = await fetch(`/api/mail?folder=${currentView}&page=${page}&limit=${emailsPerPage}`); - if (!response.ok) { - throw new Error('Failed to load emails'); - } - - const data = await response.json(); - - // Get available folders from the API response - if (data.folders) { - setAvailableFolders(data.folders); - } - - // Process emails keeping exact folder names - const processedEmails = data.emails.map((email: any) => ({ - id: Number(email.id), - accountId: 1, - from: email.from || '', - fromName: email.from?.split('@')[0] || '', - to: email.to || '', - subject: email.subject || '(No subject)', - body: email.body || '', - date: email.date || new Date().toISOString(), - read: email.read || false, - starred: email.starred || false, - folder: email.folder || 'INBOX', - cc: email.cc, - bcc: email.bcc, - flags: email.flags || [] - })); - - // Only update unread count if we're in the Inbox folder - if (currentView === 'INBOX') { - const unreadInboxEmails = processedEmails.filter( - (email: Email) => !email.read && email.folder === 'INBOX' - ).length; - setUnreadCount(unreadInboxEmails); - } - - // Update emails state based on whether we're loading more - if (isLoadMore) { - setEmails(prev => [...prev, ...processedEmails]); - } else { - setEmails(processedEmails); - } - - // Update hasMore state based on the number of emails received - setHasMore(processedEmails.length === emailsPerPage); - - } catch (err) { - console.error('Error loading emails:', err); - setError(err instanceof Error ? err.message : 'Failed to load emails'); - } finally { - setLoading(false); - setIsLoadingMore(false); - } - }; - - // Add an effect to reload emails when the view changes - useEffect(() => { - setPage(1); // Reset page when view changes - setHasMore(true); - loadEmails(); - }, [currentView]); - - // Format date for display - const formatDate = (dateString: string) => { - const date = new Date(dateString); - const now = new Date(); - - if (date.toDateString() === now.toDateString()) { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - } else { - return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); - } - }; - - // Get account color - const getAccountColor = (accountId: number) => { - const account = accounts.find(acc => acc.id === accountId); - return account ? account.color : 'bg-gray-500'; - }; - - // Update handleEmailSelect to set selectedEmail correctly - const handleEmailSelect = (emailId: number) => { - const email = emails.find(e => e.id === emailId); - if (email) { - setSelectedEmail(email); - if (!email.read) { - // Mark as read in state - setEmails(emails.map(e => - e.id === emailId ? { ...e, read: true } : e - )); - - // Update read status on server - fetch('/api/mail/mark-read', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ emailId }) - }).catch(error => { - console.error('Error marking email as read:', error); - }); - } - } - }; - - // Add these improved handlers - const handleEmailCheckbox = (e: React.ChangeEvent, emailId: number) => { - e.stopPropagation(); - if (e.target.checked) { - setSelectedEmails([...selectedEmails, emailId.toString()]); - } else { - setSelectedEmails(selectedEmails.filter(id => id !== emailId.toString())); - } - }; - - // Handles marking an individual email as read/unread - const handleMarkAsRead = (emailId: string, isRead: boolean) => { - setEmails(emails.map(email => - email.id.toString() === emailId ? { ...email, read: isRead } : email - )); - }; - - // Handles bulk actions for selected emails - const handleBulkAction = async (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => { - if (action === 'delete') { - setDeleteType('emails'); - setShowDeleteConfirm(true); - return; - } - - try { - const response = await fetch('/api/mail/bulk-actions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - emailIds: selectedEmails, - action: action - }), - }); - - if (!response.ok) { - throw new Error('Failed to perform bulk action'); - } - - // Update local state based on the action - setEmails(emails.map(email => { - if (selectedEmails.includes(email.id.toString())) { - switch (action) { - case 'mark-read': - return { ...email, read: true }; - case 'mark-unread': - return { ...email, read: false }; - case 'archive': - return { ...email, folder: 'Archive' }; - default: - return email; - } - } - return email; - })); - - // Clear selection after successful action - setSelectedEmails([]); - } catch (error) { - console.error('Error performing bulk action:', error); - alert('Failed to perform bulk action. Please try again.'); - } - }; - - // Add handleDeleteConfirm function - const handleDeleteConfirm = async () => { - try { - const response = await fetch('/api/mail/bulk-actions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - emailIds: selectedEmails, - action: 'delete' - }), - }); - - if (!response.ok) { - throw new Error('Failed to delete emails'); - } - - // Remove deleted emails from state - setEmails(emails.filter(email => !selectedEmails.includes(email.id.toString()))); - setSelectedEmails([]); - } catch (error) { - console.error('Error deleting emails:', error); - alert('Failed to delete emails. Please try again.'); - } finally { - setShowDeleteConfirm(false); - } - }; - - // Add infinite scroll handler - const handleScroll = useCallback((e: React.UIEvent) => { - const target = e.currentTarget; - if ( - target.scrollHeight - target.scrollTop === target.clientHeight && - !isLoadingMore && - hasMore - ) { - setPage(prev => prev + 1); - loadEmails(true); - } - }, [isLoadingMore, hasMore]); - - // Sort emails by date (most recent first) - const sortedEmails = useMemo(() => { - return [...emails].sort((a, b) => { - return new Date(b.date).getTime() - new Date(a.date).getTime(); - }); - }, [emails]); - - const toggleSelectAll = () => { - if (selectedEmails.length === emails.length) { - setSelectedEmails([]); - } else { - setSelectedEmails(emails.map(email => email.id.toString())); - } - }; - - // Add filtered emails based on search query - const filteredEmails = useMemo(() => { - if (!searchQuery) return emails; - - const query = searchQuery.toLowerCase(); - return emails.filter(email => - email.subject.toLowerCase().includes(query) || - email.from.toLowerCase().includes(query) || - email.to.toLowerCase().includes(query) || - email.body.toLowerCase().includes(query) - ); - }, [emails, searchQuery]); - - // Update the email list to use filtered emails - const renderEmailList = () => ( -
- {renderEmailListHeader()} - {renderBulkActionsToolbar()} - -
- {loading ? ( -
-
-
- ) : filteredEmails.length === 0 ? ( -
- -

- {searchQuery ? 'No emails match your search' : 'No emails in this folder'} -

-
- ) : ( -
- {filteredEmails.map((email) => renderEmailListItem(email))} - {isLoadingMore && ( -
-
-
- )} -
- )} -
-
- ); - - // Update the email count in the header to show filtered count - const renderEmailListHeader = () => ( -
-
-
- - setSearchQuery(e.target.value)} - /> -
-
-
-
- 0 && selectedEmails.length === filteredEmails.length} - onCheckedChange={toggleSelectAll} - className="mt-0.5" - /> -

Inbox

-
- - {searchQuery ? `${filteredEmails.length} of ${emails.length} emails` : `${emails.length} emails`} - -
-
- ); - - // Update the bulk actions toolbar to include confirmation dialog - const renderBulkActionsToolbar = () => { - if (selectedEmails.length === 0) return null; - - return ( -
-
- - {selectedEmails.length} selected - -
-
- - - -
-
- ); - }; - - // Keep only one renderEmailListWrapper function that includes both panels - const renderEmailListWrapper = () => ( -
- {/* Email list panel */} - {renderEmailList()} - - {/* Preview panel - will automatically take remaining space */} -
- {selectedEmail ? ( - <> - {/* Email actions header */} -
-
-
- -
-

- {selectedEmail.subject} -

-
-
-
-
- - - - - -
-
-
-
- - {/* Scrollable content area */} - -
- - - {selectedEmail.fromName?.charAt(0) || selectedEmail.from.charAt(0)} - - -
-

- {selectedEmail.fromName || selectedEmail.from} -

-

- to {selectedEmail.to} -

-
-
- {formatDate(selectedEmail.date)} -
-
- -
- {(() => { - try { - const parsed = parseFullEmail(selectedEmail.body); - return ( -
- {/* Display HTML content if available, otherwise fallback to text */} -
- - {/* Display attachments if present */} - {parsed.attachments && parsed.attachments.length > 0 && ( -
-

Attachments

-
- {parsed.attachments.map((attachment, index) => ( -
- - - {attachment.filename} - -
- ))} -
-
- )} -
- ); - } catch (e) { - console.error('Error parsing email:', e); - return selectedEmail.body; - } - })()} -
- - - ) : ( -
- -

Select an email to view its contents

-
- )} -
-
- ); - - // Update sidebar items when available folders change - useEffect(() => { - if (availableFolders.length > 0) { - const newItems = [ - ...initialSidebarItems, - ...availableFolders - .filter(folder => !['INBOX'].includes(folder)) // Exclude folders already in initial items - .map(folder => ({ - view: folder as MailFolder, - label: folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase(), - icon: getFolderIcon(folder), - folder: folder - })) - ]; - setSidebarItems(newItems); - } - }, [availableFolders]); - - // Update the email list item to match header checkbox alignment - const renderEmailListItem = (email: Email) => ( -
handleEmailSelect(email.id)} - > - { - const e = { target: { checked }, stopPropagation: () => {} } as React.ChangeEvent; - handleEmailCheckbox(e, email.id); - }} - onClick={(e) => e.stopPropagation()} - className="mt-0.5" - /> -
-
-
- - {currentView === 'Sent' ? email.to : ( - (() => { - const fromMatch = email.from.match(/^([^<]+)\s*<([^>]+)>$/); - return fromMatch ? fromMatch[1].trim() : email.from; - })() - )} - -
-
- - {formatDate(email.date)} - - -
-
-

- {email.subject || '(No subject)'} -

-
- {(() => { - // Get clean preview of the actual message content - let preview = ''; - try { - const parsed = parseFullEmail(email.body); - - // Try to get content from parsed email - preview = (parsed.text || parsed.html || '') - .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/<[^>]+>/g, '') - .replace(/ |‌|»|«|>/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - - // If no preview from parsed content, try direct body - if (!preview) { - preview = email.body - .replace(/<[^>]+>/g, '') - .replace(/ |‌|»|«|>/g, ' ') - .replace(/\s+/g, ' ') - .trim(); - } - - // Remove email artifacts and clean up - preview = preview - .replace(/^>+/gm, '') - .replace(/Content-Type:[^\n]+/g, '') - .replace(/Content-Transfer-Encoding:[^\n]+/g, '') - .replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '') - .replace(/boundary=[^\n]+/g, '') - .replace(/charset=[^\n]+/g, '') - .replace(/[\r\n]+/g, ' ') - .trim(); - - // Take first 100 characters - preview = preview.substring(0, 100); - - // Try to end at a complete word - if (preview.length === 100) { - const lastSpace = preview.lastIndexOf(' '); - if (lastSpace > 80) { - preview = preview.substring(0, lastSpace); - } - preview += '...'; - } - - } catch (e) { - console.error('Error generating preview:', e); - preview = ''; - } - - return preview || 'No preview available'; - })()} -
-
-
- ); - - // Render the sidebar navigation - const renderSidebarNav = () => ( - - ); - - // Add attachment handling functions - const handleFileAttachment = async (e: React.ChangeEvent) => { - if (!e.target.files) return; - - const newAttachments: Attachment[] = []; - const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB in bytes - const oversizedFiles: string[] = []; - - for (const file of e.target.files) { - if (file.size > MAX_FILE_SIZE) { - oversizedFiles.push(file.name); - continue; - } - - try { - // Read file as base64 - const base64Content = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => { - const base64 = reader.result as string; - resolve(base64.split(',')[1]); // Remove data URL prefix - }; - reader.readAsDataURL(file); - }); - - newAttachments.push({ - name: file.name, - type: file.type, - content: base64Content, - encoding: 'base64' - }); - } catch (error) { - console.error('Error processing attachment:', error); - } - } - - if (oversizedFiles.length > 0) { - alert(`The following files exceed the 10MB size limit and were not attached:\n${oversizedFiles.join('\n')}`); - } - - if (newAttachments.length > 0) { - setAttachments([...attachments, ...newAttachments]); - } - }; - - // Add handleSend function for email composition - const handleSend = async () => { - if (!composeTo) { - alert('Please specify at least one recipient'); - return; - } - - try { - const response = await fetch('/api/mail/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - to: composeTo, - cc: composeCc, - bcc: composeBcc, - subject: composeSubject, - body: composeBody, - attachments: attachments, - }), - }); - - const data = await response.json(); - - if (!response.ok) { - if (data.error === 'Attachment size limit exceeded') { - alert(`Error: ${data.error}\nThe following files are too large:\n${data.details.oversizedFiles.join('\n')}`); - } else { - alert(`Error sending email: ${data.error}`); - } - return; - } - - // Clear compose form and close modal - setComposeTo(''); - setComposeCc(''); - setComposeBcc(''); - setComposeSubject(''); - setComposeBody(''); - setAttachments([]); - setShowCompose(false); - } catch (error) { - console.error('Error sending email:', error); - alert('Failed to send email. Please try again.'); - } - }; - - // Add toggleStarred function - const toggleStarred = async (emailId: number, e?: React.MouseEvent) => { - if (e) { - e.stopPropagation(); - } - - const email = emails.find(e => e.id === emailId); - if (!email) return; - - try { - const response = await fetch('/api/mail/toggle-star', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ emailId, starred: !email.starred }), - }); - - if (!response.ok) { - throw new Error('Failed to toggle star'); - } - - // Update email in state - setEmails(emails.map(e => - e.id === emailId ? { ...e, starred: !e.starred } : e - )); - } catch (error) { - console.error('Error toggling star:', error); - } - }; - - // Add handleReply function - const handleReply = (type: 'reply' | 'replyAll' | 'forward') => { - if (!selectedEmail) return; - - const getReplySubject = () => { - const subject = selectedEmail.subject; - if (type === 'forward') { - return subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`; - } - return subject.startsWith('Re:') ? subject : `Re: ${subject}`; - }; - - const getReplyTo = () => { - switch (type) { - case 'reply': - return selectedEmail.from; - case 'replyAll': - // For Reply All, only put the original sender in To - return selectedEmail.from; - case 'forward': - return ''; - } - }; - - const getReplyCc = () => { - if (type === 'replyAll') { - // For Reply All, put all other recipients in CC, excluding the sender and current user - const allRecipients = new Set([ - ...(selectedEmail.to?.split(',') || []), - ...(selectedEmail.cc?.split(',') || []) - ]); - // Remove the sender and current user from CC - allRecipients.delete(selectedEmail.from); - allRecipients.delete(accounts[1]?.email); - return Array.from(allRecipients) - .map(email => email.trim()) - .filter(email => email) // Remove empty strings - .join(', '); - } - return ''; - }; - - const getReplyBody = () => { - try { - const parsed = parseFullEmail(selectedEmail.body); - let originalContent = ''; - - // Get the content from either HTML or text part - if (parsed.html) { - // Convert HTML to plain text for the reply - originalContent = parsed.html - .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(//gi, '\n') - .replace(/]*>/gi, '\n') - .replace(/<\/div>/gi, '') - .replace(/]*>/gi, '\n') - .replace(/<\/p>/gi, '') - .replace(/<[^>]+>/g, '') - .replace(/ |‌|»|«|>/g, match => { - switch (match) { - case ' ': return ' '; - case '‌': return ''; - case '»': return '»'; - case '«': return '«'; - case '>': return '>'; - case '<': return '<'; - case '&': return '&'; - default: return match; - } - }) - .replace(/^\s+$/gm, '') - .replace(/\n{3,}/g, '\n\n') - .trim(); - } else if (parsed.text) { - originalContent = parsed.text.trim(); - } else { - // Fallback to raw body if parsing fails - originalContent = selectedEmail.body - .replace(/<[^>]+>/g, '') - .trim(); - } - - // Clean up the content - originalContent = originalContent - .split('\n') - .map(line => line.trim()) - .filter(line => { - // Remove email client signatures and headers - return !line.match(/^(From|To|Sent|Subject|Date|Cc|Bcc):/i) && - !line.match(/^-{2,}/) && - !line.match(/^_{2,}/) && - !line.match(/^={2,}/) && - !line.match(/^This (email|message) has been/i) && - !line.match(/^Disclaimer/i) && - !line.match(/^[*_-]{3,}/) && - !line.match(/^Envoyé depuis/i) && - !line.match(/^Envoyé à partir de/i) && - !line.match(/^Sent from/i) && - !line.match(/^Outlook pour/i) && - !line.match(/^De :/i) && - !line.match(/^À :/i) && - !line.match(/^Objet :/i); - }) - .join('\n') - .trim(); - - // Format the reply - const date = new Date(selectedEmail.date); - const formattedDate = date.toLocaleString('en-GB', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: false - }); - - let replyHeader = ''; - if (type === 'forward') { - replyHeader = `\n\n---------- Forwarded message ----------\n`; - replyHeader += `From: ${selectedEmail.from}\n`; - replyHeader += `Date: ${formattedDate}\n`; - replyHeader += `Subject: ${selectedEmail.subject}\n`; - replyHeader += `To: ${selectedEmail.to}\n`; - if (selectedEmail.cc) { - replyHeader += `Cc: ${selectedEmail.cc}\n`; - } - replyHeader += `\n`; - } else { - // Simple header for reply and reply all - replyHeader = `\n\nOn ${formattedDate}, ${selectedEmail.from} wrote:\n`; - } - - // Indent the original content - const indentedContent = originalContent - .split('\n') - .map(line => line ? `> ${line}` : '>') // Keep empty lines as '>' for better readability - .join('\n'); - - return `${replyHeader}${indentedContent}`; - } catch (error) { - console.error('Error formatting reply:', error); - return `\n\nOn ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote:\n> ${selectedEmail.body}`; - } - }; - - // Open compose modal with reply details - setShowCompose(true); - setComposeTo(getReplyTo()); - setComposeSubject(getReplySubject()); - setComposeBody(getReplyBody()); - setComposeCc(getReplyCc()); - setComposeBcc(''); - // Show CC field automatically for Reply All - setShowCc(type === 'replyAll'); - setShowBcc(false); - setAttachments([]); - }; - - // Add the confirmation dialog component - const renderDeleteConfirmDialog = () => ( - - - - Delete Emails - - Are you sure you want to delete {selectedEmails.length} selected email{selectedEmails.length > 1 ? 's' : ''}? This action cannot be undone. - - - - Cancel - Delete - - - - ); - - const handleMailboxChange = async (newMailbox: string) => { - setCurrentView(newMailbox); - setSelectedEmails([]); - setSearchQuery(''); - setEmails([]); - setLoading(true); - setError(null); - setHasMore(true); - setPage(1); - - try { - // Optimize the request by adding a timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout - - const response = await fetch(`/api/mail?folder=${encodeURIComponent(newMailbox)}&page=1&limit=${emailsPerPage}`, { - signal: controller.signal - }); - - clearTimeout(timeoutId); - - if (!response.ok) { - throw new Error('Failed to fetch emails'); - } - - const data = await response.json(); - - // Process emails more efficiently - const processedEmails = data.emails.map((email: any) => ({ - id: Number(email.id), - accountId: 1, - from: email.from || '', - fromName: email.from?.split('@')[0] || '', - to: email.to || '', - subject: email.subject || '(No subject)', - body: email.body || '', - date: email.date || new Date().toISOString(), - read: email.read || false, - starred: email.starred || false, - folder: email.folder || newMailbox, - cc: email.cc, - bcc: email.bcc, - flags: email.flags || [] - })); - - setEmails(processedEmails); - setHasMore(processedEmails.length === emailsPerPage); - - // Only update unread count if we're in the Inbox folder - if (newMailbox === 'INBOX') { - const unreadInboxEmails = processedEmails.filter( - (email: Email) => !email.read && email.folder === 'INBOX' - ).length; - setUnreadCount(unreadInboxEmails); - } - } catch (err) { - if (err instanceof Error) { - if (err.name === 'AbortError') { - setError('Request timed out. Please try again.'); - } else { - setError(err.message); - } - } else { - setError('Failed to fetch emails'); - } - } finally { - setLoading(false); - } - }; + if (isLoading) { + return
Loading...
; + } if (error) { - return ( -
-
- -

{error}

- -
-
- ); + return
Error: {error}
; } return ( - <> - {/* Main layout */} -
- {/* Sidebar */} -
- {/* Courrier Title */} -
-
- - COURRIER -
-
- - {/* Compose button and refresh button */} -
- - -
- - {/* Accounts Section */} -
- - - {accountsDropdownOpen && ( -
- {accounts.map(account => ( -
- -
- ))} -
- )} -
- - {/* Navigation */} - {renderSidebarNav()} -
- - {/* Main content area */} -
- {/* Email list panel */} - {renderEmailListWrapper()} -
-
- - {/* Compose Email Modal */} - {showCompose && ( -
-
- {/* Modal Header */} -
-

- {composeSubject.startsWith('Re:') ? 'Reply' : - composeSubject.startsWith('Fwd:') ? 'Forward' : 'New Message'} -

- -
- - {/* Modal Body */} -
-
- {/* To Field */} -
- - setComposeTo(e.target.value)} - placeholder="recipient@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- - {/* CC/BCC Toggle Buttons */} -
- - -
- - {/* CC Field */} - {showCc && ( -
- - setComposeCc(e.target.value)} - placeholder="cc@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- )} - - {/* BCC Field */} - {showBcc && ( -
- - setComposeBcc(e.target.value)} - placeholder="bcc@example.com" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- )} - - {/* Subject Field */} -
- - setComposeSubject(e.target.value)} - placeholder="Enter subject" - className="w-full mt-1 bg-white border-gray-300 text-gray-900" - /> -
- - {/* Message Body */} -
- -