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
- Re-run the email channel OAuth2 setup for that channel alias to store a full token set
- Confirm the channel alias used in config matches the alias the profile was stored under (profiles are keyed by the alias)
- 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
- Finish the email channel OAuth grant before enabling IMAP for the channel
- Keep the channel alias identical in config and in the stored profile key
- Delete broken profile entries so setup recreates them with a full token set
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
- OpenAI Codex auth profile is not OAuth-based: {profile_id}
- OpenAI Codex auth profile is missing token set: {profile_id}
- Gemini auth profile is not OAuth-based: {profile_id}
- Gemini auth profile is missing token set: {profile_id}
- xAI auth profile is not OAuth-based: {profile_id}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/52bc5f80dfe52e66.
Report an issue: GitHub.