11 KiB
Email Client - Development Guide
Last Updated: 2026-01-23 Status: Bootloader Complete (Phase 8.1) Scope: Minimal Next.js bootloader for email_client package
Overview
This document guides development of the Email Client bootloader and integration with the email_client package.
Architecture
emailclient/ # Bootloader (this directory)
├── app/ # Next.js app
│ ├── page.tsx # Main page - loads email_client package
│ ├── layout.tsx # Redux provider + global styles
│ └── globals.css # Tailored email client styles
├── docker-compose.yml # Services: Postfix, Dovecot, Redis, Flask
├── .env.example # Configuration template
└── README.md # User guide
packages/email_client/ # Package (loaded by bootloader)
├── components/ui.json # Email UI component definitions
├── page-config/ # Declarative page layouts
├── permissions/roles.json # Email-specific RBAC
├── workflows/ # Email sync/send/compose workflows
└── redux/ # Email-specific Redux slices
Development Workflow
1. Local Setup
# Install dependencies
npm install
# Copy environment
cp .env.example .env.local
# Start services
docker-compose up -d
# Start dev server
npm run dev
Visit http://localhost:3000 - should load email_client package from packages/email_client/.
2. Package Integration
The bootloader (app/page.tsx) does three things:
- Load Package Metadata - Fetches
packages/email_client/package.json - Load Page Config - Fetches
packages/email_client/page-config/ - Render Components - Maps JSON config to FakeMUI components
Example flow:
// 1. Load metadata
GET /api/v1/packages/email_client/metadata
→ { id: 'email_client', name: 'Email Client', version: '1.0.0', ... }
// 2. Load page config
GET /api/v1/packages/email_client/page-config
→ { type: 'MailboxLayout', props: { ... }, children: [...] }
// 3. Render in React
<RenderComponent component={pageConfig} />
3. Redux Integration
Redux store is configured in app/layout.tsx:
const store = configureStore({
reducer: {
...coreReducers, // Auth, projects, workspace, workflow, nodes, asyncData
// Add email-specific slices from packages/email_client/redux/
}
})
Email slices to add in Phase 3:
emailAccountsSlice- List of email accountsemailFoldersSlice- Folder hierarchy per accountemailMessagesSlice- Message list with paginationemailSyncSlice- Sync status and progressemailComposeSlice- Compose form state
4. Component Hierarchy
Components are imported from @metabuilder/fakemui:
// Email-specific components (created in Phase 2)
import {
EmailCard,
FolderTree,
MailboxLayout,
ComposeWindow,
SyncStatusBadge,
AttachmentList
} from '@metabuilder/fakemui'
// Core FakeMUI components
import {
Box,
Button,
TextField,
Card,
Drawer,
Tab,
Tabs
} from '@metabuilder/fakemui'
Service Architecture
Postfix (SMTP Relay)
Port: 25 (clear), 587 (TLS)
Handles email delivery. Configure in .env.local:
POSTFIX_HOST=postfix
POSTFIX_PORT=25
POSTFIX_TLS_ENABLED=false
Test:
docker-compose exec postfix postfix status
Dovecot (IMAP/POP3)
Ports: 143 (IMAP), 993 (IMAP+TLS), 110 (POP3), 995 (POP3+TLS)
Stores emails. Configure:
DOVECOT_HOST=dovecot
DOVECOT_IMAP_PORT=143
DOVECOT_IMAP_TLS_PORT=993
Test:
# Create test account
docker-compose exec dovecot adduser test@example.com
# Connect
openssl s_client -connect localhost:993
a login test@example.com password
b list "" "*"
c select INBOX
d fetch 1 body
Redis Cache
Port: 6379
Caches sync state, user sessions, rate limits. No configuration needed.
Flask Email Service
Port: 5000
Python microservice implementing IMAP/SMTP operations.
Endpoints:
POST /sync/start # Start email sync
GET /sync/status # Check sync progress
POST /send # Send email
GET /messages/{accountId} # List messages
POST /messages/{id}/mark-read # Update message flags
PostgreSQL
Port: 5432
Metadata store for email accounts, messages, folders.
API Routing
The email client uses standard DBAL routing:
GET /api/v1/{tenant}/email_client/accounts
POST /api/v1/{tenant}/email_client/accounts
GET /api/v1/{tenant}/email_client/accounts/{id}
PUT /api/v1/{tenant}/email_client/accounts/{id}
DELETE /api/v1/{tenant}/email_client/accounts/{id}
GET /api/v1/{tenant}/email_client/folders
GET /api/v1/{tenant}/email_client/messages
POST /api/v1/{tenant}/email_client/messages/send
PUT /api/v1/{tenant}/email_client/messages/{id}
GET /api/v1/{tenant}/email_client/attachments/{id}/download
State Management
Redux Slices (Phase 3)
// email_client package will provide:
import {
emailAccountsSlice, // { accounts: EmailAccount[], loading, error }
emailFoldersSlice, // { folders: EmailFolder[], selectedFolder }
emailMessagesSlice, // { messages: EmailMessage[], pagination }
emailSyncSlice, // { isSyncing, progress, status }
emailComposeSlice // { to, cc, bcc, subject, body, attachments }
} from '@metabuilder/redux-slices'
// Use in components:
import { useAppDispatch, useAppSelector } from '@metabuilder/redux-core'
export function EmailList() {
const dispatch = useAppDispatch()
const { messages, loading } = useAppSelector(state => state.emailMessages)
useEffect(() => {
dispatch(fetchEmailMessages({ accountId, folderId }))
}, [accountId, folderId])
}
Async Data Hooks (Phase 3)
// Use useReduxAsyncData for email operations:
import { useReduxAsyncData, useReduxMutation } from '@metabuilder/api-clients'
export function SyncEmails({ accountId }) {
const { data: status, refetch } = useReduxAsyncData(
async () => {
const res = await fetch(`/api/v1/{tenant}/email_client/sync/${accountId}/status`)
return res.json()
},
{ refetchInterval: 5000 } // Poll every 5 seconds
)
const { mutate: startSync } = useReduxMutation(
async () => {
const res = await fetch(`/api/v1/{tenant}/email_client/sync/${accountId}`, {
method: 'POST'
})
return res.json()
},
{ onSuccess: () => refetch() }
)
return (
<div>
<p>Sync status: {status?.status}</p>
<button onClick={() => startSync()}>Sync Now</button>
</div>
)
}
Component Pattern
All email components use FakeMUI + data-testid for accessibility:
// Email card component
<EmailCard
from="sender@example.com"
subject="Hello"
preview="This is a test email"
receivedAt={Date.now()}
isRead={false}
isStarred={false}
onSelect={() => openEmail(id)}
onToggleRead={(isRead) => updateReadStatus(id, isRead)}
onToggleStar={(starred) => updateStarred(id, starred)}
data-testid="email-card-123" // Accessibility
/>
Testing
Unit Tests
npm run test
Test email components in __tests__/ directory.
E2E Tests
npm run test:e2e
Test workflows:
- Create email account
- Receive email (mock)
- Send email
- Mark as read/unread
- Delete email
Example test:
// tests/email-workflow.spec.ts
test('user can create account and receive email', async ({ page }) => {
await page.goto('http://localhost:3000')
// Create account
await page.click('text=Add Account')
await page.fill('input[placeholder="Email Address"]', 'test@example.com')
await page.fill('input[placeholder="Password"]', 'password123')
await page.click('button:has-text("Connect")')
// Wait for sync
await page.waitForSelector('text=Inbox')
// Should show empty inbox initially
const emailCount = await page.locator('[data-testid="email-card"]').count()
expect(emailCount).toBe(0)
})
Debugging
Redux DevTools
Install Redux DevTools browser extension. Inspect email state:
Redux → Actions filter: emailAccounts, emailMessages, emailSync
Network Tab
Monitor API calls:
GET /api/v1/{tenant}/email_client/accounts
POST /api/v1/{tenant}/email_client/accounts/{id}/sync
GET /api/v1/{tenant}/email_client/messages?accountId=...&folderId=...
Console Logging
App logs with prefix [emailclient]:
console.log('[emailclient] Syncing account:', accountId)
console.error('[emailclient] Sync failed:', error)
Docker Logs
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f email-service
docker-compose logs -f postfix
docker-compose logs -f dovecot
Performance Optimization
1. Message Virtualization
For large email lists, use React virtualization:
import { FixedSizeList } from 'react-window'
<FixedSizeList
height={600}
itemCount={messages.length}
itemSize={80}
>
{({ index, style }) => (
<EmailCard style={style} {...messages[index]} />
)}
</FixedSizeList>
2. Request Deduplication
useReduxAsyncData automatically deduplicates concurrent requests to same endpoint.
3. Lazy Loading
Load attachments on demand:
const { data: attachment } = useReduxAsyncData(
async () => {
const res = await fetch(`/api/v1/{tenant}/email_client/attachments/${id}`)
return res.blob()
},
{ enabled: isExpanded } // Only fetch if expanded
)
Security
Multi-Tenant Safety
All API queries must include tenantId filter - enforced in DBAL:
// DBAL ensures this in row_level ACL:
// "emailClientId IN (SELECT id FROM EmailClient WHERE userId = $user.id AND tenantId = $context.tenantId)"
Rate Limiting
Email operations have rate limits:
Sync start: 5 per minute per account
Message send: 10 per minute per account
List operations: 100 per minute per tenant
Attachment Validation
Attachments are scanned on upload:
- Max size: 25 MB (configurable)
- Blocked MIME types: executable, script
- Virus scan: Integration with ClamAV (future)
Troubleshooting
Email Sync Not Working
-
Check service status:
docker-compose ps -
Check Dovecot logs:
docker-compose logs dovecot | grep -i error -
Test IMAP connection:
openssl s_client -connect localhost:993 -
Check Flask logs:
docker-compose logs email-service | grep -i error
Docker Services Down
Restart:
docker-compose restart
Or full reset:
docker-compose down -v # Remove volumes
docker-compose up -d
Redux State Not Updating
- Check Redux DevTools for action dispatch
- Check console for errors
- Verify API response in Network tab
- Check DBAL ACL (multi-tenant filtering)
Next Steps
After bootloader (Phase 8.1) is complete:
- Phase 3: Redux slices for email state
- Phase 4: Custom hooks for email operations
- Phase 5: Email package component definitions
- Phase 6: Email workflow plugins (sync, send)
- Phase 7: Flask email service
- Phase 8: Full integration testing
References
- Root CLAUDE.md - Project-wide guide
- Packages Guide - Package system
- DBAL Guide - Database layer
- Workflow Guide - Workflow engine
- FakeMUI Guide - Component library