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:

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).