diff --git a/OrderProcessing.EmailWorker/Configuration/EmailOptions.cs b/OrderProcessing.EmailWorker/Configuration/EmailOptions.cs new file mode 100644 index 0000000..dedb3dd --- /dev/null +++ b/OrderProcessing.EmailWorker/Configuration/EmailOptions.cs @@ -0,0 +1,23 @@ +namespace OrderProcessing.EmailWorker.Configuration; + +public sealed class EmailOptions +{ + public const string SectionName = "Email"; + + public bool UseSmtp { get; set; } + + public string Host { get; set; } = "smtp.gmail.com"; + + public int Port { get; set; } = 587; + + public SmtpSecurityMode SecurityMode { get; set; } = SmtpSecurityMode.StartTls; + + public string UserName { get; set; } = string.Empty; + + public string Password { get; set; } = string.Empty; + + public string FromAddress { get; set; } = string.Empty; + + public string FromName { get; set; } = + "Order Processing Platform"; +} \ No newline at end of file diff --git a/OrderProcessing.EmailWorker/Configuration/SmtpSecurityMode.cs b/OrderProcessing.EmailWorker/Configuration/SmtpSecurityMode.cs new file mode 100644 index 0000000..4896bb2 --- /dev/null +++ b/OrderProcessing.EmailWorker/Configuration/SmtpSecurityMode.cs @@ -0,0 +1,7 @@ +namespace OrderProcessing.EmailWorker.Configuration; + +public enum SmtpSecurityMode +{ + StartTls, + SslOnConnect +} \ No newline at end of file diff --git a/OrderProcessing.EmailWorker/Emailing/SmtpEmailSender.cs b/OrderProcessing.EmailWorker/Emailing/SmtpEmailSender.cs new file mode 100644 index 0000000..264ba5f --- /dev/null +++ b/OrderProcessing.EmailWorker/Emailing/SmtpEmailSender.cs @@ -0,0 +1,65 @@ +using MailKit.Net.Smtp; +using MailKit.Security; +using Microsoft.Extensions.Options; +using MimeKit; +using OrderProcessing.EmailWorker.Configuration; + +namespace OrderProcessing.EmailWorker.Emailing; + +public sealed class SmtpEmailSender : IEmailSender +{ + private readonly EmailOptions _options; + private readonly ILogger _logger; + + public SmtpEmailSender(IOptions options, ILogger logger) + { + _options = options.Value; + _logger = logger; + } + + public async Task SendAsync(EmailMessage message, CancellationToken cancellationToken) + { + var mimeMessage = new MimeMessage(); + + mimeMessage.From.Add(new MailboxAddress(_options.FromName, _options.FromAddress)); + + mimeMessage.To.Add(MailboxAddress.Parse(message.Recipient)); + + mimeMessage.Subject = message.Subject; + + mimeMessage.Body = new TextPart("plain") + { + Text = message.Body + }; + + using var smtpClient = new MailKit.Net.Smtp.SmtpClient(); + + var socketOptions = GetSecureSocketOptions(); + + await smtpClient.ConnectAsync(_options.Host, _options.Port, socketOptions, cancellationToken); + + await smtpClient.AuthenticateAsync(_options.UserName, _options.Password, cancellationToken); + + await smtpClient.SendAsync(mimeMessage, cancellationToken); + + await smtpClient.DisconnectAsync(quit: true, cancellationToken); + + _logger.LogInformation( + "Email sent successfully to {Recipient} " + + "with subject {Subject}", + message.Recipient, + message.Subject); + } + + private SecureSocketOptions GetSecureSocketOptions() + { + return _options.SecurityMode switch + { + SmtpSecurityMode.StartTls => SecureSocketOptions.StartTls, + + SmtpSecurityMode.SslOnConnect => SecureSocketOptions.SslOnConnect, + + _ => throw new InvalidOperationException($"Unsupported SMTP security mode '{_options.SecurityMode}'.") + }; + } +} \ No newline at end of file diff --git a/OrderProcessing.EmailWorker/OrderProcessing.EmailWorker.csproj b/OrderProcessing.EmailWorker/OrderProcessing.EmailWorker.csproj index 86eb105..d4c80f9 100644 --- a/OrderProcessing.EmailWorker/OrderProcessing.EmailWorker.csproj +++ b/OrderProcessing.EmailWorker/OrderProcessing.EmailWorker.csproj @@ -8,12 +8,14 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/OrderProcessing.EmailWorker/Program.cs b/OrderProcessing.EmailWorker/Program.cs index 9856f12..e4b428e 100644 --- a/OrderProcessing.EmailWorker/Program.cs +++ b/OrderProcessing.EmailWorker/Program.cs @@ -54,8 +54,32 @@ "RabbitMQ prefetch count must be greater than zero.") .ValidateOnStart(); +builder.Services + .AddOptions() + .Bind(builder.Configuration.GetSection(EmailOptions.SectionName)) + .Validate( + options => !options.UseSmtp ||!string.IsNullOrWhiteSpace(options.Host), "SMTP host is required.") + .Validate( + options => !options.UseSmtp || options.Port > 0, "SMTP port must be greater than zero.") + .Validate( + options => !options.UseSmtp || !string.IsNullOrWhiteSpace(options.UserName), "SMTP username is required.") + .Validate( + options => !options.UseSmtp || !string.IsNullOrWhiteSpace(options.Password), "SMTP password is required.") + .Validate( + options => !options.UseSmtp || !string.IsNullOrWhiteSpace(options.FromAddress), "SMTP from address is required.") + .ValidateOnStart(); + + +var useSmtp = builder.Configuration.GetSection(EmailOptions.SectionName).GetValue(nameof(EmailOptions.UseSmtp)); -builder.Services.AddSingleton(); +if (useSmtp) +{ + builder.Services.AddSingleton(); +} +else +{ + builder.Services.AddSingleton(); +} builder.Services.AddScoped(); diff --git a/OrderProcessing.EmailWorker/appsettings.Development.json b/OrderProcessing.EmailWorker/appsettings.Development.json index b2dcdb6..dad01d1 100644 --- a/OrderProcessing.EmailWorker/appsettings.Development.json +++ b/OrderProcessing.EmailWorker/appsettings.Development.json @@ -4,5 +4,8 @@ "Default": "Information", "Microsoft.Hosting.Lifetime": "Information" } + }, + "Email": { + "UseSmtp": true } } diff --git a/OrderProcessing.EmailWorker/appsettings.json b/OrderProcessing.EmailWorker/appsettings.json index c5e9bb7..7a8e891 100644 --- a/OrderProcessing.EmailWorker/appsettings.json +++ b/OrderProcessing.EmailWorker/appsettings.json @@ -23,6 +23,16 @@ "NetworkRecoveryIntervalSeconds": 5, "PrefetchCount": 1 }, + "Email": { + "UseSmtp": false, + "Host": "smtp.gmail.com", + "Port": 587, + "SecurityMode": "StartTls", + "UserName": "", + "Password": "", + "FromAddress": "", + "FromName": "Order Processing Platform" + }, "ConnectionStrings": { "EmailWorkerConnection": "Server=(localdb)\\mssqllocaldb;Database=OrderProcessingDb;Trusted_Connection=True;TrustServerCertificate=True;" }