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

如何通过Outlook REST Notifications API实现新邮件推送至Azure Functions?

实现Outlook新邮件推送至Azure Functions的完整方案

我来一步步帮你拆解这个需求的实现流程,从基础配置到代码落地都给你讲清楚:

1. 先搞定Azure AD应用注册(核心前提)

要调用Microsoft Graph API实现邮件订阅,你得先在Azure AD里注册一个身份验证应用:

  • 登录Azure门户,找到Azure Active Directory > 应用注册 > 新注册,给应用起个名字,重定向URI选Web,填写你的Web App回调地址(比如https://your-webapp.azurewebsites.net/auth/callback)。
  • 注册完成后,进入API权限 > 添加权限 > Microsoft Graph > 委派权限,添加以下必要权限:
    • Mail.Read(读取邮件内容的核心权限)
    • offline_access(获取刷新令牌,用来长期维持订阅)
    • User.Read(获取用户基本身份信息)
  • 点击授予管理员同意(如果是企业租户内应用),或者让用户在首次授权时手动同意这些权限。
  • 进入证书和密码,创建一个客户端密码(客户端密钥),务必保存好这个密钥,后续调用API会用到。

2. Web App里实现订阅按钮的逻辑

用户点击订阅按钮后,需要完成OAuth2授权、令牌交换、创建Graph订阅这三步:

步骤2.1 引导用户授权登录

在Web App的订阅按钮点击事件中,跳转到Azure AD的授权端点,示例URL格式如下(替换成你的实际参数):

https://login.microsoftonline.com/common/oauth2/v2.0/authorize?
client_id=你的应用ID
&response_type=code
&redirect_uri=你的回调地址
&response_mode=query
&scope=Mail.Read offline_access User.Read
&state=随机生成的防CSRF字符串

用户完成登录授权后,会跳转到你的回调地址,携带code参数。

步骤2.2 交换授权码为令牌

在回调接口中,用拿到的code调用Azure AD的令牌端点,换取access_token和refresh_token:

var client = new HttpClient();
var tokenRequest = new FormUrlEncodedContent(new Dictionary<string, string>
{
    ["client_id"] = "你的应用ID",
    ["client_secret"] = "你的客户端密钥",
    ["code"] = 回调中获取的code值,
    ["redirect_uri"] = "你的回调地址",
    ["grant_type"] = "authorization_code"
});

var tokenResponse = await client.PostAsync("https://login.microsoftonline.com/common/oauth2/v2.0/token", tokenRequest);
var tokenData = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>();
// 把refresh_token、用户ID存入数据库,后续刷新订阅会用到

步骤2.3 创建Microsoft Graph订阅

用access_token调用Graph的订阅API,创建针对用户收件箱新邮件的订阅:

var graphClient = new HttpClient();
graphClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenData.AccessToken);

var subscriptionRequest = new
{
    changeType = "created", // 只监听新邮件创建事件
    notificationUrl = "你的Azure Functions HTTP触发URL", // 必须公网可访问
    resource = "/me/mailFolders/inbox/messages", // 目标资源为用户收件箱
    expirationDateTime = DateTime.UtcNow.AddDays(3).ToString("o"), // 订阅最长有效期3天
    clientState = "随机生成的字符串,用于验证通知合法性"
};

var subscriptionResponse = await graphClient.PostAsJsonAsync("https://graph.microsoft.com/v1.0/subscriptions", subscriptionRequest);
var subscriptionData = await subscriptionResponse.Content.ReadFromJsonAsync<GraphSubscription>();
// 把subscriptionId、用户ID、expirationDateTime存入数据库,用于后续刷新

3. Azure Functions处理通知

你的HTTP触发函数需要处理两种请求:订阅验证请求和新邮件通知请求:

步骤3.1 处理订阅验证

创建订阅时,Graph会向notificationUrl发送GET请求,携带validationToken参数,你需要直接返回这个令牌才能完成订阅验证:

[FunctionName("MailNotificationTrigger")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
{
    log.LogInformation("Received mail notification request.");

    // 处理订阅验证请求
    if (req.Query.TryGetValue("validationToken", out var validationToken))
    {
        log.LogInformation("Validation token received: {Token}", validationToken);
        return new OkObjectResult(validationToken);
    }

    // 处理新邮件通知请求
    var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    var notifications = JsonConvert.DeserializeObject<GraphNotificationPayload>(requestBody);

    foreach (var notification in notifications.Value)
    {
        // 验证clientState是否与创建订阅时一致,防止伪造通知
        if (notification.ClientState != "你创建订阅时的clientState")
        {
            log.LogWarning("Invalid client state detected.");
            continue;
        }

        // 从通知中获取邮件ID,调用Graph API获取完整邮件内容
        var messageId = notification.ResourceData.Id;
        // 这里需要用用户的refresh_token换取新的access_token,再调用https://graph.microsoft.com/v1.0/me/messages/{messageId}
        log.LogInformation("New mail received: {MessageId}", messageId);

        // 这里可以添加自定义逻辑,比如保存邮件内容到数据库、触发业务流程等
    }

    return new OkResult();
}

步骤3.2 处理新邮件通知

当有新邮件时,Graph会发送POST请求到你的函数,请求体包含通知信息。你可以用ResourceData.Id作为邮件ID,结合用户的有效access_token调用Graph API获取完整邮件内容。

4. 订阅刷新机制(关键!避免3天后失效)

Graph订阅最长有效期为3天,所以需要定时刷新订阅:

  • 创建一个定时器触发的Azure Functions,比如每天凌晨1点运行,遍历数据库中即将过期的订阅:
[FunctionName("RefreshSubscriptions")]
public static async Task Run(
    [TimerTrigger("0 0 1 * * *")] TimerInfo myTimer,
    ILogger log)
{
    log.LogInformation("Starting subscription refresh job.");

    // 从数据库获取即将过期的订阅(比如剩余有效期小于1天)
    var subscriptions = await GetExpiringSubscriptionsFromDb();

    foreach (var sub in subscriptions)
    {
        // 用用户的refresh_token换取新的access_token
        var newToken = await RefreshAccessToken(sub.UserId);

        var graphClient = new HttpClient();
        graphClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", newToken.AccessToken);

        // 刷新订阅,延长有效期3天
        var refreshRequest = new
        {
            expirationDateTime = DateTime.UtcNow.AddDays(3).ToString("o")
        };

        var response = await graphClient.PatchAsJsonAsync($"https://graph.microsoft.com/v1.0/subscriptions/{sub.SubscriptionId}", refreshRequest);
        if (response.IsSuccessStatusCode)
        {
            log.LogInformation("Subscription {Id} refreshed successfully.", sub.SubscriptionId);
            // 更新数据库中的订阅过期时间
            await UpdateSubscriptionExpiration(sub.SubscriptionId, DateTime.UtcNow.AddDays(3));
        }
        else
        {
            log.LogError("Failed to refresh subscription {Id}: {Reason}", sub.SubscriptionId, await response.Content.ReadAsStringAsync());
            // 处理刷新失败的情况,比如删除失效订阅、通知用户重新订阅
        }
    }
}

几个避坑提示

  • notificationUrl必须公网可访问:本地调试时可以用ngrok做内网穿透,否则Graph无法发送通知。
  • 权限校验:确保用户授权了Mail.Read权限,否则创建订阅或读取邮件会失败。
  • clientState验证:务必验证通知中的clientState与创建时一致,防止恶意请求。
  • 令牌持久化:妥善保存用户的refresh_token,用来长期获取access_token,避免令牌过期后订阅失效。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 03:58:25