diff --git a/app/api/mail/route.ts b/app/api/mail/route.ts index 0d48998..85ae0cf 100644 --- a/app/api/mail/route.ts +++ b/app/api/mail/route.ts @@ -101,10 +101,12 @@ export async function GET(request: Request) { ); } - // Get the current folder from the URL + // Get pagination parameters from URL const url = new URL(request.url); const folder = url.searchParams.get('folder') || 'INBOX'; - const limit = 50; // Limit number of emails per folder + const page = parseInt(url.searchParams.get('page') || '1'); + const limit = parseInt(url.searchParams.get('limit') || '24'); + const offset = (page - 1) * limit; return new Promise((resolve) => { const imap = new Imap({ @@ -159,37 +161,45 @@ export async function GET(request: Request) { return; } - // Search for emails in this folder, limited to the most recent ones - imap.search(['ALL'], (err, results) => { - if (err) { - console.error(`Error searching in ${folder}:`, err); - clearTimeout(timeout); - imap.end(); - resolve(NextResponse.json({ emails: [], error: `Failed to search in ${folder}` })); - return; - } + // Get the specified folder + const totalMessages = box.messages.total; + + // Calculate the range of messages to fetch + const start = Math.max(1, totalMessages - offset - limit + 1); + const end = totalMessages - offset; + + if (start > end) { + clearTimeout(timeout); + imap.end(); + resolve(NextResponse.json({ + emails: [], + folders: availableMailboxes, + mailUrl: process.env.NEXT_PUBLIC_IFRAME_MAIL_URL + })); + return; + } - if (!results || results.length === 0) { + // Fetch messages in the calculated range + imap.searchAsync(['ALL'], { + bodies: ['HEADER', 'TEXT'], + struct: true, + byUid: true, + start, + end + }).then((messages) => { + if (!messages || messages.length === 0) { clearTimeout(timeout); imap.end(); resolve(NextResponse.json({ emails: [], folders: availableMailboxes, - mailUrl: process.env.NEXTCLOUD_URL ? `${process.env.NEXTCLOUD_URL}/apps/mail/` : null + mailUrl: process.env.NEXT_PUBLIC_IFRAME_MAIL_URL })); return; } - // Take only the most recent emails up to the limit - const recentResults = results.slice(-limit); - const emails: any[] = []; - - const fetch = imap.fetch(recentResults, { - bodies: ['HEADER', 'TEXT'], - struct: true - }); - - fetch.on('message', (msg) => { + // Process messages + const emails = messages.map((msg) => { let header = ''; let text = ''; let messageId: number | null = null; @@ -231,26 +241,25 @@ export async function GET(request: Request) { folder: folder, flags: messageFlags }; - emails.push(email); + return email; }); }); - fetch.on('error', (err) => { - console.error(`Error fetching emails from ${folder}:`, err); - clearTimeout(timeout); - imap.end(); - resolve(NextResponse.json({ emails: [], error: `Failed to fetch emails from ${folder}` })); - }); + // Sort emails by date (most recent first) + emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - fetch.on('end', () => { - clearTimeout(timeout); - imap.end(); - resolve(NextResponse.json({ - emails: emails, - folders: availableMailboxes, - mailUrl: process.env.NEXTCLOUD_URL ? `${process.env.NEXTCLOUD_URL}/apps/mail/` : null - })); - }); + clearTimeout(timeout); + imap.end(); + resolve(NextResponse.json({ + emails, + folders: availableMailboxes, + mailUrl: process.env.NEXT_PUBLIC_IFRAME_MAIL_URL + })); + }).catch((err) => { + console.error(`Error fetching emails from ${folder}:`, err); + clearTimeout(timeout); + imap.end(); + resolve(NextResponse.json({ emails: [], error: `Failed to fetch emails from ${folder}` })); }); }); }); @@ -259,11 +268,11 @@ export async function GET(request: Request) { imap.connect(); }); } catch (error) { - console.error('Error in mail API:', error); - return NextResponse.json({ - emails: [], - error: error instanceof Error ? error.message : 'Unknown error' - }); + console.error('Error in GET /api/mail:', error); + return NextResponse.json( + { error: 'Failed to fetch emails' }, + { status: 500 } + ); } } diff --git a/app/mail/page.tsx b/app/mail/page.tsx index 3c1f229..0601f88 100644 --- a/app/mail/page.tsx +++ b/app/mail/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState, useMemo } from 'react'; +import { useEffect, useState, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; @@ -510,6 +510,10 @@ export default function MailPage() { const [unreadCount, setUnreadCount] = useState(0); const [availableFolders, setAvailableFolders] = useState([]); const [sidebarItems, setSidebarItems] = useState(initialSidebarItems); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const emailsPerPage = 24; // Debug logging for email distribution useEffect(() => { @@ -557,18 +561,21 @@ export default function MailPage() { }, [router]); // Update the loadEmails function - const loadEmails = async () => { + const loadEmails = async (isLoadMore = false) => { try { - setLoading(true); + if (isLoadMore) { + setIsLoadingMore(true); + } else { + setLoading(true); + } setError(null); - const response = await fetch(`/api/mail?folder=${currentView}`); + 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(); - console.log('Raw email data:', data); // Get available folders from the API response if (data.folders) { @@ -578,7 +585,7 @@ export default function MailPage() { // Process emails keeping exact folder names const processedEmails = data.emails.map((email: any) => ({ id: Number(email.id), - accountId: 1, // Default account ID + accountId: 1, from: email.from || '', fromName: email.from?.split('@')[0] || '', to: email.to || '', @@ -587,7 +594,7 @@ export default function MailPage() { date: email.date || new Date().toISOString(), read: email.read || false, starred: email.starred || false, - folder: email.folder || 'INBOX', // Use the folder from API response + folder: email.folder || 'INBOX', cc: email.cc, bcc: email.bcc, flags: email.flags || [] @@ -595,21 +602,33 @@ export default function MailPage() { // Update unread count for INBOX const unreadInboxEmails = processedEmails.filter( - email => !email.read && email.folder === 'INBOX' + (email: Email) => !email.read && email.folder === 'INBOX' ).length; setUnreadCount(unreadInboxEmails); - setEmails(processedEmails); + // 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]); @@ -1024,6 +1043,19 @@ export default function MailPage() { }); }, [emails]); + // 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]); + // Render the email list using sorted emails const renderEmailList = () => (
@@ -1038,18 +1070,21 @@ export default function MailPage() {
- {sortedEmails.length} emails + {emails.length} emails
- {/* Email list */} -
+ {/* Email list with scroll handler */} +
{loading ? (
- ) : sortedEmails.length === 0 ? ( + ) : emails.length === 0 ? (

@@ -1125,6 +1160,11 @@ export default function MailPage() { )}

))} + {isLoadingMore && ( +
+
+
+ )}
)}