NEXO 6.4.0 — Multi-tenant email accounts + Desktop bridge
Published 2026-04-19. Plan Consolidado fase F1.
What changes
v6.4.0 introduces a new email_accounts table (migration m46) so a single NEXO Brain install can now own more than one mailbox at the same time, each with its own IMAP / SMTP coords, role (inbox / outbox / both), enabled flag, operator notification address, and trusted-domain list. Passwords are not stored in this table — only a pointer (credential_service + credential_key) into the existing credentials table that already holds every other secret on the box. One DB, one credentials surface, one CLI.
On top of the table sits a nexo email subcommand tree built for two very different consumers:
- Operators on a terminal —
nexo email setupis an interactive wizard. It asks one question at a time (label, address, IMAP / SMTP coords, password throughgetpass, operator email, trusted senders, role), confirms, stores the password in the credentials table, and offers an IMAP+SMTP test on the spot. Designed for someone who would never open a JSON file and who shouldn't have to. - NEXO Desktop & scripts —
nexo email add --label X --email X --imap-host X --smtp-host X --role both --password-stdin --jsonis the non-interactive variant. The password is read from stdin (never on argv), so it never appears inps.list,test, andremoveall accept--jsontoo, with stable shapes documented insidecli_email.py.
Why this matters
The legacy config lived at ~/.nexo/nexo-email/config.json. It was single-tenant, stored the password in cleartext next to other settings, and was invisible to the rest of the runtime DB. F1 unifies email config with everything else: one DB, one credentials table, one CLI, and a stable JSON contract for any UI that wants to drive it.
The matching NEXO Desktop release (v0.19.0) ships a Settings → Email panel that does exactly that — lists every configured account, lets you add a new one through a small modal form, runs the IMAP+SMTP probe, and removes accounts with a confirm dialog. Operators who can't (or shouldn't) edit JSON now have a real surface, and the same Brain code keeps powering the terminal wizard for the people who prefer it.
Backwards compatibility
Existing installs are not asked to do anything. The email_config.load_email_config() loader prefers the new table and falls back to the legacy JSON if the table is empty, so today's operators keep running. On the next session, auto_update.py calls _maybe_migrate_legacy_email_config(), which runs the new src/scripts/nexo-email-migrate-config.py exactly once: it reads the legacy file, inserts a credential into credentials and an account row into email_accounts with label='primary', and is idempotent — if the migration already ran, it short-circuits.
The two existing email runners (nexo-email-monitor.py and nexo-send-reply.py) were retrofitted to call load_email_config() first and to fall back to the legacy JSON path only if the loader returns nothing. Behaviour is identical for any operator still on the old config.
What's not in this release
F0.6 (the breaking physical migration of ~/.nexo/scripts → ~/.nexo/core/scripts + ~/.nexo/personal/scripts with the safe-symlink layer removed) is intentionally deferred to v7.0.0. v6.4.0 is a non-breaking minor: the safe symlinks the previous wave introduced still cover the old paths.
Tests & auditing
8 new tests in tests/test_email_accounts.py exercise the CRUD module (add+list, upsert by label, role validation, remove, primary picker), the loader (prefers table, falls back to JSON), and the migrator end-to-end. The PR was reviewed by the second-pass nightly auditor (Opus 4.7 effort=xhigh) before merge; bandit's B324 SHA1 finding on _debt_fingerprint was resolved by passing usedforsecurity=False (the call is a content fingerprint for dedup, not a security hash).