diff --git a/Correo.sln b/Correo.sln index 29e20eb..9e256cb 100644 --- a/Correo.sln +++ b/Correo.sln @@ -24,6 +24,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Correo.PublishedLanguage", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Correo.Domain", "src\Correo.Domain\Correo.Domain.csproj", "{A2D2694C-AB68-49B0-B4B0-94BBFF48C043}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Correo.SmtpClient", "src\Correo.SmtpClient\Correo.SmtpClient.csproj", "{76793477-8219-4FFB-AA08-3F2224EC4968}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Correo.Abstractions", "src\Correo.Abstractions\Correo.Abstractions.csproj", "{ED8048DC-8509-4085-97F0-F9D9A59A689A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -46,6 +50,14 @@ Global {A2D2694C-AB68-49B0-B4B0-94BBFF48C043}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2D2694C-AB68-49B0-B4B0-94BBFF48C043}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2D2694C-AB68-49B0-B4B0-94BBFF48C043}.Release|Any CPU.Build.0 = Release|Any CPU + {76793477-8219-4FFB-AA08-3F2224EC4968}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76793477-8219-4FFB-AA08-3F2224EC4968}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76793477-8219-4FFB-AA08-3F2224EC4968}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76793477-8219-4FFB-AA08-3F2224EC4968}.Release|Any CPU.Build.0 = Release|Any CPU + {ED8048DC-8509-4085-97F0-F9D9A59A689A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED8048DC-8509-4085-97F0-F9D9A59A689A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED8048DC-8509-4085-97F0-F9D9A59A689A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED8048DC-8509-4085-97F0-F9D9A59A689A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -55,6 +67,8 @@ Global {3AEA1AB4-F068-4C39-A173-AD65F3F8E2F2} = {245E2FBE-DFDF-40B4-94B7-5DDA216E58AD} {DA47BCEE-9AE7-4F20-A04F-5866966503C5} = {245E2FBE-DFDF-40B4-94B7-5DDA216E58AD} {A2D2694C-AB68-49B0-B4B0-94BBFF48C043} = {245E2FBE-DFDF-40B4-94B7-5DDA216E58AD} + {76793477-8219-4FFB-AA08-3F2224EC4968} = {245E2FBE-DFDF-40B4-94B7-5DDA216E58AD} + {ED8048DC-8509-4085-97F0-F9D9A59A689A} = {245E2FBE-DFDF-40B4-94B7-5DDA216E58AD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {86FCF989-26FC-41E9-8A23-9485606D619D} diff --git a/src/Correo.Abstractions/Correo.Abstractions.csproj b/src/Correo.Abstractions/Correo.Abstractions.csproj new file mode 100644 index 0000000..dbc1517 --- /dev/null +++ b/src/Correo.Abstractions/Correo.Abstractions.csproj @@ -0,0 +1,7 @@ + + + + net6.0 + + + diff --git a/src/Correo.Domain/Models/EmailMessage.cs b/src/Correo.Abstractions/EmailMessage.cs similarity index 94% rename from src/Correo.Domain/Models/EmailMessage.cs rename to src/Correo.Abstractions/EmailMessage.cs index aaf4665..e70d49a 100644 --- a/src/Correo.Domain/Models/EmailMessage.cs +++ b/src/Correo.Abstractions/EmailMessage.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Correo.Domain.Models +namespace Correo.Abstractions { public record EmailMessage { diff --git a/src/Correo.Abstractions/IMailer.cs b/src/Correo.Abstractions/IMailer.cs new file mode 100644 index 0000000..194f854 --- /dev/null +++ b/src/Correo.Abstractions/IMailer.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Correo.Abstractions +{ + public interface IMailer + { + void SendEmail(EmailMessage message); + Task SendEmailAsync(EmailMessage message, CancellationToken token = default); + } +} diff --git a/src/Correo.Application/CommandHandlers/SendEmailHandler.cs b/src/Correo.Application/CommandHandlers/SendEmailHandler.cs index 5edf359..862f4ea 100644 --- a/src/Correo.Application/CommandHandlers/SendEmailHandler.cs +++ b/src/Correo.Application/CommandHandlers/SendEmailHandler.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Correo.Abstractions; using Correo.Application.Extensions; using Correo.Domain.Models; using Correo.PublishedLanguage.Commands; @@ -16,16 +17,18 @@ namespace Correo.Application.CommandHandlers public class SendEmailHandler : IRequestHandler { private readonly IMessageBusPublisher _messageBusPublisher; + private readonly IOptions _defaultSender; private readonly ILogger _logger; private readonly IMapper _mapper; - private readonly IOptions _defaultSender; + private readonly IMailer _mailer; - public SendEmailHandler(IMessageBusPublisher messageBusPublisher, ILogger logger, IMapper mapper, IOptions defaultSender) + public SendEmailHandler(IMessageBusPublisher messageBusPublisher, IOptions defaultSender, ILogger logger, IMapper mapper, IMailer mailer) { _messageBusPublisher=messageBusPublisher; + _defaultSender=defaultSender; _logger=logger; _mapper=mapper; - _defaultSender=defaultSender; + _mailer=mailer; } public async Task Handle(SendEmail command, CancellationToken cancellationToken) @@ -33,15 +36,14 @@ namespace Correo.Application.CommandHandlers try { var emailMessage = _mapper.Map(command); - if (emailMessage.From == null) - emailMessage.From = new EmailMessage.MailAddress(_defaultSender.Value.Address, _defaultSender.Value.Name); - + emailMessage.Enrich(_defaultSender.Value); emailMessage.Validate(); - - // send email + await _mailer.SendEmailAsync(emailMessage, cancellationToken); + await _messageBusPublisher.PublishAsync(new EmailSent(command.Subject), cancellationToken); } catch (Exception ex) { + _logger.LogError(ex, ex.Message); await _messageBusPublisher.PublishAsync(new EmailSentFailed(command.Subject, ex.Message), cancellationToken); throw; } diff --git a/src/Correo.Application/CommandHandlers/SendEmailHandlerSync.cs b/src/Correo.Application/CommandHandlers/SendEmailHandlerSync.cs new file mode 100644 index 0000000..6d7441b --- /dev/null +++ b/src/Correo.Application/CommandHandlers/SendEmailHandlerSync.cs @@ -0,0 +1,37 @@ +using Correo.Abstractions; +using Correo.Application.Extensions; +using Correo.Domain.Models; +using MediatR; +using Microsoft.Extensions.Options; +using System.Threading; +using System.Threading.Tasks; + +namespace Correo.Application.CommandHandlers +{ + public class SendEmailHandlerSync + { + public record Command : EmailMessage, IRequest + { + } + + public class CommandHandler : IRequestHandler + { + private readonly IOptions _defaultSender; + private readonly IMailer _mailer; + + public CommandHandler(IOptions defaultSender, IMailer mailer) + { + _defaultSender=defaultSender; + _mailer=mailer; + } + + public async Task Handle(Command command, CancellationToken cancellationToken) + { + command.Enrich(_defaultSender.Value); + command.Validate(); + await _mailer.SendEmailAsync(command, cancellationToken); + return Unit.Value; + } + } + } +} diff --git a/src/Correo.Application/Correo.Application.csproj b/src/Correo.Application/Correo.Application.csproj index 1e842d8..882d546 100644 --- a/src/Correo.Application/Correo.Application.csproj +++ b/src/Correo.Application/Correo.Application.csproj @@ -11,8 +11,10 @@ + + diff --git a/src/Correo.Application/DependencyInjectionExtensions.cs b/src/Correo.Application/DependencyInjectionExtensions.cs index 0fd3631..2a73b8a 100644 --- a/src/Correo.Application/DependencyInjectionExtensions.cs +++ b/src/Correo.Application/DependencyInjectionExtensions.cs @@ -1,4 +1,5 @@ using Correo.Domain.Models; +using Correo.SmtpClient; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -9,6 +10,7 @@ namespace Correo.Application public static void AddApplicationServices(this IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection("DefaultSender")); + services.AddSmtpClientServices(configuration.GetSection("SmtpClient")); } } } diff --git a/src/Correo.Application/Extensions/ModelExtensions.cs b/src/Correo.Application/Extensions/ModelExtensions.cs new file mode 100644 index 0000000..023bd2d --- /dev/null +++ b/src/Correo.Application/Extensions/ModelExtensions.cs @@ -0,0 +1,14 @@ +using Correo.Abstractions; +using Correo.Domain.Models; + +namespace Correo.Application.Extensions +{ + internal static class ModelExtensions + { + public static void Enrich(this EmailMessage emailMessage, DefaultSender defaultSender) + { + if (emailMessage.From == null) + emailMessage.From = new EmailMessage.MailAddress(defaultSender.Address, defaultSender.Name); + } + } +} diff --git a/src/Correo.Application/Extensions/ValidationExtensions.cs b/src/Correo.Application/Extensions/ValidationExtensions.cs index cc05b92..fc921f7 100644 --- a/src/Correo.Application/Extensions/ValidationExtensions.cs +++ b/src/Correo.Application/Extensions/ValidationExtensions.cs @@ -1,5 +1,5 @@ -using Correo.Application.Utils; -using Correo.Domain.Models; +using Correo.Abstractions; +using Correo.Application.Utils; using System; using System.Linq; diff --git a/src/Correo.Application/Mappings/MappingProfile.cs b/src/Correo.Application/Mappings/MappingProfile.cs index 32b3305..728f11c 100644 --- a/src/Correo.Application/Mappings/MappingProfile.cs +++ b/src/Correo.Application/Mappings/MappingProfile.cs @@ -1,5 +1,6 @@ using AutoMapper; -using Correo.Domain.Models; +using Correo.Abstractions; +using Correo.Application.CommandHandlers; using Correo.PublishedLanguage.Commands; namespace Correo.Application.Mappings @@ -10,6 +11,7 @@ namespace Correo.Application.Mappings { CreateMap(); CreateMap(); + CreateMap(); } } } diff --git a/src/Correo.Application/Queries/GetSystemVersion.cs b/src/Correo.Application/Queries/GetSystemVersion.cs index dc70921..d26a8b0 100644 --- a/src/Correo.Application/Queries/GetSystemVersion.cs +++ b/src/Correo.Application/Queries/GetSystemVersion.cs @@ -1,5 +1,7 @@ using MediatR; using System; +using System.IO; +using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -25,10 +27,22 @@ namespace Correo.Application.Queries public async Task Handle(Query request, CancellationToken cancellationToken) { + var version = Environment.GetEnvironmentVariable("APP_VERSION"); + var appDate = Environment.GetEnvironmentVariable("APP_DATE"); + + if (string.IsNullOrEmpty(version)) + version = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion; + + if (!DateTime.TryParse(appDate, out var lastUpdateDate)) + { + var location = Assembly.GetExecutingAssembly().Location; + lastUpdateDate = File.GetLastWriteTime(location); + } + var result = new Model() { - Version = "1.0.0", - LastUpdateDate = DateTime.Now + Version = version, + LastUpdateDate = lastUpdateDate }; return await Task.FromResult(result); diff --git a/src/Correo.Domain/Models/Settings.cs b/src/Correo.Domain/Models/Settings.cs index b479757..b3af19d 100644 --- a/src/Correo.Domain/Models/Settings.cs +++ b/src/Correo.Domain/Models/Settings.cs @@ -1,4 +1,8 @@ namespace Correo.Domain.Models { - public record DefaultSender(string Address, string Name); + public record DefaultSender + { + public string Address { get; init; } + public string Name { get; init; } + } } diff --git a/src/Correo.SmtpClient/Correo.SmtpClient.csproj b/src/Correo.SmtpClient/Correo.SmtpClient.csproj new file mode 100644 index 0000000..cc62f91 --- /dev/null +++ b/src/Correo.SmtpClient/Correo.SmtpClient.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + + + + + + + + + + + + + diff --git a/src/Correo.SmtpClient/DependencyInjectionExtensions.cs b/src/Correo.SmtpClient/DependencyInjectionExtensions.cs new file mode 100644 index 0000000..c65fbc4 --- /dev/null +++ b/src/Correo.SmtpClient/DependencyInjectionExtensions.cs @@ -0,0 +1,16 @@ +using Correo.Abstractions; +using Correo.SmtpClient.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Correo.SmtpClient +{ + public static class DependencyInjectionExtensions + { + public static void AddSmtpClientServices(this IServiceCollection services, IConfigurationSection configuration) + { + services.Configure(configuration); + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/src/Correo.SmtpClient/Extensions/ModelExtensions.cs b/src/Correo.SmtpClient/Extensions/ModelExtensions.cs new file mode 100644 index 0000000..628394a --- /dev/null +++ b/src/Correo.SmtpClient/Extensions/ModelExtensions.cs @@ -0,0 +1,39 @@ +using Correo.Abstractions; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mail; + +namespace Correo.SmtpClient.Extensions +{ + internal static class ModelExtensions + { + public static string Log(this IEnumerable addresses) + => addresses != null ? string.Join(',', addresses.Select(z => z.Address)) : string.Empty; + + public static MailAddress ToMailAddress(this EmailMessage.MailAddress address) + => new MailAddress(address.Address, address.DisplayName); + + public static void AddRange(this MailAddressCollection collection, IEnumerable addresses) + { + foreach (var item in addresses) + collection.Add(item.ToMailAddress()); + } + + public static MailMessage ToMailMessage(this EmailMessage message) + { + var mailMessage = new MailMessage + { + Subject = message.Subject, + Body = message.Body, + From = message.From.ToMailAddress(), + IsBodyHtml = message.IsBodyHtml + }; + + mailMessage.To.AddRange(message.To); + mailMessage.CC.AddRange(message.Cc); + mailMessage.Bcc.AddRange(message.Bcc); + + return mailMessage; + } + } +} diff --git a/src/Correo.SmtpClient/Models/SmtpClientOptions.cs b/src/Correo.SmtpClient/Models/SmtpClientOptions.cs new file mode 100644 index 0000000..dc43aa3 --- /dev/null +++ b/src/Correo.SmtpClient/Models/SmtpClientOptions.cs @@ -0,0 +1,19 @@ +namespace Correo.SmtpClient.Models +{ + public record SmtpClientOptions + { + public string Server { get; init; } + public int Port { get; init; } + public bool UseSsl { get; init; } + public bool UseAuthentication { get; init; } + public bool TrustServer { get; init; } + public AuthenticationOptions Authentication { get; init; } + + public record AuthenticationOptions + { + public string UserName { get; init; } + public string Domain { get; init; } + public string Password { get; init; } + } + } +} diff --git a/src/Correo.SmtpClient/SmtpClientMailer.cs b/src/Correo.SmtpClient/SmtpClientMailer.cs new file mode 100644 index 0000000..3e069db --- /dev/null +++ b/src/Correo.SmtpClient/SmtpClientMailer.cs @@ -0,0 +1,64 @@ +using Correo.Abstractions; +using Correo.SmtpClient.Extensions; +using Correo.SmtpClient.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace Correo.SmtpClient +{ + public class SmtpClientMailer : IMailer, IDisposable + { + private readonly IOptions _optionsAccessor; + private readonly System.Net.Mail.SmtpClient _smtpClient; + private readonly ILogger _logger; + + public SmtpClientMailer(IOptions optionsAccessor, ILogger logger) + { + _optionsAccessor = optionsAccessor; + _logger = logger; + + var options = _optionsAccessor.Value; + + _smtpClient = new System.Net.Mail.SmtpClient { Host = options.Server, Port = options.Port, EnableSsl = options.UseSsl }; + if (options.UseAuthentication) + { + if (string.IsNullOrEmpty(options.Authentication.Domain)) + _smtpClient.Credentials = new NetworkCredential(options.Authentication.UserName, options.Authentication.Password); + else + _smtpClient.Credentials = new NetworkCredential(options.Authentication.UserName, options.Authentication.Password, options.Authentication.Domain); + } + else + _smtpClient.UseDefaultCredentials = true; + + if (options.TrustServer) + ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; + } + + public void Dispose() + { + _smtpClient?.Dispose(); + GC.SuppressFinalize(this); + } + + public void SendEmail(EmailMessage message) + { + var mailMessage = message.ToMailMessage(); + _smtpClient.Send(mailMessage); + Log(message); + } + + public async Task SendEmailAsync(EmailMessage message, CancellationToken token = default) + { + var mailMessage = message.ToMailMessage(); + await _smtpClient.SendMailAsync(mailMessage, token); + Log(message); + } + + 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/Controllers/MailerController.cs b/src/Correo/Controllers/MailerController.cs new file mode 100644 index 0000000..a5a4130 --- /dev/null +++ b/src/Correo/Controllers/MailerController.cs @@ -0,0 +1,31 @@ +using AutoMapper; +using Correo.Application.CommandHandlers; +using Correo.PublishedLanguage.Commands; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; + +namespace Correo.Controllers +{ + [ApiController] + [Route("api/mailer")] + public class MailerController : ControllerBase + { + private readonly IMediator _mediator; + private readonly IMapper _mapper; + + public MailerController(IMediator mediator, IMapper mapper) + { + _mediator=mediator; + _mapper=mapper; + } + + [HttpPost("email")] + public async Task SendEmail([FromBody] SendEmail sendEmail) + { + var command = _mapper.Map(sendEmail); + var result = await _mediator.Send(command); + return Ok(result); + } + } +} diff --git a/src/Correo/Controllers/SystemController.cs b/src/Correo/Controllers/SystemController.cs index 84e8f23..555d259 100644 --- a/src/Correo/Controllers/SystemController.cs +++ b/src/Correo/Controllers/SystemController.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace Correo.Controllers { [ApiController] - [Route("[controller]")] + [Route("api/system")] public class SystemController : ControllerBase { private readonly IMediator _mediator; diff --git a/src/Correo/Extensions/StartupExtensions.cs b/src/Correo/Extensions/StartupExtensions.cs index 7978814..ab29ef6 100644 --- a/src/Correo/Extensions/StartupExtensions.cs +++ b/src/Correo/Extensions/StartupExtensions.cs @@ -1,4 +1,5 @@ -using MediatR; +using Correo.Application; +using MediatR; using MediatR.Pipeline; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; @@ -27,6 +28,9 @@ namespace Correo.Extensions // Messaging services.AddMessaging(configuration); + + // Application services + services.AddApplicationServices(configuration); } public static void Configure(this WebApplication app)