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>
|
/// <summary>
|
||||||
/// Email service using Limilabs Mail.dll for SMTP operations (send-only).
|
/// Email service using Limilabs Mail.dll for SMTP operations (send-only).
|
||||||
/// Commercial-grade library with superior Exchange support.
|
/// Supports both plain/STARTTLS and OAuth2 (XOAUTH2) authentication.
|
||||||
/// 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".
|
|
||||||
/// </summary>
|
/// </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()
|
static LimilabsEmailService()
|
||||||
{
|
{
|
||||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
@@ -31,7 +27,7 @@ public class LimilabsEmailService() : IEmailService
|
|||||||
ISendMessageResult? result = null;
|
ISendMessageResult? result = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ConnectAndAuthenticateSmtpAsync(smtp, context.Sender);
|
await ConnectAndAuthenticateSmtpAsync(smtp, context.Sender, cancellationToken);
|
||||||
|
|
||||||
var builder = new MailBuilder();
|
var builder = new MailBuilder();
|
||||||
builder.From.Add(new MailBox(context.Sender.Username));
|
builder.From.Add(new MailBox(context.Sender.Username));
|
||||||
@@ -62,37 +58,43 @@ public class LimilabsEmailService() : IEmailService
|
|||||||
catch (Limilabs.Client.ServerException ex)
|
catch (Limilabs.Client.ServerException ex)
|
||||||
{
|
{
|
||||||
await smtp.CloseSafelyAsync();
|
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();
|
await smtp.CloseSafelyAsync();
|
||||||
throw new InvalidOperationException($"Failed to send email via SMTP server. {ErrorMessageBuilder(result)}", ex);
|
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)
|
if (smtpAccount.SmtpUseSsl)
|
||||||
{
|
{
|
||||||
await smtp.ConnectSSLAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort);
|
await smtp.ConnectSSLAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort, cancellationToken);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await smtp.ConnectAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort);
|
await smtp.ConnectAsync(smtpAccount.SmtpServer, smtpAccount.SmtpPort, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (smtpAccount.UseOAuth2)
|
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
|
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)
|
if(result is null || result.GeneralErrors.Count == 0)
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
@@ -109,7 +111,7 @@ public class LimilabsEmailService() : IEmailService
|
|||||||
return message.ToString();
|
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)
|
foreach (var attachment in attachments)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,8 +14,13 @@ namespace DigitalData.MessagingService.Infrastructure.Services;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// IMAP email service using Limilabs Mail.dll.
|
/// IMAP email service using Limilabs Mail.dll.
|
||||||
/// Opens a fresh connection per call — stateless and thread-safe.
|
/// Opens a fresh connection per call — stateless and thread-safe.
|
||||||
|
/// Supports both password and OAuth2 (XOAUTH2) authentication.
|
||||||
/// </summary>
|
/// </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();
|
private static readonly string CacheKeyPrefix = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
@@ -27,7 +32,7 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger,
|
|||||||
// Public API
|
// Public API
|
||||||
public async Task<EmailSyncResult> SyncEmailsAsync(EmailAccount account, string folder = "INBOX", CancellationToken cancel = default)
|
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
|
try
|
||||||
{
|
{
|
||||||
#region Find UIDs
|
#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)
|
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
|
try
|
||||||
{
|
{
|
||||||
await imap.MarkMessageSeenByUIDAsync(uid, cancel);
|
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();
|
var imap = new Imap();
|
||||||
|
|
||||||
@@ -160,12 +206,27 @@ public class LimilabsImapEmailService(ILogger<LimilabsImapEmailService> Logger,
|
|||||||
else
|
else
|
||||||
await imap.ConnectAsync(account.ImapServer!, account.ImapPort, cancel: cancel);
|
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))
|
if (string.Equals(folder, "INBOX", StringComparison.OrdinalIgnoreCase))
|
||||||
await imap.SelectInboxAsync(cancel);
|
await imap.SelectInboxAsync(cancel);
|
||||||
else
|
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);
|
await imap.SelectAsync(folder, cancel);
|
||||||
|
}
|
||||||
|
|
||||||
return imap;
|
return imap;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user