import { useState, useCallback, useEffect } from 'react'; import { useSession } from 'next-auth/react'; import { useToast } from './use-toast'; import { formatEmailForReplyOrForward } from '@/lib/utils/email-formatter'; import { getCachedEmailsWithTimeout, refreshEmailsInBackground } from '@/lib/services/prefetch-service'; import { EmailAddress, EmailAttachment } from '@/lib/types'; export interface Email { id: string; from: EmailAddress[]; to: EmailAddress[]; cc?: EmailAddress[]; bcc?: EmailAddress[]; subject: string; content: { text: string; html: string; }; preview?: string; date: Date; flags: { seen: boolean; answered: boolean; flagged: boolean; draft: boolean; deleted: boolean; }; size: number; hasAttachments: boolean; folder: string; contentFetched: boolean; accountId?: string; messageId?: string; attachments?: EmailAttachment[]; } export interface EmailListResult { emails: Email[]; totalEmails: number; page: number; perPage: number; totalPages: number; folder: string; mailboxes: string[]; } export interface EmailData { to: string; cc?: string; bcc?: string; subject: string; body: string; attachments?: Array<{ name: string; content: string; type: string; }>; } export type MailFolder = string; // Hook for managing email operations export const useCourrier = () => { // State for email data const [emails, setEmails] = useState([]); const [selectedEmail, setSelectedEmail] = useState(null); const [selectedEmailIds, setSelectedEmailIds] = useState([]); const [currentFolder, setCurrentFolder] = useState('INBOX'); const [mailboxes, setMailboxes] = useState([]); // State for UI const [isLoading, setIsLoading] = useState(false); const [isSending, setIsSending] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(20); const [totalEmails, setTotalEmails] = useState(0); const [totalPages, setTotalPages] = useState(0); // Auth and notifications const { data: session } = useSession(); const { toast } = useToast(); // Load emails from the server const loadEmails = useCallback(async (isLoadMore = false, accountId?: string) => { if (!session?.user?.id) return; setIsLoading(true); setError(null); try { // Construct query parameters const queryParams = new URLSearchParams({ folder: currentFolder, page: page.toString(), perPage: perPage.toString() }); if (searchQuery) { queryParams.set('search', searchQuery); } // Add accountId to query params if provided if (accountId) { queryParams.set('accountId', accountId); } // Try to get cached emails first const currentRequestPage = page; const cachedEmails = await getCachedEmailsWithTimeout( session.user.id, accountId || 'default', currentFolder, currentRequestPage, perPage, accountId && accountId !== 'all-accounts' && accountId !== 'loading-account' ? accountId : undefined ); if (cachedEmails) { // Ensure cached data has emails array property if (Array.isArray(cachedEmails.emails)) { if (isLoadMore) { // When loading more, always append to the existing list setEmails(prevEmails => { // Create a Set of existing email IDs to avoid duplicates const existingIds = new Set(prevEmails.map(email => email.id)); // Filter out any duplicates before appending const newEmails = cachedEmails.emails.filter((email: Email) => !existingIds.has(email.id)); // Log pagination info console.log(`Added ${newEmails.length} cached emails from page ${currentRequestPage} to existing ${prevEmails.length} emails`); // Combine emails and sort them by date (newest first) const combinedEmails = [...prevEmails, ...newEmails]; return combinedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()); }); } else { // For initial load, replace emails console.log(`Setting ${cachedEmails.emails.length} cached emails for page ${currentRequestPage}`); // Ensure emails are sorted by date (newest first) setEmails(cachedEmails.emails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime())); } // Set pagination info from cache if available if (cachedEmails.totalEmails) setTotalEmails(cachedEmails.totalEmails); if (cachedEmails.totalPages) setTotalPages(cachedEmails.totalPages); // Update available mailboxes if provided if (cachedEmails.mailboxes && cachedEmails.mailboxes.length > 0) { setMailboxes(cachedEmails.mailboxes); } } else if (Array.isArray(cachedEmails)) { // Direct array response if (isLoadMore) { setEmails(prevEmails => { // Create a Set of existing email IDs to avoid duplicates const existingIds = new Set(prevEmails.map(email => email.id)); // Filter out any duplicates before appending const newEmails = cachedEmails.filter((email: Email) => !existingIds.has(email.id)); // Log pagination info console.log(`Added ${newEmails.length} cached emails from page ${currentRequestPage} to existing ${prevEmails.length} emails`); // Combine emails and sort them by date (newest first) const combinedEmails = [...prevEmails, ...newEmails]; return combinedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()); }); } else { // For initial load, replace emails console.log(`Setting ${cachedEmails.length} cached emails for page ${currentRequestPage}`); // Ensure emails are sorted by date (newest first) setEmails(cachedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime())); } } else { console.warn('Invalid cache format:', cachedEmails); } setIsLoading(false); // Still refresh in background for fresh data refreshEmailsInBackground(session.user.id, currentFolder, currentRequestPage, perPage).catch(err => { console.error('Background refresh error:', err); }); return; } // Fetch emails from API const response = await fetch(`/api/courrier?${queryParams.toString()}`); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to fetch emails'); } const data: EmailListResult = await response.json(); // Update state with the fetched data if (isLoadMore) { setEmails(prev => { // Create a Set of existing email IDs to avoid duplicates const existingIds = new Set(prev.map(email => email.id)); // Filter out any duplicates before appending const newEmails = data.emails.filter((email: Email) => !existingIds.has(email.id)); // Log pagination info console.log(`Added ${newEmails.length} fetched emails from page ${currentRequestPage} to existing ${prev.length} emails`); // Combine emails and sort them by date (newest first) const combinedEmails = [...prev, ...newEmails]; return combinedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()); }); } else { // Ensure we always set an array even if API returns invalid data console.log(`Setting ${data.emails?.length || 0} fetched emails for page ${currentRequestPage}`); // Ensure emails are sorted by date (newest first) if (Array.isArray(data.emails)) { setEmails(data.emails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime())); } else { setEmails([]); } } setTotalEmails(data.totalEmails); setTotalPages(data.totalPages); // Update available mailboxes if provided if (data.mailboxes && data.mailboxes.length > 0) { setMailboxes(data.mailboxes); } // Clear selection if not loading more if (!isLoadMore) { setSelectedEmail(null); setSelectedEmailIds([]); } } catch (err) { console.error(`Error loading emails for page ${page}:`, err); // Set emails to empty array on error to prevent runtime issues if (!isLoadMore) { setEmails([]); } setError(err instanceof Error ? err.message : 'Failed to load emails'); toast({ variant: "destructive", title: "Error", description: err instanceof Error ? err.message : 'Failed to load emails' }); } finally { setIsLoading(false); } }, [currentFolder, page, perPage, searchQuery, session?.user?.id, toast]); // Load emails when folder or page changes useEffect(() => { if (session?.user?.id) { // If page is greater than 1, we're loading more emails const isLoadingMore = page > 1; loadEmails(isLoadingMore); // If we're loading the first page, publish an event to reset scroll position if (page === 1 && typeof window !== 'undefined') { // Use a custom event to communicate with the EmailList component const event = new CustomEvent('reset-email-scroll'); window.dispatchEvent(event); } } }, [currentFolder, page, perPage, session?.user?.id, loadEmails]); // Fetch a single email's content const fetchEmailContent = useCallback(async (emailId: string, accountId?: string, folderOverride?: string) => { try { const folderToUse = folderOverride || currentFolder; const query = new URLSearchParams({ folder: folderToUse, }); if (accountId) query.set('accountId', accountId); const response = await fetch(`/api/courrier/${emailId}?${query.toString()}`); if (!response.ok) { throw new Error(`Failed to fetch email content: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error('Error fetching email content:', error); throw error; } }, [currentFolder]); // Select an email to view const handleEmailSelect = useCallback(async (emailId: string, accountId?: string, folderOverride?: string) => { setIsLoading(true); try { // Find the email in the current list const email = emails.find(e => e.id === emailId && (!accountId || e.accountId === accountId) && (!folderOverride || e.folder === folderOverride)); if (!email) { throw new Error('Email not found'); } // If content is not fetched, get the full content if (!email.contentFetched) { const fullEmail = await fetchEmailContent(emailId, accountId, folderOverride); // Merge the full content with the email const updatedEmail = { ...email, content: fullEmail.content, attachments: fullEmail.attachments, contentFetched: true }; // Update the email in the list setEmails(emails.map(e => e.id === emailId ? updatedEmail : e)); setSelectedEmail(updatedEmail); } else { setSelectedEmail(email); } // Mark the email as read if it's not already if (!email.flags.seen) { markEmailAsRead(emailId, true); } } catch (err) { console.error('Error selecting email:', err); toast({ variant: "destructive", title: "Error", description: "Could not load email content" }); } finally { setIsLoading(false); } }, [emails, fetchEmailContent, toast]); // Mark an email as read/unread const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean) => { try { const response = await fetch(`/api/courrier/${emailId}/mark-read`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isRead, folder: currentFolder }) }); if (!response.ok) { throw new Error('Failed to mark email as read'); } // Update the email in the list setEmails(emails.map(email => email.id === emailId ? { ...email, flags: { ...email.flags, seen: isRead } } : email )); // If the selected email is the one being marked, update it too if (selectedEmail && selectedEmail.id === emailId) { setSelectedEmail({ ...selectedEmail, flags: { ...selectedEmail.flags, seen: isRead } }); } return true; } catch (error) { console.error('Error marking email as read:', error); return false; } }, [emails, selectedEmail, currentFolder]); // Toggle starred status for an email const toggleStarred = useCallback(async (emailId: string) => { const email = emails.find(e => e.id === emailId); if (!email) return; const newStarredStatus = !email.flags.flagged; try { const response = await fetch(`/api/courrier/${emailId}/star`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ starred: newStarredStatus, folder: currentFolder }) }); if (!response.ok) { throw new Error('Failed to toggle star status'); } // Update the email in the list setEmails(emails.map(email => email.id === emailId ? { ...email, flags: { ...email.flags, flagged: newStarredStatus } } : email )); // If the selected email is the one being starred, update it too if (selectedEmail && selectedEmail.id === emailId) { setSelectedEmail({ ...selectedEmail, flags: { ...selectedEmail.flags, flagged: newStarredStatus } }); } } catch (error) { console.error('Error toggling star status:', error); toast({ variant: "destructive", title: "Error", description: "Could not update star status" }); } }, [emails, selectedEmail, currentFolder, toast]); // Send an email const sendEmail = useCallback(async (emailData: EmailData) => { if (!session?.user?.id) { toast({ variant: "destructive", title: "Error", description: "You must be logged in to send emails" }); return { success: false, error: "Not authenticated" }; } setIsSending(true); try { const response = await fetch('/api/courrier/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(emailData) }); const result = await response.json(); if (!response.ok) { throw new Error(result.error || 'Failed to send email'); } toast({ title: "Success", description: "Email sent successfully" }); return { success: true, messageId: result.messageId }; } catch (error) { console.error('Error sending email:', error); toast({ variant: "destructive", title: "Error", description: error instanceof Error ? error.message : 'Failed to send email' }); return { success: false, error: error instanceof Error ? error.message : 'Failed to send email' }; } finally { setIsSending(false); } }, [session?.user?.id, toast]); // Delete selected emails const deleteEmails = useCallback(async (emailIds: string[]) => { if (emailIds.length === 0) return; setIsDeleting(true); try { const response = await fetch('/api/courrier/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emailIds, folder: currentFolder }) }); if (!response.ok) { throw new Error('Failed to delete emails'); } // Remove the deleted emails from the list setEmails(emails.filter(email => !emailIds.includes(email.id))); // Clear selection if the selected email was deleted if (selectedEmail && emailIds.includes(selectedEmail.id)) { setSelectedEmail(null); } // Clear selected IDs setSelectedEmailIds([]); toast({ title: "Success", description: `${emailIds.length} email(s) deleted` }); } catch (error) { console.error('Error deleting emails:', error); toast({ variant: "destructive", title: "Error", description: "Failed to delete emails" }); } finally { setIsDeleting(false); } }, [emails, selectedEmail, currentFolder, toast]); // Toggle selection of an email const toggleEmailSelection = useCallback((emailId: string) => { setSelectedEmailIds(prev => { if (prev.includes(emailId)) { return prev.filter(id => id !== emailId); } else { return [...prev, emailId]; } }); }, []); // Select all emails const toggleSelectAll = useCallback(() => { if (selectedEmailIds.length === emails.length) { setSelectedEmailIds([]); } else { setSelectedEmailIds(emails.map(email => email.id)); } }, [emails, selectedEmailIds]); // Change folder and load emails const changeFolder = useCallback((folder: string, accountId?: string) => { setCurrentFolder(folder); setPage(1); // Reset to first page when changing folders setSelectedEmail(null); setSelectedEmailIds([]); // Load the emails for this folder with the correct accountId if (session?.user?.id) { loadEmails(false, accountId); } }, [session?.user?.id, loadEmails]); // Search emails const searchEmails = useCallback((query: string) => { setSearchQuery(query); setPage(1); loadEmails(); }, [loadEmails]); // Format an email for reply or forward const formatEmailForAction = useCallback((email: Email) => { return { id: email.id, subject: email.subject, from: email.from[0]?.address || '', to: email.to.map(addr => addr.address).join(', '), cc: email.cc?.map(addr => addr.address).join(', '), bcc: email.bcc?.map(addr => addr.address).join(', '), content: email.content.text || email.content.html || '', attachments: email.attachments }; }, []); // Return all the functionality and state values return { // Data emails, selectedEmail, selectedEmailIds, currentFolder, mailboxes, isLoading, isSending, isDeleting, error, searchQuery, page, perPage, totalEmails, totalPages, // Functions loadEmails, handleEmailSelect, markEmailAsRead, toggleStarred, sendEmail, deleteEmails, toggleEmailSelection, toggleSelectAll, changeFolder, searchEmails, formatEmailForAction, setPage, setPerPage, setSearchQuery, }; };