From 68a9e06eebb83fabe8d714a6b00d3d902d1d24b3 Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Thu, 29 May 2025 13:13:41 +0300 Subject: [PATCH] add email sender service --- .../Common/Services/EmailSenderService.cs | 7 +++ src/Configuration/packages.lock.json | 29 +++++++++++ src/HttpApi/Controllers/TestsController.cs | 33 +++++------- src/HttpApi/appsettings.Development.json | 11 ++++ src/HttpApi/appsettings.json | 11 ++++ src/HttpApi/packages.lock.json | 29 +++++++++++ src/Infrastructure/ConfigurationOptions.cs | 24 +++++++++ src/Infrastructure/Infrastructure.csproj | 1 + .../Services/MailKitEmailSenderService.cs | 50 +++++++++++++++++++ src/Infrastructure/packages.lock.json | 34 +++++++++++++ .../packages.lock.json | 29 +++++++++++ 11 files changed, 238 insertions(+), 20 deletions(-) create mode 100644 src/Application/Common/Services/EmailSenderService.cs create mode 100644 src/Infrastructure/Services/MailKitEmailSenderService.cs diff --git a/src/Application/Common/Services/EmailSenderService.cs b/src/Application/Common/Services/EmailSenderService.cs new file mode 100644 index 0000000..706d96d --- /dev/null +++ b/src/Application/Common/Services/EmailSenderService.cs @@ -0,0 +1,7 @@ +namespace cuqmbr.TravelGuide.Application.Common.Services; + +public interface EmailSenderService +{ + Task SendAsync(string[] addresses, string subject, string body, + CancellationToken cancellationToken); +} diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json index bba215c..aed0299 100644 --- a/src/Configuration/packages.lock.json +++ b/src/Configuration/packages.lock.json @@ -164,11 +164,25 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "FluentValidation": { "type": "Transitive", "resolved": "11.11.0", "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" }, + "MailKit": { + "type": "Transitive", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "MediatR": { "type": "Transitive", "resolved": "12.4.1", @@ -701,6 +715,15 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", @@ -769,6 +792,11 @@ "resolved": "5.0.0", "contentHash": "dMkqfy2el8A8/I76n2Hi1oBFEbG1SfxD2l5nhwXV3XjlnOmwxJlQbYpJH4W51odnU9sARCSAgv7S3CyAFMkpYg==" }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -845,6 +873,7 @@ "type": "Project", "dependencies": { "Application": "[1.0.0, )", + "MailKit": "[4.12.1, )", "Microsoft.Extensions.Http": "[9.0.4, )", "Newtonsoft.Json": "[13.0.3, )" } diff --git a/src/HttpApi/Controllers/TestsController.cs b/src/HttpApi/Controllers/TestsController.cs index 3e2abf0..73b7134 100644 --- a/src/HttpApi/Controllers/TestsController.cs +++ b/src/HttpApi/Controllers/TestsController.cs @@ -1,7 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; using cuqmbr.TravelGuide.Application.Common.Services; -using cuqmbr.TravelGuide.Application.Common.Persistence; namespace cuqmbr.TravelGuide.HttpApi.Controllers; @@ -9,15 +8,13 @@ namespace cuqmbr.TravelGuide.HttpApi.Controllers; public class TestsController : ControllerBase { private readonly IStringLocalizer _localizer; - private readonly UnitOfWork _unitOfWork; + private readonly EmailSenderService _emailSender; - public TestsController( - SessionCultureService cultureService, - IStringLocalizer localizer, - UnitOfWork unitOfWork) + public TestsController(SessionCultureService cultureService, + IStringLocalizer localizer, EmailSenderService emailSender) { _localizer = localizer; - _unitOfWork = unitOfWork; + _emailSender = emailSender; } [HttpGet("getLocalizedString/{inputString}")] @@ -31,19 +28,15 @@ public class TestsController : ControllerBase [HttpGet("trigger")] public async Task Trigger(CancellationToken cancellationToken) { - // await _unitOfWork.BusRepository.AddOneAsync( - // new Domain.Entities.Bus() - // { - // Number = "AB1234MK", - // Model = "This is a fancy bus model", - // Capacity = 40 - // }, - // cancellationToken); - // - // await _unitOfWork.SaveAsync(cancellationToken); - // _unitOfWork.Dispose(); + var body = +@"Hello, friend! - var vehicles = await _unitOfWork.VehicleRepository - .GetPageAsync(1, 10, cancellationToken); +This is my email message for you. + +-- +Travel Guide Service +"; + + await _emailSender.SendAsync(new string[] { "cuqmbr@ya.ru" }, "Test subject", body, cancellationToken); } } diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json index 05be9dc..1fd9334 100644 --- a/src/HttpApi/appsettings.Development.json +++ b/src/HttpApi/appsettings.Development.json @@ -27,5 +27,16 @@ "PublicKey": "sandbox_xxxxxxxxxxxx", "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } + }, + "Email": { + "Smtp": { + "Host": "mail.travel-guide.cuqmbr.xyz", + "Port": "465", + "UseTls": true, + "Username": "no-reply", + "Password": "super-secret-password", + "SenderAddress": "no-reply@travel-guide.cuqmbr.xyz", + "SenderName": "Travel Guide" + } } } diff --git a/src/HttpApi/appsettings.json b/src/HttpApi/appsettings.json index 05be9dc..1fd9334 100644 --- a/src/HttpApi/appsettings.json +++ b/src/HttpApi/appsettings.json @@ -27,5 +27,16 @@ "PublicKey": "sandbox_xxxxxxxxxxxx", "PrivateKey": "sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } + }, + "Email": { + "Smtp": { + "Host": "mail.travel-guide.cuqmbr.xyz", + "Port": "465", + "UseTls": true, + "Username": "no-reply", + "Password": "super-secret-password", + "SenderAddress": "no-reply@travel-guide.cuqmbr.xyz", + "SenderName": "Travel Guide" + } } } diff --git a/src/HttpApi/packages.lock.json b/src/HttpApi/packages.lock.json index 02503f2..59d1bfe 100644 --- a/src/HttpApi/packages.lock.json +++ b/src/HttpApi/packages.lock.json @@ -106,6 +106,11 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "FluentValidation": { "type": "Transitive", "resolved": "11.11.0", @@ -125,6 +130,15 @@ "resolved": "2.14.1", "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" }, + "MailKit": { + "type": "Transitive", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "MediatR": { "type": "Transitive", "resolved": "12.4.1", @@ -848,6 +862,15 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "Mono.TextTemplating": { "type": "Transitive", "resolved": "3.0.0", @@ -982,6 +1005,11 @@ "System.Composition.Runtime": "7.0.0" } }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -1109,6 +1137,7 @@ "type": "Project", "dependencies": { "Application": "[1.0.0, )", + "MailKit": "[4.12.1, )", "Microsoft.Extensions.Http": "[9.0.4, )", "Newtonsoft.Json": "[13.0.3, )" } diff --git a/src/Infrastructure/ConfigurationOptions.cs b/src/Infrastructure/ConfigurationOptions.cs index b0642ee..87ac871 100644 --- a/src/Infrastructure/ConfigurationOptions.cs +++ b/src/Infrastructure/ConfigurationOptions.cs @@ -5,6 +5,8 @@ public sealed class ConfigurationOptions public static string SectionName { get; } = ""; public PaymentProcessingConfigurationOptions PaymentProcessing { get; set; } = new(); + + public EmailConfigurationOptions Email { get; set; } = new(); } public sealed class PaymentProcessingConfigurationOptions @@ -22,3 +24,25 @@ public sealed class LiqPayConfigurationOptions public string PrivateKey { get; set; } } + +public sealed class EmailConfigurationOptions +{ + public SmtpConfigurationOptions Smtp { get; set; } = new(); +} + +public sealed class SmtpConfigurationOptions +{ + public string Host { get; set; } + + public ushort Port { get; set; } + + public bool UseTls { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } + + public string SenderAddress { get; set; } + + public string SenderName { get; set; } +} diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj index 2ae6a29..cf7f00a 100644 --- a/src/Infrastructure/Infrastructure.csproj +++ b/src/Infrastructure/Infrastructure.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Infrastructure/Services/MailKitEmailSenderService.cs b/src/Infrastructure/Services/MailKitEmailSenderService.cs new file mode 100644 index 0000000..36eb50d --- /dev/null +++ b/src/Infrastructure/Services/MailKitEmailSenderService.cs @@ -0,0 +1,50 @@ +using cuqmbr.TravelGuide.Application.Common.Services; +using MailKit.Net.Smtp; +using Microsoft.Extensions.Options; +using MimeKit; + +namespace cuqmbr.TravelGuide.Infrastructure.Services; + +public sealed class MailKitEmailSenderService : EmailSenderService +{ + + private readonly SmtpConfigurationOptions _configuration; + + public MailKitEmailSenderService( + IOptions configuration) + { + _configuration = configuration.Value.Email.Smtp; + } + + public async Task SendAsync(string[] addresses, string subject, + string body, CancellationToken cancellationToken) + { + var message = new MimeMessage(); + + message.From.Add(new MailboxAddress( + _configuration.SenderName, _configuration.SenderAddress)); + foreach (var address in addresses) + { + message.To.Add(new MailboxAddress("", address)); + } + message.Subject = subject; + + message.Body = new TextPart("plain") + { + Text = body + }; + + + using var client = new SmtpClient(); + + await client.ConnectAsync(_configuration.Host, + _configuration.Port, _configuration.UseTls, + cancellationToken); + + await client.AuthenticateAsync(_configuration.Username, + _configuration.Password, cancellationToken); + + await client.SendAsync(message, cancellationToken); + await client.DisconnectAsync(true, cancellationToken); + } +} diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json index 47ecef3..ccfa74f 100644 --- a/src/Infrastructure/packages.lock.json +++ b/src/Infrastructure/packages.lock.json @@ -2,6 +2,16 @@ "version": 1, "dependencies": { "net9.0": { + "MailKit": { + "type": "Direct", + "requested": "[4.12.1, )", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "Microsoft.Extensions.Http": { "type": "Direct", "requested": "[9.0.4, )", @@ -40,6 +50,11 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "FluentValidation": { "type": "Transitive", "resolved": "11.11.0", @@ -289,11 +304,25 @@ "Microsoft.IdentityModel.Logging": "8.11.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "QuikGraph": { "type": "Transitive", "resolved": "2.5.0", "contentHash": "sG+mrPpXwxlXknRK5VqWUGiOmDACa9X+3ftlkQIMgOZUqxVOQSe0+HIU9PTjwqazy0pqSf8MPDXYFGl0GYWcKw==" }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -308,6 +337,11 @@ "resolved": "1.6.2", "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" + }, "application": { "type": "Project", "dependencies": { diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json index 1a02d20..34e1b41 100644 --- a/tst/Application.IntegrationTests/packages.lock.json +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -65,6 +65,11 @@ "Microsoft.Extensions.Options": "8.0.0" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "zy8TMeTP+1FH2NrLaNZtdRbBdq7u5MI+NFZQOBSM69u5RFkciinwzV2eveY6Kjf5MzgsYvvl6kTStsj3JrXqkg==" + }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", @@ -87,6 +92,15 @@ "Microsoft.Extensions.Dependencyinjection.Abstractions": "2.1.0" } }, + "MailKit": { + "type": "Transitive", + "resolved": "4.12.1", + "contentHash": "rIqJm92qtHvk1hDchsJ95Hy7n46A7imE24ol++ikXBsjf3Bi1qDBu4H91FfY6LrYXJaxRlc2gIIpC8AOJrCbqg==", + "dependencies": { + "MimeKit": "4.12.0", + "System.Formats.Asn1": "8.0.1" + } + }, "MediatR": { "type": "Transitive", "resolved": "12.4.1", @@ -775,6 +789,15 @@ "System.Security.Principal.Windows": "4.5.0" } }, + "MimeKit": { + "type": "Transitive", + "resolved": "4.12.0", + "contentHash": "PFUHfs6BZxKYM/QPJksAwXphbJf0SEfdSfsoQ6p6yvFRaJPofFJMBiotWhFRrdSUzfp6C6K49EjBIqIwZ2TJqA==", + "dependencies": { + "BouncyCastle.Cryptography": "2.5.1", + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", @@ -848,6 +871,11 @@ "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, + "System.Formats.Asn1": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", @@ -1012,6 +1040,7 @@ "type": "Project", "dependencies": { "Application": "[1.0.0, )", + "MailKit": "[4.12.1, )", "Microsoft.Extensions.Http": "[9.0.4, )", "Newtonsoft.Json": "[13.0.3, )" }