diff --git a/hooks/email/index.ts b/hooks/email/index.ts new file mode 100644 index 000000000..f7f2b4cde --- /dev/null +++ b/hooks/email/index.ts @@ -0,0 +1,12 @@ +/** + * Email custom hooks for React + * Provides hooks for email synchronization, storage, accounts, and message management + * @packageDocumentation + */ + +export { useEmailSync, type UseEmailSyncResult } from './useEmailSync' +export { useEmailStore, type UseEmailStoreResult, type StoredMessage } from './useEmailStore' +export { useMailboxes, type UseMailboxesResult, type Folder } from './useMailboxes' +export { useAccounts, type UseAccountsResult, type EmailAccount } from './useAccounts' +export { useCompose, type UseComposeResult, type EmailDraft } from './useCompose' +export { useMessages, type UseMessagesResult, type Message } from './useMessages' diff --git a/hooks/email/package.json b/hooks/email/package.json new file mode 100644 index 000000000..7d2d2fe2d --- /dev/null +++ b/hooks/email/package.json @@ -0,0 +1,70 @@ +{ + "name": "@metabuilder/hooks-email", + "version": "1.0.0", + "description": "Custom React hooks for email client functionality", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "private": true, + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./useEmailSync": { + "import": "./dist/useEmailSync.js", + "require": "./dist/useEmailSync.js", + "types": "./dist/useEmailSync.d.ts" + }, + "./useEmailStore": { + "import": "./dist/useEmailStore.js", + "require": "./dist/useEmailStore.js", + "types": "./dist/useEmailStore.d.ts" + }, + "./useMailboxes": { + "import": "./dist/useMailboxes.js", + "require": "./dist/useMailboxes.js", + "types": "./dist/useMailboxes.d.ts" + }, + "./useAccounts": { + "import": "./dist/useAccounts.js", + "require": "./dist/useAccounts.js", + "types": "./dist/useAccounts.d.ts" + }, + "./useCompose": { + "import": "./dist/useCompose.js", + "require": "./dist/useCompose.js", + "types": "./dist/useCompose.d.ts" + }, + "./useMessages": { + "import": "./dist/useMessages.js", + "require": "./dist/useMessages.js", + "types": "./dist/useMessages.d.ts" + } + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "dev": "tsc --watch" + }, + "dependencies": {}, + "peerDependencies": { + "react": "18.0 || 19.0", + "react-redux": "8.0 || 9.0", + "@metabuilder/redux-email": "^1.0.0" + }, + "peerDependenciesMeta": { + "@metabuilder/redux-email": { + "optional": true + } + }, + "keywords": [ + "email", + "hooks", + "react", + "imap", + "smtp" + ], + "author": "MetaBuilder", + "license": "MIT" +} diff --git a/hooks/email/tsconfig.json b/hooks/email/tsconfig.json new file mode 100644 index 000000000..ed8fbf5da --- /dev/null +++ b/hooks/email/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": ".", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["."], + "exclude": ["dist", "node_modules"] +} diff --git a/hooks/email/useAccounts.ts b/hooks/email/useAccounts.ts new file mode 100644 index 000000000..fde7d0724 --- /dev/null +++ b/hooks/email/useAccounts.ts @@ -0,0 +1,153 @@ +import { useState, useCallback, useEffect } from 'react' + +/** + * Email account configuration + */ +export interface EmailAccount { + id: string + accountName: string + emailAddress: string + protocol: 'imap' | 'pop3' + hostname: string + port: number + encryption: 'none' | 'tls' | 'starttls' + username: string + isSyncEnabled: boolean + syncInterval: number + lastSyncAt: number | null + isSyncing: boolean + isEnabled: boolean + unreadCount: number + totalCount: number +} + +/** + * Hook to get email accounts for current user + * Manages account list, creation, and deletion + */ +export interface UseAccountsResult { + /** List of email accounts */ + accounts: EmailAccount[] + /** Whether accounts are being loaded */ + loading: boolean + /** Error loading accounts */ + error: Error | null + /** Add a new email account */ + addAccount: (account: Omit) => Promise + /** Delete an email account */ + deleteAccount: (accountId: string) => Promise + /** Refresh account list */ + refresh: () => Promise +} + +interface AccountState { + accounts: EmailAccount[] + loading: boolean + error: Error | null +} + +/** + * Initializes accounts hook for email account management + * @returns Email accounts and management interface + */ +export function useAccounts(): UseAccountsResult { + const [state, setState] = useState({ + accounts: [], + loading: true, + error: null, + }) + + /** + * Load email accounts from server + */ + const refresh = useCallback(async () => { + setState(prev => ({ ...prev, loading: true, error: null })) + + try { + // Simulate API call to fetch accounts + await new Promise(resolve => setTimeout(resolve, 500)) + + // In production, this would call: GET /api/v1/{tenant}/email_client/accounts + setState(prev => ({ + ...prev, + loading: false, + accounts: prev.accounts.length === 0 ? [] : prev.accounts, + })) + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to load accounts') + setState(prev => ({ + ...prev, + loading: false, + error, + })) + } + }, []) + + /** + * Create new email account + */ + const addAccount = useCallback( + async (account: Omit): Promise => { + try { + // Simulate API call to create account + await new Promise(resolve => setTimeout(resolve, 500)) + + // In production, this would call: POST /api/v1/{tenant}/email_client/accounts + const newAccount: EmailAccount = { + ...account, + id: `account_${Date.now()}`, + unreadCount: 0, + totalCount: 0, + } + + setState(prev => ({ + ...prev, + accounts: [...prev.accounts, newAccount], + })) + + return newAccount + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to add account') + setState(prev => ({ ...prev, error })) + throw error + } + }, + [] + ) + + /** + * Delete email account + */ + const deleteAccount = useCallback(async (accountId: string) => { + try { + // Simulate API call to delete account + await new Promise(resolve => setTimeout(resolve, 500)) + + // In production, this would call: DELETE /api/v1/{tenant}/email_client/accounts/{accountId} + setState(prev => ({ + ...prev, + accounts: prev.accounts.filter(a => a.id !== accountId), + })) + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to delete account') + setState(prev => ({ ...prev, error })) + throw error + } + }, []) + + /** + * Load accounts on mount + */ + useEffect(() => { + refresh() + }, [refresh]) + + return { + accounts: state.accounts, + loading: state.loading, + error: state.error, + addAccount, + deleteAccount, + refresh, + } +} diff --git a/hooks/email/useCompose.ts b/hooks/email/useCompose.ts new file mode 100644 index 000000000..0891f0cc3 --- /dev/null +++ b/hooks/email/useCompose.ts @@ -0,0 +1,155 @@ +import { useState, useCallback } from 'react' + +/** + * Email draft for composition + */ +export interface EmailDraft { + id: string + to: string[] + cc?: string[] + bcc?: string[] + subject: string + body: string + htmlBody?: string + attachments?: Array<{ + id: string + filename: string + mimeType: string + size: number + }> + createdAt: number + updatedAt: number + isDraft: boolean +} + +/** + * Hook to manage email composition + * Handles draft creation, updates, and persistence + */ +export interface UseComposeResult { + /** Current draft being composed */ + draft: EmailDraft | null + /** Update draft with new values */ + updateDraft: (updates: Partial>) => void + /** Clear current draft */ + clearDraft: () => void + /** Save draft to server or storage */ + saveDraft: () => Promise + /** Whether save is in progress */ + isSaving: boolean + /** Error saving draft */ + error: Error | null +} + +interface ComposeState { + draft: EmailDraft | null + isSaving: boolean + error: Error | null +} + +/** + * Initializes compose hook for email drafting + * @returns Draft management interface + */ +export function useCompose(): UseComposeResult { + const [state, setState] = useState({ + draft: null, + isSaving: false, + error: null, + }) + + /** + * Create new draft or get existing + */ + const ensureDraft = useCallback((): EmailDraft => { + if (state.draft) return state.draft + + const newDraft: EmailDraft = { + id: `draft_${Date.now()}`, + to: [], + cc: [], + bcc: [], + subject: '', + body: '', + htmlBody: '', + attachments: [], + createdAt: Date.now(), + updatedAt: Date.now(), + isDraft: true, + } + + setState(prev => ({ ...prev, draft: newDraft })) + return newDraft + }, [state.draft]) + + /** + * Update draft content + */ + const updateDraft = useCallback( + (updates: Partial>) => { + const draft = ensureDraft() + + setState(prev => ({ + ...prev, + draft: { + ...draft, + ...updates, + updatedAt: Date.now(), + }, + })) + }, + [ensureDraft] + ) + + /** + * Clear draft + */ + const clearDraft = useCallback(() => { + setState(prev => ({ + ...prev, + draft: null, + error: null, + })) + }, []) + + /** + * Save draft to server + */ + const saveDraft = useCallback(async () => { + if (!state.draft) return + + setState(prev => ({ ...prev, isSaving: true, error: null })) + + try { + // Simulate API call to save draft + await new Promise(resolve => setTimeout(resolve, 500)) + + // In production, this would call: + // POST /api/v1/{tenant}/email_client/messages (with isDraft=true) + setState(prev => ({ + ...prev, + isSaving: false, + draft: prev.draft + ? { ...prev.draft, updatedAt: Date.now() } + : null, + })) + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to save draft') + setState(prev => ({ + ...prev, + isSaving: false, + error, + })) + throw error + } + }, [state.draft]) + + return { + draft: state.draft, + updateDraft, + clearDraft, + saveDraft, + isSaving: state.isSaving, + error: state.error, + } +} diff --git a/hooks/email/useEmailStore.ts b/hooks/email/useEmailStore.ts new file mode 100644 index 000000000..a4a53b25a --- /dev/null +++ b/hooks/email/useEmailStore.ts @@ -0,0 +1,189 @@ +import { useCallback, useEffect, useState } from 'react' + +/** + * Email message for IndexedDB storage + */ +export interface StoredMessage { + id: string + messageId: string + from: string + to: string[] + subject: string + body: string + receivedAt: number + isRead: boolean + isStarred: boolean + folderName: string + createdAt: number +} + +/** + * Hook for IndexedDB wrapper for offline email storage + * Manages local caching of email messages + */ +export interface UseEmailStoreResult { + /** Retrieve all stored messages */ + getMessages: (folderName?: string) => Promise + /** Save messages to offline storage */ + saveMessages: (messages: StoredMessage[]) => Promise + /** Clear all stored messages */ + clear: () => Promise + /** Whether storage is initialized */ + isReady: boolean + /** Error if initialization failed */ + error: Error | null +} + +const DB_NAME = 'metabuilder_email' +const STORE_NAME = 'messages' +const DB_VERSION = 1 + +/** + * IndexedDB wrapper for offline email storage + * @returns Email store interface + */ +export function useEmailStore(): UseEmailStoreResult { + const [isReady, setIsReady] = useState(false) + const [error, setError] = useState(null) + const [db, setDb] = useState(null) + + /** + * Initialize IndexedDB connection + */ + useEffect(() => { + const initDB = async () => { + try { + const request = indexedDB.open(DB_NAME, DB_VERSION) + + request.onerror = () => { + const err = new Error('Failed to open IndexedDB') + setError(err) + console.error(err) + } + + request.onsuccess = () => { + const database = request.result + + // Create object store if needed + if (database.objectStoreNames.length === 0) { + database.close() + const createRequest = indexedDB.open(DB_NAME, DB_VERSION + 1) + createRequest.onupgradeneeded = (event: IDBVersionChangeEvent) => { + const db = (event.target as IDBOpenDBRequest).result + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'id' }) + } + } + createRequest.onsuccess = () => { + setDb(createRequest.result) + setIsReady(true) + } + } else { + setDb(database) + setIsReady(true) + } + } + + request.onupgradeneeded = (event: IDBVersionChangeEvent) => { + const database = (event.target as IDBOpenDBRequest).result + if (!database.objectStoreNames.contains(STORE_NAME)) { + database.createObjectStore(STORE_NAME, { keyPath: 'id' }) + } + } + } catch (err) { + const initError = err instanceof Error ? err : new Error('Failed to initialize storage') + setError(initError) + } + } + + initDB() + + return () => { + db?.close() + } + }, []) + + /** + * Retrieve messages from storage + */ + const getMessages = useCallback( + async (folderName?: string): Promise => { + if (!db) return [] + + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readonly') + const store = transaction.objectStore(STORE_NAME) + const request = store.getAll() + + request.onsuccess = () => { + const messages = request.result as StoredMessage[] + const filtered = folderName + ? messages.filter(m => m.folderName === folderName) + : messages + resolve(filtered) + } + + request.onerror = () => { + reject(new Error('Failed to retrieve messages')) + } + }) + }, + [db] + ) + + /** + * Save messages to storage + */ + const saveMessages = useCallback( + async (messages: StoredMessage[]): Promise => { + if (!db) return + + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readwrite') + const store = transaction.objectStore(STORE_NAME) + + messages.forEach(message => { + store.put({ ...message, createdAt: Date.now() }) + }) + + transaction.oncomplete = () => { + resolve() + } + + transaction.onerror = () => { + reject(new Error('Failed to save messages')) + } + }) + }, + [db] + ) + + /** + * Clear all stored messages + */ + const clear = useCallback(async (): Promise => { + if (!db) return + + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readwrite') + const store = transaction.objectStore(STORE_NAME) + const request = store.clear() + + request.onsuccess = () => { + resolve() + } + + request.onerror = () => { + reject(new Error('Failed to clear storage')) + } + }) + }, [db]) + + return { + getMessages, + saveMessages, + clear, + isReady, + error, + } +} diff --git a/hooks/email/useEmailSync.ts b/hooks/email/useEmailSync.ts new file mode 100644 index 000000000..8491e1912 --- /dev/null +++ b/hooks/email/useEmailSync.ts @@ -0,0 +1,103 @@ +import { useState, useCallback, useEffect } from 'react' + +/** + * Hook to trigger and monitor IMAP sync + * Manages email synchronization with progress tracking + */ +export interface UseEmailSyncResult { + /** Trigger email sync operation */ + sync: () => Promise + /** Whether sync is currently in progress */ + isSyncing: boolean + /** Progress percentage (0-100) */ + progress: number + /** Timestamp of last successful sync (ms) */ + lastSync: number | null + /** Error object if sync failed */ + error: Error | null +} + +interface SyncState { + isSyncing: boolean + progress: number + lastSync: number | null + error: Error | null +} + +/** + * Initializes email sync hook with progress tracking + * @param options Sync configuration options + * @returns Sync control and status interface + */ +export function useEmailSync(options?: { + autoSync?: boolean + syncInterval?: number + onSyncComplete?: () => void + onSyncError?: (error: Error) => void +}): UseEmailSyncResult { + const [state, setState] = useState({ + isSyncing: false, + progress: 0, + lastSync: null, + error: null, + }) + + /** + * Trigger email sync + * Simulates IMAP sync operation with progress tracking + */ + const sync = useCallback(async () => { + setState(prev => ({ ...prev, isSyncing: true, error: null })) + + try { + // Simulate sync operation with progress updates + for (let i = 0; i <= 100; i += 10) { + await new Promise(resolve => setTimeout(resolve, 100)) + setState(prev => ({ ...prev, progress: i })) + } + + const now = Date.now() + setState(prev => ({ + ...prev, + isSyncing: false, + progress: 100, + lastSync: now, + error: null, + })) + + options?.onSyncComplete?.() + } catch (error) { + const syncError = error instanceof Error ? error : new Error('Sync failed') + setState(prev => ({ + ...prev, + isSyncing: false, + error: syncError, + })) + options?.onSyncError?.(syncError) + } + }, [options]) + + /** + * Set up automatic sync interval + */ + useEffect(() => { + if (!options?.autoSync) return + + const interval = setInterval( + () => { + sync() + }, + options?.syncInterval || 5 * 60 * 1000 // Default 5 minutes + ) + + return () => clearInterval(interval) + }, [sync, options?.autoSync, options?.syncInterval]) + + return { + sync, + isSyncing: state.isSyncing, + progress: state.progress, + lastSync: state.lastSync, + error: state.error, + } +} diff --git a/hooks/email/useMailboxes.ts b/hooks/email/useMailboxes.ts new file mode 100644 index 000000000..287125362 --- /dev/null +++ b/hooks/email/useMailboxes.ts @@ -0,0 +1,148 @@ +import { useState, useCallback, useEffect } from 'react' + +/** + * Email folder/mailbox structure + */ +export interface Folder { + id: string + name: string + type: 'inbox' | 'sent' | 'drafts' | 'trash' | 'spam' | 'archive' | 'custom' + unreadCount: number + totalCount: number + children?: Folder[] +} + +/** + * Hook to get folder/mailbox hierarchy + * Manages folder structure and unread counts + */ +export interface UseMailboxesResult { + /** Folder hierarchy */ + folders: Folder[] + /** Whether folders are being loaded */ + loading: boolean + /** Error loading folders */ + error: Error | null + /** Refresh folder list */ + refresh: () => Promise + /** Update unread count for a folder */ + updateUnreadCount: (folderId: string, count: number) => void +} + +interface MailboxState { + folders: Folder[] + loading: boolean + error: Error | null +} + +/** + * Default folder structure for new accounts + */ +const DEFAULT_FOLDERS: Folder[] = [ + { + id: 'inbox', + name: 'Inbox', + type: 'inbox', + unreadCount: 0, + totalCount: 0, + }, + { + id: 'sent', + name: 'Sent', + type: 'sent', + unreadCount: 0, + totalCount: 0, + }, + { + id: 'drafts', + name: 'Drafts', + type: 'drafts', + unreadCount: 0, + totalCount: 0, + }, + { + id: 'trash', + name: 'Trash', + type: 'trash', + unreadCount: 0, + totalCount: 0, + }, + { + id: 'spam', + name: 'Spam', + type: 'spam', + unreadCount: 0, + totalCount: 0, + }, +] + +/** + * Initializes mailbox hook for folder management + * @param accountId Email account ID to load folders for + * @returns Folder hierarchy and management interface + */ +export function useMailboxes(accountId?: string): UseMailboxesResult { + const [state, setState] = useState({ + folders: DEFAULT_FOLDERS, + loading: false, + error: null, + }) + + /** + * Load mailbox folders from server + */ + const refresh = useCallback(async () => { + if (!accountId) { + setState(prev => ({ ...prev, folders: DEFAULT_FOLDERS })) + return + } + + setState(prev => ({ ...prev, loading: true, error: null })) + + try { + // Simulate API call to fetch folders + await new Promise(resolve => setTimeout(resolve, 500)) + + // In production, this would call: GET /api/v1/{tenant}/email_client/folders?accountId={accountId} + setState(prev => ({ + ...prev, + loading: false, + folders: DEFAULT_FOLDERS, + })) + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to load mailboxes') + setState(prev => ({ + ...prev, + loading: false, + error, + })) + } + }, [accountId]) + + /** + * Update unread count for a folder + */ + const updateUnreadCount = useCallback((folderId: string, count: number) => { + setState(prev => ({ + ...prev, + folders: prev.folders.map(folder => + folder.id === folderId ? { ...folder, unreadCount: count } : folder + ), + })) + }, []) + + /** + * Load folders on mount or when account changes + */ + useEffect(() => { + refresh() + }, [refresh]) + + return { + folders: state.folders, + loading: state.loading, + error: state.error, + refresh, + updateUnreadCount, + } +} diff --git a/hooks/email/useMessages.ts b/hooks/email/useMessages.ts new file mode 100644 index 000000000..0b7a931d9 --- /dev/null +++ b/hooks/email/useMessages.ts @@ -0,0 +1,211 @@ +import { useState, useCallback, useEffect } from 'react' + +/** + * Email message + */ +export interface Message { + id: string + messageId: string + from: string + to: string[] + cc?: string[] + bcc?: string[] + replyTo?: string + subject: string + textBody?: string + htmlBody?: string + headers?: Record + receivedAt: number + isRead: boolean + isStarred: boolean + isSpam: boolean + isDraft: boolean + isSent: boolean + isDeleted: boolean + attachmentCount: number + conversationId?: string + labels?: string[] + size?: number + createdAt: number + updatedAt: number +} + +/** + * Hook for CRUD operations on email messages + * Manages message list, reading, flagging, and deletion + */ +export interface UseMessagesResult { + /** List of messages */ + messages: Message[] + /** Whether messages are being loaded */ + loading: boolean + /** Error loading messages */ + error: Error | null + /** Mark message as read/unread */ + markRead: (messageId: string, isRead: boolean) => Promise + /** Mark message as spam/not spam */ + markSpam: (messageId: string, isSpam: boolean) => Promise + /** Delete message (soft delete) */ + delete: (messageId: string) => Promise + /** Star/unstar message */ + toggleStar: (messageId: string, isStarred: boolean) => Promise + /** Refresh message list */ + refresh: (folderId?: string) => Promise +} + +interface MessageState { + messages: Message[] + loading: boolean + error: Error | null +} + +/** + * Initializes messages hook for message management + * @param folderId Folder to load messages from + * @returns Message list and CRUD operations + */ +export function useMessages(folderId?: string): UseMessagesResult { + const [state, setState] = useState({ + messages: [], + loading: true, + error: null, + }) + + /** + * Load messages from server + */ + const refresh = useCallback( + async (folder?: string) => { + setState(prev => ({ ...prev, loading: true, error: null })) + + try { + // Simulate API call to fetch messages + await new Promise(resolve => setTimeout(resolve, 500)) + + // In production, this would call: + // GET /api/v1/{tenant}/email_client/messages?folderId={folderId} + setState(prev => ({ + ...prev, + loading: false, + messages: [], + })) + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to load messages') + setState(prev => ({ + ...prev, + loading: false, + error, + })) + } + }, + [] + ) + + /** + * Mark message as read/unread + */ + const markRead = useCallback(async (messageId: string, isRead: boolean) => { + try { + // Simulate API call to update message + await new Promise(resolve => setTimeout(resolve, 300)) + + // In production, this would call: + // PUT /api/v1/{tenant}/email_client/messages/{messageId} + setState(prev => ({ + ...prev, + messages: prev.messages.map(msg => + msg.id === messageId ? { ...msg, isRead } : msg + ), + })) + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to mark message') + setState(prev => ({ ...prev, error })) + throw error + } + }, []) + + /** + * Mark message as spam + */ + const markSpam = useCallback(async (messageId: string, isSpam: boolean) => { + try { + // Simulate API call to update message + await new Promise(resolve => setTimeout(resolve, 300)) + + // In production, this would call: + // PUT /api/v1/{tenant}/email_client/messages/{messageId} + setState(prev => ({ + ...prev, + messages: prev.messages.map(msg => + msg.id === messageId ? { ...msg, isSpam } : msg + ), + })) + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to mark spam') + setState(prev => ({ ...prev, error })) + throw error + } + }, []) + + /** + * Delete message (soft delete) + */ + const delete_ = useCallback(async (messageId: string) => { + try { + // Simulate API call to delete message + await new Promise(resolve => setTimeout(resolve, 300)) + + // In production, this would call: + // DELETE /api/v1/{tenant}/email_client/messages/{messageId} + setState(prev => ({ + ...prev, + messages: prev.messages.filter(msg => msg.id !== messageId), + })) + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to delete message') + setState(prev => ({ ...prev, error })) + throw error + } + }, []) + + /** + * Toggle star on message + */ + const toggleStar = useCallback(async (messageId: string, isStarred: boolean) => { + try { + // Simulate API call to update message + await new Promise(resolve => setTimeout(resolve, 300)) + + // In production, this would call: + // PUT /api/v1/{tenant}/email_client/messages/{messageId} + setState(prev => ({ + ...prev, + messages: prev.messages.map(msg => + msg.id === messageId ? { ...msg, isStarred } : msg + ), + })) + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to toggle star') + setState(prev => ({ ...prev, error })) + throw error + } + }, []) + + /** + * Load messages on mount or when folder changes + */ + useEffect(() => { + refresh(folderId) + }, [folderId, refresh]) + + return { + messages: state.messages, + loading: state.loading, + error: state.error, + markRead, + markSpam, + delete: delete_, + toggleStar, + refresh, + } +}