You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

带全域委派的服务账户执行Apps Script时Gmail API 403权限错误

使用服务账户全域委派更新Gmail签名时遇403权限错误

我正在运行一个Google Apps Script,通过带全域委派(DWD)的服务账户,为Google Workspace域内用户批量更新Gmail签名。脚本能成功生成AccessToken,也能更新被模拟的超级管理员的签名,但操作非管理员用户(如user@mydomain.com)时,始终返回403 Forbidden错误:

403 Forbidden: { "error": { "code": 403, "message": "Delegation denied for admin@email.com", ... } }

已验证的配置

已完成以下标准排查,确认配置无误:

  • 被模拟用户:服务账户模拟的admin@email.com是活跃超级管理员
  • 服务账户Client ID:脚本属性中配置的Client ID与管理控制台的DWD条目完全一致(已尝试生成新ID)
  • 全域委派范围:管理控制台注册的范围仅包含所需的两个权限:https://www.googleapis.com/auth/gmail.settings.basic,https://www.googleapis.com/auth/admin.directory.user.readonly
  • API请求格式:使用正确的请求路径:PUT https://gmail.googleapis.com/gmail/v1/users/{userEmail}/settings/sendAs/{userEmail}
  • 同步等待:每次配置修改后均等待30分钟以上

脚本代码

function updateSignaturesFromSheet() {
  const privateKey = PropertiesService.getScriptProperties().getProperty('PRIVATE_KEY');
  const clientEmail = PropertiesService.getScriptProperties().getProperty('CLIENT_EMAIL');
  
  const adminEmail = 'admin@email.com'; // Admin email registered in DWD

  if (!privateKey || !clientEmail) {
    Logger.log(' ERROR: CLIENT_EMAIL or PRIVATE_KEY not set in Script Properties.');
    return;
  }

  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const data = sheet.getDataRange().getValues();

  Logger.log(` Found ${data.length - 1} rows of user data to process.`);

  let accessToken;
  try {
    accessToken = getServiceAccountAccessToken(clientEmail, privateKey, adminEmail);
  } catch(e) {
    Logger.log(` Failed to get DWD token: ${e.message}`);
    return;
  }

  for (let i = 1; i < data.length; i++) {
    const [email, name, position, phone, website, logoURL] = data[i];
    if (!email || !name) continue;

    const userEmail = email.toString().trim();
    const companyName = "Company Name";
    const cleanPhone = phone ? phone.toString().replace(/[^0-9+()\s-]/g, '') : '';
    const telLink = cleanPhone.replace(/[^0-9+]/g, '');
    
    const signatureHtml = 
      `<div style="font-family:Arial, sans-serif; font-size:14px; color:#333;">
        ${name}<br>
        ${position ? `${position}<br>` : ''}
        <strong>${companyName}</strong><br>
        ${logoURL ? `<img src="${logoURL}" width="150" style="display:block;margin:5px 0;">` : ''}
        ${cleanPhone ? `Office: <a href="tel:${telLink}" style="color:#000;">${cleanPhone}</a><br>` : ''}
        ${website ? `<a href="${website}" target="_blank" style="color:#0073e6;">${website}</a>` : ''}
      </div>`;
    
    try {
      const url = `https://gmail.googleapis.com/gmail/v1/users/${userEmail}/settings/sendAs/${userEmail}`;
      const options = {
        method: 'PATCH',
        headers: {
          'Authorization': `Bearer ${accessToken}`,
          'Content-Type': 'application/json'
        },
        payload: JSON.stringify({ signature: signatureHtml }),
        muteHttpExceptions: true
      };

      const response = UrlFetchApp.fetch(url, options);
      const responseCode = response.getResponseCode();

      if (responseCode === 200)
        Logger.log(` Signature updated for: ${userEmail}`);
      else
        Logger.log(`Failed for ${userEmail} (Code: ${responseCode}): ${response.getContentText()}`);

    } catch (err) {
      Logger.log(` Error for ${email}: ${err.message}`);
    }
  }

  Logger.log(' Signature update process complete.');
}

function getServiceAccountAccessToken(clientEmail, privateKey, adminEmail) {
  const correctedPrivateKey = privateKey.trim().replace(/\\n/g, '\n');
  
  const jwtHeader = Utilities.base64EncodeWebSafe(JSON.stringify({ alg: "RS256", typ: "JWT" }));
  
  const jwtClaim = Utilities.base64EncodeWebSafe(JSON.stringify({
    iss: clientEmail,
    scope: "https://www.googleapis.com/auth/gmail.settings.basic https://www.googleapis.com/auth/admin.directory.user.readonly", 
    aud: "https://oauth2.googleapis.com/token",
    exp: Math.floor(Date.now() / 1000) + 3600,
    iat: Math.floor(Date.now() / 1000),
    sub: adminEmail
  }));

  const signatureInput = `${jwtHeader}.${jwtClaim}`;
  
  const signature = Utilities.base64EncodeWebSafe(
    Utilities.computeRsaSha256Signature(signatureInput, correctedPrivateKey)
  );
  const jwt = `${signatureInput}.${signature}`;

  const tokenResponse = UrlFetchApp.fetch("https://oauth2.googleapis.com/token", {
    method: "post",
    payload: {
      grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
      assertion: jwt
    },
    muteHttpExceptions: true
  });

  const tokenData = JSON.parse(tokenResponse.getContentText());
  if (!tokenData.access_token)
    throw new Error(`Failed to get access token: ${tokenResponse.getContentText()}`);

  return tokenData.access_token;
}

解决建议

  1. 修正全域委派范围格式
    在Google Workspace管理控制台的「域宽委派」中,多个权限范围需用空格分隔而非逗号。若之前用了逗号,会导致范围未正确识别,需修改为:https://www.googleapis.com/auth/gmail.settings.basic https://www.googleapis.com/auth/admin.directory.user.readonly。

  2. 切换API请求方法
    将脚本中的method: 'PATCH'改为method: 'PUT',Gmail API更新签名时PUT方法更稳定,能确保覆盖原有签名配置。

  3. 验证AccessToken权限
    用jwt.io解码生成的AccessToken,检查scope字段是否包含正确的两个权限,sub字段是否为admin@email.com,确保令牌具备委派访问的权限。

  4. 确认用户主邮箱地址
    确保脚本中使用的userEmail是用户的主邮箱地址而非别名。可调用Gmail API的users.sendAs.list接口,确认目标地址为用户isPrimary: true的别名。

  5. 检查管理员权限完整性
    登录Google Workspace管理控制台,进入「管理员角色」,确认超级管理员角色包含「Gmail设置管理」权限,确保admin@email.com未被移除相关权限。

内容的提问来源于stack exchange,提问作者nano nano

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 05:40:54