From 991cb525f046ccd4ec30b6f753dc16182b6ba282 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Fri, 20 Jan 2023 19:23:28 +0200 Subject: [PATCH] SendGrid integration --- Correo.sln | 9 ++- dependencies.props | 1 + src/Correo.SendGrid/Correo.SendGrid.csproj | 18 ++++++ .../DependencyInjectionExtensions.cs | 16 +++++ .../Extensions/ModelExtensions.cs | 40 ++++++++++++ src/Correo.SendGrid/Models/ErrorModels.cs | 9 +++ src/Correo.SendGrid/Models/SendGridOptions.cs | 7 +++ src/Correo.SendGrid/SendGridService.cs | 61 +++++++++++++++++++ src/Correo/Correo.csproj | 1 + src/Correo/Extensions/SmtpExtensions.cs | 17 +++--- src/Correo/Extensions/StartupExtensions.cs | 2 +- src/Correo/appsettings.json | 5 +- 12 files changed, 176 insertions(+), 10 deletions(-) create mode 100644 src/Correo.SendGrid/Correo.SendGrid.csproj create mode 100644 src/Correo.SendGrid/DependencyInjectionExtensions.cs create mode 100644 src/Correo.SendGrid/Extensions/ModelExtensions.cs create mode 100644 src/Correo.SendGrid/Models/ErrorModels.cs create mode 100644 src/Correo.SendGrid/Models/SendGridOptions.cs create mode 100644 src/Correo.SendGrid/SendGridService.cs diff --git a/Correo.sln b/Correo.sln index e701e8f..6c2f4c1 100644 --- a/Correo.sln +++ b/Correo.sln @@ -28,7 +28,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Correo.NetSmtpClient", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Correo.Abstractions", "src\Correo.Abstractions\Correo.Abstractions.csproj", "{ED8048DC-8509-4085-97F0-F9D9A59A689A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Correo.MailKit", "src\Correo.MailKit\Correo.MailKit.csproj", "{325B77E1-D752-4578-8BF7-793905C38DCD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Correo.MailKit", "src\Correo.MailKit\Correo.MailKit.csproj", "{325B77E1-D752-4578-8BF7-793905C38DCD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Correo.SendGrid", "src\Correo.SendGrid\Correo.SendGrid.csproj", "{1457426A-CD47-4201-BE3C-A40B38FCB1FA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -64,6 +66,10 @@ Global {325B77E1-D752-4578-8BF7-793905C38DCD}.Debug|Any CPU.Build.0 = Debug|Any CPU {325B77E1-D752-4578-8BF7-793905C38DCD}.Release|Any CPU.ActiveCfg = Release|Any CPU {325B77E1-D752-4578-8BF7-793905C38DCD}.Release|Any CPU.Build.0 = Release|Any CPU + {1457426A-CD47-4201-BE3C-A40B38FCB1FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1457426A-CD47-4201-BE3C-A40B38FCB1FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1457426A-CD47-4201-BE3C-A40B38FCB1FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1457426A-CD47-4201-BE3C-A40B38FCB1FA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -76,6 +82,7 @@ Global {76793477-8219-4FFB-AA08-3F2224EC4968} = {245E2FBE-DFDF-40B4-94B7-5DDA216E58AD} {ED8048DC-8509-4085-97F0-F9D9A59A689A} = {245E2FBE-DFDF-40B4-94B7-5DDA216E58AD} {325B77E1-D752-4578-8BF7-793905C38DCD} = {245E2FBE-DFDF-40B4-94B7-5DDA216E58AD} + {1457426A-CD47-4201-BE3C-A40B38FCB1FA} = {245E2FBE-DFDF-40B4-94B7-5DDA216E58AD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {86FCF989-26FC-41E9-8A23-9485606D619D} diff --git a/dependencies.props b/dependencies.props index 03a5ea3..2480ea9 100644 --- a/dependencies.props +++ b/dependencies.props @@ -9,5 +9,6 @@ 9.0.0 6.0.30 3.4.3 + 9.28.1 \ No newline at end of file diff --git a/src/Correo.SendGrid/Correo.SendGrid.csproj b/src/Correo.SendGrid/Correo.SendGrid.csproj new file mode 100644 index 0000000..92a48f4 --- /dev/null +++ b/src/Correo.SendGrid/Correo.SendGrid.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + + + + + + + + + + + + + + diff --git a/src/Correo.SendGrid/DependencyInjectionExtensions.cs b/src/Correo.SendGrid/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..c6b1fbe --- /dev/null +++ b/src/Correo.SendGrid/DependencyInjectionExtensions.cs @@ -0,0 +1,16 @@ +using Correo.Abstractions; +using Correo.SendGrid.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Correo.SendGrid +{ + public static class DependencyInjectionExtensions + { + public static void AddSendGridService(this IServiceCollection services, IConfigurationSection configuration) + { + services.Configure(configuration); + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/src/Correo.SendGrid/Extensions/ModelExtensions.cs b/src/Correo.SendGrid/Extensions/ModelExtensions.cs new file mode 100644 index 0000000..85e2645 --- /dev/null +++ b/src/Correo.SendGrid/Extensions/ModelExtensions.cs @@ -0,0 +1,40 @@ +using Correo.Abstractions; +using SendGrid.Helpers.Mail; +using System.Collections.Generic; +using System.Linq; + +namespace Correo.SendGrid.Extensions +{ + internal static class ModelExtensions + { + private static EmailAddress ToEmailAddress(this EmailMessage.MailAddress address) + => new EmailAddress(address.Address, address.DisplayName); + + private static List ToEmailAddressList(this IEnumerable addresses) + => addresses.Select(z => new EmailAddress(z.Address, z.DisplayName)).ToList(); + + public static SendGridMessage ToSendGridMessage(this EmailMessage message) + { + var subject = message.Subject; + var plainContent = !message.IsBodyHtml ? message.Body : null; + var htmlContent = message.IsBodyHtml ? message.Body : null; + var from = message.From.ToEmailAddress(); + var to = message.To.First().ToEmailAddress(); + var mailMessage = MailHelper.CreateSingleEmail(from, to, subject, plainContent, htmlContent); + + if (message.To.Count() > 1) + { + var tos = message.To.Skip(1).Select(z => new EmailAddress(z.Address, z.DisplayName)).ToList(); + mailMessage.AddTos(tos); + } + + if (message.Cc != null && message.Cc.Any()) + mailMessage.AddCcs(message.Cc.ToEmailAddressList()); + + if (message.Bcc != null && message.Bcc.Any()) + mailMessage.AddBccs(message.Bcc.ToEmailAddressList()); + + return mailMessage; + } + } +} diff --git a/src/Correo.SendGrid/Models/ErrorModels.cs b/src/Correo.SendGrid/Models/ErrorModels.cs new file mode 100644 index 0000000..b950e2b --- /dev/null +++ b/src/Correo.SendGrid/Models/ErrorModels.cs @@ -0,0 +1,9 @@ +namespace Correo.SendGrid.Models +{ + internal record Error + { + public string Message { get; init; } + public string Field { get; init; } + public string Help { get; init; } + } +} diff --git a/src/Correo.SendGrid/Models/SendGridOptions.cs b/src/Correo.SendGrid/Models/SendGridOptions.cs new file mode 100644 index 0000000..028be47 --- /dev/null +++ b/src/Correo.SendGrid/Models/SendGridOptions.cs @@ -0,0 +1,7 @@ +namespace Correo.SendGrid.Models +{ + internal record SendGridOptions + { + public string ApiKey { get; init; } + } +} diff --git a/src/Correo.SendGrid/SendGridService.cs b/src/Correo.SendGrid/SendGridService.cs new file mode 100644 index 0000000..e111fdb --- /dev/null +++ b/src/Correo.SendGrid/SendGridService.cs @@ -0,0 +1,61 @@ +using Correo.Abstractions; +using Correo.Abstractions.Extensions; +using Correo.SendGrid.Extensions; +using Correo.SendGrid.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using SendGrid; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Correo.SendGrid +{ + internal class SendGridService : IMailer + { + private readonly IOptions _optionsAccessor; + private readonly SendGridClient _client; + private readonly ILogger _logger; + + public SendGridService(IOptions optionsAccessor, ILogger logger) + { + _optionsAccessor = optionsAccessor; + _logger = logger; + _client = new SendGridClient(_optionsAccessor.Value.ApiKey); + } + + public void SendEmail(EmailMessage message) + { + var mailMessage = message.ToSendGridMessage(); + _client.SendEmailAsync(mailMessage).GetAwaiter().GetResult(); + Log(message); + } + + public async Task SendEmailAsync(EmailMessage message, CancellationToken token = default) + { + var mailMessage = message.ToSendGridMessage(); + var response = await _client.SendEmailAsync(mailMessage, token); + if (response.IsSuccessStatusCode) + Log(message); + else + { + var body = await response.DeserializeResponseBodyAsync(); + var found = body.TryGetValue("errors", out var jsonData); + if (found) + { + string errorsString = Convert.ToString(jsonData); + var errors = JsonConvert.DeserializeObject(errorsString); + var errorMessage = string.Join("; ", errors.Select(z => z.Message)); + throw new Exception(errorMessage); + } + + throw new Exception(response.StatusCode.ToString()); + } + } + + private void Log(EmailMessage message) + => _logger.LogInformation($"Email sent: Subject: {message.Subject}; From: {message.From.Address}; To: {message.To.Log()}; Cc: {message.Cc.Log()}; Bcc: {message.Bcc.Log()};"); + } +} diff --git a/src/Correo/Correo.csproj b/src/Correo/Correo.csproj index 1bf5f31..d51adaa 100644 --- a/src/Correo/Correo.csproj +++ b/src/Correo/Correo.csproj @@ -18,6 +18,7 @@ + diff --git a/src/Correo/Extensions/SmtpExtensions.cs b/src/Correo/Extensions/SmtpExtensions.cs index 3910f33..cee12ee 100644 --- a/src/Correo/Extensions/SmtpExtensions.cs +++ b/src/Correo/Extensions/SmtpExtensions.cs @@ -1,5 +1,6 @@ using Correo.MailKit; using Correo.NetSmtpClient; +using Correo.SendGrid; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; @@ -8,22 +9,24 @@ namespace Correo.Extensions { public static class SmtpExtensions { - public static void AddSmtpClient(this IServiceCollection services, IConfiguration configuration) + public static void AddSmtpService(this IServiceCollection services, IConfiguration configuration) { - var configSection = configuration.GetSection("SmtpClient"); - var mediator = configSection.GetValue("Mediator", ".NET"); - + var mediator = configuration.GetValue("SmtpMediator", ".NET"); if (mediator.Equals(".NET", StringComparison.InvariantCultureIgnoreCase)) { - services.AddNetSmtpClient(configSection); + services.AddNetSmtpClient(configuration.GetSection("SmtpClient")); } else if (mediator.Equals("MailKit", StringComparison.InvariantCultureIgnoreCase)) { - services.AddMailKitSmtpClient(configSection); + services.AddMailKitSmtpClient(configuration.GetSection("SmtpClient")); + } + else if (mediator.Equals("SendGrid", StringComparison.InvariantCultureIgnoreCase)) + { + services.AddSendGridService(configuration.GetSection("SmtpService")); } else { - throw new NotImplementedException($"SmtpClient:Mediator={mediator} is not implemented."); + throw new NotImplementedException($"SmtpMediator '{mediator}' is not implemented."); } } } diff --git a/src/Correo/Extensions/StartupExtensions.cs b/src/Correo/Extensions/StartupExtensions.cs index 8da630a..0d83145 100644 --- a/src/Correo/Extensions/StartupExtensions.cs +++ b/src/Correo/Extensions/StartupExtensions.cs @@ -30,7 +30,7 @@ namespace Correo.Extensions services.AddMessaging(configuration); // Add SMTP client - services.AddSmtpClient(configuration); + services.AddSmtpService(configuration); // Application services services.AddApplicationServices(configuration); diff --git a/src/Correo/appsettings.json b/src/Correo/appsettings.json index 3539786..7341fcb 100644 --- a/src/Correo/appsettings.json +++ b/src/Correo/appsettings.json @@ -29,8 +29,8 @@ "Address": "noreply@homelab.com", "Name": "HomeLab" }, + "SmtpMediator": ".NET", //.NET, MailKit, SendGrid "SmtpClient": { - "Mediator": ".NET", //.NET, MailKit "Server": "smtp.gmail.com", "Port": "587", "UseAuthentication": true, @@ -41,5 +41,8 @@ }, "EnableSsl": true, "TrustServer": false + }, + "SmtpService": { + "ApiKey": "xxxxxxxxxxx" } } \ No newline at end of file