193 lines
6.6 KiB
C#
193 lines
6.6 KiB
C#
using LiteCharms.Features.Email.Configuration;
|
|
using LiteCharms.Features.Email.Extensions;
|
|
using LiteCharms.Features.Email.Models;
|
|
using LiteCharms.Features.Shop;
|
|
|
|
namespace LiteCharms.Features.Email;
|
|
|
|
public class EmailService(IOptions<SmtpSettings> 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<Result<Response>> 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<Response>("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();
|
|
|
|
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>(response);
|
|
}
|
|
|
|
if (response.Contains("451"))
|
|
{
|
|
Status = EmailStatuses.ConnectionAborted;
|
|
|
|
return Result.Fail<Response>(response);
|
|
}
|
|
|
|
EmailTelemetry.EmailsFailed.Add(1, new TagList { { "error_message", response } });
|
|
|
|
Status = EmailStatuses.Disconnected;
|
|
|
|
return Result.Fail<Response>("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<Result<Response>> 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<Response>(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<Response>(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<Response>(new Error(ex.Message).CausedBy(ex));
|
|
}
|
|
}
|
|
|
|
public async ValueTask<Result> 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);
|
|
}
|
|
}
|