private bool Verify(string publicKey, string payload, string signature, string timestamp)
        {
            var validator   = new RequestValidator();
            var ecPublicKey = validator.ConvertPublicKeyToECDSA(publicKey);

            return(validator.VerifySignature(ecPublicKey, payload, signature, timestamp));
        }
        public static IApplicationBuilder UseEmailCallback(this IApplicationBuilder app, IConfiguration config)
        {
            // Get the Twilio Section
            var section = config.GetSection(SECTION_NAME);
            var opt     = section.Get <EmailOptions>();

            if (opt?.SendGrid?.CallbacksEnabled ?? false)
            {
                var sgRequestValidator = new RequestValidator();
                var publicKey          = sgRequestValidator.ConvertPublicKeyToECDSA(opt.SendGrid.VerificationKey);

                app = app.Map("/api/email-callback", (app) =>
                {
                    app.Run(async ctx =>
                    {
                        var req          = ctx.Request;
                        var res          = ctx.Response;
                        var cancellation = ctx.RequestAborted;

                        var handler = ctx.RequestServices.GetService <IEmailCallbackHandler>();
                        if (handler == null)
                        {
                            // Helps during configuration for making sure the endpoint is working
                            res.StatusCode = StatusCodes.Status200OK;
                            await res.WriteAsync($"No implementation of {nameof(IEmailCallbackHandler)} was registered.", cancellation);
                        }
                        else if (req.Method == "GET")
                        {
                            // Helps during configuration for making sure the endpoint is accessible
                            res.StatusCode = StatusCodes.Status200OK;
                            await res.WriteAsync("Welcome to the email callback endpoint for SendGrid webhooks!");
                        }
                        else if (req.Method != "POST")
                        {
                            // SendGrid will POST to this endpoint
                            res.StatusCode = StatusCodes.Status405MethodNotAllowed;
                            await res.WriteAsync($"{req.Method} method is not supported.");
                        }
                        else
                        {
                            // Get signature and timestamp from headers
                            var signature = req.Headers[RequestValidator.SIGNATURE_HEADER];
                            var timestamp = req.Headers[RequestValidator.TIMESTAMP_HEADER];

                            // Read the body
                            string body;
                            using (var sr = new StreamReader(req.Body))
                            {
                                body = await sr.ReadToEndAsync();
                            }

                            // Authenticate the source as SendGrid
                            if (signature == StringValues.Empty || timestamp == StringValues.Empty || !sgRequestValidator.VerifySignature(publicKey, body, signature, timestamp))
                            {
                                res.StatusCode = StatusCodes.Status401Unauthorized;
                                await res.WriteAsync("Invalid signature.", cancellation);
                            }
                            else
                            {
                                // Decode the webhook event into a list of DTOs
                                List <SendGridEventNotification> sgEventNotifications;
                                try
                                {
                                    sgEventNotifications = JsonConvert.DeserializeObject <List <SendGridEventNotification> >(body) ?? new List <SendGridEventNotification>();
                                }
                                catch (Exception)
                                {
                                    res.StatusCode = StatusCodes.Status422UnprocessableEntity;
                                    await res.WriteAsync("Failed to parse the body contents.", cancellation);
                                    return;
                                }

                                try
                                {
                                    // Map the SendGrid events to EmailEventNotifications
                                    var emailEventNotifications = new List <EmailEventNotification>(sgEventNotifications.Count);
                                    foreach (var sgEventNotification in sgEventNotifications)
                                    {
                                        int emailId  = sgEventNotification.EmailId;
                                        int?tenantId = sgEventNotification.TenantId;
                                        string error = sgEventNotification.Reason;
                                        DateTimeOffset eventTimestamp = sgEventNotification.Timestamp != 0 ? DateTimeOffset.FromUnixTimeSeconds(sgEventNotification.Timestamp) : DateTimeOffset.Now;

                                        EmailEvent emailEvent;
                                        var sgEvent = sgEventNotification.Event;

                                        // https://sendgrid.com/docs/for-developers/tracking-events/event/
                                        switch (sgEvent)
                                        {
                                        // Tracked
                                        case "dropped":     // SG rejected it (spam, unsubscribe)
                                            emailEvent = EmailEvent.Dropped;
                                            break;

                                        case "delivered":     // Recipient server accepted it
                                            emailEvent = EmailEvent.Delivered;
                                            break;

                                        case "bounce":     // Recipient server rejected it (type = "bounce" if permanently or "blocked" if temporarily)
                                            emailEvent = EmailEvent.Bounce;
                                            break;

                                        // Engagement
                                        case "open":     // User opened the email
                                            emailEvent = EmailEvent.Open;
                                            break;

                                        case "click":     // User clicked a link in the email
                                            emailEvent = EmailEvent.Click;
                                            break;

                                        case "spamreport":     // User marked email as spam
                                            emailEvent = EmailEvent.SpamReport;
                                            break;

                                        // No point tracking those, TMI
                                        case "processed":    // SG accepted it
                                        case "deferred":     // Recipient server temporary unavailable (SG retries up to 72h)

                                        // Never used
                                        case "unsubscribe":       // Only when SG subscription mgmt features are enabled
                                        case "group_unsubscribe": // Only when SG subscription mgmt features are enabled
                                        case "group_resubscribe": // Only when SG subscription mgmt features are enabled

                                        default:
                                            // Nothing to handle
                                            continue;
                                        }

                                        emailEventNotifications.Add(new EmailEventNotification
                                        {
                                            Event     = emailEvent,
                                            EmailId   = emailId,
                                            TenantId  = tenantId,
                                            Error     = error,
                                            Timestamp = eventTimestamp
                                        });
                                    }

                                    if (emailEventNotifications.Any())
                                    {
                                        // Custom handler
                                        await handler.HandleCallback(emailEventNotifications, cancellation);
                                    }

                                    // Return 200 upon success
                                    res.StatusCode = StatusCodes.Status200OK;
                                }
                                catch (Exception)
                                {
                                    // Log the error
                                    res.StatusCode = StatusCodes.Status400BadRequest;
                                    await res.WriteAsync("Failed to process the events.");
                                }
                            }
                        }
                    });
                });
            }

            return(app);
        }