aspnetcore-webhooks-sample
aspnetcore-webhooks-sample copied to clipboard
Listen for Mail Change Notification Using Microsoft Graph API in C# ASP.NET Core
How do I develop an ASP.NET Core application that listens to change notification upon receiving a new mail based on some condition? I've already created a sample app by referring to this YouTube tutorial: https://www.youtube.com/watch?v=fThiCZmIcMQ&list=PLWZJrkeLOrbbOve1DVVQsauZX2LN3IEHL&index=12.
However, I'm encountering two challenges:
I need to get the user credentials (Client ID, Tenant ID, and Application Secret) through an API, then create a subscription for the particular user, and listen to the notification when a new mail arrives, and give the response back to the application user, who has the MS account linked with.
The user will input the credentials on a different organization.
What I have done so far:
Created an Office 365 account on trial.
Registered an app in Azure Portal (entraid) with which I set up my supported accounts to Multitenant.
Configured ngork and setup it to listen to my application port.
Created an ASP.NET Core 7 application and updated the AppSettings with accurate admin credentials (Tenant ID, Client ID, Client Secret, ngork URL, etc.).
I also referred to this GitHub page, but a lot of breaking changes are there in the Graph API, so it gets difficult to update to my application: https://github.com/microsoftgraph/aspnetcore-webhooks-sample
Please help me to handle challenge 1 and 2 and resolve the issue.
[ApiController]
[Route("[controller]")]
public class GraphWatchController : ControllerBase
{
private readonly ILogger<GraphWatchController> _logger;
private readonly IConfiguration _configuration;
public GraphWatchController(ILogger<GraphWatchController> logger,IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
public IResult Get(string clientId, string clientSecret, string tenantId, string mailBox)
{
var graphClient = new GraphServiceClient(new ClientSecretCredential(
tenantId, clientId, clientSecret));
var sub = new Subscription
{
ChangeType = "created,updated",
NotificationUrl = _configuration.Ngrok + "/api/graphlisten",
LifecycleNotificationUrl = _configuration.Ngrok + "/api/graphlifecycle",
Resource = $"/me/mailFolders('{mailBox}')/messages",
ExpirationDateTime = DateTime.UtcNow.AddMinutes(15),
ClientState = "SecretClientState"
};
var newSubscription = graphClient
.Subscriptions.PostAsync(sub).Result;
return Results.Ok($"Subscribed. Id: {newSubscription.Id}, Expiration: {newSubscription.ExpirationDateTime}");
}
}
[ApiController]
[Route("[controller]")]
public class GraphListenController : ControllerBase
{
private readonly ILogger<GraphListenController> _logger;
public GraphListenController(ILogger<GraphListenController> logger)
{
_logger = logger;
}
/// <summary>
/// POST /listen
/// </summary>
/// <param name="validationToken">Optional. Validation token sent by Microsoft Graph during endpoint validation phase</param>
/// <returns>IActionResult</returns>
[HttpPost]
[AllowAnonymous]
public async Task<IResult> Post([FromQuery] string? validationToken = null)
{
// If there is a validation token in the query string,
// send it back in a 200 OK text/plain response
if (!string.IsNullOrEmpty(validationToken))
{
return Results.Ok(validationToken);
}
// Use the Graph client's serializer to deserialize the body
using var bodyStream = new MemoryStream();
await Request.Body.CopyToAsync(bodyStream);
bodyStream.Seek(0, SeekOrigin.Begin);
var notifications = KiotaJsonSerializer.Deserialize<ChangeNotificationCollection>(bodyStream);
if (notifications == null || notifications.Value == null) return Results.Accepted();
// Validate any tokens in the payload
var areTokensValid = await notifications.AreTokensValid(_tenantIds, _appIds);
if (!areTokensValid) return Results.Unauthorized();
// Process non-encrypted notifications first
// These will be notifications for user mailbox
var messageNotifications = new Dictionary<string, ChangeNotification>();
foreach (var notification in notifications.Value.Where(n => n.EncryptedContent == null))
{
// Find the subscription in our store
var subscription = _subscriptionStore
.GetSubscriptionRecord(notification.SubscriptionId.ToString() ?? string.Empty);
// If this isn't a subscription we know about, or if client state doesn't match,
// ignore it
if (subscription != null && subscription.ClientState == notification.ClientState)
{
_logger.LogInformation($"Received notification for: {notification.Resource}");
// Add notification to list to process. If there is more than
// one notification for a given resource, we'll only process it once
messageNotifications[notification.Resource!] = notification;
}
}
return Results.Accepted();
}
}
[ApiController]
[Route("[controller]")]
public class GraphLifeCycleController : ControllerBase
{
private readonly ILogger<GraphLifeCycleController> _logger;
public GraphLifeCycleController(ILogger<GraphLifeCycleController> logger)
{
_logger = logger;
}
/// <summary>
/// POST /lifecycle
/// </summary>
/// <param name="validationToken">Optional. Validation token sent by Microsoft Graph during endpoint validation phase</param>
/// <returns>IActionResult</returns>
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Post([FromQuery] string? validationToken = null)
{
// If there is a validation token in the query string,
// send it back in a 200 OK text/plain response
if (!string.IsNullOrEmpty(validationToken))
{
return Ok(validationToken);
}
// Use the Graph client's serializer to deserialize the body
using var bodyStream = new MemoryStream();
await Request.Body.CopyToAsync(bodyStream);
bodyStream.Seek(0, SeekOrigin.Begin);
var notifications = KiotaJsonSerializer.Deserialize<ChangeNotificationCollection>(bodyStream);
if (notifications == null || notifications.Value == null) return Accepted();
// Process any lifecycle events
var lifecycleNotifications = notifications.Value.Where(n => n.LifecycleEvent != null);
foreach (var lifecycleNotification in lifecycleNotifications)
{
_logger.LogInformation("Received {eventType} notification for subscription {subscriptionId}",
lifecycleNotification.LifecycleEvent.ToString(), lifecycleNotification.SubscriptionId);
if (lifecycleNotification.LifecycleEvent == LifecycleEventType.ReauthorizationRequired)
{
// The subscription needs to be renewed
try
{
await RenewSubscriptionAsync(lifecycleNotification);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error renewing subscription");
}
}
}
// Return 202 to Graph to confirm receipt of notification.
// Not sending this will cause Graph to retry the notification.
return Accepted();
}
}
- [1] there are the three controllers I've created for subscribe, listen and renew subscription.
- [2] saved all credentials in app.settings file.
- [3] in watch api, user will give the credentials to create the subscription for
- [4] the lifecycle api the subscription will get renewed automatically
- [5] the listen api is to listen when a new mail is created or updated in the office 365 mailbox, of the account the user gave to create subscription.
the above code is not fully functional, adapted from https://github.com/microsoftgraph/aspnetcore-webhooks-sample
please help me to implement like this