zeroclaw-labs/zeroclaw · error · anyhow::Error
Gmail OAuth token is not configured for sending
Error message
Gmail OAuth token is not configured for sending
What it means
Thrown by the Channel::send implementation of GmailPushChannel before it builds the RFC-2822 message: sending via POST /gmail/v1/users/me/messages/send requires config.oauth_token, and it is empty. Note the wording differs from the watch/history guards ('for sending') — a channel can register watches yet still be unable to send if the token lacks gmail.send scope or is missing, and this guard catches only the missing case.
Solutions
- Set [channels.gmail.<alias>] oauth_token to a token whose OAuth grant includes the gmail.send scope
- If this channel is receive-only, remove it from the reply routing / outbound channel selection so send is never attempted
- Re-run the OAuth consent flow if the stored token was revoked or never had send scope — an empty string here almost always means the field was never populated
- Validate at startup that every enabled channel that appears in outbound routing has a non-empty token
Defensive patterns
Strategy: validation
Validate before calling
// Before routing an outbound message to gmail_push:
let can_send = |c: &GmailPushChannel| !c.config.oauth_token.trim().is_empty();
if !can_send(&channel) {
anyhow::bail!("gmail channel cannot send: oauth_token missing");
}
channel.send(&msg).await?; Try / catch
match channel.send(&msg).await {
Ok(()) => {}
Err(e) if e.to_string().contains("not configured for sending") => {
// route via an alternate outbound channel or queue for the operator
}
Err(e) => return Err(e),
} Prevention
- Separate receive-capable and send-capable channel selection in your router so token-less gmail channels never get outbound traffic
- Confirm the OAuth grant includes gmail.send, not just gmail.readonly, when you intend to reply
- Smoke-test send at startup for channels in the outbound rotation
When it happens
Trigger: The orchestrator routes an outbound SendMessage to a gmail_push channel handle whose config.oauth_token is empty — the guard fires before header sanitization and the messages.send request. Callers listed are register_watch, fetch_history_inner, fetch_message, handle_notification, send and health_check flows that end up invoking send.
Common situations: Read-only ingestion setup (token intentionally omitted) that later receives a reply request; enabled channel with topic/webhook configured but oauth_token blank; token env var empty in the deployed environment.
Related errors
- Gmail OAuth token is not configured
- Auth profile not found
- cloud_ops.iac_tools must not be empty when cloud_ops is…
- failed to build HTTP client
- gateway.path_prefix contains invalid character
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/f404c9e972ebbbde.
Report an issue: GitHub.
Appendix: source
Thrown at crates/zeroclaw-channels/src/gmail_push.rs:568
// Gmail push delivery has no typing-indicator concept.
Ok(())
}
fn name(&self) -> &str {
"gmail_push"
}
async fn send(&self, message: &SendMessage) -> Result<()> {
// Send via Gmail API (drafts.send or messages.send)
let token = self.config.oauth_token.clone();
if token.is_empty() {
::zeroclaw_log::record!(
ERROR,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
.with_outcome(::zeroclaw_log::EventOutcome::Failure),
"Gmail OAuth token is not configured for sending"
);
anyhow::bail!("Gmail OAuth token is not configured for sending");
}
let subject = message.subject.as_deref().unwrap_or("ZeroClaw Message");
// Sanitize headers to prevent CRLF injection attacks.
let safe_recipient = sanitize_header_value(&message.recipient);
let safe_subject = sanitize_header_value(subject);
let rfc2822 = format!(
"To: {}\r\nSubject: {}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n{}",
safe_recipient, safe_subject, message.content
);
let encoded = BASE64.encode(rfc2822.as_bytes());
// Gmail API uses URL-safe base64 with no padding
let url_safe = encoded.replace('+', "-").replace('/', "_").replace('=', "");
let body = serde_json::json!({
"raw": url_safe,
});
View on GitHub (pinned to 88bb9c8533)