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.

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)

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.