Enforce TOTP / 2FA for external directory (SQL/LDAP) accounts

**Summary:** MFA enforcement (`verify_mfa_secret_hash`) appears to be wired only for **internal-directory** accounts. In `crates/common/src/auth/authentication.rs` (v0.16.12) the external-directory auth branch calls `directory.authenticate(credentials)`, which verifies the password but never checks `otp_auth` / calls `verify_mfa_secret_hash` — so accounts backed by an external SQL or LDAP directory cannot have TOTP enforced.

**Use case / who benefits:** deployments that use an external directory as the source of truth — e.g. a hosting control panel that stores mail accounts in its own database and exposes them to Stalwart via the SQL directory. These setups can’t offer working 2FA on mailboxes today.

**What I observed** (v0.16.12, latest; external MySQL directory, `columnSecret` = one hash column):

- Provisioning an `otpauth://…` value as an additional account secret (returned via `columnSecret`) does **not** enforce TOTP — IMAP **and** JMAP still authenticate with the password alone.

- External-directory accounts have no settable `x:Account` object, so the OTP Auth URL can’t be attached via the management API either.

- The Account Manager docs say *“for external directories, administrators provision the OTP Auth URL as an account secret,”* but there doesn’t appear to be a code path that consumes it for the SQL/LDAP auth flow.

**Requested behavior:** enforce TOTP for external-directory accounts — e.g. recognize an `otpauth://`-prefixed value returned by the directory (SQL `columnSecret` row / LDAP attribute) as an MFA requirement in the external-auth path, mirroring the internal `credential.otp_auth` flow. App-password support for these accounts would help too, since mail clients can’t do interactive TOTP.

**Environment:** Stalwart v0.16.12, external MySQL SQL directory.

Thanks!

Following up with an offer to contribute this as a PR.

I scoped it against main (v0.16.12):

  • otp_auth today exists only on the internal registry account (crates/registry/src/schema/structs.rs, crates/jmap/src/registry/mapping/principal.rs) and is enforced in crates/common/src/auth/authentication.rs via verify_mfa_secret_hash. There are no references to it in any directory backend (crates/directory/src/backend/*), so SQL/LDAP directories have no notion of a TOTP secret.
  • verify_mfa_secret_hash already lives in crates/directory/src/core/secret.rs, and mfa_token is already passed into directory.authenticate(...) on the external path — so both the enforcement primitive and the token are present; they’re just never wired together for external accounts.

Proposed PR shape (happy to adjust to your preferred design):

  1. Add otp_auth: Option<String> to the directory Principal.
  2. SQL/LDAP backends: classify an otpauth://… value out of the returned secrets rather than treating it as a password hash.
  3. Enforce it in directory.authenticate by calling the existing verify_mfa_secret_hash, returning MissingMfaToken / Invalid mirroring the internal path.
  4. Backend tests (password-only unchanged; otpauth present → TOTP required).

Before opening anything: would you accept this as a contribution, and does the approach match how you’d want external-directory MFA modeled? Happy to sign the CLA. Thanks!