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