using System.Collections.Concurrent; using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Esim.SendMail.Services; /// /// High-performance email service optimized for millions of messages /// Uses MailKit with connection pooling and batch processing /// public interface IEmailService { Task SendEmailAsync(string to, string subject, string body, bool isHtml = true); Task SendBatchAsync(IEnumerable messages); void Dispose(); } public class EmailMessage { public decimal Id { get; set; } public string To { get; set; } = null!; public string Subject { get; set; } = null!; public string Body { get; set; } = null!; public bool IsHtml { get; set; } = true; } public class EmailResult { public int MessageId { get; set; } public bool Success { get; set; } public string? ErrorMessage { get; set; } } public class HighPerformanceEmailService : IEmailService, IDisposable { private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly string _smtpServer; private readonly int _smtpPort; private readonly string _senderEmail; private readonly string _senderName; private readonly string _senderPassword; private readonly bool _enableSsl; private readonly int _connectionPoolSize; private readonly int _maxConcurrentSends; private readonly int _retryDelayMs; // Connection pool for SMTP clients private readonly ConcurrentBag _connectionPool; private readonly SemaphoreSlim _poolSemaphore; private bool _disposed = false; public HighPerformanceEmailService(ILogger logger, IConfiguration configuration) { _logger = logger; _configuration = configuration; _smtpServer = _configuration["Email:SmtpServer"] ?? "smtp.gmail.com"; _smtpPort = int.Parse(_configuration["Email:SmtpPort"] ?? "587"); _senderEmail = _configuration["Email:SenderEmail"] ?? ""; _senderName = _configuration["Email:SenderName"] ?? "EsimLao"; _senderPassword = _configuration["Email:SenderPassword"] ?? ""; _enableSsl = bool.Parse(_configuration["Email:EnableSsl"] ?? "true"); _connectionPoolSize = int.Parse(_configuration["Email:ConnectionPoolSize"] ?? "5"); _maxConcurrentSends = int.Parse(_configuration["Email:MaxConcurrentSends"] ?? "10"); _retryDelayMs = int.Parse(_configuration["Email:RetryDelayMs"] ?? "1000"); _connectionPool = new ConcurrentBag(); _poolSemaphore = new SemaphoreSlim(_connectionPoolSize, _connectionPoolSize); // Pre-initialize connection pool InitializeConnectionPool(); } private void InitializeConnectionPool() { _logger.LogInformation("Initializing SMTP connection pool with {PoolSize} connections", _connectionPoolSize); for (int i = 0; i < _connectionPoolSize; i++) { try { var client = CreateConnectedClient(); if (client != null) { _connectionPool.Add(client); } } catch (Exception ex) { _logger.LogWarning("Failed to create initial SMTP connection {Index}: {Message}", i + 1, ex.Message); } } _logger.LogInformation("Connection pool initialized with {Count} connections", _connectionPool.Count); } private SmtpClient? CreateConnectedClient() { try { var client = new SmtpClient(); // Connect with timeout client.Timeout = 30000; // 30 seconds client.Connect(_smtpServer, _smtpPort, _enableSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.None); client.Authenticate(_senderEmail, _senderPassword); return client; } catch (Exception ex) { _logger.LogError("Failed to create SMTP connection: {Message}", ex.Message); return null; } } private async Task GetClientFromPoolAsync() { await _poolSemaphore.WaitAsync(); if (_connectionPool.TryTake(out var client)) { // Check if connection is still alive if (client.IsConnected) { return client; } // Connection dead, try to reconnect try { client.Dispose(); } catch { } } // Create new connection return CreateConnectedClient(); } private void ReturnClientToPool(SmtpClient? client) { if (client != null && client.IsConnected) { _connectionPool.Add(client); } else { // Try to create a new connection try { var newClient = CreateConnectedClient(); if (newClient != null) { _connectionPool.Add(newClient); } } catch { } } _poolSemaphore.Release(); } public async Task SendEmailAsync(string to, string subject, string body, bool isHtml = true) { SmtpClient? client = null; try { client = await GetClientFromPoolAsync(); if (client == null) { _logger.LogError("Failed to get SMTP client from pool"); return false; } var message = CreateMimeMessage(to, subject, body, isHtml); await client.SendAsync(message); _logger.LogDebug("Email sent to {To}", to); return true; } catch (Exception ex) { _logger.LogError("Failed to send email to {To}: {Message}", to, ex.Message); // Force reconnect on error if (client != null) { try { client.Disconnect(true); } catch { } try { client.Dispose(); } catch { } client = null; } return false; } finally { ReturnClientToPool(client); } } /// /// Send batch of emails with parallel processing /// Returns number of successfully sent emails /// public async Task SendBatchAsync(IEnumerable messages) { var messageList = messages.ToList(); if (!messageList.Any()) return 0; _logger.LogInformation("Sending batch of {Count} emails", messageList.Count); int successCount = 0; var semaphore = new SemaphoreSlim(_maxConcurrentSends); var tasks = new List>(); foreach (var msg in messageList) { await semaphore.WaitAsync(); var task = Task.Run(async () => { try { return await SendEmailAsync(msg.To, msg.Subject, msg.Body, msg.IsHtml); } finally { semaphore.Release(); } }); tasks.Add(task); } var results = await Task.WhenAll(tasks); successCount = results.Count(r => r); _logger.LogInformation("Batch complete: {SuccessCount}/{TotalCount} successful", successCount, messageList.Count); return successCount; } private MimeMessage CreateMimeMessage(string to, string subject, string body, bool isHtml) { var message = new MimeMessage(); message.From.Add(new MailboxAddress(_senderName, _senderEmail)); message.To.Add(MailboxAddress.Parse(to)); message.Subject = subject; var bodyBuilder = new BodyBuilder(); if (isHtml) { bodyBuilder.HtmlBody = body; } else { bodyBuilder.TextBody = body; } message.Body = bodyBuilder.ToMessageBody(); return message; } public void Dispose() { if (_disposed) return; _disposed = true; _logger.LogInformation("Disposing email service..."); while (_connectionPool.TryTake(out var client)) { try { if (client.IsConnected) { client.Disconnect(true); } client.Dispose(); } catch { } } _poolSemaphore.Dispose(); _logger.LogInformation("Email service disposed"); } }