如何通过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
相关产品推荐
相关产品推荐

