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'; export interface EmailAddress { name: string; address: string; } export interface Email { id: string; from: EmailAddress[]; to: EmailAddress[]; cc?: EmailAddress[]; bcc?: EmailAddress[]; subject: string; content: string; preview?: string; date: string; read: boolean; starred: boolean; attachments?: { filename: string; contentType: string; size: number; content?: string }[]; folder: string; hasAttachments: boolean; contentFetched?: boolean; } 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); // Keep reference to the current page for this request const currentRequestPage = page; try { // Build query params const queryParams = new URLSearchParams({ folder: currentFolder, page: currentRequestPage.toString(), perPage: perPage.toString() }); if (searchQuery) { queryParams.set('search', searchQuery); } // Add accountId if provided if (accountId) { queryParams.set('accountId', accountId); } // First try Redis cache with low timeout const cachedEmails = await getCachedEmailsWithTimeout(session.user.id, currentFolder, currentRequestPage, perPage, 100); 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 ${currentRequestPage}:`, 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) => { try { const response = await fetch(`/api/courrier/${emailId}?folder=${encodeURIComponent(currentFolder)}`); 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) => { setIsLoading(true); try { // Find the email in the current list const email = emails.find(e => e.id === emailId); 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); // 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.read) { 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, read: isRead } : email )); // If the selected email is the one being marked, update it too if (selectedEmail && selectedEmail.id === emailId) { setSelectedEmail({ ...selectedEmail, read: 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.starred; 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, starred: newStarredStatus } : email )); // If the selected email is the one being starred, update it too if (selectedEmail && selectedEmail.id === emailId) { setSelectedEmail({ ...selectedEmail, starred: 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 // The loadEmails function will be called by the useEffect above // due to the dependency on currentFolder }, []); // 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, type: 'reply' | 'reply-all' | 'forward') => { if (!email) return null; return formatEmailForReplyOrForward(email, type); }, []); // 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, }; };