diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs
index 05971a1..7192b5c 100644
--- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsEmailService.cs
@@ -12,14 +12,10 @@ 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".
+/// Supports both plain/STARTTLS and OAuth2 (XOAUTH2) authentication.
///
-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 attachments)
+ internal static void AddAttachments(MailBuilder builder, IEnumerable attachments)
{
foreach (var attachment in attachments)
{
diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
index a9a6f55..e6b3769 100644
--- a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsImapEmailService.cs
@@ -14,8 +14,13 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
///
/// IMAP email service using Limilabs Mail.dll.
/// Opens a fresh connection per call — stateless and thread-safe.
+/// Supports both password and OAuth2 (XOAUTH2) authentication.
///
-public class LimilabsImapEmailService(ILogger Logger, IRepository Repository) : IImapEmailService
+public class LimilabsImapEmailService(
+ ILogger Logger,
+ IRepository Repository,
+ IOAuth2TokenService oauth2TokenService,
+ LimilabsEmailService smtpService) : IImapEmailService
{
private static readonly string CacheKeyPrefix = Guid.NewGuid().ToString();
@@ -27,7 +32,7 @@ public class LimilabsImapEmailService(ILogger Logger,
// Public API
public async Task 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 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 Logger,
}
}
- private static async Task 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 OpenAsync(EmailAccount account, string folder = "INBOX", bool createIfMissing = false, CancellationToken cancel = default)
{
var imap = new Imap();
@@ -160,12 +206,27 @@ public class LimilabsImapEmailService(ILogger 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;
}