mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
feat(hooks): create email custom hooks package
This commit is contained in:
12
hooks/email/index.ts
Normal file
12
hooks/email/index.ts
Normal file
@@ -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'
|
||||
70
hooks/email/package.json
Normal file
70
hooks/email/package.json
Normal file
@@ -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"
|
||||
}
|
||||
27
hooks/email/tsconfig.json
Normal file
27
hooks/email/tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
153
hooks/email/useAccounts.ts
Normal file
153
hooks/email/useAccounts.ts
Normal file
@@ -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<EmailAccount, 'id' | 'unreadCount' | 'totalCount'>) => Promise<EmailAccount>
|
||||
/** Delete an email account */
|
||||
deleteAccount: (accountId: string) => Promise<void>
|
||||
/** Refresh account list */
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
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<AccountState>({
|
||||
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<EmailAccount, 'id' | 'unreadCount' | 'totalCount'>): Promise<EmailAccount> => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
155
hooks/email/useCompose.ts
Normal file
155
hooks/email/useCompose.ts
Normal file
@@ -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<Omit<EmailDraft, 'id' | 'createdAt' | 'updatedAt'>>) => void
|
||||
/** Clear current draft */
|
||||
clearDraft: () => void
|
||||
/** Save draft to server or storage */
|
||||
saveDraft: () => Promise<void>
|
||||
/** 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<ComposeState>({
|
||||
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<Omit<EmailDraft, 'id' | 'createdAt' | 'updatedAt'>>) => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
189
hooks/email/useEmailStore.ts
Normal file
189
hooks/email/useEmailStore.ts
Normal file
@@ -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<StoredMessage[]>
|
||||
/** Save messages to offline storage */
|
||||
saveMessages: (messages: StoredMessage[]) => Promise<void>
|
||||
/** Clear all stored messages */
|
||||
clear: () => Promise<void>
|
||||
/** 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<Error | null>(null)
|
||||
const [db, setDb] = useState<IDBDatabase | null>(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<StoredMessage[]> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
103
hooks/email/useEmailSync.ts
Normal file
103
hooks/email/useEmailSync.ts
Normal file
@@ -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<void>
|
||||
/** 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<SyncState>({
|
||||
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,
|
||||
}
|
||||
}
|
||||
148
hooks/email/useMailboxes.ts
Normal file
148
hooks/email/useMailboxes.ts
Normal file
@@ -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<void>
|
||||
/** 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<MailboxState>({
|
||||
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,
|
||||
}
|
||||
}
|
||||
211
hooks/email/useMessages.ts
Normal file
211
hooks/email/useMessages.ts
Normal file
@@ -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<string, string>
|
||||
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<void>
|
||||
/** Mark message as spam/not spam */
|
||||
markSpam: (messageId: string, isSpam: boolean) => Promise<void>
|
||||
/** Delete message (soft delete) */
|
||||
delete: (messageId: string) => Promise<void>
|
||||
/** Star/unstar message */
|
||||
toggleStar: (messageId: string, isStarred: boolean) => Promise<void>
|
||||
/** Refresh message list */
|
||||
refresh: (folderId?: string) => Promise<void>
|
||||
}
|
||||
|
||||
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<MessageState>({
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user