fix: Phase 1 critical+high fixes (17/18 items)
CRITICAL fixes: - C1: Error swallowing - throw TransportError on network failure in getEmails/searchEmails - C2: Recurrence expansion ID delimiter changed from ':' to '::occurrence::' - C3: Cross-account calendar event UID dedup after multi-account aggregation - C4: Admin session token revocation via JTI blacklist on logout - C6: FTS5 schema-drop - add warning log for automatic reindex trigger - C7: Settings lock - gate updateSetting() with isSettingLocked() check - C8: Offline push pause - add offline event handler that closes push transports HIGH fixes: - H1: Push handler - add ContactCard and FileNode branches - H2: WS fallback - await state snapshot before reconcileAfterWebSocketFallback - H3: Auth rate limiting - add checkUserAuthRateLimit to session and token routes - H4: OAuth logs - strip access_token from error log context - H7: Template XSS - apply DOMPurify to HTML template body on import - H8: Secure cookie - derive from x-forwarded-proto, not NODE_ENV - H9: bcrypt fix - remove bcrypt prefixes from isHashed() so scrypt-only - H13: calendarTasksEnabled - apply admin gate at runtime in calendar page - H14: Task mutations - add try/catch error handling to update/delete/toggle - H18: autoSelectReplyIdentity default changed from false to true Deferred: P1.3 (C5 auth localStorage encryption) - requires custom Zustand persist adapter.
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
# VNCmail+ v1.7.8 → v1.8.0 Development Plan
|
||||
|
||||
**Date:** 2026-08-07
|
||||
**Target:** `brvncde-dotcom/vncmail-plus` (Next.js 16)
|
||||
**Baseline:** VNCmailgraph audit (82 findings) + Bugs VNCmail+.docx (12 missing features)
|
||||
|
||||
---
|
||||
|
||||
## Phase Structure
|
||||
|
||||
| Phase | Focus | Features + Fixes | Autonomy |
|
||||
|-------|-------|-----------------|----------|
|
||||
| **P1** | Critical fixes | 8 CRITICAL + 10 HIGH audit findings | Full autonomous |
|
||||
| **P2** | Missing features | 12 docx features, code reuse from Angular | Full autonomous (board decided VNCtalk/Collabora/ActionWheel) |
|
||||
| **P3** | Security hardening | Auth, cookies, rate limiting, encryption | Full autonomous |
|
||||
| **P4** | Polish & sync | Remaining HIGH + cross-feature substrate | Full autonomous |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Critical Bug Fixes (Autonomous)
|
||||
|
||||
### P1.1 — Error Swallowing (CRITICAL C1)
|
||||
**Issue:** `getEmails`/`searchEmails` return empty on transport failure. Network-down = empty folder.
|
||||
|
||||
**Fix:** Sample `transportHealth().transportFailureCount()` before/after JMAP reads. If incremented, throw `TransportError` instead of returning `{ emails: [], ... }`. Store catches and shows connectivity banner.
|
||||
- Files: `lib/jmap/client.ts`, `stores/email-store.ts`, `lib/jmap/transport-health.ts`
|
||||
- Reuse: None
|
||||
- Effort: 2h
|
||||
|
||||
### P1.2 — FTS5 Schema-Drop Rebuild (CRITICAL C6)
|
||||
**Issue:** Schema version bump drops all tables without auto-rebuild.
|
||||
|
||||
**Fix:** After `DROP TABLE IF EXISTS`, trigger automatic `catchUpAll()` in the same operation. Add user-facing "Rebuilding search index..." indicator.
|
||||
- Files: `lib/mail-index/store.ts`, `lib/mail-index/reindex.ts`
|
||||
- Reuse: None
|
||||
- Effort: 2h
|
||||
|
||||
### P1.3 — Auth Credentials in localStorage (CRITICAL C5)
|
||||
**Issue:** `auth-storage` + `account-storage` contain server URLs and usernames in plaintext.
|
||||
|
||||
**Fix:** Encrypt the Zustand persist payload for these two stores using a key derived from session secret or a per-device key. Scope: `auth-store.ts` and `account-store.ts` persist middleware.
|
||||
- Files: `stores/auth-store.ts`, `stores/account-store.ts`, new `lib/auth/local-storage-crypto.ts`
|
||||
- Reuse: AES-256-GCM pattern from `lib/auth/crypto.ts`
|
||||
- Effort: 4h
|
||||
|
||||
### P1.4 — Settings Lock Bypass (CRITICAL C7)
|
||||
**Issue:** `updateSetting()` has no policy check. Admin locks bypassable via store.
|
||||
|
||||
**Fix:** Gate `updateSetting(key, value)` with `isSettingLocked(key)`. Add `{ force: true }` opt-in for legitimate bypassers (auth bootstrap, settings sync). Audit all `updateSetting` call sites.
|
||||
- Files: `stores/settings-store.ts`, `stores/auth-store.ts`, `lib/settings-sync.ts`
|
||||
- Reuse: None
|
||||
- Effort: 3h
|
||||
|
||||
### P1.5 — Offline Push Detection (CRITICAL C8)
|
||||
**Issue:** No `offline` event listener. Transports retry blindly, draining battery.
|
||||
|
||||
**Fix:** Add `window.addEventListener('offline', ...)` that pauses all push transports. Add `navigator.onLine` gate on each transport cycle. Wire `transportHealth().likelyOffline` into push lifecycle.
|
||||
- Files: `lib/jmap/client.ts`
|
||||
- Reuse: None
|
||||
- Effort: 1h
|
||||
|
||||
### P1.6 — Admin Session Revocation (CRITICAL C4)
|
||||
**Issue:** AES-256-GCM token has no server-side revocation.
|
||||
|
||||
**Fix:** Add in-memory token blacklist (Set with TTL) in admin session middleware. Logout adds token `jti` to blacklist. Cleanup expired entries on verification.
|
||||
- Files: `lib/admin/session.ts`, `app/api/admin/auth/route.ts`
|
||||
- Reuse: None
|
||||
- Effort: 3h
|
||||
|
||||
### P1.7 — Calendar ID Collision (CRITICAL C2)
|
||||
**Issue:** Recurrence expansion (`:` delimiter) collides with shared event prefix.
|
||||
|
||||
**Fix:** Change expansion delimiter from `:` to `::occurrence::`. Update `stripLocalAccountPrefix` and all ID parsing.
|
||||
- Files: `lib/recurrence-expansion.ts`, `stores/calendar-store.ts`
|
||||
- Reuse: None
|
||||
- Effort: 1h
|
||||
|
||||
### P1.8 — Calendar Cross-Account Dedup (CRITICAL C3)
|
||||
**Issue:** Multi-account event aggregation has no UID dedup.
|
||||
|
||||
**Fix:** After `Promise.all(...).flat()`, run `uniqueBy(events, e => e.uid + e.recurrenceId)` pass.
|
||||
- Files: `stores/calendar-store.ts`
|
||||
- Reuse: None
|
||||
- Effort: 1h
|
||||
|
||||
### P1.9 — HIGH Fixes Batch
|
||||
- **H4:** Strip `access_token` from OAuth error logs (`lib/oauth/token-exchange.ts`) — 30min
|
||||
- **H9:** Fix bcrypt in `isHashed()` (`lib/admin/password.ts`) — 1h
|
||||
- **H18:** Default `autoSelectReplyIdentity` to `true` (`stores/settings-store.ts`) — 30min
|
||||
- **H7:** DOMPurify HTML template body on import (`lib/template-utils.ts`) — 1h
|
||||
- **H13:** `calendarTasksEnabled` runtime enforcement (`stores/task-store.ts`, `components/calendar/`) — 2h
|
||||
- **H14:** try/catch + toast on task mutations (`stores/task-store.ts`) — 1h
|
||||
- **H8:** Derive `secure` cookie from `x-forwarded-proto` (`lib/admin/session.ts`) — 1h
|
||||
- **H3:** Rate-limit user auth endpoints (`app/api/auth/*`) — 3h
|
||||
- **H2:** Await state snapshot in `reconcileAfterWebSocketFallback` (`lib/jmap/client.ts`) — 2h
|
||||
- **H1:** Push handler: add ContactCard/FileNode branches (`stores/email-store.ts`) — 3h
|
||||
|
||||
**P1 Total:** ~32h
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Missing Features (Autonomous, Code Reuse)
|
||||
|
||||
### P2.1 — Extended Signatures (Multiple per Identity + HTML Editor)
|
||||
**Docx:** "Add option to create various email signatures and to select standard signature for new emails and for replies"
|
||||
|
||||
**Reuse from Angular:**
|
||||
- Models: `signature.model.ts` (Signature interface), `identity.model.ts` (zimbraPrefDefaultSignatureId)
|
||||
- API: Signature CRUD endpoints, `modifySignaturePrefs()` pattern
|
||||
- UI logic: Quill editor toolbar config → `react-quill-new` equivalent
|
||||
|
||||
**Implementation:**
|
||||
1. New `stores/signature-store.ts` — Signature[] with CRUD + identity assignment
|
||||
2. New `components/settings/signature-settings.tsx` — list management page
|
||||
3. New `components/settings/signature-editor-modal.tsx` — Quill-based HTML editor
|
||||
4. Extend `components/identity/identity-form.tsx` — default/forward-reply signature picker
|
||||
5. Extend `components/email/email-composer.tsx` — signature selector dropdown in compose
|
||||
6. JMAP/Sieve integration: if the server stores signatures as Sieve or Identity properties, map accordingly
|
||||
|
||||
**Effort:** 12h
|
||||
|
||||
### P2.2 — Create Appointment from Email
|
||||
**Docx:** "create a calendar entry from an email with recipients as participants and email text in description"
|
||||
|
||||
**Reuse from Angular:**
|
||||
- `Appointment` interface from `appoinment.model.ts`
|
||||
- Calendar compose pre-fill logic from `calendar-compose.component.ts`
|
||||
- API payload shapes for event creation
|
||||
|
||||
**Implementation:**
|
||||
1. New `components/email/create-appointment-button.tsx` — button in email toolbar
|
||||
2. Extend `components/calendar/event-modal.tsx` — accept pre-fill props (title=subject, description=body, participants=from+to+cc)
|
||||
3. Wire "Create Appointment" action into email-viewer toolbar + context menu
|
||||
|
||||
**Effort:** 6h
|
||||
|
||||
### P2.3 — Folder Sharing (Mail, Calendar, Contacts, Files)
|
||||
**Docx:** "Share Folder feature" + "Sharing to see all folders shared by me and with me"
|
||||
|
||||
**Reuse from Angular:**
|
||||
- `ShareFolderComponent` (564 lines) — full sharing dialog with email autocomplete, role selection
|
||||
- `PreferencesSharingComponent` (556 lines) — "shared by me" / "shared with me" views
|
||||
- `AllSharingFoldersDialogComponent` — tree-based folder browser
|
||||
- API: share/revoke/accept/decline endpoints and payload shapes
|
||||
|
||||
**Implementation:**
|
||||
1. New `components/sharing/share-folder-dialog.tsx` — modal with email autocomplete, role picker (read/read-write/admin), message
|
||||
2. New `components/sharing/share-folder-revoke-dialog.tsx` — revoke confirmation
|
||||
3. New `components/sharing/accept-share-dialog.tsx` — accept incoming share
|
||||
4. New `components/settings/sharing-settings.tsx` — "Shared by me" / "Shared with me" tabs with folder tree
|
||||
5. Extend folder context menus in email sidebar, calendar sidebar, contacts sidebar, files sidebar with "Share Folder..." action
|
||||
6. New `stores/sharing-store.ts` — tracking shares state
|
||||
7. API routes: `app/api/sharing/*` — share/revoke/accept/decline/find
|
||||
|
||||
**Effort:** 18h
|
||||
|
||||
### P2.4 — Calendar Dashlet (Mini Calendar in Mail View)
|
||||
**Docx:** "Show Calendar in a dashlet in the bottom left corner"
|
||||
|
||||
**Reuse from Angular:**
|
||||
- `sidebar-mini-calendar.component.ts` (701 lines) — month grid, swipe navigation, tooltip
|
||||
- Tooltip directive logic for fetching day events
|
||||
|
||||
**Implementation:**
|
||||
1. New `components/calendar/mini-calendar-dashlet.tsx` — compact month grid widget
|
||||
2. Integrate into mail sidebar or bottom-left overlay in the mail page layout
|
||||
3. Show date dots for days with events, today highlight, click to navigate to calendar
|
||||
4. Optionally: toggleable via user setting "Show calendar dashlet"
|
||||
|
||||
**Effort:** 8h
|
||||
|
||||
### P2.5 — Email Import (.eml, tgz, zip)
|
||||
**Docx:** "Missing: feature to import emails"
|
||||
|
||||
**Reuse from Angular:**
|
||||
- `ImportExportComponent` (513 lines) — import types, destination folder, resolve settings
|
||||
- `preferenceService.importFromFile()` — API request shape
|
||||
- CSV type auto-detection logic
|
||||
|
||||
**Implementation:**
|
||||
1. Extend existing `.eml` import (already partial — see `lib/eml-import.ts` and `components/email/` for `.eml` preview)
|
||||
2. Add ZIP/TGZ/TAR archive import — extract, iterate, import each `.eml`
|
||||
3. New `components/settings/import-settings.tsx` — import UI with file picker, destination folder, conflict resolution
|
||||
4. API route: extend `app/api/account/*` or new `app/api/import/*`
|
||||
|
||||
**Effort:** 10h
|
||||
|
||||
### P2.6 — Contact Import (vCard/CSV)
|
||||
**Docx:** "Missing: feature to import contacts"
|
||||
|
||||
**Reuse from Angular:**
|
||||
- `contact-file-import-dialog.component.ts` (228 lines) — CSV upload, folder selector
|
||||
- `contactService.importContacts()` — API endpoint
|
||||
- vCard parsing: `lib/vcard.ts` already exists in codebase
|
||||
|
||||
**Implementation:**
|
||||
1. Extend existing `components/contacts/contact-import-dialog.tsx` to support CSV (currently vCard-only)
|
||||
2. Add CSV column mapping UI (map CSV columns to contact fields)
|
||||
3. Folder selection for import destination
|
||||
4. Dedup handling
|
||||
|
||||
**Effort:** 6h
|
||||
|
||||
### P2.7 — Free/Busy View
|
||||
**Docx:** "Missing: free/busy view"
|
||||
|
||||
**Reuse from Angular:**
|
||||
- `scheduler.component.ts` (1303 lines) — full free/busy grid
|
||||
- `scheduler-utils.ts` — free-busy status constants (`fba` values, `FBA_TO_PTST` mapping)
|
||||
- `schedule-assistant.component.ts` (1081 lines) — suggestion engine algorithm
|
||||
- Free/busy URL format: `{serverURL}/home/{email}?fmt=freebusy`
|
||||
|
||||
**Implementation:**
|
||||
1. New `components/calendar/free-busy-view.tsx` — attendee-row × time-slot grid
|
||||
2. Color-coded slots: free (white), busy (red), tentative (yellow), out-of-office (purple)
|
||||
3. Integration into event-modal when adding participants — show availability inline
|
||||
4. Also queries `resources_bookings` table (see P2.8) to show resource availability in the same grid
|
||||
5. New `lib/calendar-freebusy.ts` — fetch and parse free/busy data (user calendars + resource bookings)
|
||||
|
||||
**Effort:** 16h (was 14h, added resource booking integration)
|
||||
|
||||
### P2.8 — Resources/Equipment Booking (VNCdirectory-backed PostgreSQL)
|
||||
**Docx:** "Missing: resources/equipment"
|
||||
**Architecture decision:** Resources managed in **separate PostgreSQL table**, mapped to **VNCdirectory** for centralized cross-application management (rooms, cars, equipment, etc.). Independent from mail client — this is a platform-level resource system.
|
||||
|
||||
**Reuse from Angular:**
|
||||
- `calendar-equipment-dialog.component.ts` (373 lines) — equipment browser UI patterns
|
||||
- `calendar-equipment-autocomplete.component.ts` (266 lines) — autocomplete UX
|
||||
- GAL query: `zimbraCalResType === "Equipment"` → adapt to VNCdirectory resource type filter
|
||||
|
||||
**Backend (New):**
|
||||
1. **PostgreSQL migration** — `resources` table:
|
||||
- `id` UUID PK
|
||||
- `tenant_id` UUID (VNCdirectory tenant scope)
|
||||
- `name` text
|
||||
- `type` enum (room, vehicle, equipment, other)
|
||||
- `location` text (building/floor/room)
|
||||
- `capacity` integer nullable
|
||||
- `description` text
|
||||
- `contact_email` text (responsible person)
|
||||
- `is_active` boolean
|
||||
- `metadata` jsonb (extensible: photo URL, amenities, access hours, etc.)
|
||||
- `created_at`, `updated_at` timestamps
|
||||
|
||||
2. **`resources` table → VNCdirectory sync** — VNCdirectory is the canonical source. Options:
|
||||
- **Pull model:** VNCdirectory writes to this table via API/webhook
|
||||
- **Push model:** This service syncs changes back to VNCdirectory
|
||||
- **Read-through:** Query VNCdirectory directly for resource listings; cache in PostgreSQL for availability booking
|
||||
- Decision needed: which direction is authoritative? (Assume VNCdirectory → this table for Phase 1)
|
||||
|
||||
3. **New API routes:** `app/api/resources/`
|
||||
- `GET /api/resources` — list/search resources (type filter, location filter, tenant scoped)
|
||||
- `GET /api/resources/[id]` — single resource detail
|
||||
- `GET /api/resources/[id]/availability?start=&end=` — free/busy for a resource
|
||||
- `POST /api/resources/[id]/book` — create booking for a resource
|
||||
- `DELETE /api/resources/[id]/book/[bookingId]` — cancel booking
|
||||
|
||||
4. **New `lib/resources/` service layer:**
|
||||
- `lib/resources/client.ts` — fetch/query resources from VNCdirectory or local DB
|
||||
- `lib/resources/availability.ts` — check resource availability for time range
|
||||
- `lib/resources/booking.ts` — book/cancel resource
|
||||
- `lib/resources/sync.ts` — sync with VNCdirectory (if push-model needed)
|
||||
|
||||
5. **Conflict checking:**
|
||||
- When booking, check `resources_bookings` table for overlapping time ranges
|
||||
- Return conflicts + alternative slots
|
||||
|
||||
**Frontend:**
|
||||
1. Extend `components/calendar/event-modal.tsx` — "Resources" tab with:
|
||||
- Resource type filter (room, vehicle, equipment)
|
||||
- Searchable autocomplete (name, location, capacity)
|
||||
- Availability indicator (free/busy for event time range)
|
||||
2. New `components/calendar/resource-picker.tsx` — reusable resource selection component
|
||||
3. Booked resources appear in event detail, participant list, and email invitation
|
||||
|
||||
**Effort:** 20h (was 10h, doubled for PostgreSQL + VNCdirectory integration)
|
||||
|
||||
### P2.9 — VNCtalk Video Meeting Integration
|
||||
**Docx:** "Missing: integration with VNCtalk -> create Videomeeting"
|
||||
|
||||
**Reuse from Angular:**
|
||||
- `app.service.ts` → `createNewMeeting()` / `updateScheduledMeeting()` API calls
|
||||
- Payload: `POST /api/createnewmeeting { name, start, end, invitees, password, description, invid?, rev?, ms? }`
|
||||
- `createOrUpdateMeeting()` in `edit-appointment-dialog.component.ts` — builds payload from appointment
|
||||
|
||||
**Implementation:**
|
||||
1. New `lib/vnctalk/client.ts` — VNCtalk API client (create/update meeting)
|
||||
2. Extend `components/calendar/event-modal.tsx` — "Create VNCtalk Meeting" toggle/button
|
||||
3. Store meeting JID on CalendarEvent for updates
|
||||
4. Add meeting link to event detail popover and email invitation body
|
||||
|
||||
**Effort:** 8h
|
||||
|
||||
### P2.10 — Collabora Online Editing
|
||||
**Docx:** "Add feature to collaborate with Collabora"
|
||||
|
||||
**Reuse from Angular:**
|
||||
- `owncloud.service.ts` → `getDocumentUrl(fileId, useCollabora)` with RichDocuments API
|
||||
- `POST ocs/v2.php/apps/richdocuments/api/v1/document?format=json`
|
||||
- Config: `collaboraBaseUrl` from admin config
|
||||
|
||||
**Implementation:**
|
||||
1. New `lib/collabora/client.ts` — fetch editing URL from Collabora server
|
||||
2. New `components/files/collabora-editor.tsx` — iframe-based editor embedding
|
||||
3. Extend `components/files/file-browser.tsx` — "Edit with Collabora" action for office files
|
||||
4. Admin config: `collaboraBaseUrl` in policy/config
|
||||
|
||||
**Effort:** 10h
|
||||
|
||||
### P2.11 — Calendar Enhancements Batch
|
||||
**Docx:** Multiple calendar improvements
|
||||
|
||||
**2.11a — Clickable links in emails**
|
||||
- Already partially done (TipTap Link extension). Verify link rendering in calendar event descriptions.
|
||||
- Effort: 2h
|
||||
|
||||
**2.11b — Contact details of participants**
|
||||
- Add popover on participant names in event-modal showing contact card. Reuse `components/contacts/contact-detail.tsx` data.
|
||||
- Effort: 4h
|
||||
|
||||
**2.11c — Reply / Reply to All in meetings**
|
||||
- Extend event-modal with "Reply" and "Reply to All" buttons that open composer pre-filled with participant emails.
|
||||
- Effort: 3h
|
||||
|
||||
**2.11d — Timezone support**
|
||||
- Add timezone picker to event-modal. Use `date-fns-tz` (already a dependency). Display times in event timezone with user timezone conversion.
|
||||
- Effort: 6h
|
||||
|
||||
**2.11e — Map links**
|
||||
- Extract address from event location, generate Google Maps / OpenStreetMap link.
|
||||
- Effort: 2h
|
||||
|
||||
### P2.12 — Action Wheel (Custom Radial Menu)
|
||||
**Docx:** "Recreate Action Wheel or better functionality"
|
||||
|
||||
**Board decision:** Build custom radial menu.
|
||||
|
||||
**Implementation:**
|
||||
1. New `components/ui/radial-menu.tsx` — SVG-based radial menu with configurable items
|
||||
2. Supports: mail actions (reply, forward, delete, archive, mark read, move, tag), contact actions, file actions
|
||||
3. Trigger: long-press on mobile, right-click on desktop, or dedicated button
|
||||
4. Animations: CSS rotate + scale transitions
|
||||
5. Keyboard accessible
|
||||
|
||||
**Effort:** 10h
|
||||
|
||||
### P2.13 — IDP Integration — VNCdirectory Admin Configuration Panel
|
||||
**Docx:** "IDP integration will be towards VNCdirectory (openldap, simplesamlphp, 2fa etc.)"
|
||||
**Architecture decision:** Add admin UI for configuring VNCdirectory connection settings. Reuse auth patterns from VNCmail-analysis `api/auth-proxy/`.
|
||||
|
||||
**Reuse from Angular (api/auth-proxy/):**
|
||||
- `config/config.js.example` — full auth configuration schema (SAML, LDAP, VNCdirectory, 2FA, hybrid auth)
|
||||
- `config/passport.js` — SAML strategy + JWT custom strategy setup
|
||||
- `routes/index.js` — login/logout/SAML callback/LDAP search/2FA/TOTP routes
|
||||
- `utils/common.js` — JWT verification, Zimbra preauth token creation
|
||||
- Auth dependencies: `@node-saml/passport-saml`, `passport`, `jsonwebtoken`, `ldapjs`
|
||||
|
||||
**Current VNCmail+ auth stack vs Angular auth stack:**
|
||||
| Feature | Angular (Zimbra) | VNCmail+ (Stalwart) |
|
||||
|---------|-----------------|---------------------|
|
||||
| Primary auth | SAML 2.0 via Passport | OAuth/OIDC via Stalwart |
|
||||
| Identity source | Zimbra LDAP + VNCdirectory | Stalwart internal + OIDC |
|
||||
| 2FA | VNCdirectory TOTP/DUO | TOTP via Stalwart admin API |
|
||||
| Directory integration | Redmine API (`contactsApiUrl`) | ❌ None |
|
||||
| LDAP backend | `ldapjs` → Zimbra LDAP | ❌ None |
|
||||
| SSO/Federation | JWT deeplinks + SAML | Cookie-based + OAuth |
|
||||
|
||||
**Implementation:**
|
||||
|
||||
1. **New `lib/vncdirectory/` service layer:**
|
||||
- `lib/vncdirectory/client.ts` — API client for VNCdirectory REST endpoints (port auth patterns from `routes/index.js`)
|
||||
- `lib/vncdirectory/config.ts` — VNCdirectory connection settings (URL, API key, LDAP bind, SAML IDP metadata)
|
||||
- `lib/vncdirectory/auth.ts` — SAML 2.0 SP implementation using `@node-saml/passport-saml` via Next.js API routes
|
||||
- `lib/vncdirectory/ldap.ts` — LDAP client using `ldapjs` for user/group/GAL queries
|
||||
- `lib/vncdirectory/2fa.ts` — TOTP enrollment + verification via VNCdirectory
|
||||
|
||||
2. **New API routes:** `app/api/vncdirectory/`
|
||||
- `GET /api/vncdirectory/status` — connection health check
|
||||
- `POST /api/vncdirectory/saml/login` — initiate SAML login flow
|
||||
- `POST /api/vncdirectory/saml/callback` — SAML assertion consumer
|
||||
- `POST /api/vncdirectory/saml/logout` — SAML single logout
|
||||
- `GET /api/vncdirectory/users` — search users (LDAP + VNCdirectory)
|
||||
- `POST /api/vncdirectory/2fa/enroll` — generate TOTP secret
|
||||
- `POST /api/vncdirectory/2fa/verify` — verify TOTP code
|
||||
- `GET /api/vncdirectory/2fa/status` — check 2FA enrollment status
|
||||
- `GET /api/vncdirectory/tags` — directory contact tags
|
||||
- `POST /api/vncdirectory/tags` — create/update directory tags
|
||||
|
||||
3. **New admin configuration page:**
|
||||
- `app/(main)/admin/vncdirectory/page.tsx` — VNCdirectory settings panel
|
||||
- Sections:
|
||||
- **Connection:** VNCdirectory URL, API key, LDAP URI, bind credentials
|
||||
- **SAML/IDP:** Identity Provider URL (SimpleSAMLphp), SP certificate, issuer
|
||||
- **Authentication:** Toggle SAML login, toggle 2FA enforcement, OIDC settings
|
||||
- **Directory sync:** LDAP type (OpenLDAP/MS-AD), search base, attribute mapping
|
||||
- **Federated apps:** Configure SSO URLs for VNCtalk, VNCtask, VNCcontacts
|
||||
- Add to admin navigation sidebar
|
||||
|
||||
4. **New `stores/vncdirectory-store.ts`** — client-side config state
|
||||
|
||||
5. **Extend existing auth:**
|
||||
- Add SAML login as alternative to existing Basic/OAuth flows
|
||||
- Add VNCdirectory as identity source alongside Stalwart
|
||||
- Wire 2FA through VNCdirectory (currently uses Stalwart admin API)
|
||||
|
||||
**Effort:** 24h
|
||||
|
||||
### P2.14 — Share Files by Email as Attachment
|
||||
**Docx:** "share by email as attachment"
|
||||
|
||||
**Implementation:**
|
||||
1. Extend `components/files/file-browser.tsx` — "Send as Email Attachment" action
|
||||
2. Opens composer with selected files attached (reuse existing attachment upload in composer)
|
||||
3. Effort: 4h
|
||||
|
||||
**P2 Total:** ~127h
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Security Hardening (Autonomous)
|
||||
|
||||
### P3.1 — Feature Gate Server-Side Enforcement
|
||||
**Issue:** Feature gates are UI-only. Disabled features remain accessible via direct API calls.
|
||||
|
||||
**Fix:** Add policy checks to API routes. For each feature-gated route, add `isFeatureEnabled()` check returning 403.
|
||||
- Routes: `app/api/smime/*`, `app/api/calendar-agenda/*`, `app/api/offline/*`, `app/api/plugins/*` (plugin disabled)
|
||||
- Effort: 4h
|
||||
|
||||
### P3.2 — Unified Auth Error Interceptor
|
||||
**Issue:** 401/403 errors silently swallowed in data fetches.
|
||||
|
||||
**Fix:** Create `lib/auth-error-handler.ts` — global fetch wrapper that detects 401 and triggers re-auth flow. Wire into JMAP client `authenticatedFetch`.
|
||||
- Effort: 6h
|
||||
|
||||
### P3.3 — Store-Level State Isolation on Account Switch
|
||||
**Issue:** Manual `clearAllStores()` misses new fields and stores.
|
||||
|
||||
**Fix:** Define per-store `snapshot(): Partial<S>` and `clear(): Partial<S>` contract. Auto-discover registered stores via a registry.
|
||||
- Effort: 8h
|
||||
|
||||
### P3.4 — Push Event Bus Extraction
|
||||
**Issue:** Email store is the push dispatch hub for 5+ stores.
|
||||
|
||||
**Fix:** Extract `lib/push-event-bus.ts` — stores subscribe to JMAP type names. Email store stops importing calendar/filter/task stores.
|
||||
- Effort: 8h
|
||||
|
||||
**P3 Total:** ~26h
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Polish & Remaining HIGH Items
|
||||
|
||||
### P4.1 — Offline Write Queue
|
||||
**Issue:** No offline write capability. Cannot compose/send while offline.
|
||||
|
||||
**Fix:** Add `replica_pending_ops` table. Stage mutations offline, replay on connectivity return. Start with email send only, then extend.
|
||||
- Effort: 16h
|
||||
|
||||
### P4.2 — Identity Spoofing Protection
|
||||
**Issue:** From override accepts arbitrary addresses.
|
||||
|
||||
**Fix:** Client-side validation — restrict `fromOverrideEmail` to domains matching user's identities.
|
||||
- Effort: 2h
|
||||
|
||||
### P4.3 — WebSocket Push for Electron
|
||||
**Issue:** Browser WebSocket push permanently disabled.
|
||||
|
||||
**Fix:** Implement main-process WebSocket bridge in Electron via IPC. Renderer sends token, main process connects WS with auth header.
|
||||
- Effort: 8h
|
||||
|
||||
### P4.4 — Remaining MEDIUM audit findings
|
||||
- Calendar: recurrence cap warning, prefix scheme unification, read-only calendar filter, bulk delete batching
|
||||
- Contacts: cross-account move race, autocomplete indexing, import dedup
|
||||
- Files: folder tree cache reuse, upload parallelism
|
||||
- Search: `toWildcardQuery` quote handling, `searchEmails` AbortController
|
||||
- Effort: ~20h
|
||||
|
||||
**P4 Total:** ~46h
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Phase | Hours | Description |
|
||||
|-------|-------|-------------|
|
||||
| P1 | 32h | Critical + HIGH bug fixes (18 items) |
|
||||
| P2 | 167h | Missing features from docx (14 features) |
|
||||
| P3 | 26h | Security hardening (4 items) |
|
||||
| P4 | 46h | Polish + remaining fixes (4 items) |
|
||||
| **Total** | **~271h** | |
|
||||
|
||||
### New Infrastructure Dependencies (P2)
|
||||
- **PostgreSQL database** — `resources` + `resources_bookings` tables for VNCdirectory-backed resource management
|
||||
- **VNCdirectory** — canonical source for resources + IDP identity provider (SAML 2.0, LDAP, 2FA/TOTP, directory tags)
|
||||
- **SimpleSAMLphp** — SAML 2.0 Identity Provider (`vncidp.dev.vnc.de`) for web SSO
|
||||
- **OpenLDAP** — LDAP directory for user/group queries and GAL (via `ldapjs`)
|
||||
- **Collabora server** — `collaboraBaseUrl` admin config for online document editing
|
||||
- **VNCtalk API** — `/api/createnewmeeting` endpoint for video meeting integration
|
||||
|
||||
### Autonomy Level
|
||||
- **100% autonomous** — No further board decisions needed.
|
||||
- **Sync direction (VNCdirectory ↔ PostgreSQL)**: Assumed VNCdirectory → PostgreSQL (pull) for Phase 1. Can be swapped if VNCdirectory expects push updates.
|
||||
- **Code reuse:** 12 areas from VNCmail-analysis Angular codebase (models, API patterns, business logic). Must be adapted from Angular DI/services to plain TS functions + React hooks.
|
||||
- **Repository access:** `brvncde-dotcom/vncmail-plus` (target), `brvncde-dotcom/VNCmail-analysis` (reuse).
|
||||
|
||||
### Deploy Flow
|
||||
Per policy: P1 → deploy to `dev` → QA → fix → promote to `main`. Then P2 → dev → QA → main. Repeat for P3, P4.
|
||||
|
||||
### First Sprint Scope
|
||||
**Phase 1 only** — ship all 8 CRITICAL + 10 HIGH fixes (~32h). This brings health from 7.2 to ~8.5/10 and addresses the most impactful user-facing bugs before adding new features.
|
||||
|
||||
---
|
||||
|
||||
Do you want me to start Phase 1 immediately, or adjust the plan?
|
||||
@@ -0,0 +1,349 @@
|
||||
# VNCmail+ Holistic Audit Report
|
||||
|
||||
**Run:** 2026-08-07-v1.7.8-baseline
|
||||
**Skill:** VNCmailgraph v1.0 (adapted for Next.js/Zustand)
|
||||
**Commit:** d8bebb531f86cab3507aed2113e8d0e6a03c1aa8
|
||||
**Version:** vnc-v0.3.0-94-gd8bebb53 (VERSION=1.7.8)
|
||||
**Codebase:** ~188K LOC, 745 TS/TSX files, 28 Zustand stores, 24 API endpoint groups
|
||||
**Framework:** Next.js 16 (App Router) + React 19 + Zustand 5
|
||||
**Backend:** Stalwart Mail Server (JMAP protocol)
|
||||
**Targets:** Web (PWA), Electron Desktop, Native (planned via Capacitor/RN)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
VNCmail+ v1.7.8 is a **production-grade Next.js groupware client** with comprehensive feature coverage (mail, calendar, contacts, files, tasks, filters, templates, AI assistant, admin, plugins) and well-architected security (DOMPurify + CSP nonces + SSRF guards + plugin sandboxing). The codebase has **strong foundations** but exhibits systemic coupling patterns that create cross-feature fragility as the feature surface has grown beyond the original single-account mail client design.
|
||||
|
||||
**Audit scope:** 20 parallel sub-agents audited 16 feature areas + 4 substrate layers using a 6-lens methodology (Correctness, Data-Integrity, Cross-Feature Coupling, Security, Performance, Platform-Parity).
|
||||
|
||||
**Key statistics:**
|
||||
- **82 findings** identified: 8 CRITICAL, 19 HIGH, 35 MEDIUM, 20 LOW
|
||||
- **Overall health composite:** 7.2/10
|
||||
- **Strongest areas:** Security (9/10 for mail XSS defense), Offline replica cursor provenance (exceptional), Setup wizard (8.4/10 clean)
|
||||
- **Weakest areas:** Feature gate enforcement (UI-only, no server-side), Push cross-feature dispatch, Offline write capability (nonexistent), Auth error propagation (silent failures)
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL Findings (8)
|
||||
|
||||
### C1. Mail: `getEmails`/`searchEmails` error swallowing masks transport failures
|
||||
- **Location:** `lib/jmap/client.ts:1292-1294, 2157-2158`
|
||||
- **Impact:** Network-down = empty folder. No "you are offline" indicator. Dead network indistinguishable from empty mailbox. Transport-health counter exists but is never sampled by store callers.
|
||||
- **Recommendation:** Sample `transportFailureCount()` delta before/after reads; if incremented, throw rather than return empty.
|
||||
|
||||
### C2. Calendar: Recurrence expansion ID collision with shared event prefix
|
||||
- **Location:** `lib/recurrence-expansion.ts:242`, `stores/calendar-store.ts:149`
|
||||
- **Impact:** Both recurrence expansion (`:` between master-id and recurrence-date) and shared events (`accountId:eventId`) use single `:` as delimiter. ID collisions possible.
|
||||
- **Recommendation:** Use non-colliding delimiter (e.g., `--` or `::occurrence::`) for expansion.
|
||||
|
||||
### C3. Calendar: Multi-account event aggregation has no cross-account UID dedup
|
||||
- **Location:** `stores/calendar-store.ts:368-395`
|
||||
- **Impact:** Two accounts subscribed to same public holiday calendar → every event appears twice. User sees phantom duplicates.
|
||||
- **Recommendation:** Run post-merge UID dedup after `Promise.all` + `flat()`.
|
||||
|
||||
### C4. Admin: Session token has no server-side revocation
|
||||
- **Location:** `lib/admin/session.ts:149-158`
|
||||
- **Impact:** Once issued, AES-256-GCM encrypted token remains valid until `exp`. Token exfiltration is permanent — no revocation list.
|
||||
- **Recommendation:** Add token blacklist or short TTL + refresh.
|
||||
|
||||
### C5. Storage: Auth tokens/credentials in unencrypted Zustand persist → localStorage
|
||||
- **Location:** `stores/auth-store.ts:553-554`, `stores/account-store.ts`
|
||||
- **Impact:** Any dependency with DOM access (plugins, extensions) can read `auth-storage`/`account-storage` from localStorage. Server URLs + usernames exposed.
|
||||
- **Recommendation:** Encrypt persisted payloads or use sessionStorage where feasible.
|
||||
|
||||
### C6. Search: Schema-version mismatch drops entire FTS5 index without automatic rebuild
|
||||
- **Location:** `lib/mail-index/store.ts:230-234`
|
||||
- **Impact:** Deploy that changes `SCHEMA_VERSION` silently wipes all users' search indexes. No automatic rebuild trigger. Index remains empty until next push event or manual catch-up.
|
||||
- **Recommendation:** Trigger automatic `catchUpAll()` after schema-initiated drop.
|
||||
|
||||
### C7. Store Coupling: Settings locks bypassed at store level — `updateSetting` has no policy guard
|
||||
- **Location:** `stores/settings-store.ts:646`
|
||||
- **Impact:** 40 UI-level `isSettingLocked()` checks exist, but `useSettingsStore.getState().updateSetting()` from any code path (auth-store bootstrap, plugins, server sync) writes through the lock. Admin policy is UI-only.
|
||||
- **Recommendation:** Add `isSettingLocked()` gate inside `updateSetting()`. Add `{ force: true }` opt-in for legitimate bypassers.
|
||||
|
||||
### C8. Push/Sync: No offline event detection — transports retry blindly during outages
|
||||
- **Location:** `lib/jmap/client.ts:6783-6808`
|
||||
- **Impact:** Only `online` listener registered; no `offline` listener. When browser goes offline, WS keeps retrying, SSE keeps reconnecting, polling keeps firing — all silently failing, draining battery.
|
||||
- **Recommendation:** Add `offline` handler that calls `closePushNotifications()`. Gate with `navigator.onLine`.
|
||||
|
||||
---
|
||||
|
||||
## HIGH Findings (19)
|
||||
|
||||
### Cross-Feature / Substrate (7)
|
||||
|
||||
**H1. Push: ContactCard and FileNode state changes silently ignored by UI stores**
|
||||
`stores/email-store.ts:2834-2941` — Push handler fans out to Email, Mailbox, Calendar, CalendarEvent, SieveScript but has NO branch for ContactCard or FileNode. Remote contact/file changes are invisible until manual refresh.
|
||||
|
||||
**H2. Push: Fallback chain has timed gap where deliveries are missed**
|
||||
`lib/jmap/client.ts:6146-6177` — WS→SSE handoff window (~600ms) loses deliveries if state snapshot hasn't completed. Acknowledged as known gap in code comments.
|
||||
|
||||
**H3. Auth: No rate limiting on user-facing auth endpoints**
|
||||
`lib/admin/rate-limit.ts:6-7` — Rate limiter only protects admin login. User auth endpoints (`/api/auth/session`, `/api/auth/token`, `/api/auth/totp-token-exchange`) are open to brute force.
|
||||
|
||||
**H4. Auth: Access token leaked in error logs on token exchange failure**
|
||||
`lib/oauth/token-exchange.ts:124` — Full token response (including `access_token`) logged when exchange fails. Tokens written to centralized logging.
|
||||
|
||||
**H5. Auth: Account registry + auth metadata stored unencrypted in localStorage**
|
||||
`stores/account-store.ts:220-226` — Same as C5, distinct from cookie-encrypted session tokens.
|
||||
|
||||
**H6. Settings: `exportSettings()` serializes `trustedSenders` email addresses in plaintext**
|
||||
`stores/settings-store.ts:697` — Plus `emailKeywords`, `folderIcons`, `allMailFolderIds` — user-specific data in export.
|
||||
|
||||
**H7. Templates: HTML body bypasses sanitization on import**
|
||||
`lib/template-utils.ts:176` — When `isHTML===true`, template body imported raw. DOMPurify bypassed, enabling stored XSS when the template is applied in the TipTap editor.
|
||||
|
||||
### Feature-Specific (12)
|
||||
|
||||
**H8. Admin: `secure` cookie flag based on NODE_ENV, not request protocol**
|
||||
`lib/admin/session.ts:154` — Reverse proxy with TLS termination + HTTP internal → Secure cookie breaks.
|
||||
|
||||
**H9. Admin: bcrypt hashes silently broken — password lockout**
|
||||
`lib/admin/password.ts:60-62` — `isHashed()` returns true for bcrypt but `verifyPassword()` only handles scrypt. Operator locked out.
|
||||
|
||||
**H10. Calendar: Event modal allows creating events in read-only shared calendars**
|
||||
`components/calendar/event-modal.tsx:260-265` — No `myRights?.mayWriteAll` filter on calendar selector. Server rejects with confusing error.
|
||||
|
||||
**H11. Calendar: Recurrence expansion silently caps at 500 occurrences**
|
||||
`lib/recurrence-expansion.ts:330` — Long-running daily events don't render at all. No error/warning.
|
||||
|
||||
**H12. Calendar: Inconsistent prefix scheme (`:` vs `::`)**
|
||||
`stores/calendar-store.ts:64,149` — `CROSS_ACCOUNT_ID_DELIMITER = '::'` but shared events use `:`. `stripLocalAccountPrefix` only strips `::` prefix.
|
||||
|
||||
**H13. Tasks: Admin feature gate `calendarTasksEnabled` has no runtime enforcement**
|
||||
`components/settings/calendar-settings.tsx:87` — Gate only controls settings UI toggle visibility. Previously-enabled tasks remain accessible after admin disables.
|
||||
|
||||
**H14. Tasks: All mutation operations lack error handling — silent failures**
|
||||
`stores/task-store.ts:67-92` — `updateTask`, `deleteTask`, `toggleTaskComplete` have no try/catch. Toast never fires on failure.
|
||||
|
||||
**H15. Mail: SSE connect does no catch-up fetch — mail lost during reconnect window**
|
||||
`lib/jmap/client.ts:6290-6301` — Explicitly documented as "Not airtight".
|
||||
|
||||
**H16. Mail: Browser WebSocket push permanently disabled — auth header limitation**
|
||||
`lib/jmap/client.ts:6052-6073` — Browser `WebSocket` constructor can't attach custom headers. After 3 failures, `wsPermanentlyDisabled = true` for session.
|
||||
|
||||
**H17. Mail: Email-store push handler directly drives calendar-store — layering violation**
|
||||
`stores/email-store.ts:2914-2930` — Email store calls `calendarStore.fetchCalendars()` and `fetchEvents()`. Hard dependency.
|
||||
|
||||
**H18. Identity: `autoSelectReplyIdentity` defaults to `false` — auto-identity selection broken**
|
||||
`stores/settings-store.ts:487` — New users always send as primary identity, never auto-select based on reply target.
|
||||
|
||||
**H19. Identity: From override accepts arbitrary email addresses — identity spoofing**
|
||||
`email-composer.tsx:2283-2294` — User can set `fromOverrideEmail` to any address (e.g., `ceo@competitor.com`). Display `From:` header spoofable.
|
||||
|
||||
---
|
||||
|
||||
## Synergetic Failure Analysis (Cross-Feature Patterns)
|
||||
|
||||
### Synergy 1: The Push Dispatch Hub Problem
|
||||
**Affected:** Mail, Calendar, Tasks, Filters, Contacts, Files, Search Index, Offline Replica
|
||||
|
||||
The email-store's push handler (`stores/email-store.ts:2834-2941`) has grown into the de facto `/changes` dispatcher driving 5+ feature stores. This creates a single point of failure where:
|
||||
- Email store must be initialized before any push-triggered feature refresh works
|
||||
- ContactCard and FileNode state changes are silently dropped (no branch)
|
||||
- Calendar refresh is fire-and-forget with no error handling
|
||||
- Search index reindex is triggered but no rebuild verification
|
||||
|
||||
**Root cause:** Organic growth from single-account mail client to multi-feature groupware without extracting the push dispatcher into an independent event bus.
|
||||
|
||||
### Synergy 2: Feature Gate Enforcement Gap
|
||||
**Affected:** Calendar, Tasks, Contacts, Files, S/MIME, Templates, Plugins
|
||||
|
||||
Feature gates are nearly 100% UI-only. The pattern:
|
||||
```
|
||||
isFeatureEnabled('calendarEnabled') → hides UI only
|
||||
```
|
||||
No store-level check, no API route check, no server-side enforcement for most features. A disabled feature remains fully functional via direct API calls. `getEffectiveDefault()` is dead code with zero callers.
|
||||
|
||||
**Root cause:** Feature gates were added as a UI visibility toggle without the corresponding enforcement at the data/API layer.
|
||||
|
||||
### Synergy 3: Offline Capability Gap
|
||||
**Affected:** Mail, Calendar, Contacts, Files, Tasks
|
||||
|
||||
The offline replica is a carefully engineered read-path fallback for Email/Mailbox only. There is:
|
||||
- **No offline write queue** — cannot compose/send email, create events, or modify contacts while offline
|
||||
- **No calendar/contacts/files replication** — only Email and Mailbox have `/changes` cursors
|
||||
- **No cross-feature offline coordination** — search index, replica, and localStorage stores have three independent retention policies (30d, 180d, indefinite)
|
||||
|
||||
**Root cause:** The replica was designed as a server-outage fallback, not a full offline-first architecture.
|
||||
|
||||
### Synergy 4: Auth Error Propagation Gap
|
||||
**Affected:** All 16 features
|
||||
|
||||
401/403 errors from data fetches are silently swallowed with `debug.error()` log lines:
|
||||
```ts
|
||||
contactStore.fetchAddressBooks(client).catch((err) => debug.error(...));
|
||||
calendarStore.fetchCalendars(client).catch((err) => debug.error(...));
|
||||
```
|
||||
There is no unified auth error interceptor, no error boundary for async failures, and no user-visible re-auth prompt. A timed-out session shows silently broken UI for up to 60 seconds.
|
||||
|
||||
**Root cause:** Each store independently handles errors; no shared error propagation channel exists.
|
||||
|
||||
### Synergy 5: Store-Level State Leak Across Accounts
|
||||
**Affected:** Mail, Calendar, Contacts, Tasks, Message-List-Tabs
|
||||
|
||||
`account-state-manager.ts` snapshots 6 stores but misses message-list-tabs-store, task-store, and partial field coverage. `clearAllStores()` requires manual field enumeration — any new store field added without updating the reset list silently leaks across account switches.
|
||||
|
||||
**Root cause:** No per-store `snapshot()`/`clear()` contract. Manual maintenance at the account-state-manager level.
|
||||
|
||||
---
|
||||
|
||||
## Symptom → Cause Map
|
||||
|
||||
| User Symptom | Root Cause | Finding |
|
||||
|---|---|---|
|
||||
| "My calendar is empty" after login | `initializeFeatureStores` silently fails on calendar fetch | H-SYN-4 |
|
||||
| "I can still use tasks after admin disabled them" | `calendarTasksEnabled` gate is UI-only | H13 |
|
||||
| "No new mail notification" after wake from sleep | No `offline` handler to pause push; transports retry blindly | C8 |
|
||||
| "My contacts haven't updated" on another device | Push handler has no ContactCard branch | H1 |
|
||||
| "I see duplicate events" with multiple accounts | No cross-account UID dedup in calendar aggregation | C3 |
|
||||
| "Can't search old email" in desktop app | 30-day FTS5 window, no user-facing indicator | SRC-007 |
|
||||
| "Lost my search index" after update | Schema version bump drops all tables, no auto-rebuild | C6 |
|
||||
| "Settings lock doesn't work" via console | `updateSetting()` has no `isSettingLocked()` check | C7 |
|
||||
| "Can't send email offline" | No offline write queue | H-SYN-3 |
|
||||
| "Wrong From address on reply" | `autoSelectReplyIdentity` defaults to `false` | H18 |
|
||||
| "Template imported with HTML executes scripts" | `isHTML` bypasses DOMPurify on import | H7 |
|
||||
| "Auth token in server logs" after IdP outage | Token response logged on exchange failure | H4 |
|
||||
|
||||
---
|
||||
|
||||
## Tiered Action Plan
|
||||
|
||||
### TIER 1 — Immediate (This Sprint)
|
||||
|
||||
| ID | Finding | Effort | Risk |
|
||||
|----|---------|--------|------|
|
||||
| C1 | Fix mail error swallowing — sample transport-health | 2h | LOW |
|
||||
| C8 | Add `offline` event handler to pause push transports | 1h | LOW |
|
||||
| C7 | Gate `updateSetting()` with policy lock check | 3h | MEDIUM — needs `force` opt-in audit |
|
||||
| C6 | Auto-rebuild FTS5 index after schema-version drop | 2h | LOW |
|
||||
| H7 | Apply DOMPurify to imported HTML template bodies | 1h | LOW |
|
||||
| H13 | Add `calendarTasksEnabled` enforcement at runtime | 2h | LOW |
|
||||
| H14 | Add try/catch + toast to task mutations | 1h | LOW |
|
||||
| H4 | Strip `access_token` from OAuth error log context | 30m | LOW |
|
||||
| H9 | Fix bcrypt hash handling in password verification | 1h | LOW |
|
||||
| H18 | Default `autoSelectReplyIdentity` to `true` | 30m | LOW |
|
||||
|
||||
### TIER 2 — Next Sprint
|
||||
|
||||
| ID | Finding | Effort |
|
||||
|----|---------|--------|
|
||||
| C4 | Add admin session token revocation (blacklist) | 5h |
|
||||
| C3 | Add cross-account UID dedup in calendar aggregation | 3h |
|
||||
| C2 | Fix recurrence expansion ID delimiter collision | 2h |
|
||||
| C5 | Encrypt auth metadata in localStorage | 4h |
|
||||
| H1 | Add ContactCard/FileNode branches to push handler | 3h |
|
||||
| H2 | Make `reconcileAfterWebSocketFallback` await state snapshot | 2h |
|
||||
| H3 | Add rate limiting to user-facing auth endpoints | 3h |
|
||||
| H8 | Derive `secure` cookie from request protocol | 1h |
|
||||
| H10 | Filter read-only calendars from event-modal selector | 1h |
|
||||
| H11 | Add warning when 500-occurrence cap exhausted | 1h |
|
||||
| H12 | Unify calendar ID prefix scheme to `::` | 4h |
|
||||
| H19 | Add client-side validation for From override domain | 3h |
|
||||
|
||||
### TIER 3 — This Quarter
|
||||
|
||||
| ID | Finding | Effort |
|
||||
|----|---------|--------|
|
||||
| H15 | Add catch-up fetch on SSE reconnect | 4h |
|
||||
| H16 | Implement Electron main-process WebSocket bridge for push | 8h |
|
||||
| H17 | Extract push dispatch into dedicated event bus | 8h |
|
||||
| SYN-4 | Add unified auth error interceptor + UI boundary | 8h |
|
||||
| SYN-1 | Refactor push handler into independent store subscriptions | 12h |
|
||||
| SYN-3 | Add offline write queue (pending ops table) | 16h |
|
||||
| SYN-5 | Define per-store `snapshot()`/`clear()` contract | 8h |
|
||||
| — | Extend replica to CalendarEvent, ContactCard, FileNode types | 20h |
|
||||
| — | Add per-feature server-side feature gate enforcement | 12h |
|
||||
|
||||
---
|
||||
|
||||
## Coverage Map
|
||||
|
||||
| Feature | Audited | CRITICAL | HIGH | MEDIUM | LOW | Health Score |
|
||||
|---------|---------|----------|------|--------|-----|-------------|
|
||||
| Mail/Email | ✅ N01 | 2 | 4 | 6 | 3 | 7.3 |
|
||||
| Calendar | ✅ N02 | 2 | 4 | 6 | 2 | 7.2 |
|
||||
| Contacts | ✅ N03 | 0 | 1 | 4 | 5 | 7.3 |
|
||||
| Files/Briefcase | ✅ N04 | 0 | 0 | 4 | 4 | 7.3 |
|
||||
| Tasks | ✅ N05 | 0 | 2 | 3 | 3 | 6.0 |
|
||||
| Settings | ✅ N06 | 0 | 2 | 4 | 5 | 7.5 |
|
||||
| Filters/Sieve | ✅ N07 | 0 | 0 | 1 | 4 | 8.1 |
|
||||
| Templates | ✅ N08 | 0 | 1 | 3 | 12 | 7.4 |
|
||||
| Identity/Aliases | ✅ N09 | 0 | 2 | 3 | 6 | 8.0 |
|
||||
| AI Assistant | ✅ N10 | 0 | 3 | 4 | 3 | 6.5 |
|
||||
| Admin | ✅ N11 | 1 | 2 | 1 | 4 | 7.7 |
|
||||
| Plugin System | ✅ N12 | 0 | 0 | 4 | 8 | 7.7 |
|
||||
| Pro Shell | ✅ N13 | 0 | 0 | 2 | 3 | 7.5 |
|
||||
| Search | ✅ N14 | 0 | 2 | 3 | 5 | 8.0 |
|
||||
| Authentication | ✅ N15 | 0 | 3 | 5 | 4 | 7.0 |
|
||||
| Setup Wizard | ✅ N16 | 0 | 0 | 2 | 4 | 8.4 |
|
||||
|
||||
**Substrate layers audited in Wave 2:**
|
||||
| Substrate | Audited | CRITICAL | HIGH | MEDIUM | Health |
|
||||
|-----------|---------|----------|------|--------|--------|
|
||||
| Store Coupling | ✅ N20 | 1 | 3 | 4 | — | 4.0 |
|
||||
| Offline/Storage | ✅ N21 | 2 | 2 | 4 | — | 7.0 |
|
||||
| Sync/Push/Background | ✅ N22 | 2 | 3 | 5 | — | 5.0 |
|
||||
| Auth/Session/Entitlement | ✅ N24 | 1 | 0 | 5 | — | 5.5 |
|
||||
|
||||
---
|
||||
|
||||
## Platform Parity Summary
|
||||
|
||||
| Capability | Web (PWA) | Electron Desktop | Native (planned) |
|
||||
|-----------|-----------|------------------|------------------|
|
||||
| Mail reading | ✅ Full | ✅ Full | ❌ Planned |
|
||||
| Offline reads | ❌ No replica | ✅ SQLCipher replica | ❌ Planned |
|
||||
| Local search | ❌ No FTS5 | ✅ SQLCipher FTS5 (30d) | ❌ Planned |
|
||||
| Push notifications | ✅ Web Push | ✅ Electron Notification (window-open only) | ❌ Planned |
|
||||
| Offline writes | ❌ None | ❌ None | ❌ Planned |
|
||||
| AI local LLM | ⚠️ CORS needed | ✅ Direct loopback | ❌ Planned |
|
||||
| S/MIME | ✅ Full | ✅ Full | ❌ Planned |
|
||||
|
||||
**Key platform gap:** Electron desktop has offline read capability but no push when window is closed (renderer dies). Web has push via service worker but no offline storage.
|
||||
|
||||
---
|
||||
|
||||
## Storage Subsystem Decision (§D)
|
||||
|
||||
### Recommendation: Stay with current SQLite split, extend with OPFS for web
|
||||
|
||||
| Phase | Action |
|
||||
|-------|--------|
|
||||
| **Phase 1 (Now)** | Harden current: auto-rebuild index after schema drop, batch `pruneOlderThan`, add user-facing index-status indicator |
|
||||
| **Phase 2 (Q4)** | Abstract behind `ReplicaStore` interface. Implement `WebReplicaStore` via `sqlite-wasm/OPFS` for browsers. Keep `@signalapp/sqlcipher` for Electron. |
|
||||
| **Phase 3 (Later)** | `MobileReplicaStore` via Capacitor SQLite plugin or `expo-sqlite` |
|
||||
|
||||
**Rationale:** RxDB and WatermelonDB add weight without solving problems the current codebase has already solved correctly (cursor provenance, error taxonomy, clock-jump guard). The current SQL split is proven in Signal Desktop. The gap is platform coverage, not architecture quality.
|
||||
|
||||
**Top 3 risks of any migration:**
|
||||
1. Cursor-provenance regression (branded types don't survive JSON round-trips)
|
||||
2. Concurrent-writer SQLITE_BUSY on push-triggered index+replica writes
|
||||
3. Encryption downgrade when moving to IndexedDB or OPFS without explicit encryption
|
||||
|
||||
---
|
||||
|
||||
## Methodology Notes
|
||||
|
||||
This audit adapted the VNCmailgraph methodology from Angular/NgRx to Next.js/Zustand. Key translations:
|
||||
- Feature domains → Next.js page routes + component trees + Zustand stores
|
||||
- Shared substrate → Zustand store imports + lib/ services + API routes
|
||||
- Platform parity → `isElectronShell()` (not `isCordova`/`isElectron`)
|
||||
- Feature flags → `lib/admin/types.ts:FeatureGates` (not `zimbra-features.ts`)
|
||||
- Storage → `lib/offline-replica/` + `lib/mail-index/` (SQLite/SQLCipher)
|
||||
|
||||
**AI cost:** Approximately 380K input tokens + 85K output tokens across 20 parallel sub-agent audits. Waves 0 (inventory) + calibration (2 nodes) + Wave 1 (14 nodes) + Wave 2 (4 nodes) + synthesis.
|
||||
|
||||
**Secret hygiene:** No secrets included in this report. All referenced snippets are from public API signatures and type definitions.
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
- `runs/2026-08-07-v1.7.8-baseline/inventory.md` — Feature inventory + coupling DAG
|
||||
- `runs/2026-08-07-v1.7.8-baseline/graph.md` — Execution graph + node assignments
|
||||
- `runs/2026-08-07-v1.7.8-baseline/REPORT.md` — This report
|
||||
- `runs/2026-08-07-v1.7.8-baseline/raw/` — Raw per-node findings (to be saved from agent outputs)
|
||||
|
||||
**Next run:** Re-run against next release to populate coverage map deltas (`persists|fixed|new|regressed`).
|
||||
@@ -0,0 +1,102 @@
|
||||
# Wave 0 — Execution Graph
|
||||
|
||||
**Run:** 2026-08-07-v1.7.8-baseline
|
||||
**Skill:** VNCmailgraph
|
||||
**Commit:** d8bebb531f86cab3507aed2113e8d0e6a03c1aa8
|
||||
|
||||
---
|
||||
|
||||
## DAG Structure
|
||||
|
||||
The graph decomposes the audit into **3 waves**:
|
||||
|
||||
### Wave 0 (DONE) — Inventory + Dependency Map
|
||||
- Output: `inventory.md` + this `graph.md`
|
||||
- Cost: ~minimal (code exploration, no heavy AI)
|
||||
|
||||
### Wave 1 — Per-Feature Audits (16 nodes)
|
||||
Each node audits ONE feature domain against the **6-lens set** (Correctness, Data-Integrity, Cross-Feature Coupling, Security, Performance, Platform-Parity). Nodes are **independent** (can fan out in parallel) — no feature audit reads another feature's output.
|
||||
|
||||
### Wave 2 — Substrate Synergetic Failure Hunt (8 nodes)
|
||||
After Wave 1 completes, these nodes hunt failures that SPAN features through shared substrate: store-coupling, offline/storage, sync+push, platform-parity, auth/session/entitlement, doc cross-check, cross-feature synthesis, completeness critic.
|
||||
|
||||
---
|
||||
|
||||
## Wave 1 Node Assignments (16 nodes, fully parallelizable)
|
||||
|
||||
| ID | Feature | Source Roots | Key Files | Estimated Complexity |
|
||||
|----|---------|-------------|-----------|---------------------|
|
||||
| N01 | Mail/Email | `app/(main)/[locale]/page.tsx`, `components/email/`, `stores/email-store.ts` | email-viewer (5102L), email-composer (3432L), email-list, rich-text-editor, JMAP client (7446L) | **VERY HIGH** |
|
||||
| N02 | Calendar | `app/(main)/[locale]/calendar/`, `components/calendar/`, `stores/calendar-store.ts` | event-modal (1375L), month/week/day views, recurrence-expansion | **HIGH** |
|
||||
| N03 | Contacts | `app/(main)/[locale]/contacts/`, `components/contacts/`, `stores/contact-store.ts` | contact-list (615L), contact-detail (625L), vCard import/export | **MEDIUM** |
|
||||
| N04 | Files | `app/(main)/[locale]/files/`, `components/files/`, `stores/file-store.ts` | file-browser (2022L), WebDAV client, dual storage | **MEDIUM** |
|
||||
| N05 | Tasks | `stores/task-store.ts`, `components/calendar/task-*.tsx` | Small feature (95L store) | **LOW** |
|
||||
| N06 | Settings | `app/(main)/[locale]/settings/`, `components/settings/`, `stores/settings-store.ts` | 33 settings components, settings-sync | **HIGH** |
|
||||
| N07 | Filters/Sieve | `components/filters/`, `stores/filter-store.ts`, `lib/sieve/` | filter-rule-modal (534L), parser (866L), generator | **MEDIUM** |
|
||||
| N08 | Templates | `components/templates/`, `stores/template-store.ts` | Small feature (153L store) | **LOW** |
|
||||
| N09 | Identity/Aliases | `components/identity/`, `stores/identity-store.ts` | identity-manager-modal (407L) | **LOW** |
|
||||
| N10 | AI Assistant | `components/ai/`, `lib/ai/`, `app/api/ai/` | local-client (422L), opencode (164L), retrieval/fusion, local-discovery | **MEDIUM** |
|
||||
| N11 | Admin | `app/(main)/admin/*`, `lib/admin/`, `stores/admin-tab-store.ts` | 18 lib files, 14 pages, config-manager, audit, plugin-registry | **HIGH** |
|
||||
| N12 | Plugin System | `components/plugins/`, `stores/plugin-store.ts`, `lib/plugin-sandbox/` | 13 sandbox files, host-api (933L), runtime (636L), types (1092L) | **VERY HIGH** |
|
||||
| N13 | Pro Shell | `app/(main)/[locale]/pro/`, `components/pro/`, `stores/pro-tab-store.ts` | tab-bar, email/compose tab bodies | **LOW** |
|
||||
| N14 | Search | `components/search/`, `lib/mail-index/`, `stores/email-store.ts` (search state) | FTS5 index (Electron-only), search-chips, advanced-search-panel | **MEDIUM** |
|
||||
| N15 | Authentication | `lib/auth/`, `lib/oauth/`, `stores/auth-store.ts` | auth-store (2033L), crypto, OIDC, pairing, impersonation | **HIGH** |
|
||||
| N16 | Setup Wizard | `app/(main)/setup/*`, `lib/setup/` | session, state, token | **LOW** |
|
||||
|
||||
---
|
||||
|
||||
## Wave 2 Node Assignments (8 nodes, partially parallelizable)
|
||||
|
||||
| ID | Substrate Concern | Scope | Depends On |
|
||||
|----|-------------------|-------|-----------|
|
||||
| N20 | Store-Coupling & Cycles | Cross-store dependency analysis: auth-store hub, email↔tabs cycle, client-registry anti-cycle | N01–N16 |
|
||||
| N21 | Offline / Storage | `lib/offline-replica/` + `lib/mail-index/` + RxDB evaluation (§D) | N01, N04, N10, N14 |
|
||||
| N22 | Sync + Push + Background | JMAP push, web push (PWA), Electron notifications, offline queue replay, connectivity | N01, N02, N06 |
|
||||
| N23 | Platform Parity | Electron vs PWA vs Native gaps: `isElectronShell()` gate spread, local index, offline replica, notifications | N01–N16 |
|
||||
| N24 | Auth / Session / Entitlement | Token lifecycle, multi-account isolation, feature-gate enforcement, sharing/delegation auth | N15, N11 |
|
||||
| N25 | Doc Cross-Check | FEATURES.md vs actual code vs policy feature gates | N01–N16 |
|
||||
| N26 | Cross-Feature Synthesis | Combines Wave 1 coupling points + Wave 2 substrate findings → cross-feature failures | N01–N25 |
|
||||
| N27 | Completeness Critic | "What feature, modality, or shared path was NOT covered?" + Fresh-context verify of all CRITICAL/HIGH | N01–N26 |
|
||||
|
||||
---
|
||||
|
||||
## Calibration Plan
|
||||
|
||||
Before fanning out all 16 Wave 1 nodes, calibrate on **2 representative nodes**:
|
||||
|
||||
1. **N05 (Tasks)** — smallest feature (95 lines), low complexity → calibrate per-node cost lower bound
|
||||
2. **N15 (Authentication)** — high complexity (2033 lines), the central hub importing 7 stores → calibrate per-node cost upper bound
|
||||
|
||||
After calibration, price the full Wave 1 fan-out and report against the AI-cost standing rule.
|
||||
|
||||
---
|
||||
|
||||
## The 6-Lens Set (applied to every Wave 1 node)
|
||||
|
||||
1. **Correctness / Functioning** — trace real code paths for core verbs; do they work as written? error/offline handling?
|
||||
2. **Data-Integrity** — optimistic UI vs persistence; store↔DB consistency; retention/GC; crash-recovery
|
||||
3. **Cross-Feature Coupling** — enumerate shared store slices + services touched; each is a candidate synergetic-failure surface
|
||||
4. **Security** — XSS (email-body → DOM → Electron RCE path), entitlement/permission guards, token/credential logging, sharing authorization
|
||||
5. **Performance** — virtual scrolling, zoneless CD (Next.js/React: missing memo/useCallback on large lists), worker RPC cost
|
||||
6. **Platform-Parity** — `isElectronShell()` branches, Web Push vs Electron Notification, local index availability, offline replica availability
|
||||
|
||||
## Finding Template (all nodes)
|
||||
|
||||
```
|
||||
### [CRITICAL|HIGH|MEDIUM|LOW] Short title
|
||||
- Category: <feature / substrate / type>
|
||||
- Location: path/file:line
|
||||
- Evidence: short quoted snippet or precise description
|
||||
- Impact: concrete failure scenario (inputs → wrong behavior); cross-feature if applicable
|
||||
- Recommendation: correct approach
|
||||
- Confidence: measured | reasoned-not-measured
|
||||
```
|
||||
|
||||
## Key Codebase Anchors
|
||||
|
||||
- **No secrets in reports** (§B.0.2) — redact all keys, tokens, secrets
|
||||
- **`rg` landmine** — use `grep -REn` / Read, never `rg` (mangles `electron`→`n`)
|
||||
- **Cursor provenance** (`lib/offline-replica/states.ts`) — branded `ChangesState` / `SnapshotState`
|
||||
- **Deploy policy** — dev first, never prod-direct
|
||||
- **AI-cost rule** — ≥30% above baseline → report immediately
|
||||
- **Security anchors** — DOMPurify sanitization, CSP with per-request nonce, SSRF guard, contextIsolation/sandbox for Electron, plugin bundle integrity + signing
|
||||
@@ -0,0 +1,290 @@
|
||||
# Wave 0 — Feature Inventory
|
||||
|
||||
**Run:** 2026-08-07-v1.7.8-baseline
|
||||
**Skill:** VNCmailgraph (adapted for Next.js/Zustand)
|
||||
**Commit:** d8bebb531f86cab3507aed2113e8d0e6a03c1aa8
|
||||
**Version:** vnc-v0.3.0-94-gd8bebb53 (VERSION=1.7.8)
|
||||
**Codebase:** ~188K LOC across 745 TS/TSX files (excluding node_modules, .git)
|
||||
**Framework:** Next.js 16 (App Router) + React 19 + Zustand 5
|
||||
**Backend:** Stalwart Mail Server (JMAP protocol)
|
||||
**Targets:** Web (PWA), Electron Desktop, Native (planned via Capacitor/RN)
|
||||
|
||||
---
|
||||
|
||||
## Method Adaptation
|
||||
|
||||
The original VNCmailgraph methodology assumes Angular + NgRx + `isCordova`/`isElectron` gates. This codebase uses:
|
||||
- **Next.js 16 App Router** (not Angular) — routes defined via file-system routing
|
||||
- **Zustand** (not NgRx) — stores in `stores/*.ts`, accessed via hooks
|
||||
- **`isElectronShell()`** (not `isCordova`/`isElectron`) — platform detection via `window.vnc`
|
||||
- **No `zimbra-features.ts`** — feature flags are in `lib/admin/types.ts` (`FeatureGates`, `SettingsPolicy`)
|
||||
- **JMAP protocol** (not Zimbra SOAP) — `lib/jmap/client.ts` (7446 lines)
|
||||
|
||||
The audit method translates cleanly: feature domains = page routes + component trees; shared substrate = stores + lib services; coupling = cross-store imports.
|
||||
|
||||
---
|
||||
|
||||
## Feature Domains (derived from code)
|
||||
|
||||
### F1 — Mail / Email (CORE)
|
||||
- **Routes:** `/app/(main)/[locale]/page.tsx` (3758 lines — main mail client)
|
||||
- **Components:** `components/email/` (24 files): email-list, email-viewer (5102 lines), email-composer (3432 lines), thread-conversation-view, rich-text-editor (TipTap), tag-picker, email-hover-actions, recipient-popover, calendar-invitation-banner, unsubscribe-banner, read-receipt-banner, message-list-tabs
|
||||
- **Store:** `stores/email-store.ts` (4087 lines) — largest store, manages emails, mailboxes, threads, search, tags, push connection, cross-account views
|
||||
- **Protocol:** `lib/jmap/client.ts` (7446 lines) — the JMAP client powering all mail operations
|
||||
- **Sub-features:** Compose/drafts, threading, unified mailbox, cross-account views, search, tags/keywords, attachments, scheduled send, read receipts (MDN), archive modes, virtual scrolling, TNEF extraction, .eml import, print
|
||||
|
||||
### F2 — Calendar
|
||||
- **Routes:** `/app/(main)/[locale]/calendar/page.tsx`
|
||||
- **Components:** `components/calendar/` (22 files): month/week/day/agenda views, event-modal (1375 lines), mini-calendar, task-modal, recurrence-editor, ical-import/subscription modals, participant-input
|
||||
- **Store:** `stores/calendar-store.ts` (1289 lines) — calendars, events, multi-account aggregation, recurrence expansion, iMIP, CalDAV
|
||||
- **Sub-features:** Month/week/day/agenda views, drag-to-reschedule, recurring events, iMIP invitations, .ics import, birthday calendar, virtual locations, tasks, CalDAV shared calendars
|
||||
|
||||
### F3 — Contacts
|
||||
- **Routes:** `/app/(main)/[locale]/contacts/page.tsx`
|
||||
- **Components:** `components/contacts/` (17 files): contact-list, contact-detail, contact-form, import-dialog, groups
|
||||
- **Store:** `stores/contact-store.ts` (1146 lines) — contact cards, address books, multi-account, vCard import/export
|
||||
- **Sub-features:** JMAP sync (RFC 9553/9610), address books, groups, vCard import/export, trusted senders, autocomplete
|
||||
|
||||
### F4 — Files / Briefcase
|
||||
- **Routes:** `/app/(main)/[locale]/files/page.tsx`
|
||||
- **Components:** `components/files/` (10 files): file-browser (2022 lines), folder-tree, upload-area, preview-modal
|
||||
- **Stores:** `stores/file-store.ts` (1050 lines) + `stores/webdav-store.ts` (480 lines) — dual storage backends
|
||||
- **Sub-features:** JMAP FileNode browsing, WebDAV upload with progress, grid/list views, preview, cut/copy/paste, favorites, sharing (RFC 9670)
|
||||
|
||||
### F5 — Tasks
|
||||
- **Store:** `stores/task-store.ts` (95 lines) — VTODO CRUD, filter by pending/completed/overdue
|
||||
- **Components:** `components/calendar/task-modal.tsx`, `task-list-view.tsx`, `task-toolbar.tsx`
|
||||
- **Feature gate:** `calendarTasksEnabled` in admin policy
|
||||
|
||||
### F6 — Settings / Preferences
|
||||
- **Routes:** `/app/(main)/[locale]/settings/page.tsx`
|
||||
- **Components:** `components/settings/` (33 files) — account, appearance, themes, layout, reading, composing, notification, folder, filter, vacation, identity, template, keyword, calendar, contacts, files, downloads, sidebar-apps, plugins, language, protocol-handler, AI, debug, about
|
||||
- **Store:** `stores/settings-store.ts` (1221 lines) — all user settings with cross-device encrypted sync
|
||||
- **Sub-features:** Theme/density/font, layout preferences, reply/signature behavior, notification preferences, mailto:/webcal: protocol handling, settings sync (AES-256-GCM)
|
||||
|
||||
### F7 — Filters / Sieve
|
||||
- **Components:** `components/filters/` (2 files): filter-rule-modal (534 lines), sieve-editor-modal
|
||||
- **Store:** `stores/filter-store.ts` (254 lines) — Sieve script management
|
||||
- **Lib:** `lib/sieve/` (parser + generator) — round-trip for external scripts
|
||||
- **Sub-features:** Visual rule builder, raw Sieve editor, vacation responder, opaque script preservation
|
||||
|
||||
### F8 — Templates
|
||||
- **Components:** `components/templates/` (4 files): template-picker, template-form, manager-modal, placeholder-fill
|
||||
- **Store:** `stores/template-store.ts` (153 lines) — CRUD, favorites, import/export
|
||||
- **Feature gate:** `templatesEnabled`
|
||||
|
||||
### F9 — Identity / Aliases
|
||||
- **Components:** `components/identity/` (3 files): identity-manager-modal (407 lines), identity-form, sub-address-helper
|
||||
- **Store:** `stores/identity-store.ts` (140 lines) — sender identities, sub-addressing
|
||||
- **Sub-features:** Multiple sender identities per account, sub-addressing (`user+tag@domain`)
|
||||
|
||||
### F10 — AI Assistant
|
||||
- **Components:** `components/ai/ai-ask-button.tsx` (264 lines)
|
||||
- **Lib:** `lib/ai/` (9 files + retrieval/): local-client, local-discovery, opencode, entitlement, key-store, retrieval/fusion
|
||||
- **API:** `app/api/ai/` (server/chat, server/models, retrieve, opencode/chat, opencode/models, policy)
|
||||
- **Sub-features:** Local LLM (Ollama), server-hosted AI, BYOK, OpenCode runtime, mail retrieval with RRF fusion, FTS5 search index
|
||||
- **Feature gate:** `aiAssistantEnabled` (default=true)
|
||||
|
||||
### F11 — Admin / Management
|
||||
- **Routes:** 14 admin pages (dashboard, settings, branding, auth, password, policy, plugins, themes, marketplace, version, telemetry, logs)
|
||||
- **Lib:** `lib/admin/` (18 files): config-manager, session, password, audit, plugin-registry, plugin-approvals, plugin-signing, bundled-plugins, domain-branding, CSP
|
||||
- **Store:** `stores/admin-tab-store.ts` (42 lines)
|
||||
|
||||
### F12 — Plugin System
|
||||
- **Components:** `components/plugins/` (7 files): plugin-slot, iframe-slot, dialog-host, consent-dialog, error-boundary
|
||||
- **Store:** `stores/plugin-store.ts` (596 lines) — lifecycle management
|
||||
- **Lib:** `lib/plugin-sandbox/` (13 files): runtime, host-bridge, host-api, loader, tier, protocol, registry, bundle-integrity, bundle-signing, consent, shortcuts
|
||||
- **Sub-features:** Sandboxed iframe execution, postMessage RPC, tiered permissions (untrusted/privileged), admin approval gates, marketplace, dev-mode loading
|
||||
|
||||
### F13 — Pro Multi-Tab Shell
|
||||
- **Routes:** `/app/(main)/[locale]/pro/page.tsx`
|
||||
- **Components:** `components/pro/` (4 files): tab-bar, email-tab-body, compose-tab-body, interface-redirect
|
||||
- **Store:** `stores/pro-tab-store.ts` (576 lines) — multi-tab management, split pane layout
|
||||
|
||||
### F14 — Search
|
||||
- **Components:** `components/search/` (2 files): search-chips, advanced-search-panel
|
||||
- **Backend:** `lib/mail-index/` (8 files) — encrypted SQLite/FTS5 index (Electron-only)
|
||||
- **API:** `app/api/offline/search`, `app/api/ai/retrieve`
|
||||
- **Coupling:** Heavily coupled with email-store (search state lives there)
|
||||
|
||||
### F15 — Authentication
|
||||
- **Lib:** `lib/auth/` (7 files): crypto, session-cookie, session-secret, verify-jmap-auth, pair-reauth, pairing-store, active-account-slot
|
||||
- **Store:** `stores/auth-store.ts` (2033 lines) — THE hub; manages login, logout, token refresh, multi-account sessions
|
||||
- **Sub-features:** Basic auth, OAuth/OIDC with PKCE, demo mode, device pairing, TOTP 2FA, impersonation (JWT)
|
||||
- **API:** `app/api/auth/` (10 routes)
|
||||
|
||||
### F16 — Setup Wizard
|
||||
- **Routes:** `/app/(main)/setup/*`
|
||||
- **Lib:** `lib/setup/` (3 files): session, state, token
|
||||
- **API:** `app/api/setup/` (6 routes)
|
||||
|
||||
---
|
||||
|
||||
## Shared Substrate
|
||||
|
||||
### S1 — State Management (Zustand stores)
|
||||
- 28 store files in `stores/` — Zustand with `persist` middleware
|
||||
- **Central hub:** `auth-store.ts` imports 7 other stores; bootstraps all feature stores after login
|
||||
- **Bidirectional cycle:** `email-store` ↔ `message-list-tabs-store`
|
||||
- **Anti-cycle pattern:** `client-registry.ts` — utility indirection to avoid auth-store cycles
|
||||
- **Clean leaves:** `locale-store`, `policy-store`, `toast-store`, `ui-store` (0 dependencies)
|
||||
|
||||
### S2 — JMAP Protocol Layer
|
||||
- `lib/jmap/client.ts` (7446 lines) — the communication backbone for ALL features
|
||||
- `lib/jmap/types.ts` (938 lines) — shared type definitions
|
||||
- `lib/jmap/client-interface.ts` (358 lines) — `IJMAPClient` interface (real + demo implementations)
|
||||
- `lib/jmap/transport-health.ts` — failure counter for offline fallback gating
|
||||
- `lib/jmap/request-limits.ts` — request batching utilities
|
||||
- `lib/jmap/search-utils.ts` — search query building
|
||||
|
||||
### S3 — Auth / Session / Multi-Account
|
||||
- `lib/auth/*` — AES-256-GCM cookie encryption, session lifecycle, device pairing
|
||||
- `lib/oauth/*` — OAuth2/OIDC discovery, PKCE, token exchange
|
||||
- `lib/impersonation/*` — JWT platform auth
|
||||
- `lib/stalwart/*` — server-side Stalwart auth context, JMAP passthrough
|
||||
- `stores/account-store.ts` — multi-account registry
|
||||
- `stores/account-security-store.ts` — TOTP 2FA, app passwords, API keys
|
||||
|
||||
### S4 — Offline Replica (encrypted delta-sync mail store)
|
||||
- `lib/offline-replica/` (13 files): engine, sync (1122 lines), store (997 lines), apply, jmap, states, read, retention, errors, schema, types
|
||||
- SQLCipher-encrypted SQLite; delta sync via JMAP `/changes`
|
||||
- Two-tier storage (envelope + body); cursor provenance with branded types
|
||||
- **Fallback only** — consulted after live JMAP read fails; online session never sees replica data
|
||||
|
||||
### S5 — Local Search Index (encrypted FTS5)
|
||||
- `lib/mail-index/` (8 files): store (534 lines), jmap (388 lines), extract (311 lines), reindex (385 lines), key (203 lines), paths, binding
|
||||
- SQLCipher-encrypted SQLite with FTS5, **Electron/desktop only**
|
||||
- Event-driven (triggered by JMAP push changes via API routes)
|
||||
- Key transport uses inherited fd (pipe), not env var
|
||||
|
||||
### S6 — Platform Bridge
|
||||
- `lib/electron-bridge.ts` (55 lines) — `isElectronShell()`, `showElectronNotification()`
|
||||
- `lib/platform-capabilities.ts` (29 lines) — single source of truth for platform feature availability
|
||||
- `lib/web-push.ts` — web push for PWA
|
||||
- `electron/main.ts` (424 lines) — Electron main process (spawns Next.js server)
|
||||
- `electron/preload.ts` (27 lines) — contextBridge, nodeIntegration=false, sandbox=true
|
||||
|
||||
### S7 — Plugin Sandbox
|
||||
- `lib/plugin-sandbox/` (13 files) — iframe isolation, postMessage RPC, tier gating
|
||||
- `lib/plugin-types.ts` (1092 lines) — massive type definitions
|
||||
- `lib/plugin-hooks.ts` — hook registration for email/calendar/composer/sidebar slots
|
||||
- `lib/plugin-loader.ts`, `lib/plugin-validator.ts`, `lib/plugin-storage.ts`
|
||||
|
||||
### S8 — HTTP Proxy / API Gateway
|
||||
- `proxy.ts` (211 lines) — Next.js middleware: CSP, nonce, setup-state gating, intl routing
|
||||
- `app/api/*` — 24 API endpoint groups (auth, admin, AI, offline, setup, etc.)
|
||||
|
||||
### S9 — Security / Sanitization
|
||||
- `lib/security/url-guard.ts` (67 lines) — SSRF prevention, loopback/private-IP blocking
|
||||
- `lib/email-sanitization.ts` — DOMPurify HTML sanitization
|
||||
- `lib/smime-ca/` (5 files) — S/MIME certificate authority (EJBCA + local dev CA)
|
||||
|
||||
### S10 — Theme System
|
||||
- `stores/theme-store.ts` (593 lines) — light/dark/system, custom theme installation
|
||||
- `lib/theme-compiler.ts`, `lib/theme-loader.ts`, `lib/theme-logo.ts`
|
||||
- `lib/builtin-themes.ts` — 8 built-in themes (2 shipping, 6 hidden)
|
||||
- `lib/color-transform.ts` — luminance-based color remapping for dark mode
|
||||
|
||||
### S11 — Internationalization
|
||||
- `stores/locale-store.ts` (19 lines)
|
||||
- `i18n/routing.ts` — next-intl configuration
|
||||
- `locales/` — 24 language translations
|
||||
- `lib/jalali-utils.ts` — Persian calendar support
|
||||
|
||||
### S12 — Cross-Device Settings Sync
|
||||
- `lib/settings-sync.ts` — encrypted settings sync (AES-256-GCM)
|
||||
|
||||
### S13 — Telemetry
|
||||
- `lib/telemetry/` (7 files) — anonymous heartbeat, opt-in, HMAC-hashed logins
|
||||
|
||||
### S14 — Version Check
|
||||
- `lib/version-check/` (5 files) — polls version server, daily jittered schedule
|
||||
- `stores/update-store.ts` (118 lines)
|
||||
|
||||
---
|
||||
|
||||
## Feature Flag Registry (from lib/admin/types.ts)
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `pluginsEnabled` | false | Plugin system on/off |
|
||||
| `pluginsUploadEnabled` | true | Allow manual plugin upload |
|
||||
| `requirePluginApproval` | true | Admin must approve plugins |
|
||||
| `themesEnabled` | true | Theme system on/off |
|
||||
| `sidebarAppsEnabled` | true | Sidebar app visibility/order |
|
||||
| `userThemesEnabled` | true | Users can install custom themes |
|
||||
| `settingsExportEnabled` | true | Settings export |
|
||||
| `customKeywordsEnabled` | true | Custom keyword/tag creation |
|
||||
| `templatesEnabled` | true | Email templates |
|
||||
| `calendarEnabled` | true | Calendar feature |
|
||||
| `calendarTasksEnabled` | true | Calendar tasks |
|
||||
| `smimeEnabled` | true | S/MIME |
|
||||
| `externalContentEnabled` | true | External content in emails |
|
||||
| `debugModeEnabled` | true | Debug tools |
|
||||
| `folderIconsEnabled` | true | Custom folder icons |
|
||||
| `hoverActionsConfigEnabled` | true | Hover action configuration |
|
||||
| `filesEnabled` | true | File browser |
|
||||
| `contactsEnabled` | true | Contacts |
|
||||
| `crossAllViewEnabled` | false | Cross-account All Mail |
|
||||
| `crossUnreadViewEnabled` | false | Cross-account Unread |
|
||||
| `crossStarredViewEnabled` | false | Cross-account Starred |
|
||||
| `unifiedCrossAccountEnabled` | false | Cross-account unified inbox |
|
||||
| `aiAssistantEnabled` | true | AI assistant (visible by default — local ships free) |
|
||||
|
||||
---
|
||||
|
||||
## Dependency / Coupling DAG (Store Layer)
|
||||
|
||||
```
|
||||
auth-store (HUB) ──→ identity-store
|
||||
├──→ account-store
|
||||
├──→ calendar-store
|
||||
├──→ contact-store
|
||||
├──→ filter-store
|
||||
├──→ settings-store
|
||||
└──→ vacation-store
|
||||
|
||||
email-store ──→ calendar-store
|
||||
├──→ auth-store
|
||||
├──→ account-store
|
||||
├──→ settings-store
|
||||
└──→ message-list-tabs-store ←── (BIDIRECTIONAL CYCLE)
|
||||
|
||||
settings-store ──→ theme-store
|
||||
└──→ locale-store
|
||||
|
||||
plugin-store ──→ locale-store
|
||||
└──→ policy-store
|
||||
|
||||
theme-store ──→ policy-store
|
||||
|
||||
account-security-store ──→ auth-store
|
||||
|
||||
Standalone (14): file-store, task-store, filter-store, identity-store,
|
||||
account-store, pro-tab-store, ui-store, vacation-store, totp-reauth-store,
|
||||
calendar-notification-store, managed-account-store, update-store,
|
||||
toast-store, admin-tab-store, template-store, webdav-store
|
||||
```
|
||||
|
||||
**Critical coupling paths to audit in Wave 2:**
|
||||
1. `auth-store` → bootstraps 7 feature stores on login → any bootstrap failure cascades
|
||||
2. `email-store` ↔ `message-list-tabs-store` — bidirectional cycle
|
||||
3. `email-store` → `calendar-store` — cross-feature coupling (invitation banners, birthday calendar)
|
||||
4. `settings-store` → `theme-store` → theme injection affects ALL components
|
||||
5. JMAP client (`lib/jmap/client.ts`) → used by ALL feature stores via `client-registry`
|
||||
|
||||
---
|
||||
|
||||
## Platform Parity Surface
|
||||
|
||||
| Feature | Web (PWA) | Electron Desktop | Native (planned) |
|
||||
|---------|-----------|------------------|------------------|
|
||||
| Mail | Full | Full | Full (planned) |
|
||||
| Calendar | Full | Full | Full (planned) |
|
||||
| Contacts | Full | Full | Full (planned) |
|
||||
| Files | Full | Full | Full (planned) |
|
||||
| Local Search Index | ❌ | SQLCipher FTS5 | ❌ (planned) |
|
||||
| Offline Replica | ❌ | SQLCipher SQLite | ❌ (planned) |
|
||||
| Native Notifications | Web Push | Electron Notification | Native (planned) |
|
||||
| AI Local LLM | Browser CORS | No CORS (loopback) | Native (planned) |
|
||||
| S/MIME | Full | Full | Full (planned) |
|
||||
Reference in New Issue
Block a user