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

  1. Set [channels.gmail.<alias>] oauth_token to a token whose OAuth grant includes the gmail.send scope
  2. If this channel is receive-only, remove it from the reply routing / outbound channel selection so send is never attempted
  3. 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
  4. 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

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


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)