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

使用Microsoft Graph代发邮件的权限问题及401错误排查求助

问题:Outlook认证后调用Microsoft Graph API发送邮件返回401错误

近期实现功能时遇到问题:允许用户通过Outlook邮箱完成认证,之后由API代表已认证用户预约发送邮件。目前认证流程正常,能成功获取并保存access token,调用/me接口也能正常返回用户数据,但尝试发送邮件时出现401错误:

{"statusCode":401,"code":null,"requestId":null,"date":"2023-04-21T14:18:46.748Z","body":{"_writeState":{"0":0,"1":0},"_readableState":{"objectMode":false,"highWaterMark":16384,"buffer":{"head":null,"tail":null,"length":0},"length":0,"pipes":null,"pipesCount":0,"flowing":null,"ended":false,"endEmitted":false,"reading":false,"sync":false,"needReadable":false,"emittedReadable":false,"readableListening":false,"resumeScheduled":false,"emitClose":true,"autoDestroy":false,"destroyed":false,"defaultEncoding":"utf8","awaitDrainWriters":null,"multiAwaitDrain":false,"readingMore":false,"decoder":null,"encoding":null},"readable":true,"_events":{"error":[null,null]},"_eventsCount":6,"_writableState":{"objectMode":false,"highWaterMark":16384,"finalCalled":false,"needDrain":false,"ending":false,"ended":false,"finished":false,"destroyed":false,"decodeStrings":true,"defaultEncoding":"utf8","length":10,"writing":true,"corked":0,"sync":false,"bufferProcessing":false,"writelen":10,"afterWriteTickInfo":null,"bufferedRequest":null,"lastBufferedRequest":null,"pendingcb":1,"prefinished":false,"errorEmitted":false,"emitClose":true,"autoDestroy":false,"bufferedRequestCount":0,"corkedRequestsFree":{"next":null,"entry":null}},"writable":true,"allowHalfOpen":true,"_transformState":{"needTransform":false,"transforming":true,"writechunk":{"type":"Buffer","data":[31,139,8,0,0,0,0,0,4,10]},"writeencoding":"buffer"},"_hadError":false,"bytesWritten":0,"_handle":{"buffer":{"type":"Buffer","data":[31,139,8,0,0,0,0,0,4,10]},"availOutBefore":16384,"availInBefore":10,"inOff":0,"flushFlag":2},"_outBuffer":{"type":"Buffer","data":[85,85,85,85,85,85,80,0,0,0,0,0,0,0]},"_outOffset":0,"_chunkSize":16384,"_defaultFlushFlag":2,"_finishFlushFlag":2,"_defaultFullFlushFlag":3,"_maxOutputLength":2147483647,"_level":-1,"_strategy":0}}

推测是权限配置问题,已尝试多种权限(含代码中注释的所有权限),但仍报错,寻求排查建议。

认证及发送邮件代码

配置及工具函数

import * as msal from "@azure/msal-node";
import { Client } from "@microsoft/microsoft-graph-client";

export const msalConfig: msal.Configuration = {
  auth: {
    clientId: “CLIENT_ID”,
    authority: "https://login.microsoftonline.com/TENENT_ID”,
    clientSecret: “CLIENT_SECRET”,
  },
};

export const cca = new msal.ConfidentialClientApplication(msalConfig);

export const getGraphClient = (accessToken: string | null) => {
  const graphClient = Client.init({
    authProvider: (done) => {
      done(null, accessToken);
    },
  });
  return graphClient;
};

export const protectedResources = {
  graphMe: {
    meEndpoint: "https://graph.microsoft.com/v1.0/me",
    usersEndpoint: "https://graph.microsoft.com/v1.0/users/",
    scopes: [
      "openid",
      "profile",
      "user.read",
      "mail.send",
      "offline_access",
      //  "Mail.Send",
      //  "https://graph.microsoft.com/.default",
      //  "https://graph.microsoft.com/Mail.Send.Shared",
      //  "https://graph.microsoft.com/Mail.Read",
      //  "https://graph.microsoft.com/profile",
      //  "https://graph.microsoft.com/.default",
      //  "https://outlook.office.com/IMAP.AccessAsUser.All",
    ],
  },
};

启动认证流程

// Code to start the authentication flow
const state = request.query.state as string;

try {
    // Get the authorization URL
    const authCodeUrlParameters: msal.AuthorizationUrlRequest = {
        scopes: protectedResources.graphMe.scopes,
        redirectUri: "https://example.com/outlookOauth2Callback",
        codeChallengeMethod: "S256",
        state: state,
    };
    const authorizeUrl = await cca.getAuthCodeUrl(authCodeUrlParameters);
    response.redirect(authorizeUrl);
    return;
} catch (error) {
    console.log(error);
    response.status(500).send(error);
    return;
}

获取Access Token

// Code to get the access Token
const code = request.query.code as string;
const state = request.query.state as string;
try {
    const tokenRequest: msal.AuthorizationCodeRequest = {
        code: code,
        scopes: protectedResources.graphMe.scopes,
        redirectUri:
        "https://example.com/outlookOauth2Callback",
        codeVerifier: request.query.code_verifier as string,
    };

    const accessToken = await cca.acquireTokenByCode(tokenRequest);
    await saveUserToken(code, accessToken);
    console.log("Successfully authorized");
    response.redirect("https://example.com/callback.html?code=" + code + "&state=" + state);
     return;
} catch (error) {
    console.log(error);
    response.status(500).send(error);
    return;
}

发送邮件代码

// Code to send the email
const graphUser = await getGraphClient(token.accessToken)
      .api(protectedResources.graphMe.meEndpoint)
      .get();

functions.logger.log("Graph User: " + JSON.stringify(graphUser));

const message = {
      subject: subject,
      body: {
        contentType: "HTML",
        content: body,
      },
      toRecipients: [
        {
          emailAddress: {
            toName: toName,
            address: toEmail,
          },
        },
      ],
      from: { emailAddress: { fromName: fromName, address: graphUser.email } },
};

const response = await getGraphClient(token[1].accessToken)
   .api(protectedResources.graphMe.usersEndpoint + `${graphUser.id}/sendMail`)
   .post({ message });

functions.logger.log("Response: " + JSON.stringify(response));
排查建议
  • 修正Token引用错误:发送邮件时使用了token[1].accessToken,但调用/me接口用的是token.accessToken,确认saveUserToken保存的Token结构是否正确。如果保存的是完整的AuthenticationResult对象,token[1]是无效引用,应统一使用token.accessToken。

  • 验证权限范围正确性:

    • 确保Azure AD应用中已添加Mail.Send委托权限,且完成了管理员同意(租户内应用场景)。
    • 检查获取Token时返回的scope字段,确认包含https://graph.microsoft.com/Mail.Send(Graph API权限区分大小写,小写mail.send无效)。
  • 更换API端点:

    • 代表当前认证用户发送邮件,直接使用/me/sendMail端点即可,无需调用/users/{id}/sendMail(后者多用于应用权限或代表其他用户发送的场景)。修改代码:
      const response = await getGraphClient(token.accessToken)
         .api("https://graph.microsoft.com/v1.0/me/sendMail")
         .post({ message });
      
  • 检查Token有效性:

    • 解析Token查看exp字段,确认发送邮件时Token未过期;若为离线场景,确保已获取refresh_token,并在Token过期时调用acquireTokenByRefreshToken刷新。
  • 精简权限范围:保留User.Read、Mail.Send、offline_access即可,移除openid、profile等冗余权限,避免权限冲突。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 21:17:01