using System.Text;
using DigitalData.MessagingService.Application.Common.Interfaces;
using DigitalData.MessagingService.Domain.Exceptions;
using Limilabs.Client.SMTP;
using Limilabs.Mail;
using Limilabs.Mail.Headers;
using DigitalData.MessagingService.Infrastructure.Services.Extensions;
using DigitalData.MessagingService.Application.Common.Dto;
namespace DigitalData.MessagingService.Infrastructure.Services;
///
/// Email service using Limilabs Mail.dll for SMTP operations (send-only).
/// Commercial-grade library with superior Exchange support.
/// SMTP configuration is injected via IOptions<EmailAccountsOptions> from appsettings.json.
/// Uses the first account in the list whose equals "default",
/// or falls back to the first account if none is named "default".
///
public class LimilabsEmailService() : IEmailService
{
// Register encoding provider for Limilabs (requires windows-1252 and other code pages)
static LimilabsEmailService()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
public async Task SendEmailAsync(EmailContext context, CancellationToken cancellationToken = default)
{
using var smtp = new Smtp();
ISendMessageResult? result = null;
try
{
await ConnectAndAuthenticateSmtpAsync(smtp, context.Sender);
var builder = new MailBuilder();
builder.From.Add(new MailBox(context.Sender.Username));
foreach (var recipient in context.Recipients)
builder.To.Add(new MailBox(recipient));
builder.Subject = context.Subject;
if (context.IsHtml)
builder.Html = context.Body;
else
builder.Text = context.Body;
AddAttachments(builder, context.Attachments);
var mail = builder.Create();
result = await smtp.SendMessageAsync(mail, cancellationToken);
if (result.Status != SendMessageStatus.Success)
{
throw new InvalidOperationException($"Failed to send email. Status: {result.Status}. {ErrorMessageBuilder(result)}");
}
await smtp.CloseAsync(cancellationToken);
}
catch (Limilabs.Client.ServerException ex)
{
await smtp.CloseSafelyAsync();
throw new AuthenticationFailedException($"SMTP authentication failed. Check credentials or OAuth2 configuration. {ErrorMessageBuilder(result)}", ex);
}
catch (Exception ex)
{
await smtp.CloseSafelyAsync();
throw new InvalidOperationException($"Failed to send email via SMTP server. {ErrorMessageBuilder(result)}", ex);
}
}
private static async Task ConnectAndAuthenticateSmtpAsync(Smtp smtp, EmailAccountDto smtpAccount)
{
if (smtpAccount.SmtpUseSsl)
{
await smtp.ConnectSSLAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort);
}
else
{
await smtp.ConnectAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort);
}
if (smtpAccount.UseOAuth2)
{
throw new NotSupportedException("OAuth2 is not configured for this SMTP account. UseOAuth2 must be false.");
}
else
{
await smtp.LoginAsync(smtpAccount.Username, smtpAccount.Password);
}
}
private static string ErrorMessageBuilder(ISendMessageResult? result = null)
{
if(result is null || result.GeneralErrors.Count == 0)
return string.Empty;
else if(result.GeneralErrors.Count == 1)
return $"Error: {result.GeneralErrors.FirstOrDefault()}";
var message = new StringBuilder("Errors:\n");
foreach (var error in result.GeneralErrors)
{
message.AppendLine($" • {error}");
}
return message.ToString();
}
private static void AddAttachments(MailBuilder builder, IEnumerable attachments)
{
foreach (var attachment in attachments)
{
if (attachment.IsInline)
{
var visual = builder.AddVisual(attachment.Content);
visual.FileName = attachment.FileName;
visual.ContentId = string.IsNullOrWhiteSpace(attachment.ContentId)
? attachment.FileName
: attachment.ContentId;
if (!string.IsNullOrWhiteSpace(attachment.ContentType))
visual.ContentType = ContentType.Parse(attachment.ContentType);
}
else
{
var part = builder.AddAttachment(attachment.Content);
part.FileName = attachment.FileName;
if (!string.IsNullOrWhiteSpace(attachment.ContentType))
part.ContentType = ContentType.Parse(attachment.ContentType);
if (!string.IsNullOrWhiteSpace(attachment.ContentId))
part.ContentId = attachment.ContentId;
}
}
}
}