using System.Collections.Concurrent; using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; using Microsoft.Extensions.Configuration; 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 int 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 static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(HighPerformanceEmailService)); 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(IConfiguration configuration) { _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() { log.Info($"Initializing SMTP connection pool with {_connectionPoolSize} connections"); for (int i = 0; i < _connectionPoolSize; i++) { try { var client = CreateConnectedClient(); if (client != null) { _connectionPool.Add(client); } } catch (Exception ex) { log.Warn($"Failed to create initial SMTP connection {i + 1}: {ex.Message}"); } } log.Info($"Connection pool initialized with {_connectionPool.Count} connections"); } 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) { log.Error($"Failed to create SMTP connection: {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) { log.Error("Failed to get SMTP client from pool"); return false; } var message = CreateMimeMessage(to, subject, body, isHtml); await client.SendAsync(message); log.Debug($"Email sent to {to}"); return true; } catch (Exception ex) { log.Error($"Failed to send email to {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; log.Info($"Sending batch of {messageList.Count} emails"); 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); log.Info($"Batch complete: {successCount}/{messageList.Count} successful"); 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; log.Info("Disposing email service..."); while (_connectionPool.TryTake(out var client)) { try { if (client.IsConnected) { client.Disconnect(true); } client.Dispose(); } catch { } } _poolSemaphore.Dispose(); log.Info("Email service disposed"); } }