EmailService.cs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. using System.Collections.Concurrent;
  2. using MailKit.Net.Smtp;
  3. using MailKit.Security;
  4. using MimeKit;
  5. using Microsoft.Extensions.Configuration;
  6. using Microsoft.Extensions.Logging;
  7. namespace Esim.SendMail.Services;
  8. /// <summary>
  9. /// High-performance email service optimized for millions of messages
  10. /// Uses MailKit with connection pooling and batch processing
  11. /// </summary>
  12. public interface IEmailService
  13. {
  14. Task<bool> SendEmailAsync(string to, string subject, string body, bool isHtml = true);
  15. Task<int> SendBatchAsync(IEnumerable<EmailMessage> messages);
  16. void Dispose();
  17. }
  18. public class EmailMessage
  19. {
  20. public decimal Id { get; set; }
  21. public string To { get; set; } = null!;
  22. public string Subject { get; set; } = null!;
  23. public string Body { get; set; } = null!;
  24. public bool IsHtml { get; set; } = true;
  25. }
  26. public class EmailResult
  27. {
  28. public int MessageId { get; set; }
  29. public bool Success { get; set; }
  30. public string? ErrorMessage { get; set; }
  31. }
  32. public class HighPerformanceEmailService : IEmailService, IDisposable
  33. {
  34. private readonly ILogger<HighPerformanceEmailService> _logger;
  35. private readonly IConfiguration _configuration;
  36. private readonly string _smtpServer;
  37. private readonly int _smtpPort;
  38. private readonly string _senderEmail;
  39. private readonly string _senderName;
  40. private readonly string _senderPassword;
  41. private readonly bool _enableSsl;
  42. private readonly int _connectionPoolSize;
  43. private readonly int _maxConcurrentSends;
  44. private readonly int _retryDelayMs;
  45. // Connection pool for SMTP clients
  46. private readonly ConcurrentBag<SmtpClient> _connectionPool;
  47. private readonly SemaphoreSlim _poolSemaphore;
  48. private bool _disposed = false;
  49. public HighPerformanceEmailService(ILogger<HighPerformanceEmailService> logger, IConfiguration configuration)
  50. {
  51. _logger = logger;
  52. _configuration = configuration;
  53. _smtpServer = _configuration["Email:SmtpServer"] ?? "smtp.gmail.com";
  54. _smtpPort = int.Parse(_configuration["Email:SmtpPort"] ?? "587");
  55. _senderEmail = _configuration["Email:SenderEmail"] ?? "";
  56. _senderName = _configuration["Email:SenderName"] ?? "EsimLao";
  57. _senderPassword = _configuration["Email:SenderPassword"] ?? "";
  58. _enableSsl = bool.Parse(_configuration["Email:EnableSsl"] ?? "true");
  59. _connectionPoolSize = int.Parse(_configuration["Email:ConnectionPoolSize"] ?? "5");
  60. _maxConcurrentSends = int.Parse(_configuration["Email:MaxConcurrentSends"] ?? "10");
  61. _retryDelayMs = int.Parse(_configuration["Email:RetryDelayMs"] ?? "1000");
  62. _connectionPool = new ConcurrentBag<SmtpClient>();
  63. _poolSemaphore = new SemaphoreSlim(_connectionPoolSize, _connectionPoolSize);
  64. // Pre-initialize connection pool
  65. InitializeConnectionPool();
  66. }
  67. private void InitializeConnectionPool()
  68. {
  69. _logger.LogInformation("Initializing SMTP connection pool with {PoolSize} connections", _connectionPoolSize);
  70. for (int i = 0; i < _connectionPoolSize; i++)
  71. {
  72. try
  73. {
  74. var client = CreateConnectedClient();
  75. if (client != null)
  76. {
  77. _connectionPool.Add(client);
  78. }
  79. }
  80. catch (Exception ex)
  81. {
  82. _logger.LogWarning("Failed to create initial SMTP connection {Index}: {Message}", i + 1, ex.Message);
  83. }
  84. }
  85. _logger.LogInformation("Connection pool initialized with {Count} connections", _connectionPool.Count);
  86. }
  87. private SmtpClient? CreateConnectedClient()
  88. {
  89. try
  90. {
  91. var client = new SmtpClient();
  92. // Connect with timeout
  93. client.Timeout = 30000; // 30 seconds
  94. client.Connect(_smtpServer, _smtpPort, _enableSsl ? SecureSocketOptions.StartTls : SecureSocketOptions.None);
  95. client.Authenticate(_senderEmail, _senderPassword);
  96. return client;
  97. }
  98. catch (Exception ex)
  99. {
  100. _logger.LogError("Failed to create SMTP connection: {Message}", ex.Message);
  101. return null;
  102. }
  103. }
  104. private async Task<SmtpClient?> GetClientFromPoolAsync()
  105. {
  106. await _poolSemaphore.WaitAsync();
  107. if (_connectionPool.TryTake(out var client))
  108. {
  109. // Check if connection is still alive
  110. if (client.IsConnected)
  111. {
  112. return client;
  113. }
  114. // Connection dead, try to reconnect
  115. try
  116. {
  117. client.Dispose();
  118. }
  119. catch { }
  120. }
  121. // Create new connection
  122. return CreateConnectedClient();
  123. }
  124. private void ReturnClientToPool(SmtpClient? client)
  125. {
  126. if (client != null && client.IsConnected)
  127. {
  128. _connectionPool.Add(client);
  129. }
  130. else
  131. {
  132. // Try to create a new connection
  133. try
  134. {
  135. var newClient = CreateConnectedClient();
  136. if (newClient != null)
  137. {
  138. _connectionPool.Add(newClient);
  139. }
  140. }
  141. catch { }
  142. }
  143. _poolSemaphore.Release();
  144. }
  145. public async Task<bool> SendEmailAsync(string to, string subject, string body, bool isHtml = true)
  146. {
  147. SmtpClient? client = null;
  148. try
  149. {
  150. client = await GetClientFromPoolAsync();
  151. if (client == null)
  152. {
  153. _logger.LogError("Failed to get SMTP client from pool");
  154. return false;
  155. }
  156. var message = CreateMimeMessage(to, subject, body, isHtml);
  157. await client.SendAsync(message);
  158. _logger.LogDebug("Email sent to {To}", to);
  159. return true;
  160. }
  161. catch (Exception ex)
  162. {
  163. _logger.LogError("Failed to send email to {To}: {Message}", to, ex.Message);
  164. // Force reconnect on error
  165. if (client != null)
  166. {
  167. try { client.Disconnect(true); } catch { }
  168. try { client.Dispose(); } catch { }
  169. client = null;
  170. }
  171. return false;
  172. }
  173. finally
  174. {
  175. ReturnClientToPool(client);
  176. }
  177. }
  178. /// <summary>
  179. /// Send batch of emails with parallel processing
  180. /// Returns number of successfully sent emails
  181. /// </summary>
  182. public async Task<int> SendBatchAsync(IEnumerable<EmailMessage> messages)
  183. {
  184. var messageList = messages.ToList();
  185. if (!messageList.Any()) return 0;
  186. _logger.LogInformation("Sending batch of {Count} emails", messageList.Count);
  187. int successCount = 0;
  188. var semaphore = new SemaphoreSlim(_maxConcurrentSends);
  189. var tasks = new List<Task<bool>>();
  190. foreach (var msg in messageList)
  191. {
  192. await semaphore.WaitAsync();
  193. var task = Task.Run(async () =>
  194. {
  195. try
  196. {
  197. return await SendEmailAsync(msg.To, msg.Subject, msg.Body, msg.IsHtml);
  198. }
  199. finally
  200. {
  201. semaphore.Release();
  202. }
  203. });
  204. tasks.Add(task);
  205. }
  206. var results = await Task.WhenAll(tasks);
  207. successCount = results.Count(r => r);
  208. _logger.LogInformation("Batch complete: {SuccessCount}/{TotalCount} successful", successCount, messageList.Count);
  209. return successCount;
  210. }
  211. private MimeMessage CreateMimeMessage(string to, string subject, string body, bool isHtml)
  212. {
  213. var message = new MimeMessage();
  214. message.From.Add(new MailboxAddress(_senderName, _senderEmail));
  215. message.To.Add(MailboxAddress.Parse(to));
  216. message.Subject = subject;
  217. var bodyBuilder = new BodyBuilder();
  218. if (isHtml)
  219. {
  220. bodyBuilder.HtmlBody = body;
  221. }
  222. else
  223. {
  224. bodyBuilder.TextBody = body;
  225. }
  226. message.Body = bodyBuilder.ToMessageBody();
  227. return message;
  228. }
  229. public void Dispose()
  230. {
  231. if (_disposed) return;
  232. _disposed = true;
  233. _logger.LogInformation("Disposing email service...");
  234. while (_connectionPool.TryTake(out var client))
  235. {
  236. try
  237. {
  238. if (client.IsConnected)
  239. {
  240. client.Disconnect(true);
  241. }
  242. client.Dispose();
  243. }
  244. catch { }
  245. }
  246. _poolSemaphore.Dispose();
  247. _logger.LogInformation("Email service disposed");
  248. }
  249. }