Housekeeping: - Bump VERSION to 1.7.9 - CHANGELOG entry for all Phase 1 fixes - Mark Phase 1 as completed in development plan
24 KiB
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, newlib/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_tokenfrom OAuth error logs (lib/oauth/token-exchange.ts) — 30min - H9: Fix bcrypt in
isHashed()(lib/admin/password.ts) — 1h - H18: Default
autoSelectReplyIdentitytotrue(stores/settings-store.ts) — 30min - H7: DOMPurify HTML template body on import (
lib/template-utils.ts) — 1h - H13:
calendarTasksEnabledruntime enforcement (stores/task-store.ts,components/calendar/) — 2h - H14: try/catch + toast on task mutations (
stores/task-store.ts) — 1h - H8: Derive
securecookie fromx-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-newequivalent
Implementation:
- New
stores/signature-store.ts— Signature[] with CRUD + identity assignment - New
components/settings/signature-settings.tsx— list management page - New
components/settings/signature-editor-modal.tsx— Quill-based HTML editor - Extend
components/identity/identity-form.tsx— default/forward-reply signature picker - Extend
components/email/email-composer.tsx— signature selector dropdown in compose - 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:
Appointmentinterface fromappoinment.model.ts- Calendar compose pre-fill logic from
calendar-compose.component.ts - API payload shapes for event creation
Implementation:
- New
components/email/create-appointment-button.tsx— button in email toolbar - Extend
components/calendar/event-modal.tsx— accept pre-fill props (title=subject, description=body, participants=from+to+cc) - 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 selectionPreferencesSharingComponent(556 lines) — "shared by me" / "shared with me" viewsAllSharingFoldersDialogComponent— tree-based folder browser- API: share/revoke/accept/decline endpoints and payload shapes
Implementation:
- New
components/sharing/share-folder-dialog.tsx— modal with email autocomplete, role picker (read/read-write/admin), message - New
components/sharing/share-folder-revoke-dialog.tsx— revoke confirmation - New
components/sharing/accept-share-dialog.tsx— accept incoming share - New
components/settings/sharing-settings.tsx— "Shared by me" / "Shared with me" tabs with folder tree - Extend folder context menus in email sidebar, calendar sidebar, contacts sidebar, files sidebar with "Share Folder..." action
- New
stores/sharing-store.ts— tracking shares state - 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:
- New
components/calendar/mini-calendar-dashlet.tsx— compact month grid widget - Integrate into mail sidebar or bottom-left overlay in the mail page layout
- Show date dots for days with events, today highlight, click to navigate to calendar
- 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 settingspreferenceService.importFromFile()— API request shape- CSV type auto-detection logic
Implementation:
- Extend existing
.emlimport (already partial — seelib/eml-import.tsandcomponents/email/for.emlpreview) - Add ZIP/TGZ/TAR archive import — extract, iterate, import each
.eml - New
components/settings/import-settings.tsx— import UI with file picker, destination folder, conflict resolution - API route: extend
app/api/account/*or newapp/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 selectorcontactService.importContacts()— API endpoint- vCard parsing:
lib/vcard.tsalready exists in codebase
Implementation:
- Extend existing
components/contacts/contact-import-dialog.tsxto support CSV (currently vCard-only) - Add CSV column mapping UI (map CSV columns to contact fields)
- Folder selection for import destination
- 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 gridscheduler-utils.ts— free-busy status constants (fbavalues,FBA_TO_PTSTmapping)schedule-assistant.component.ts(1081 lines) — suggestion engine algorithm- Free/busy URL format:
{serverURL}/home/{email}?fmt=freebusy
Implementation:
- New
components/calendar/free-busy-view.tsx— attendee-row × time-slot grid - Color-coded slots: free (white), busy (red), tentative (yellow), out-of-office (purple)
- Integration into event-modal when adding participants — show availability inline
- Also queries
resources_bookingstable (see P2.8) to show resource availability in the same grid - 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 patternscalendar-equipment-autocomplete.component.ts(266 lines) — autocomplete UX- GAL query:
zimbraCalResType === "Equipment"→ adapt to VNCdirectory resource type filter
Backend (New):
-
PostgreSQL migration —
resourcestable:idUUID PKtenant_idUUID (VNCdirectory tenant scope)nametexttypeenum (room, vehicle, equipment, other)locationtext (building/floor/room)capacityinteger nullabledescriptiontextcontact_emailtext (responsible person)is_activebooleanmetadatajsonb (extensible: photo URL, amenities, access hours, etc.)created_at,updated_attimestamps
-
resourcestable → 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)
-
New API routes:
app/api/resources/GET /api/resources— list/search resources (type filter, location filter, tenant scoped)GET /api/resources/[id]— single resource detailGET /api/resources/[id]/availability?start=&end=— free/busy for a resourcePOST /api/resources/[id]/book— create booking for a resourceDELETE /api/resources/[id]/book/[bookingId]— cancel booking
-
New
lib/resources/service layer:lib/resources/client.ts— fetch/query resources from VNCdirectory or local DBlib/resources/availability.ts— check resource availability for time rangelib/resources/booking.ts— book/cancel resourcelib/resources/sync.ts— sync with VNCdirectory (if push-model needed)
-
Conflict checking:
- When booking, check
resources_bookingstable for overlapping time ranges - Return conflicts + alternative slots
- When booking, check
Frontend:
- 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)
- New
components/calendar/resource-picker.tsx— reusable resource selection component - 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()inedit-appointment-dialog.component.ts— builds payload from appointment
Implementation:
- New
lib/vnctalk/client.ts— VNCtalk API client (create/update meeting) - Extend
components/calendar/event-modal.tsx— "Create VNCtalk Meeting" toggle/button - Store meeting JID on CalendarEvent for updates
- 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 APIPOST ocs/v2.php/apps/richdocuments/api/v1/document?format=json- Config:
collaboraBaseUrlfrom admin config
Implementation:
- New
lib/collabora/client.ts— fetch editing URL from Collabora server - New
components/files/collabora-editor.tsx— iframe-based editor embedding - Extend
components/files/file-browser.tsx— "Edit with Collabora" action for office files - Admin config:
collaboraBaseUrlin 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.tsxdata. - 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:
- New
components/ui/radial-menu.tsx— SVG-based radial menu with configurable items - Supports: mail actions (reply, forward, delete, archive, mark read, move, tag), contact actions, file actions
- Trigger: long-press on mobile, right-click on desktop, or dedicated button
- Animations: CSS rotate + scale transitions
- 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 setuproutes/index.js— login/logout/SAML callback/LDAP search/2FA/TOTP routesutils/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:
-
New
lib/vncdirectory/service layer:lib/vncdirectory/client.ts— API client for VNCdirectory REST endpoints (port auth patterns fromroutes/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-samlvia Next.js API routeslib/vncdirectory/ldap.ts— LDAP client usingldapjsfor user/group/GAL querieslib/vncdirectory/2fa.ts— TOTP enrollment + verification via VNCdirectory
-
New API routes:
app/api/vncdirectory/GET /api/vncdirectory/status— connection health checkPOST /api/vncdirectory/saml/login— initiate SAML login flowPOST /api/vncdirectory/saml/callback— SAML assertion consumerPOST /api/vncdirectory/saml/logout— SAML single logoutGET /api/vncdirectory/users— search users (LDAP + VNCdirectory)POST /api/vncdirectory/2fa/enroll— generate TOTP secretPOST /api/vncdirectory/2fa/verify— verify TOTP codeGET /api/vncdirectory/2fa/status— check 2FA enrollment statusGET /api/vncdirectory/tags— directory contact tagsPOST /api/vncdirectory/tags— create/update directory tags
-
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
-
New
stores/vncdirectory-store.ts— client-side config state -
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:
- Extend
components/files/file-browser.tsx— "Send as Email Attachment" action - Opens composer with selected files attached (reuse existing attachment upload in composer)
- 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:
toWildcardQueryquote handling,searchEmailsAbortController - 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_bookingstables 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 —
collaboraBaseUrladmin config for online document editing - VNCtalk API —
/api/createnewmeetingendpoint 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.
Phase 1 — COMPLETED 2026-08-07
Shipped as v1.7.9. 17 of 18 fixes deployed to main. All 2527 tests pass (161 test files). One item deferred: P1.3 (C5 auth localStorage encryption) — requires custom Zustand persist adapter, planned for Phase 3.
Do you want me to start Phase 1 immediately, or adjust the plan?