{"record":{"id":"f404c9e972ebbbde","repo":"zeroclaw-labs/zeroclaw","slug":"gmail-oauth-token-is-not-configured-for-sending","errorCode":null,"errorMessage":"Gmail OAuth token is not configured for sending","messagePattern":"Gmail OAuth token is not configured for sending","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/gmail_push.rs","lineNumber":568,"sourceCode":"        // Gmail push delivery has no typing-indicator concept.\n        Ok(())\n    }\n\n    fn name(&self) -> &str {\n        \"gmail_push\"\n    }\n\n    async fn send(&self, message: &SendMessage) -> Result<()> {\n        // Send via Gmail API (drafts.send or messages.send)\n        let token = self.config.oauth_token.clone();\n        if token.is_empty() {\n            ::zeroclaw_log::record!(\n                ERROR,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Failure),\n                \"Gmail OAuth token is not configured for sending\"\n            );\n            anyhow::bail!(\"Gmail OAuth token is not configured for sending\");\n        }\n\n        let subject = message.subject.as_deref().unwrap_or(\"ZeroClaw Message\");\n        // Sanitize headers to prevent CRLF injection attacks.\n        let safe_recipient = sanitize_header_value(&message.recipient);\n        let safe_subject = sanitize_header_value(subject);\n        let rfc2822 = format!(\n            \"To: {}\\r\\nSubject: {}\\r\\nContent-Type: text/plain; charset=utf-8\\r\\n\\r\\n{}\",\n            safe_recipient, safe_subject, message.content\n        );\n        let encoded = BASE64.encode(rfc2822.as_bytes());\n        // Gmail API uses URL-safe base64 with no padding\n        let url_safe = encoded.replace('+', \"-\").replace('/', \"_\").replace('=', \"\");\n\n        let body = serde_json::json!({\n            \"raw\": url_safe,\n        });\n","sourceCodeStart":550,"sourceCodeEnd":586,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/gmail_push.rs#L550-L586","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before routing an outbound message to gmail_push:\nlet can_send = |c: &GmailPushChannel| !c.config.oauth_token.trim().is_empty();\nif !can_send(&channel) {\n    anyhow::bail!(\"gmail channel cannot send: oauth_token missing\");\n}\nchannel.send(&msg).await?;","typeGuard":null,"tryCatchPattern":"match channel.send(&msg).await {\n    Ok(()) => {}\n    Err(e) if e.to_string().contains(\"not configured for sending\") => {\n        // route via an alternate outbound channel or queue for the operator\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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"],"tags":["gmail","oauth-token","send","configuration","rust"],"backgroundTag":"missing-oauth-token","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}