aspnetcore-webhooks-sample icon indicating copy to clipboard operation
aspnetcore-webhooks-sample copied to clipboard

Listen for Mail Change Notification Using Microsoft Graph API in C# ASP.NET Core

Open sruthi-kurup opened this issue 1 year ago • 0 comments

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();
    }
}

image

  • [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

sruthi-kurup avatar Jan 04 '24 01:01 sruthi-kurup