zeroclaw-labs/zeroclaw · error

Email OAuth2 profile is not OAuth-based: {profile_id}

Error message

Email OAuth2 profile is not OAuth-based: {profile_id}

What it means

get_valid_email_oauth2_token selected a profile for the email channel alias (e.g. "email.hotmail") but its token_set is None: the stored profile is not OAuth-based. Email channels require an OAuth2 token set (access + refresh token) so the resolver can refresh before IMAP connects; a bearer-token or malformed profile is a hard error.

Source

Thrown at crates/zeroclaw-providers/src/auth/mod.rs:516

        channel_alias: &str,
        profile_override: Option<&str>,
        token_url: &str,
        client_id: &str,
        scopes: &[String],
    ) -> Result<Option<String>> {
        const SKEW_SECS: u64 = 90;

        let data = self.store.load().await?;
        let Some(profile_id) = select_profile_id(&data, channel_alias, profile_override) else {
            return Ok(None);
        };

        let Some(profile) = data.profiles.get(&profile_id) else {
            return Ok(None);
        };

        let Some(token_set) = profile.token_set.as_ref() else {
            anyhow::bail!("Email OAuth2 profile is not OAuth-based: {profile_id}");
        };

        if !token_set.is_expiring_within(Duration::from_secs(SKEW_SECS)) {
            return Ok(Some(token_set.access_token.clone()));
        }

        let Some(refresh_token) = token_set.refresh_token.clone() else {
            // No refresh token; return the (possibly expired) access token and
            // let the IMAP auth failure surface as a log event.
            return Ok(Some(token_set.access_token.clone()));
        };

        let refresh_lock = refresh_lock_for_profile(&profile_id);
        let _guard = refresh_lock.lock().await;

        // Re-load after acquiring lock to avoid duplicate refreshes.
        let data = self.store.load().await?;
        let Some(latest_profile) = data.profiles.get(&profile_id) else {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Re-run the email channel OAuth2 setup for that channel alias to store a full token set
  2. Confirm the channel alias used in config matches the alias the profile was stored under (profiles are keyed by the alias)
  3. Inspect the stored profile and delete the broken entry so setup can recreate it cleanly
Defensive patterns

Strategy: validation

Validate before calling

let data = auth.load_profiles().await?;
let profile_id = data.profiles.keys().find(|k| k.starts_with(&format!("{channel_alias}:")));
if let Some(id) = profile_id {
    anyhow::ensure!(
        data.profiles[id].token_set.is_some(),
        "email profile {id} has no OAuth token set; re-run the channel OAuth setup"
    );
}
let token = auth.get_valid_email_oauth2_token(channel_alias, None, url, id, scopes).await?;

Type guard

fn is_oauth_profile(p: &AuthProfile) -> bool {
    p.token_set.is_some()
}

Try / catch

match auth.get_valid_email_oauth2_token(alias, None, url, id, scopes).await {
    Ok(tok) => tok,
    Err(e) if e.to_string().contains("not OAuth-based") => {
        eprintln!("channel '{alias}' needs a completed OAuth grant; re-run setup");
        return Err(e);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling imap_connect or get_oauth2_token for an email channel whose profile was never completed through the email OAuth2 setup flow, or whose profiles entry lost the token_set field (partial setup, manual edit, wrong channel alias key).

Common situations: Email channel configured in config.toml but the OAuth grant was never finished; channel alias renamed so a new empty profile is selected; profiles file migrated between machines without the token set.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/52bc5f80dfe52e66. Report an issue: GitHub.