diff --git a/app/api/mail/route.ts b/app/api/mail/route.ts index 074ceb3..714e8bf 100644 --- a/app/api/mail/route.ts +++ b/app/api/mail/route.ts @@ -113,7 +113,6 @@ export async function GET() { }); return new Promise((resolve) => { - // Create a map to store emails by folder const emailsByFolder: { [key: string]: any[] } = {}; imap.once('ready', () => { @@ -121,21 +120,19 @@ export async function GET() { if (err) { console.error('Error getting mailboxes:', err); imap.end(); - resolve(NextResponse.json({ emails: [], error: 'Failed to get mailboxes' })); + resolve(NextResponse.json({ emails: {}, error: 'Failed to get mailboxes' })); return; } const availableMailboxes = Object.keys(boxes); console.log('Available mailboxes:', availableMailboxes); - // Process only the mailboxes we want to use + // Only process these specific folders const foldersToCheck = ['INBOX', 'Sent', 'Trash', 'Spam', 'Drafts', 'Archives', 'Archive']; let foldersProcessed = 0; const processFolder = (folderName: string) => { console.log(`Processing folder: ${folderName}`); - - // Initialize array for this folder emailsByFolder[folderName] = []; imap.openBox(folderName, false, (err, box) => { @@ -158,10 +155,7 @@ export async function GET() { return; } - // Search for messages in this folder - const searchCriteria = ['ALL']; - - imap.search(searchCriteria, (err, results) => { + imap.search(['ALL'], (err, results) => { if (err) { console.error(`Search error in ${folderName}:`, err); foldersProcessed++; @@ -226,7 +220,6 @@ export async function GET() { }); msg.once('end', () => { - // Add email to its folder's array emailsByFolder[folderName].push(email); }); }); @@ -246,22 +239,17 @@ export async function GET() { }); }; - // Process each folder sequentially foldersToCheck.forEach(folder => processFolder(folder)); }); }); function finishProcessing() { - // Combine all emails from all folders - const allEmails = Object.entries(emailsByFolder).flatMap(([folder, emails]) => emails); - console.log('Emails by folder:', Object.fromEntries( Object.entries(emailsByFolder).map(([folder, emails]) => [folder, emails.length]) )); - console.log('All folders processed, total emails:', allEmails.length); const response = { - emails: allEmails, + emailsByFolder: emailsByFolder, mailUrl: process.env.NEXTCLOUD_URL ? `${process.env.NEXTCLOUD_URL}/apps/mail/` : null }; imap.end(); @@ -271,7 +259,7 @@ export async function GET() { imap.once('error', (err) => { console.error('IMAP error:', err); resolve(NextResponse.json({ - emails: [], + emailsByFolder: {}, error: 'IMAP connection error' })); }); @@ -281,7 +269,7 @@ export async function GET() { } catch (error) { console.error('Error in mail API:', error); return NextResponse.json({ - emails: [], + emailsByFolder: {}, error: error instanceof Error ? error.message : 'Unknown error' }); } diff --git a/app/mail/page.tsx b/app/mail/page.tsx index a62bdb2..0bae4ee 100644 --- a/app/mail/page.tsx +++ b/app/mail/page.tsx @@ -488,7 +488,7 @@ export default function MailPage() { { id: 1, name: 'Mail', email: 'alma@governance-labs.org', color: 'bg-blue-500' } ]); const [selectedAccount, setSelectedAccount] = useState(null); - const [currentView, setCurrentView] = useState('INBOX'); + const [currentView, setCurrentView] = useState('INBOX'); const [showCompose, setShowCompose] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [selectedEmails, setSelectedEmails] = useState([]); @@ -519,97 +519,28 @@ export default function MailPage() { const [attachments, setAttachments] = useState([]); const [folders, setFolders] = useState([]); const [unreadCount, setUnreadCount] = useState(0); + const [emailsByFolder, setEmailsByFolder] = useState>({}); - // 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]); - - // Update the filteredEmails logic - const filteredEmails = useMemo(() => { - if (currentView === 'starred') { - return emails.filter(email => email.starred); - } - - // For all other views, match exactly with the IMAP folder name - return emails.filter(email => email.folder === currentView); - }, [emails, currentView]); - - // Move getSelectedEmail inside the component - const getSelectedEmail = () => { - return emails.find(email => email.id === selectedEmail?.id); - }; - - // Check for stored credentials - useEffect(() => { - const checkCredentials = async () => { - try { - console.log('Checking for stored credentials...'); - const response = await fetch('/api/mail'); - 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'); - } - console.log('Credentials verified, loading emails...'); - setLoading(false); - loadEmails(); - } catch (err) { - console.error('Error checking credentials:', err); - setError(err instanceof Error ? err.message : 'Failed to check credentials'); - setLoading(false); - } - }; - - checkCredentials(); - }, [router]); - - // Update the loadEmails function + // Load emails const loadEmails = async () => { try { setLoading(true); - setError(null); - const response = await fetch('/api/mail'); if (!response.ok) { throw new Error('Failed to load emails'); } const data = await response.json(); - console.log('Raw email data:', data); + console.log('Received emails by folder:', Object.keys(data.emailsByFolder)); + + setEmailsByFolder(data.emailsByFolder); - // Process emails keeping exact folder names - const processedEmails = data.emails.map((email: any) => ({ - ...email, - id: Number(email.id), - from: email.from || '', - 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 // Keep exact IMAP folder name - })); - // Update unread count for INBOX - const unreadInboxEmails = processedEmails.filter( - email => !email.read && email.folder === 'INBOX' + const unreadInboxEmails = (data.emailsByFolder['INBOX'] || []).filter( + email => !email.read ).length; setUnreadCount(unreadInboxEmails); - setEmails(processedEmails); } catch (err) { console.error('Error loading emails:', err); setError(err instanceof Error ? err.message : 'Failed to load emails'); @@ -618,956 +549,84 @@ export default function MailPage() { } }; - // Add an effect to reload emails when the view changes - useEffect(() => { - 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 current folder's emails + const currentEmails = useMemo(() => { + if (currentView === 'starred') { + // Only for starred view, we need to combine and filter + return Object.values(emailsByFolder) + .flat() + .filter(email => email.starred); } - }; + // For all other views, just return the emails from that folder + return emailsByFolder[currentView] || []; + }, [currentView, emailsByFolder]); - // 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); - }); - } - } - }; - - 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())); - } - }; - - // Update the toggleStarred function - const toggleStarred = async (emailId: number, e: React.MouseEvent) => { - e.stopPropagation(); - - // Update the email in state - setEmails(prevEmails => - prevEmails.map(email => - email.id === emailId - ? { ...email, starred: !email.starred } - : email - ) - ); - - try { - const response = await fetch('/api/mail/toggle-star', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ emailId }) - }); - - if (!response.ok) { - throw new Error('Failed to toggle star'); - } - } catch (error) { - console.error('Error toggling star:', error); - // Revert the change if the server request fails - setEmails(prevEmails => - prevEmails.map(email => - email.id === emailId - ? { ...email, starred: !email.starred } - : email - ) - ); - } - }; - - // Handle reply with MIME encoding - const handleReply = async (type: 'reply' | 'replyAll' | 'forward') => { - const selectedEmailData = getSelectedEmail(); - if (!selectedEmailData) return; - - setShowCompose(true); - const subject = `${type === 'forward' ? 'Fwd: ' : 'Re: '}${selectedEmailData.subject}`; - let to = ''; - let cc = ''; - let content = ''; - - // Parse the original email content using MIME decoder - const parsedEmail = parseFullEmail(selectedEmailData.body); - const decodedBody = parsedEmail?.text || parsedEmail?.html || selectedEmailData.body; - - // Format the date properly - const emailDate = new Date(selectedEmailData.date).toLocaleString('en-US', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - hour12: true - }); - - switch (type) { - case 'reply': - to = selectedEmailData.from; - content = `\n\nOn ${emailDate}, ${selectedEmailData.fromName} wrote:\n> ${decodedBody.split('\n').join('\n> ')}`; - break; - - case 'replyAll': - to = selectedEmailData.from; - - // Get our email address from the selected account - const ourEmail = accounts.find(acc => acc.id === selectedEmailData.accountId)?.email || ''; - - // Handle CC addresses - const ccList = new Set(); - - // Add original TO recipients (except ourselves and the person we're replying to) - selectedEmailData.to.split(',') - .map(addr => addr.trim()) - .filter(addr => addr !== ourEmail && addr !== selectedEmailData.from) - .forEach(addr => ccList.add(addr)); - - // Add original CC recipients (if any) - if (selectedEmailData.cc) { - selectedEmailData.cc.split(',') - .map(addr => addr.trim()) - .filter(addr => addr !== ourEmail && addr !== selectedEmailData.from) - .forEach(addr => ccList.add(addr)); - } - - // Convert Set to string - cc = Array.from(ccList).join(', '); - - // If we have CC recipients, show the CC field - if (cc) { - setShowCc(true); - } - - content = `\n\nOn ${emailDate}, ${selectedEmailData.fromName} wrote:\n> ${decodedBody.split('\n').join('\n> ')}`; - break; - - case 'forward': - // Safely handle attachments - const attachments = parsedEmail?.attachments || []; - const attachmentInfo = attachments.length > 0 - ? '\n\n-------- Attachments --------\n' + - attachments.map(att => att.filename).join('\n') - : ''; - - content = `\n\n---------- Forwarded message ----------\n` + - `From: ${selectedEmailData.fromName} <${selectedEmailData.from}>\n` + - `Date: ${emailDate}\n` + - `Subject: ${selectedEmailData.subject}\n` + - `To: ${selectedEmailData.to}\n` + - (selectedEmailData.cc ? `Cc: ${selectedEmailData.cc}\n` : '') + - `\n\n${decodedBody}${attachmentInfo}`; - - // Handle attachments if present - if (attachments.length > 0) { - handleForwardedAttachments(attachments); - } - break; - } - - // Set the form state - setComposeSubject(subject); - setComposeTo(to); - setComposeCc(cc); - setComposeBody(content); - }; - - // Handle forwarded attachments - const handleForwardedAttachments = (attachments: Array<{ - filename: string; - contentType: string; - encoding: string; - content: string; - }>) => { - try { - // Store attachment information in state - setAttachments(attachments.map(att => ({ - name: att.filename, - type: att.contentType, - content: att.content, - encoding: att.encoding - }))); - } catch (error) { - console.error('Error handling attachments:', error); - } - }; - - // Modified send function to handle MIME encoding - const handleSend = async () => { - try { - // Create multipart message - const boundary = `----=_Part_${Date.now()}`; - let mimeContent = [ - `Content-Type: multipart/mixed; boundary="${boundary}"`, - '', - `--${boundary}`, - 'Content-Type: text/plain; charset=utf-8', - 'Content-Transfer-Encoding: 7bit', - '', - composeBody, - '' - ]; - - // Add attachments if any - if (attachments && attachments.length > 0) { - for (const attachment of attachments) { - mimeContent = mimeContent.concat([ - `--${boundary}`, - `Content-Type: ${attachment.type}`, - `Content-Transfer-Encoding: ${attachment.encoding}`, - `Content-Disposition: attachment; filename="${attachment.name}"`, - '', - attachment.content, - '' - ]); - } - } - - // Close the multipart message - mimeContent.push(`--${boundary}--`); - - // Send the email with MIME content - await fetch('/api/mail/send', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - to: composeTo, - cc: composeCc, - bcc: composeBcc, - subject: composeSubject, - body: mimeContent.join('\r\n'), - attachments: attachments - }) - }); - - // Clear the form - setShowCompose(false); - setComposeTo(''); - setComposeCc(''); - setComposeBcc(''); - setComposeSubject(''); - setComposeBody(''); - setShowCc(false); - setShowBcc(false); - setAttachments([]); - - } catch (error) { - console.error('Error sending email:', error); - } - }; - - const handleBulkDelete = () => { - setDeleteType('emails'); - setShowDeleteConfirm(true); - }; - - const handleDeleteConfirm = async () => { - try { - if (selectedEmails.length > 0) { - await fetch('/api/mail/delete', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ emailIds: selectedEmails }) - }); - setEmails(emails.filter(email => !selectedEmails.includes(email.id.toString()))); - setSelectedEmails([]); - setShowBulkActions(false); - } - } catch (error) { - console.error('Error deleting emails:', error); - } - setShowDeleteConfirm(false); - }; - - const handleAccountAction = (accountId: number, action: 'edit' | 'delete') => { - setShowAccountActions(null); - if (action === 'delete') { - setDeleteType('account'); - setItemToDelete(accountId); - setShowDeleteConfirm(true); - } - }; - - const toggleSelectAll = () => { - if (selectedEmails.length === filteredEmails.length) { - setSelectedEmails([]); - setShowBulkActions(false); - } else { - const allEmailIds = filteredEmails.map(email => email.id.toString()); - setSelectedEmails(allEmailIds); - setShowBulkActions(true); - } - }; - - const handleFileAttachment = (e: React.ChangeEvent) => { - if (e.target.files) { - setAttachments(Array.from(e.target.files).map(file => ({ - name: file.name, - type: file.type, - content: URL.createObjectURL(file), - encoding: 'base64' - }))); - } - }; - - // Add debug logging to help track the filtering - useEffect(() => { - console.log('Current view:', currentView); - console.log('Total emails:', emails.length); - console.log('Filtered emails:', filteredEmails.length); - console.log('Filter criteria:', { - starred: filteredEmails.filter(e => e.starred).length, - sent: filteredEmails.filter(e => e.category?.includes('sent')).length, - deleted: filteredEmails.filter(e => e.deleted).length - }); - }, [currentView, emails, filteredEmails]); - - // Add a function to move to trash - const moveToTrash = async (emailId: number) => { - // Update the email in state - setEmails(prevEmails => - prevEmails.map(email => - email.id === emailId - ? { ...email, deleted: true, category: 'trash' } - : email - ) - ); - - try { - const response = await fetch('/api/mail/move-to-trash', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ emailId }) - }); - - if (!response.ok) { - throw new Error('Failed to move to trash'); - } - } catch (error) { - console.error('Error moving to trash:', error); - // Revert the change if the server request fails - setEmails(prevEmails => - prevEmails.map(email => - email.id === emailId - ? { ...email, deleted: false, category: 'inbox' } - : email - ) - ); - } - }; - - // Add this debug component to help us see what's happening - const DebugInfo = () => { - if (process.env.NODE_ENV !== 'development') return null; - - return ( -
-
Current view: {currentView}
-
Total emails: {emails.length}
-
Filtered emails: {filteredEmails.length}
-
Categories present: { - [...new Set(emails.map(e => e.category))].join(', ') - }
-
Flags present: { - [...new Set(emails.flatMap(e => e.flags || []))].join(', ') - }
-
- ); - }; - - // Render the sidebar navigation - const renderSidebarNav = () => ( - - ); - - // Add debug logging for the email list - useEffect(() => { - console.log('Current view:', currentView); - console.log('Filtered emails:', filteredEmails.map(email => ({ - id: email.id, - subject: email.subject, - folder: email.folder, - from: email.from - }))); - }, [currentView, filteredEmails]); - - // Email list panel section + // Render email list const renderEmailList = () => ( -
- {/* Email list header */} -
-
-
-

