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");
}
}