using LiteCharms.Features.Email.Configuration; using LiteCharms.Features.Email.Extensions; using LiteCharms.Features.Email.Models; namespace LiteCharms.Features.Email; public class EmailService(IOptions options) : IDisposable { private readonly SmtpSettings settings = options.Value; private readonly SmtpClient client = new(); private readonly int sendMaxCount = 10; private int sendCount = 0; public EmailStatuses Status { get; private set; } = EmailStatuses.Disconnected; public async ValueTask> SendEmailAsync(Message message, CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Send"); activity?.SetTag("email.recipient", message.Recipient?.Address); try { if (Status != EmailStatuses.Connected) { activity?.SetStatus(ActivityStatusCode.Error, "Disconnected"); return Result.Fail("Smtp service is disconnected."); } var email = new MimeMessage(); email.From.Add(new MailboxAddress(message.Sender!.Name, message.Sender.Address!)); email.To.Add(new MailboxAddress(message.Recipient!.Name, message.Recipient!.Address!)); email.Subject = message.Subject!; var bodyBuilder = new BodyBuilder(); if (message.Body!.Properties.HasAttachments) foreach (var attachment in message.Body?.Attachments!) bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken); if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message; if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message; email.Body = bodyBuilder.ToMessageBody(); var response = await client.SendAsync(email, cancellationToken); bool emailSent = response.Contains("OK", StringComparison.InvariantCultureIgnoreCase); message.Dispose(); Interlocked.Increment(ref sendCount); if (sendCount % sendMaxCount == 0) { using var delayActivity = EmailTelemetry.Source.StartActivity("Rate Limit Pause"); sendCount = 0; await Task.Delay(1000, cancellationToken); } if (emailSent) { EmailTelemetry.EmailsSent.Add(1, new TagList { { "host", settings.Host } }); return Result.Ok(Response.Create(EmailStatuses.Success)); } await DisconnectAsync(cancellationToken); if (response.Contains("421")) { Status = EmailStatuses.TooManyConnections; return Result.Fail(response); } if (response.Contains("451")) { Status = EmailStatuses.ConnectionAborted; return Result.Fail(response); } EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } }); Status = EmailStatuses.Disconnected; return Result.Fail("General error, disconnected"); } catch (Exception ex) { activity?.SetStatus(ActivityStatusCode.Error, ex.Message); activity?.AddException(ex); EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask> ConnectAsync(CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Connect"); activity?.SetTag("email.smtp.connect", settings.Host); try { if (Status is EmailStatuses.Connected) return Result.Ok(Response.Create(Status)); await client.ConnectAsync(settings.Host!, settings.Port, settings.UseSsl, cancellationToken); await client.AuthenticateAsync(settings.Credentials!.Username!, settings.Credentials.Password!, cancellationToken); Status = EmailStatuses.Connected; activity?.SetStatus(ActivityStatusCode.Ok, "Connected"); return Result.Ok(Response.Create(Status)); } catch (MailKit.ProtocolException ex) { activity?.SetStatus(ActivityStatusCode.Error, ex.Message); activity?.AddException(ex); EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); Status = EmailStatuses.ProtocolError; return Result.Fail(new Error(ex.Message).CausedBy(ex)); } catch (Exception ex) when (ex is MailKit.Security.SslHandshakeException || ex is MailKit.Security.AuthenticationException) { activity?.SetStatus(ActivityStatusCode.Error, ex.Message); activity?.AddException(ex); EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); Status = EmailStatuses.AuthenticationError; return Result.Fail(new Error(ex.Message).CausedBy(ex)); } catch (Exception ex) { activity?.SetStatus(ActivityStatusCode.Error, ex.Message); activity?.AddException(ex); EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); Status = EmailStatuses.GeneralError; return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public async ValueTask DisconnectAsync(CancellationToken cancellationToken = default) { using var activity = EmailTelemetry.Source.StartActivity("Email Disconnect"); activity?.SetTag("email.smtp.disconnect", settings.Host); try { if (Status is EmailStatuses.Disconnected) return Result.Ok(); await client.DisconnectAsync(true, cancellationToken); activity?.SetStatus(ActivityStatusCode.Ok, "Disconnected"); Status = EmailStatuses.Disconnected; return Result.Ok(); } catch (Exception ex) { activity?.SetStatus(ActivityStatusCode.Error, ex.Message); activity?.AddException(ex); EmailTelemetry.EmailsFailed.Add(1, new TagList { { "exception", ex.GetType().Name } }); Status = EmailStatuses.GeneralError; return Result.Fail(new Error(ex.Message).CausedBy(ex)); } } public void Dispose() { client.Dispose(); GC.SuppressFinalize(this); } }