Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gmail-quota-project-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---

Fix Gmail helpers (`+read`, `+forward`, `+reply`, send-as resolution, original-attachment forwarding) returning a 403 accessNotConfigured error for ADC callers by adding the missing `x-goog-user-project` header
44 changes: 37 additions & 7 deletions crates/google-workspace-cli/src/helpers/gmail/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,11 +374,21 @@ pub(super) async fn fetch_message_metadata(
crate::validate::encode_path_segment(message_id)
);

// Set quota project from ADC for billing/quota attribution. This mirrors the
// header the executor adds for standard resource commands: without it, an ADC
// caller with no explicit quota project gets a 403 accessNotConfigured from
// gmail.googleapis.com even though the same request via the executor succeeds.
let quota_project = auth::get_quota_project();

let resp = crate::client::send_with_retry(|| {
client
let mut request = client
.get(&url)
.bearer_auth(token)
.query(&[("format", "full")])
.query(&[("format", "full")]);
if let Some(quota_project) = &quota_project {
request = request.header("x-goog-user-project", quota_project);
}
request
})
.await
.map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to fetch message: {e}")))?;
Expand Down Expand Up @@ -452,10 +462,20 @@ async fn fetch_send_as_identities(
client: &reqwest::Client,
token: &str,
) -> Result<Vec<SendAsIdentity>, GwsError> {
// Set quota project from ADC for billing/quota attribution. This mirrors the
// header the executor adds for standard resource commands: without it, an ADC
// caller with no explicit quota project gets a 403 accessNotConfigured from
// gmail.googleapis.com even though the same request via the executor succeeds.
let quota_project = auth::get_quota_project();

let resp = crate::client::send_with_retry(|| {
client
let mut request = client
.get("https://gmail.googleapis.com/gmail/v1/users/me/settings/sendAs")
.bearer_auth(token)
.bearer_auth(token);
if let Some(quota_project) = &quota_project {
request = request.header("x-goog-user-project", quota_project);
}
request
})
.await
.map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to fetch sendAs settings: {e}")))?;
Expand Down Expand Up @@ -697,9 +717,19 @@ async fn fetch_attachment_data(
crate::validate::encode_path_segment(attachment_id),
);

let resp = crate::client::send_with_retry(|| client.get(&url).bearer_auth(token))
.await
.map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to fetch attachment: {e}")))?;
// Set quota project from ADC for billing/quota attribution. See fetch_message_metadata
// above for why this is required to avoid a 403 accessNotConfigured for ADC callers.
let quota_project = auth::get_quota_project();

let resp = crate::client::send_with_retry(|| {
let mut request = client.get(&url).bearer_auth(token);
if let Some(quota_project) = &quota_project {
request = request.header("x-goog-user-project", quota_project);
}
request
})
.await
.map_err(|e| GwsError::Other(anyhow::anyhow!("Failed to fetch attachment: {e}")))?;

if !resp.status().is_success() {
let status = resp.status().as_u16();
Expand Down
Loading