diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsPop3EmailService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsPop3EmailService.cs
new file mode 100644
index 0000000..8a796c1
--- /dev/null
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/LimilabsPop3EmailService.cs
@@ -0,0 +1,166 @@
+using DigitalData.MessagingService.Application.Common.Dto;
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
+using DigitalData.MessagingService.Domain.Entities;
+using Limilabs.Client.POP3;
+using Limilabs.Mail;
+using Microsoft.Extensions.Logging;
+using System.Collections.Concurrent;
+using System.Text;
+
+namespace DigitalData.MessagingService.Infrastructure.Services;
+
+///
+/// POP3 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 LimilabsPop3EmailService(
+ ILogger Logger,
+ IRepository Repository,
+ IOAuth2TokenService oauth2TokenService) : IPop3EmailService
+{
+ private const string Pop3Folder = "INBOX";
+
+ static LimilabsPop3EmailService()
+ {
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+ }
+
+ public async Task SyncEmailsAsync(EmailAccount account, CancellationToken cancellationToken = default)
+ {
+ using var pop3 = await OpenAsync(account, cancellationToken);
+
+ try
+ {
+ // POP3 uses string UIDs (UIDL command)
+ var uidMap = await pop3.GetUIDAsync(cancellationToken);
+
+ if (uidMap.Count == 0)
+ {
+ await pop3.CloseAsync(false, cancellationToken);
+ return new EmailSyncResult();
+ }
+
+ var emails = new List(uidMap.Count);
+ var failedCount = 0;
+
+ foreach (var kvp in uidMap)
+ {
+ // kvp.Key = message number (long), kvp.Value = POP3 UID (string)
+ var msgNumber = kvp.Key;
+ var pop3Uid = kvp.Value;
+
+ // Use a stable numeric hash of the string UID for storage (ReceivedEmail.Uid is long)
+ var numericUid = (long)Math.Abs((uint)pop3Uid.GetHashCode());
+
+ if (await Repository.AnyAsync(x => x.Uid == numericUid && x.AccountId == account.Id, cancellationToken))
+ continue;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ try
+ {
+ var eml = await pop3.GetMessageByNumberAsync(msgNumber, cancellationToken);
+ var mail = new MailBuilder().CreateFromEml(eml);
+
+ var attachments = new List();
+
+ foreach (var att in mail.Attachments)
+ {
+ attachments.Add(new EmailAttachmentDto
+ {
+ FileName = att.FileName ?? "attachment",
+ Content = att.Data,
+ ContentType = att.ContentType?.ToString() ?? "application/octet-stream",
+ IsInline = false,
+ ContentId = att.ContentId
+ });
+ }
+
+ foreach (var vis in mail.Visuals)
+ {
+ attachments.Add(new EmailAttachmentDto
+ {
+ FileName = vis.FileName ?? "inline",
+ Content = vis.Data,
+ ContentType = vis.ContentType?.ToString() ?? "application/octet-stream",
+ IsInline = true,
+ ContentId = vis.ContentId
+ });
+ }
+
+ var email = new ReceivedEmailDto
+ {
+ Uid = numericUid,
+ AccountId = account.Id,
+ From = mail.From.FirstOrDefault()?.Address ?? string.Empty,
+ To = [.. mail.To.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)],
+ Cc = [.. mail.Cc.SelectMany(m => m.GetMailboxes()).Select(mb => mb.Address)],
+ Subject = mail.Subject ?? string.Empty,
+ TextBody = mail.Text ?? string.Empty,
+ HtmlBody = mail.Html ?? string.Empty,
+ Date = mail.Date ?? DateTime.MinValue,
+ IsSeen = false, // POP3 has no seen/unseen flags
+ Attachments = attachments,
+ Folder = Pop3Folder
+ };
+
+ emails.Add(email);
+ SetLastPop3SyncDate(account.Id, DateTime.UtcNow);
+ }
+ catch (Exception ex)
+ {
+ failedCount += 1;
+ Logger.LogWarning(ex,
+ "Failed to fetch POP3 message number={Number} for account {Username}. Skipping.",
+ msgNumber, account.Username);
+ }
+ }
+
+ // Close without deleting messages (leaveOnServer = false means do not delete = leave on server)
+ await pop3.CloseAsync(false, cancellationToken);
+
+ await Repository.CreateRangeAsync(emails, cancellationToken);
+
+ return new EmailSyncResult(ProcessedCount: emails.Count, FailedCount: failedCount);
+ }
+ catch
+ {
+ try { await pop3.CloseAsync(false, cancellationToken); } catch { /* ignore */ }
+ throw;
+ }
+ }
+
+ private async Task OpenAsync(EmailAccount account, CancellationToken cancellationToken)
+ {
+ var pop3 = new Pop3();
+
+ if (account.Pop3UseSsl)
+ await pop3.ConnectSSLAsync(account.Pop3Server!, cancellationToken);
+ else
+ await pop3.ConnectAsync(account.Pop3Server!, cancellationToken);
+
+ if (account.UseOAuth2)
+ {
+ var token = await oauth2TokenService.GetAccessTokenAsync(account, cancellationToken);
+ await pop3.LoginOAUTH2Async(account.Username, token, cancellationToken);
+ }
+ else
+ {
+ await pop3.LoginAsync(account.Username, account.Password, cancellationToken);
+ }
+
+ return pop3;
+ }
+
+ #region POP3 Last Sync Date Cache
+ private readonly ConcurrentDictionary _cache = new();
+
+ public DateTime? GetLastPop3SyncDate(int accountId)
+ => _cache.GetValueOrDefault(accountId);
+
+ private void SetLastPop3SyncDate(int accountId, DateTime date)
+ => _cache[accountId] = date;
+ #endregion
+}
diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/MicrosoftOAuth2TokenService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/MicrosoftOAuth2TokenService.cs
new file mode 100644
index 0000000..bd9cf0d
--- /dev/null
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/MicrosoftOAuth2TokenService.cs
@@ -0,0 +1,52 @@
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Domain.Entities;
+using Microsoft.Identity.Client;
+using Microsoft.Extensions.Logging;
+using System.Collections.Concurrent;
+
+namespace DigitalData.MessagingService.Infrastructure.Services;
+
+///
+/// Acquires OAuth2 access tokens using Microsoft Identity (MSAL) with the client credentials flow.
+/// Supports Microsoft 365 / Exchange Online accounts.
+/// Tokens are cached in-memory and reused until 5 minutes before expiry.
+///
+public class MicrosoftOAuth2TokenService(ILogger Logger) : IOAuth2TokenService
+{
+ private static readonly string[] Scopes = ["https://outlook.office365.com/.default"];
+
+ private readonly ConcurrentDictionary _cache = new();
+
+ public async Task GetAccessTokenAsync(EmailAccount account, CancellationToken cancellationToken = default)
+ {
+ if (_cache.TryGetValue(account.Id, out var cached) && cached.Expiry > DateTimeOffset.UtcNow.AddMinutes(5))
+ {
+ Logger.LogDebug("Returning cached OAuth2 token for account {Username} (Id: {Id}).", account.Username, account.Id);
+ return cached.Token;
+ }
+
+ if (string.IsNullOrWhiteSpace(account.OAuth2ClientId))
+ throw new InvalidOperationException($"OAuth2ClientId is not configured for account '{account.Username}' (Id: {account.Id}).");
+
+ if (string.IsNullOrWhiteSpace(account.OAuth2ClientSecret))
+ throw new InvalidOperationException($"OAuth2ClientSecret is not configured for account '{account.Username}' (Id: {account.Id}).");
+
+ var tenantId = string.IsNullOrWhiteSpace(account.OAuth2TenantId) ? "common" : account.OAuth2TenantId;
+
+ var app = ConfidentialClientApplicationBuilder
+ .Create(account.OAuth2ClientId)
+ .WithClientSecret(account.OAuth2ClientSecret)
+ .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
+ .Build();
+
+ Logger.LogDebug("Acquiring new OAuth2 token for account {Username} (Id: {Id}) from tenant {Tenant}.",
+ account.Username, account.Id, tenantId);
+
+ var result = await app.AcquireTokenForClient(Scopes)
+ .ExecuteAsync(cancellationToken);
+
+ _cache[account.Id] = (result.AccessToken, result.ExpiresOn);
+
+ return result.AccessToken;
+ }
+}