Neah/hooks/use-email-state.ts

844 lines
29 KiB
TypeScript

import { useReducer, useCallback, useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { useToast } from './use-toast';
import {
emailReducer,
initialState,
EmailState,
EmailAction,
normalizeFolderAndAccount,
Account
} from '@/lib/reducers/emailReducer';
import {
getCachedEmailsWithTimeout,
refreshEmailsInBackground
} from '@/lib/services/prefetch-service';
import { Email, EmailData } from './use-courrier';
import { formatEmailForReplyOrForward } from '@/lib/utils/email-formatter';
// Add a global dispatcher for compatibility with older code
// This is a temporary solution until we fully migrate to the reducer pattern
declare global {
interface Window {
dispatchEmailAction?: (action: EmailAction) => void;
__emailStateDispatch?: (action: EmailAction) => void;
}
}
export const useEmailState = () => {
const [state, dispatch] = useReducer(emailReducer, initialState);
const { data: session } = useSession();
const { toast } = useToast();
// Expose dispatch function to window for external components
useEffect(() => {
// Make dispatch available globally for older code
window.dispatchEmailAction = dispatch;
window.__emailStateDispatch = dispatch;
// Clean up on unmount
return () => {
window.dispatchEmailAction = undefined;
window.__emailStateDispatch = undefined;
};
}, [dispatch]);
// Helper function to log operations
const logEmailOp = useCallback((operation: string, details: string, data?: any) => {
const timestamp = new Date().toISOString().split('T')[1].substring(0, 12);
console.log(`[${timestamp}][EMAIL-STATE][${operation}] ${details}`);
if (data) {
console.log(`[${timestamp}][EMAIL-STATE][DATA]`, data);
}
}, []);
// Load emails from the server
const loadEmails = useCallback(async (isLoadMore = false, accountId?: string) => {
if (!session?.user?.id) return;
dispatch({ type: 'SET_LOADING', payload: true });
try {
// Get normalized parameters using helper function
const { normalizedFolder, effectiveAccountId, prefixedFolder } =
normalizeFolderAndAccount(state.currentFolder, accountId);
logEmailOp('LOAD_EMAILS', `Loading emails for ${prefixedFolder} (account: ${effectiveAccountId})`);
// Construct query parameters
const queryParams = new URLSearchParams({
folder: normalizedFolder,
page: state.page.toString(),
perPage: state.perPage.toString(),
accountId: effectiveAccountId
});
// Try to get cached emails first
logEmailOp('CACHE_CHECK', `Checking cache for ${prefixedFolder}, page: ${state.page}`);
const cachedEmails = await getCachedEmailsWithTimeout(
session.user.id,
prefixedFolder,
state.page,
state.perPage,
100,
effectiveAccountId
);
if (cachedEmails) {
logEmailOp('CACHE_HIT', `Using cached data for ${prefixedFolder}`);
// Ensure cached data has emails array property
if (Array.isArray(cachedEmails.emails)) {
// Dispatch appropriate action based on if we're loading more
dispatch({
type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS',
payload: cachedEmails.emails
});
// Set pagination info from cache if available
if (cachedEmails.totalEmails) {
dispatch({ type: 'SET_TOTAL_EMAILS', payload: cachedEmails.totalEmails });
}
if (cachedEmails.totalPages) {
dispatch({ type: 'SET_TOTAL_PAGES', payload: cachedEmails.totalPages });
}
// Update available mailboxes if provided
if (cachedEmails.mailboxes && cachedEmails.mailboxes.length > 0) {
dispatch({ type: 'SET_MAILBOXES', payload: cachedEmails.mailboxes });
}
}
// Still refresh in background for fresh data
logEmailOp('BACKGROUND_REFRESH', `Starting background refresh for ${prefixedFolder}`);
refreshEmailsInBackground(
session.user.id,
normalizedFolder,
state.page,
state.perPage,
effectiveAccountId
).catch(err => {
console.error('Background refresh error:', err);
});
return;
}
// Fetch emails from API if no cache hit
logEmailOp('API_FETCH', `Fetching emails from API: ${queryParams.toString()}`);
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 = await response.json();
// Ensure all emails have proper account ID and folder format
if (Array.isArray(data.emails)) {
// Log email dates for debugging
if (data.emails.length > 0) {
logEmailOp('EMAIL_DATES', `First few email dates before processing:`,
data.emails.slice(0, 5).map((e: any) => ({
id: e.id.substring(0, 8),
subject: e.subject?.substring(0, 20),
date: e.date,
dateObj: new Date(e.date),
timestamp: new Date(e.date).getTime()
}))
);
}
data.emails.forEach((email: Email) => {
// If email doesn't have an accountId, set it to the effective one
if (!email.accountId) {
email.accountId = effectiveAccountId;
}
// Ensure folder has the proper prefix format
if (email.folder && !email.folder.includes(':')) {
email.folder = `${email.accountId}:${email.folder}`;
}
// Ensure date is a valid Date object (handle strings or timestamps)
if (email.date && !(email.date instanceof Date)) {
try {
// Convert to a proper Date object if it's a string or number
const dateObj = new Date(email.date);
// Verify it's a valid date
if (!isNaN(dateObj.getTime())) {
email.date = dateObj;
}
} catch (err) {
// If conversion fails, log and use current date as fallback
console.error(`Invalid date format for email ${email.id}: ${email.date}`);
email.date = new Date();
}
}
});
}
// Update state with fetched data
dispatch({
type: isLoadMore ? 'APPEND_EMAILS' : 'SET_EMAILS',
payload: Array.isArray(data.emails) ? data.emails : []
});
if (data.totalEmails) {
dispatch({ type: 'SET_TOTAL_EMAILS', payload: data.totalEmails });
}
if (data.totalPages) {
dispatch({ type: 'SET_TOTAL_PAGES', payload: data.totalPages });
}
// Update available mailboxes if provided
if (data.mailboxes && data.mailboxes.length > 0) {
dispatch({ type: 'SET_MAILBOXES', payload: data.mailboxes });
}
} catch (err) {
logEmailOp('ERROR', `Failed to load emails: ${err instanceof Error ? err.message : String(err)}`);
dispatch({
type: 'SET_ERROR',
payload: err instanceof Error ? err.message : 'Failed to load emails'
});
toast({
variant: "destructive",
title: "Error",
description: err instanceof Error ? err.message : 'Failed to load emails'
});
}
}, [session?.user?.id, state.currentFolder, state.page, state.perPage, toast, logEmailOp]);
// Change folder
const changeFolder = useCallback(async (folder: string, accountId?: string) => {
logEmailOp('CHANGE_FOLDER', `Changing to folder ${folder} with account ${accountId || 'default'}`);
try {
// This will handle all the state updates in a single atomic operation
dispatch({
type: 'CHANGE_FOLDER',
payload: { folder, accountId: accountId || 'default' }
});
// After dispatch, the state will be updated with consistent values
// We'll load emails in the useEffect that watches for folder changes
} catch (error) {
logEmailOp('ERROR', `Failed to change folder: ${error instanceof Error ? error.message : String(error)}`);
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : 'Failed to change folder'
});
}
}, [logEmailOp]);
// Select account
const selectAccount = useCallback((account: Account) => {
logEmailOp('SELECT_ACCOUNT', `Selecting account: ${account.email} (${account.id})`);
// Handle the entire account selection in a single atomic operation
dispatch({ type: 'SELECT_ACCOUNT', payload: account });
// The folder loading will be triggered by the useEffect watching for currentFolder changes
}, [logEmailOp]);
// Handle email selection
const handleEmailSelect = useCallback(async (emailId: string, accountId: string, folder: string) => {
logEmailOp('SELECT_EMAIL', `Selecting email ${emailId} from account ${accountId} in folder ${folder}`);
if (!emailId) {
dispatch({
type: 'SELECT_EMAIL',
payload: { emailId: '', accountId: '', folder: '', email: null }
});
return;
}
try {
// Find the email in the current list
const existingEmail = state.emails.find(e => e.id === emailId);
if (existingEmail && existingEmail.contentFetched) {
// Use the existing email if it has content already
dispatch({
type: 'SELECT_EMAIL',
payload: { emailId, accountId, folder, email: existingEmail }
});
// Mark as read if not already
if (!existingEmail.flags.seen) {
markEmailAsRead(emailId, true, accountId);
}
return;
}
// Need to fetch the email content
dispatch({ type: 'SET_LOADING', payload: true });
// Extract account ID from folder name if present and none was explicitly provided
const { normalizedFolder, effectiveAccountId } = normalizeFolderAndAccount(folder, accountId);
// Fetch email content from API
const response = await fetch(`/api/courrier/${emailId}?folder=${normalizedFolder}&accountId=${effectiveAccountId}`);
if (!response.ok) {
throw new Error(`Failed to fetch email content: ${response.status}`);
}
const emailData = await response.json();
// Mark the email as read on the server
markEmailAsRead(emailId, true, effectiveAccountId);
// Select the email
dispatch({
type: 'SELECT_EMAIL',
payload: { emailId, accountId: effectiveAccountId, folder, email: emailData }
});
} catch (error) {
logEmailOp('ERROR', `Failed to select email: ${error instanceof Error ? error.message : String(error)}`);
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : 'Failed to select email'
});
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
}, [state.emails, logEmailOp]);
// Toggle email selection for multi-select
const toggleEmailSelection = useCallback((emailId: string) => {
dispatch({ type: 'TOGGLE_EMAIL_SELECTION', payload: emailId });
}, []);
// Toggle select all
const toggleSelectAll = useCallback(() => {
dispatch({ type: 'TOGGLE_SELECT_ALL' });
}, []);
// Mark email as read/unread
const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean, accountId?: string) => {
try {
// Find the email to get its account ID if not provided
const email = state.emails.find(e => e.id === emailId);
const effectiveAccountId = accountId || email?.accountId || 'default';
const folder = email?.folder || state.currentFolder;
// Extract normalized folder
const { normalizedFolder } = normalizeFolderAndAccount(folder, effectiveAccountId);
logEmailOp('MARK_READ', `Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in ${normalizedFolder}`);
// Update UI state immediately (optimistic update)
dispatch({
type: 'MARK_EMAIL_AS_READ',
payload: { emailId, isRead, accountId: effectiveAccountId }
});
// NOTE: Don't update unread counts here - that's now handled by the updateUnreadCounts function
// which is triggered by the email update above via the useEffect
// Make API call to update on server
const response = await fetch(`/api/courrier/${emailId}/mark-read`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
isRead,
folder: normalizedFolder,
accountId: effectiveAccountId
})
});
if (!response.ok) {
throw new Error('Failed to mark email as read');
}
return true;
} catch (error) {
logEmailOp('ERROR', `Failed to mark email as read: ${error instanceof Error ? error.message : String(error)}`);
toast({
variant: "destructive",
title: "Error",
description: 'Failed to update email read status'
});
return false;
}
}, [state.emails, state.currentFolder, toast, logEmailOp]);
// Toggle starred status
const toggleStarred = useCallback(async (emailId: string) => {
try {
// Find the email in current list
const email = state.emails.find(e => e.id === emailId);
if (!email) {
throw new Error('Email not found');
}
const newFlaggedStatus = !email.flags.flagged;
logEmailOp('TOGGLE_STAR', `Setting starred status to ${newFlaggedStatus} for email ${emailId}`);
// TODO: Implement optimistic update
// Make API call
const response = await fetch(`/api/courrier/${emailId}/flag`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
flagged: newFlaggedStatus,
folder: email.folder,
accountId: email.accountId
})
});
if (!response.ok) {
throw new Error('Failed to update star status');
}
// Reload emails to get updated state
loadEmails();
return true;
} catch (error) {
logEmailOp('ERROR', `Failed to toggle star: ${error instanceof Error ? error.message : String(error)}`);
toast({
variant: "destructive",
title: "Error",
description: 'Failed to update star status'
});
return false;
}
}, [state.emails, toast, loadEmails, logEmailOp]);
// Delete emails
const deleteEmails = useCallback(async (emailIds: string[]) => {
if (emailIds.length === 0) return;
dispatch({ type: 'SET_LOADING', payload: true });
try {
logEmailOp('DELETE', `Deleting ${emailIds.length} emails`);
// Find the first email to get account ID and folder
const firstEmail = state.emails.find(e => e.id === emailIds[0]);
const accountId = firstEmail?.accountId || 'default';
const folder = firstEmail?.folder || state.currentFolder;
const { normalizedFolder } = normalizeFolderAndAccount(folder, accountId);
// Make API call to delete emails
const response = await fetch('/api/courrier/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
emailIds,
folder: normalizedFolder,
accountId
})
});
if (!response.ok) {
throw new Error('Failed to delete emails');
}
// Clear selections
dispatch({ type: 'CLEAR_SELECTED_EMAILS' });
// Reload emails to get updated list
loadEmails();
toast({
title: "Emails Deleted",
description: `${emailIds.length} email(s) moved to trash`
});
return true;
} catch (error) {
logEmailOp('ERROR', `Failed to delete emails: ${error instanceof Error ? error.message : String(error)}`);
toast({
variant: "destructive",
title: "Error",
description: 'Failed to delete emails'
});
return false;
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
}, [state.emails, state.currentFolder, toast, loadEmails, logEmailOp]);
// Send email
const sendEmail = useCallback(async (emailData: EmailData) => {
dispatch({ type: 'SET_LOADING', payload: true });
try {
logEmailOp('SEND', `Sending email to ${emailData.to}`);
// Make API call to send email
const response = await fetch('/api/courrier/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(emailData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to send email');
}
const result = await response.json();
toast({
title: "Email Sent",
description: "Your message has been sent successfully"
});
// Refresh emails to show the sent email
loadEmails();
return { success: true, ...result };
} catch (error) {
logEmailOp('ERROR', `Failed to send email: ${error instanceof Error ? error.message : String(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 {
dispatch({ type: 'SET_LOADING', payload: false });
}
}, [toast, loadEmails, logEmailOp]);
// Search emails
const searchEmails = useCallback(async (query: string) => {
// Set loading state
dispatch({ type: 'SET_LOADING', payload: true });
try {
if (!session?.user?.id) return;
logEmailOp('SEARCH', `Searching for "${query}" in ${state.currentFolder}`);
// Extract account ID from current folder
const { normalizedFolder, effectiveAccountId } = normalizeFolderAndAccount(state.currentFolder);
// Construct query params for search
const queryParams = new URLSearchParams({
folder: normalizedFolder,
search: query,
accountId: effectiveAccountId
});
// Call API for search
const response = await fetch(`/api/courrier/search?${queryParams.toString()}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to search emails');
}
const results = await response.json();
// Update emails with search results
dispatch({ type: 'SET_EMAILS', payload: results.emails || [] });
if (results.totalEmails) {
dispatch({ type: 'SET_TOTAL_EMAILS', payload: results.totalEmails });
}
if (results.totalPages) {
dispatch({ type: 'SET_TOTAL_PAGES', payload: results.totalPages });
}
} catch (error) {
logEmailOp('ERROR', `Search failed: ${error instanceof Error ? error.message : String(error)}`);
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : 'Failed to search emails'
});
toast({
variant: "destructive",
title: "Error",
description: 'Failed to search emails'
});
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
}, [session?.user?.id, state.currentFolder, toast, logEmailOp]);
// Format email for reply, reply all, or forward
const formatEmailForAction = useCallback((email: any, type: 'reply' | 'reply-all' | 'forward') => {
return formatEmailForReplyOrForward(email, type);
}, []);
// Update page
const setPage = useCallback((page: number) => {
dispatch({ type: 'SET_PAGE', payload: page });
}, []);
// Set emails directly
const setEmails = useCallback((emails: Email[]) => {
dispatch({ type: 'SET_EMAILS', payload: emails });
}, []);
// Handle loading more emails
const handleLoadMore = useCallback(() => {
if (state.page < state.totalPages && !state.isLoading) {
dispatch({ type: 'INCREMENT_PAGE' });
}
}, [state.page, state.totalPages, state.isLoading]);
// Effect to load emails when folder changes
useEffect(() => {
if (session?.user?.id && state.currentFolder) {
// Extract account ID from folder for consistent loading
const { effectiveAccountId } = normalizeFolderAndAccount(state.currentFolder);
logEmailOp('FOLDER_CHANGE', `Loading emails for folder ${state.currentFolder} with account ${effectiveAccountId}`);
// Load emails with the correct account ID
loadEmails(false, effectiveAccountId);
}
}, [session?.user?.id, state.currentFolder, loadEmails, logEmailOp]);
// Effect to load more emails when page changes
useEffect(() => {
if (session?.user?.id && state.page > 1) {
// Extract account ID for consistency
const { effectiveAccountId } = normalizeFolderAndAccount(state.currentFolder);
logEmailOp('PAGINATION', `Loading page ${state.page} for ${state.currentFolder} with account ${effectiveAccountId}`);
// Load more emails with the correct account ID
loadEmails(true, effectiveAccountId);
}
}, [session?.user?.id, state.page, state.currentFolder, loadEmails, logEmailOp]);
// Calculate and update unread counts from emails
const updateUnreadCounts = useCallback(() => {
// This function will count unread emails in each folder and update the unreadCountMap
if (state.emails.length === 0) return;
// Create a temporary count map
const tempUnreadMap: Record<string, Record<string, number>> = {};
// Initialize with existing accounts and folders to avoid losing counts
// when switching folders
state.accounts.forEach(account => {
tempUnreadMap[account.id] = { ...state.unreadCountMap[account.id] || {} };
// Initialize each folder with 0 if it doesn't exist
account.folders.forEach(folder => {
// Extract base folder name if it has a prefix
const baseFolderName = folder.includes(':') ? folder.split(':')[1] : folder;
const prefixedFolder = folder.includes(':') ? folder : `${account.id}:${baseFolderName}`;
// Initialize both prefixed and unprefixed versions
if (tempUnreadMap[account.id][baseFolderName] === undefined) {
tempUnreadMap[account.id][baseFolderName] = 0;
}
if (tempUnreadMap[account.id][prefixedFolder] === undefined) {
tempUnreadMap[account.id][prefixedFolder] = 0;
}
});
});
// Count unread emails from current folder
state.emails.forEach(email => {
// Check if email is unread
const isUnread = email.flags && !email.flags.seen;
// Only count if it's unread
if (isUnread && email.accountId) {
// Get folder information
let folder = email.folder;
let accountId = email.accountId;
// Make sure the account exists in our map
if (!tempUnreadMap[accountId]) {
tempUnreadMap[accountId] = {};
}
// Extract folder name if it has a prefix
let baseFolderName = folder;
if (folder.includes(':')) {
const parts = folder.split(':');
// Don't override accountId from the folder prefix
baseFolderName = parts[1];
}
// Ensure we have entries for both formats
if (!tempUnreadMap[accountId][baseFolderName]) {
tempUnreadMap[accountId][baseFolderName] = 0;
}
if (!tempUnreadMap[accountId][folder]) {
tempUnreadMap[accountId][folder] = 0;
}
// Increment both formats to ensure they're both available
tempUnreadMap[accountId][baseFolderName]++;
tempUnreadMap[accountId][folder]++;
}
});
// Log the unread counts for debugging
logEmailOp('UNREAD_COUNTS', 'Updated unread counts:', tempUnreadMap);
// Check if the unread counts have actually changed before updating state
let hasChanged = false;
// Compare with current unread count map
Object.entries(tempUnreadMap).forEach(([accountId, folderCounts]) => {
Object.entries(folderCounts).forEach(([folder, count]) => {
if (state.unreadCountMap[accountId]?.[folder] !== count) {
hasChanged = true;
}
});
});
// Only update if there are actual changes to avoid infinite updates
if (hasChanged) {
// Create a single dispatch to update all counts at once
const updatedMap = { ...state.unreadCountMap };
Object.entries(tempUnreadMap).forEach(([accountId, folderCounts]) => {
updatedMap[accountId] = { ...updatedMap[accountId] || {}, ...folderCounts };
});
// Replace the entire unread count map with one action
dispatch({
type: 'SET_UNREAD_COUNTS',
payload: updatedMap
});
}
}, [state.emails, state.accounts, state.unreadCountMap, dispatch, logEmailOp]);
// Call updateUnreadCounts when relevant state changes
useEffect(() => {
// Only update unread counts when emails or flag status changes
// NOT when the unreadCountMap itself changes (that would cause infinite loop)
const updateCountsWithDebounce = setTimeout(() => {
updateUnreadCounts();
}, 300); // Debounce to handle multiple email updates
return () => clearTimeout(updateCountsWithDebounce);
// Deliberately exclude unreadCountMap to prevent infinite loops
}, [updateUnreadCounts, state.emails]);
// Fetch unread counts from API
const fetchUnreadCounts = useCallback(async () => {
if (!session?.user?.id) return;
try {
logEmailOp('FETCH_UNREAD', 'Fetching unread counts from API');
// Make API call to get unread counts for all folders
const response = await fetch('/api/courrier/unread-counts');
if (!response.ok) {
throw new Error('Failed to fetch unread counts');
}
const data = await response.json();
if (data.counts && typeof data.counts === 'object') {
logEmailOp('UNREAD_API', 'Received unread counts from API', data.counts);
// Merge with existing unread counts rather than replacing
const mergedCounts = { ...state.unreadCountMap };
// Update unread counts in state
Object.entries(data.counts).forEach(([accountId, folderCounts]: [string, any]) => {
if (!mergedCounts[accountId]) {
mergedCounts[accountId] = {};
}
Object.entries(folderCounts).forEach(([folder, count]: [string, any]) => {
mergedCounts[accountId][folder] = typeof count === 'number' ? count : 0;
});
});
// Only dispatch if there are actual changes
let hasChanges = false;
Object.entries(mergedCounts).forEach(([accountId, folderCounts]) => {
Object.entries(folderCounts).forEach(([folder, count]) => {
if (state.unreadCountMap[accountId]?.[folder] !== count) {
hasChanges = true;
}
});
});
if (hasChanges) {
dispatch({
type: 'SET_UNREAD_COUNTS',
payload: mergedCounts
});
}
}
} catch (error) {
logEmailOp('ERROR', `Failed to fetch unread counts: ${error instanceof Error ? error.message : String(error)}`);
// Don't show toast for this error as it's not critical
}
}, [session?.user?.id, state.unreadCountMap, dispatch, logEmailOp]);
// Fetch unread counts when accounts are loaded
useEffect(() => {
let isMounted = true;
if (state.accounts.length > 0) {
fetchUnreadCounts().then(() => {
if (!isMounted) return;
}).catch(error => {
if (!isMounted) return;
logEmailOp('ERROR', `Background unread count fetch failed: ${String(error)}`);
});
}
return () => {
isMounted = false;
};
}, [state.accounts.length, fetchUnreadCounts, logEmailOp]);
// Set up periodic refresh of unread counts (every 60 seconds)
useEffect(() => {
let isMounted = true;
const intervalId = setInterval(() => {
if (!isMounted || state.accounts.length === 0) return;
fetchUnreadCounts().catch(error => {
if (!isMounted) return;
logEmailOp('ERROR', `Periodic unread count fetch failed: ${String(error)}`);
});
}, 60000);
return () => {
isMounted = false;
clearInterval(intervalId);
};
}, [state.accounts.length, fetchUnreadCounts, logEmailOp]);
// Return all state values and actions
return {
// State values
...state,
// Actions
loadEmails,
handleEmailSelect,
toggleEmailSelection,
toggleSelectAll,
markEmailAsRead,
toggleStarred,
changeFolder,
deleteEmails,
sendEmail,
searchEmails,
formatEmailForAction,
setPage,
setEmails,
selectAccount,
handleLoadMore
};
};