feat(infrastructure): add OAuth2 auth support and SendAndAppend capability to LimilabsEmailService and LimilabsImapEmailService
This commit is contained in:
@@ -12,14 +12,10 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="EmailAccount.Name"/> equals <c>"default"</c>,
|
||||
/// or falls back to the first account if none is named "default".
|
||||
/// Supports both plain/STARTTLS and OAuth2 (XOAUTH2) authentication.
|
||||
/// </summary>
|
||||
public class LimilabsEmailService() : IEmailService
|
||||
public class LimilabsEmailService(IOAuth2TokenService oauth2TokenService) : IEmailService
|
||||
{
|
||||
// Register encoding provider for Limilabs (requires windows-1252 and other code pages)
|
||||
static LimilabsEmailService()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
@@ -31,7 +27,7 @@ public class LimilabsEmailService() : IEmailService
|
||||
ISendMessageResult? result = null;
|
||||
try
|
||||
{
|
||||
await ConnectAndAuthenticateSmtpAsync(smtp, context.Sender);
|
||||
await ConnectAndAuthenticateSmtpAsync(smtp, context.Sender, cancellationToken);
|
||||
|
||||
var builder = new MailBuilder();
|
||||
builder.From.Add(new MailBox(context.Sender.Username));
|
||||
@@ -62,37 +58,43 @@ public class LimilabsEmailService() : IEmailService
|
||||
catch (Limilabs.Client.ServerException ex)
|
||||
{
|
||||
await smtp.CloseSafelyAsync();
|
||||
throw new AuthenticationFailedException($"SMTP authentication failed. Check credentials or OAuth2 configuration. {ErrorMessageBuilder(result)}", ex);
|
||||
throw new AuthenticationFailedException($"SMTP authentication failed. {ErrorMessageBuilder(result)}", ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception ex) when (ex is not AuthenticationFailedException && ex is not InvalidOperationException)
|
||||
{
|
||||
await smtp.CloseSafelyAsync();
|
||||
throw new InvalidOperationException($"Failed to send email via SMTP server. {ErrorMessageBuilder(result)}", ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await smtp.CloseSafelyAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ConnectAndAuthenticateSmtpAsync(Smtp smtp, EmailAccount smtpAccount)
|
||||
internal async Task ConnectAndAuthenticateSmtpAsync(Smtp smtp, EmailAccount smtpAccount, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (smtpAccount.SmtpUseSsl)
|
||||
{
|
||||
await smtp.ConnectSSLAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort);
|
||||
await smtp.ConnectSSLAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await smtp.ConnectAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort);
|
||||
await smtp.ConnectAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort, cancellationToken);
|
||||
}
|
||||
|
||||
if (smtpAccount.UseOAuth2)
|
||||
{
|
||||
throw new NotSupportedException("OAuth2 is not configured for this SMTP account. UseOAuth2 must be false.");
|
||||
var token = await oauth2TokenService.GetAccessTokenAsync(smtpAccount, cancellationToken);
|
||||
await smtp.LoginOAUTH2Async(smtpAccount.Username, token, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await smtp.LoginAsync(smtpAccount.Username, smtpAccount.Password);
|
||||
await smtp.LoginAsync(smtpAccount.Username, smtpAccount.Password, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ErrorMessageBuilder(ISendMessageResult? result = null)
|
||||
internal static string ErrorMessageBuilder(ISendMessageResult? result = null)
|
||||
{
|
||||
if(result is null || result.GeneralErrors.Count == 0)
|
||||
return string.Empty;
|
||||
@@ -109,7 +111,7 @@ public class LimilabsEmailService() : IEmailService
|
||||
return message.ToString();
|
||||
}
|
||||
|
||||
private static void AddAttachments(MailBuilder builder, IEnumerable<EmailAttachmentDto> attachments)
|
||||
internal static void AddAttachments(MailBuilder builder, IEnumerable<EmailAttachmentDto> attachments)
|
||||
{
|
||||
foreach (var attachment in attachments)
|
||||
{
|
||||
|
||||
@@ -14,8 +14,13 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
|
||||
/// <summary>
|
||||
/// IMAP email service using Limilabs Mail.dll.
|
||||
/// Opens a fresh connection per call — stateless and thread-safe.
|
||||
/// Supports both password and OAuth2 (XOAUTH2) authentication.
|
||||
/// </summary>
|
||||
public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger, IRepository<ReceivedEmail> Repository) : IImapEmailService
|
||||
public class LimilabsImapEmailService(
|
||||
ILogger<LimilabsImapEmailService> Logger,
|
||||
IRepository<ReceivedEmail> Repository,
|
||||
IOAuth2TokenService oauth2TokenService,
|
||||
LimilabsEmailService smtpService) : IImapEmailService
|
||||
{
|
||||
private static readonly string CacheKeyPrefix = Guid.NewGuid().ToString();
|
||||
|
||||
@@ -27,7 +32,7 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger,
|
||||
// Public API
|
||||
public async Task<EmailSyncResult> SyncEmailsAsync(EmailAccount account, string folder = "INBOX", CancellationToken cancel = default)
|
||||
{
|
||||
using var imap = await OpenAsync(account, folder, cancel);
|
||||
using var imap = await OpenAsync(account, folder, cancel: cancel);
|
||||
try
|
||||
{
|
||||
#region Find UIDs
|
||||
@@ -138,7 +143,7 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger,
|
||||
|
||||
public async Task MarkAsSeenAsync(EmailAccount account, long uid, string folder = "INBOX", CancellationToken cancel = default)
|
||||
{
|
||||
using var imap = await OpenAsync(account, folder, cancel);
|
||||
using var imap = await OpenAsync(account, folder, cancel: cancel);
|
||||
try
|
||||
{
|
||||
await imap.MarkMessageSeenByUIDAsync(uid, cancel);
|
||||
@@ -151,7 +156,48 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger,
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Imap> OpenAsync(EmailAccount account, string folder = "INBOX", CancellationToken cancel = default)
|
||||
public async Task SendAndAppendAsync(EmailContext context, string sentFolder = "Sent", CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Send via SMTP first
|
||||
await smtpService.SendEmailAsync(context, cancellationToken);
|
||||
|
||||
// Then upload a copy to the IMAP Sent folder (no need to SELECT first)
|
||||
using var imap = await OpenAsync(context.Sender, "INBOX", cancel: cancellationToken);
|
||||
try
|
||||
{
|
||||
var builder = new MailBuilder();
|
||||
builder.From.Add(new Limilabs.Mail.Headers.MailBox(context.Sender.Username));
|
||||
|
||||
foreach (var recipient in context.Recipients)
|
||||
builder.To.Add(new Limilabs.Mail.Headers.MailBox(recipient));
|
||||
|
||||
builder.Subject = context.Subject;
|
||||
|
||||
if (context.IsHtml)
|
||||
builder.Html = context.Body;
|
||||
else
|
||||
builder.Text = context.Body;
|
||||
|
||||
LimilabsEmailService.AddAttachments(builder, context.Attachments);
|
||||
|
||||
var mail = builder.Create();
|
||||
|
||||
var uploadInfo = new Limilabs.Client.IMAP.UploadMessageInfo
|
||||
{
|
||||
Flags = [Limilabs.Client.IMAP.Flag.Seen]
|
||||
};
|
||||
|
||||
await imap.UploadMessageAsync(sentFolder, mail, uploadInfo, cancellationToken);
|
||||
await imap.CloseAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await imap.CloseSafelyAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Imap> OpenAsync(EmailAccount account, string folder = "INBOX", bool createIfMissing = false, CancellationToken cancel = default)
|
||||
{
|
||||
var imap = new Imap();
|
||||
|
||||
@@ -160,12 +206,27 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger,
|
||||
else
|
||||
await imap.ConnectAsync(account.ImapServer!, account.ImapPort, cancel: cancel);
|
||||
|
||||
await imap.LoginAsync(account.Username, account.Password, cancel);
|
||||
if (account.UseOAuth2)
|
||||
{
|
||||
var token = await oauth2TokenService.GetAccessTokenAsync(account, cancel);
|
||||
await imap.LoginOAUTH2Async(account.Username, token, cancel);
|
||||
}
|
||||
else
|
||||
await imap.LoginAsync(account.Username, account.Password, cancel);
|
||||
|
||||
if (string.Equals(folder, "INBOX", StringComparison.OrdinalIgnoreCase))
|
||||
await imap.SelectInboxAsync(cancel);
|
||||
else
|
||||
{
|
||||
if (createIfMissing)
|
||||
{
|
||||
var folders = await imap.GetFoldersAsync(cancel);
|
||||
if (!folders.Any(f => string.Equals(f.Name, folder, StringComparison.OrdinalIgnoreCase)))
|
||||
await imap.CreateFolderAsync(folder, cancel);
|
||||
}
|
||||
|
||||
await imap.SelectAsync(folder, cancel);
|
||||
}
|
||||
|
||||
return imap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user