using foodmarket.Application.Common.Email; using foodmarket.Infrastructure.Persistence; using MailKit.Net.Smtp; using MailKit.Security; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using MimeKit; namespace foodmarket.Infrastructure.Email; /// SMTP-отправка через MailKit. Зарегистрирован Singleton, но на /// каждую отправку создаёт scope для свежего DbContext'а — конфиг /// (PlatformSettings) перечитывается на каждой отправке без рестарта. /// /// Если SMTP не настроен (host пуст / from-email пуст) — кидает /// EmailNotConfiguredException. Контроллер ловит и возвращает 400/500. public class MailKitEmailSender : IEmailSender { private readonly IServiceScopeFactory _scopes; private readonly IDataProtectionProvider _dpProvider; private readonly ILogger _logger; public MailKitEmailSender(IServiceScopeFactory scopes, IDataProtectionProvider dpProvider, ILogger logger) { _scopes = scopes; _dpProvider = dpProvider; _logger = logger; } public async Task SendAsync(string toEmail, string subject, string body, CancellationToken ct = default) { using var scope = _scopes.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var s = await db.PlatformSettings.AsNoTracking().FirstOrDefaultAsync(ct); if (s is null || string.IsNullOrWhiteSpace(s.SmtpHost) || string.IsNullOrWhiteSpace(s.FromEmail)) { throw new EmailNotConfiguredException( "SMTP не настроен. Откройте «Системная консоль → Настройки платформы» и заполните SmtpHost, FromEmail, креды."); } var msg = new MimeMessage(); msg.From.Add(new MailboxAddress(s.FromName ?? "Food Market", s.FromEmail)); msg.To.Add(MailboxAddress.Parse(toEmail)); msg.Subject = subject; msg.Body = new TextPart("plain") { Text = body }; // Implicit TLS (SmtpUseSsl) — обычно 465. STARTTLS (SmtpStartTls) — 587. // Если оба false — открытое соединение (SmtpClient.Connect c None). var secureOption = s.SmtpUseSsl ? SecureSocketOptions.SslOnConnect : (s.SmtpStartTls ? SecureSocketOptions.StartTls : SecureSocketOptions.None); var port = s.SmtpPort ?? (s.SmtpUseSsl ? 465 : 587); using var client = new SmtpClient(); await client.ConnectAsync(s.SmtpHost, port, secureOption, ct); if (!string.IsNullOrEmpty(s.SmtpUsername)) { var password = string.Empty; if (!string.IsNullOrEmpty(s.SmtpPasswordEncrypted)) { try { var protector = _dpProvider.CreateProtector("foodmarket.smtp"); password = protector.Unprotect(s.SmtpPasswordEncrypted); } catch (Exception ex) { throw new EmailNotConfiguredException( "Не удалось расшифровать SMTP-пароль (DataProtection ключ изменился?). " + "Введите пароль заново в настройках платформы. Detail: " + ex.Message); } } await client.AuthenticateAsync(s.SmtpUsername, password, ct); } try { await client.SendAsync(msg, ct); _logger.LogInformation("Email sent to {To} subject={Subject}", toEmail, subject); } finally { await client.DisconnectAsync(true, ct); } } }