feat(hooks): create email custom hooks package

This commit is contained in:
2026-01-23 19:33:52 +00:00
parent 111236a17f
commit f8077b5273
9 changed files with 1068 additions and 0 deletions

12
hooks/email/index.ts Normal file
View 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
View 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
View 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
View 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
View 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,
}
}

View 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
View 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
View 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
View 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,
}
}