public async Task Run( #pragma warning disable CA1801 // Remove unused parameter [TimerTrigger("0 */1 * * * *")] TimerInfo timerInfo) #pragma warning restore CA1801 // Remove unused parameter { if (_appSettings is null || _appSettings.Value is null || _appSettings.Value.EmailToSmsSettings is null || string.IsNullOrEmpty(_appSettings.Value.EmailToSmsSettings.ResellerApiKey) || string.IsNullOrEmpty(_appSettings.Value.EmailToSmsSettings.ResellerApiSecret) || string.IsNullOrEmpty(_appSettings.Value.EmailToSmsSettings.MailboxAddress)) { _logger.LogWarning("Missing Email to SMS configuration. Aborting!"); return; } // TODO Move GraphServiceClient and auth to DI IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder .Create(_appSettings.Value.EmailToSmsSettings.MicrosoftGraphApiClientId) .WithTenantId(_appSettings.Value.EmailToSmsSettings.MicrosoftGraphApiTenantID) .WithClientSecret(_appSettings.Value.EmailToSmsSettings.MicrosoftGraphApiSecret) .Build(); var graphServiceClient = new GraphServiceClient(new ClientCredentialProvider(confidentialClientApplication)); // TODO, make this better or move to config. // Hard coding OWNit conveyancing for now. var clientIds = new Dictionary <string, int>(); clientIds.Add("b6rtmjuc", 87627); var messageExceptions = new List <Exception>(); ISMSService smsService = new BurstSMSService( _appSettings.Value.EmailToSmsSettings.ResellerApiKey, _appSettings.Value.EmailToSmsSettings.ResellerApiSecret); var mailboxEmailAddress = _appSettings.Value.EmailToSmsSettings.MailboxAddress; var currentMessageRequest = graphServiceClient.Users[mailboxEmailAddress] .MailFolders.Inbox .Messages .Request() .Select(m => (new { m.Id, m.From, m.Subject, m.Body, m.InternetMessageHeaders, m.ParentFolderId, m.Categories })); // Get IDs of standard folders to be able to move messages once they're processed var inboxFolder = await graphServiceClient.Users[mailboxEmailAddress].MailFolders.Inbox.Request().GetAsync(); var invalidMessagesFolder = await graphServiceClient.Users[mailboxEmailAddress].EnsureFolder("Inbox", _invalidMessagesFolderName); var clientsFolder = await graphServiceClient.Users[mailboxEmailAddress].EnsureFolder("Inbox", _clientsFolderName); var clientFolderIds = new Dictionary <string, string>(); // Loop through pages do { var currentMessages = await currentMessageRequest.GetAsync(); // Loop through messages in current page foreach (var message in currentMessages) { var parsedBodyText = string.Empty; try { var messageBodyHtmlDoc = new HtmlAgilityPack.HtmlDocument(); messageBodyHtmlDoc.LoadHtml(message.Body.Content); // - Using InnerText strips HTML. // - Then we replace Windows CRLF's in case there are any. We must strip these first because if we // stripped LF first then we'd have a bunch of CR's floating around all alone. // - Then strip Unix LF's, in case there are any. parsedBodyText = HttpUtility.HtmlDecode(messageBodyHtmlDoc.DocumentNode.InnerText) .Replace("\r\n", "", StringComparison.Ordinal) .Replace("\n", "", StringComparison.Ordinal); var smsEmailJson = JsonConvert.DeserializeObject <SmsEmailJson>(parsedBodyText); // Check for required values if (!smsEmailJson.IsValid(out List <string> missingFields)) { // Create a support ticket with the details. var ticketResponse = await _mediator.Send(new CreateTicketCommand() { FromEmail = GetBestEmail(smsEmailJson), TicketPriority = TicketPriority.Low, Subject = GenerateTicketSubject(smsEmailJson), DescriptionHtml = GenerateMessageMissingFields(smsEmailJson, missingFields) }); // Store the category for a bad response from the gateway await graphServiceClient.AddMessageCategoriesAsync(mailboxEmailAddress, message, new[] { $"Ticket: {ticketResponse.Id}", EmailCategories.MissingRequiredData }); await graphServiceClient.Users[mailboxEmailAddress].Messages[message.Id].Move(invalidMessagesFolder.Id).Request().PostAsync(); } else { // Data appears valid, ensure folder exists for client, checking cached IDs first if (smsEmailJson.SmsGo.Value == true) { // Send SMS. Will throw if there's a problem. var clientId = clientIds[smsEmailJson.SmsKey]; var cleanMobileNumber = SMSHelper.CleanMobileNumber(smsEmailJson.MobileNumber); if (_appSettings.Value.EmailToSmsSettings.SkipSendingSms) { _logger.LogWarning( "Skipping SMS Send: Client ID: {clientId}, Mobile Number: {mobileNumber}, Message: '{message}', Send At: {sendAt}, Replies to Email: {repliesToEmail}", clientId, cleanMobileNumber, smsEmailJson.Message, smsEmailJson.SendAt, smsEmailJson.RepliesToEmail); await graphServiceClient.AddMessageCategoriesAsync(mailboxEmailAddress, message, EmailCategories.SmsSkipped); } else { var sendAt = SMSHelper.LocalisedSendAt(smsEmailJson.SendAt, smsEmailJson.SendAtTimeZoneID, _logger); if (sendAt != smsEmailJson.SendAt) { _logger.LogInformation( "SendAt was translated from '{SendAt}' to '{ParsedSendAt}' based on supplied Time Zone ID '{TimeZoneID}'.", smsEmailJson.SendAt, sendAt, smsEmailJson.SendAtTimeZoneID); } var sendSmsResponse = await smsService.SendSms(clientId, cleanMobileNumber, smsEmailJson.Message, sendAt, smsEmailJson.RepliesToEmail); if (sendSmsResponse.Error is null) { // Create a support ticket with the details. var ticketResponse = await _mediator.Send(new CreateTicketCommand() { FromEmail = GetBestEmail(smsEmailJson), TicketPriority = TicketPriority.Low, Subject = GenerateTicketSubject(smsEmailJson), DescriptionHtml = GenerateMessageUnknownGatewayResponse(smsEmailJson) }); await graphServiceClient.AddMessageCategoriesAsync(mailboxEmailAddress, message, $"Ticket: {ticketResponse.Id}", EmailCategories.UnknownError); } else if (sendSmsResponse.Error.Code == "SUCCESS") { _logger.LogInformation( "SMS Sent: Cost: {cost}, Sms Message ID: {messageId}, Code: {code}, Descripton: {description}, Send At: {sendAt}", sendSmsResponse.Cost, sendSmsResponse.MessageId, sendSmsResponse.Error?.Code, sendSmsResponse.Error?.Description, sendSmsResponse.SendAt); await graphServiceClient.AddMessageCategoriesAsync(mailboxEmailAddress, message, EmailCategories.SmsSent); } else { // Create a support ticket with the details. var ticketResponse = await _mediator.Send(new CreateTicketCommand() { FromEmail = GetBestEmail(smsEmailJson), TicketPriority = TicketPriority.Low, Subject = GenerateTicketSubject(smsEmailJson), DescriptionHtml = GenerateMessageGatewayError(message, smsEmailJson, sendSmsResponse) }); // Store the category for a bad response from the gateway await graphServiceClient.AddMessageCategoriesAsync(mailboxEmailAddress, message, $"Ticket: {ticketResponse.Id}", EmailCategories.GatewayBadRequest); _logger.LogError( "SMS Gateway returned error response. Code: '{code}', Description: '{description}'.", sendSmsResponse.Error.Code, sendSmsResponse.Error.Description); } } } else { await graphServiceClient.AddMessageCategoriesAsync(mailboxEmailAddress, message, EmailCategories.SmsGoFalse); } // Finally, move to client folder string currentClientFolderId; if (clientFolderIds.ContainsKey(smsEmailJson.OrgKey)) { currentClientFolderId = clientFolderIds[smsEmailJson.OrgKey]; } else { var currentClientFolder = await graphServiceClient.Users[mailboxEmailAddress].EnsureFolder(clientsFolder, smsEmailJson.OrgKey); currentClientFolderId = currentClientFolder.Id; } await graphServiceClient.Users[mailboxEmailAddress].Messages[message.Id].Move(currentClientFolderId).Request().PostAsync(); } } catch (Exception ex) { messageExceptions.Add(ex); // Just in case something goes wrong, we don't want a failure during cleanup to prevent processing future messages try { if (ex is JsonReaderException jex) { // Create a support ticket with the details. var ticketResponse = await _mediator.Send(new CreateTicketCommand() { FromEmail = _fallbackEmailFrom, TicketPriority = TicketPriority.Low, Subject = $"SMS Failure - couldn't parse JSON", DescriptionHtml = GenerateMessageJsonParseError(parsedBodyText, jex) }); await graphServiceClient.AddMessageCategoriesAsync(mailboxEmailAddress, message, $"Ticket: {ticketResponse.Id}", EmailCategories.InvalidJson); } else { var ticketResponse = await _mediator.Send(new CreateTicketCommand() { FromEmail = _fallbackEmailFrom, TicketPriority = TicketPriority.Low, Subject = $"SMS Failure - Unknown error", DescriptionHtml = GenerateMessageUnknownException(message, ex) }); await graphServiceClient.AddMessageCategoriesAsync(mailboxEmailAddress, message, EmailCategories.UnknownError); } // Move to generic invalid message folder await graphServiceClient.Users[mailboxEmailAddress].Messages[message.Id].Move(invalidMessagesFolder.Id).Request().PostAsync(); } catch (Exception cleanupEx) { _logger.LogError(cleanupEx, "Exception encountered during message cleanup!"); messageExceptions.Add(cleanupEx); } } } currentMessageRequest = currentMessages.NextPageRequest; } while (currentMessageRequest != null); if (messageExceptions.Count > 0) { throw new AggregateException("Processing of one or more messages has failed", messageExceptions); } }