- {currentView === 'INBOX' ? 'Inbox' : - currentView === 'starred' ? 'Starred' : - currentView.charAt(0).toUpperCase() + currentView.slice(1)} -

-
-
- {filteredEmails.length} emails -
-
-
- - {/* Email list */} -
- {loading ? ( -
-
-
- ) : filteredEmails.length === 0 ? ( -
- -

- {currentView === 'INBOX' && 'No emails in inbox'} - {currentView === 'starred' && 'No starred emails'} - {currentView === 'Sent' && 'No sent emails'} - {currentView === 'Trash' && 'No emails in trash'} - {currentView === 'Drafts' && 'No drafts'} - {currentView === 'Spam' && 'No spam emails'} - {currentView === 'Archives' && 'No archived emails'} -

- {/* Debug info */} -
-

Current view: {currentView}

-

Total emails: {emails.length}

-

Folders present: {[...new Set(emails.map(e => e.folder))].join(', ')}

+
+ {currentEmails.map((email) => ( +
handleEmailSelect(email)} + > +
+
+ e.stopPropagation()} + onCheckedChange={(checked) => { + if (checked) { + setSelectedEmails([...selectedEmails, email.id.toString()]); + } else { + setSelectedEmails(selectedEmails.filter(id => id !== email.id.toString())); + } + }} + /> + + {currentView === 'Sent' ? email.to : (email.fromName || email.from)} +
-
- ) : ( -
- {filteredEmails.map((email) => ( -
handleEmailSelect(email.id)} +
+ + {formatDate(email.date)} + + {/* Show folder badge if it doesn't match current view */} + {email.folder !== currentView && currentView === 'starred' && ( + + {email.folder} + + )} + -
-
-
-

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

-

- {email.body.replace(/<[^>]*>/g, '').substring(0, 100)}... -

-
- {/* Debug info - only in development */} - {process.env.NODE_ENV === 'development' && ( -
- Folder: {email.folder} -
- )} -
- ))} -
- )} -
-
- ); - - if (error) { - return ( -
-
- -

{error}

- -
-
- ); - } - - return ( - <> - {/* Main layout */} -
- {/* Sidebar */} -
- {/* Courrier Title */} -
-
- - COURRIER -
-
- - {/* Compose button */} -
-
- -
- - {/* Accounts Section */} -
- - - {accountsDropdownOpen && ( -
- {accounts.map(account => ( -
- -
- ))} -
- )}
- - {/* Navigation */} - {renderSidebarNav()} -
- - {/* Main content area */} -
- {/* 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

+
+

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

+

+ {email.body.replace(/<[^>]*>/g, '').substring(0, 100)}... +

+
+ {/* Debug info - only in development */} + {process.env.NODE_ENV === 'development' && ( +
+ Folder: {email.folder}
)}
-
+ ))}
- - {/* 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 */} -
- -