diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaOAuth2Query.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaOAuth2Query.cs
new file mode 100644
index 0000000..51bf42c
--- /dev/null
+++ b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaOAuth2Query.cs
@@ -0,0 +1,77 @@
+#if NET
+using AutoMapper;
+using DigitalData.MessagingService.Application.Common.Dto;
+using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
+using DigitalData.MessagingService.Application.EmailAccounts.Queries;
+using DigitalData.MessagingService.Application.EmailReceiving.Queries;
+using DigitalData.MessagingService.Domain.Entities;
+using DigitalData.MessagingService.Domain.Exceptions;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace DigitalData.MessagingService.Application.EmailReceiving.Queries;
+
+///
+/// Query to fetch emails from an IMAP mailbox using OAuth2 authentication.
+/// The account must have UseOAuth2 = true and valid OAuth2 credentials configured.
+///
+public record ReadEmailViaOAuth2Query : IRequest
+{
+ ///
+ /// Identifies the email account to use.
+ ///
+ public required GetEmailAccountQuery Account { get; init; }
+
+ ///
+ /// Mail query used to filter and limit the emails retrieved.
+ ///
+ public MailSearchFilter Mail { get; init; } = new();
+}
+
+public class ReadEmailViaOAuth2QueryHandler(
+ IMapper Mapper,
+ ILogger Logger,
+ IRepository EmailAccountRepo,
+ IReceivedEmailRepository MailRepo,
+ IImapEmailService imapEmailService) : IRequestHandler
+{
+ public async Task Handle(ReadEmailViaOAuth2Query request, CancellationToken cancellationToken)
+ {
+ var accounts = await EmailAccountRepo.FindAsync(
+ request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username,
+ cancellationToken: cancellationToken);
+
+ if (accounts.Count() > 1)
+ Logger.LogWarning("Multiple email accounts found ({Criteria}). Using first.",
+ request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}");
+
+ var account = accounts.FirstOrDefault()
+ ?? throw new NotFoundException($"No email account found (Id: {request.Account.Id}, Username: {request.Account.Username}).");
+
+ if (!account.UseOAuth2)
+ throw new BadRequestException(
+ $"Account '{account.Username}' (Id: {account.Id}) is not configured for OAuth2. Set UseOAuth2 = true.");
+
+ if (string.IsNullOrWhiteSpace(account.OAuth2ClientId) ||
+ string.IsNullOrWhiteSpace(account.OAuth2ClientSecret))
+ throw new BadRequestException(
+ $"OAuth2 credentials (ClientId, ClientSecret) are not configured for account '{account.Username}'.");
+
+ if (string.IsNullOrWhiteSpace(account.ImapServer))
+ throw new BadRequestException(
+ $"IMAP is not configured for account '{account.Username}' (Id: {account.Id}). Set ImapServer in EmailAccounts configuration.");
+
+ var mails = await MailRepo.FindAsync(request.Mail, account, cancellationToken);
+
+ var lastSync = imapEmailService.GetLastImapSyncDate(account.Id, request.Mail.Folder);
+
+ return new ReadEmailQueryResponse
+ {
+ LastSync = lastSync,
+ Emails = Mapper.Map>(mails)
+ };
+ }
+}
+#endif
diff --git a/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaPop3Query.cs b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaPop3Query.cs
new file mode 100644
index 0000000..0f3bee0
--- /dev/null
+++ b/src/core/DigitalData.MessagingService.Application/EmailReceiving/Queries/ReadEmailViaPop3Query.cs
@@ -0,0 +1,74 @@
+#if NET
+using AutoMapper;
+using DigitalData.MessagingService.Application.Common.Dto;
+using DigitalData.MessagingService.Application.Common.Dto.MailSearch;
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
+using DigitalData.MessagingService.Application.EmailAccounts.Queries;
+using DigitalData.MessagingService.Application.EmailReceiving.Queries;
+using DigitalData.MessagingService.Domain.Entities;
+using DigitalData.MessagingService.Domain.Exceptions;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace DigitalData.MessagingService.Application.EmailReceiving.Queries;
+
+///
+/// Query to fetch emails from a POP3 mailbox.
+/// Triggers an on-demand sync and returns stored results filtered by .
+///
+public record ReadEmailViaPop3Query : IRequest
+{
+ ///
+ /// Identifies the email account to use.
+ ///
+ public required GetEmailAccountQuery Account { get; init; }
+
+ ///
+ /// Mail query used to filter and limit the emails retrieved from local storage.
+ /// Note: POP3 has no folder concept — all messages are stored under "INBOX".
+ ///
+ public MailSearchFilter Mail { get; init; } = new();
+}
+
+public class ReadEmailViaPop3QueryHandler(
+ IMapper Mapper,
+ ILogger Logger,
+ IRepository EmailAccountRepo,
+ IReceivedEmailRepository MailRepo,
+ IPop3EmailService pop3EmailService) : IRequestHandler
+{
+ public async Task Handle(ReadEmailViaPop3Query request, CancellationToken cancellationToken)
+ {
+ var accounts = await EmailAccountRepo.FindAsync(
+ request.Account.Id is int id ? x => x.Id == id : x => x.Username == request.Account.Username,
+ cancellationToken: cancellationToken);
+
+ if (accounts.Count() > 1)
+ Logger.LogWarning("Multiple email accounts found ({Criteria}). Using first.",
+ request.Account.Id is not null ? $"Id: {request.Account.Id}" : $"Username: {request.Account.Username}");
+
+ var account = accounts.FirstOrDefault()
+ ?? throw new NotFoundException($"No email account found (Id: {request.Account.Id}, Username: {request.Account.Username}).");
+
+ if (string.IsNullOrWhiteSpace(account.Pop3Server))
+ throw new BadRequestException(
+ $"POP3 is not configured for account '{account.Username}' (Id: {account.Id}). Set Pop3Server in EmailAccounts configuration.");
+
+ // Trigger on-demand POP3 sync before querying local storage
+ await pop3EmailService.SyncEmailsAsync(account, cancellationToken);
+
+ // POP3 has no folder concept — always query INBOX
+ var filter = request.Mail with { Folder = "INBOX" };
+ var mails = await MailRepo.FindAsync(filter, account, cancellationToken);
+
+ var lastSync = pop3EmailService.GetLastPop3SyncDate(account.Id);
+
+ return new ReadEmailQueryResponse
+ {
+ LastSync = lastSync,
+ Emails = Mapper.Map>(mails)
+ };
+ }
+}
+#endif
diff --git a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailViaImapCommand.cs b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailViaImapCommand.cs
new file mode 100644
index 0000000..8d5e391
--- /dev/null
+++ b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailViaImapCommand.cs
@@ -0,0 +1,81 @@
+#if NET
+using AutoMapper;
+using DigitalData.MessagingService.Application.Common.Dto;
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
+using DigitalData.MessagingService.Application.EmailAccounts.Queries;
+using DigitalData.MessagingService.Domain.Entities;
+using DigitalData.MessagingService.Domain.Exceptions;
+using MediatR;
+using Microsoft.Extensions.Logging;
+using System.Text.Json.Serialization;
+
+namespace DigitalData.MessagingService.Application.EmailSending.Commands;
+
+///
+/// Command to send an email using IMAP account credentials (queued via RabbitMQ).
+/// After processing, the sent message is appended to the IMAP Sent Items folder.
+///
+public record PublishEmailViaImapCommand : IRequest
+{
+ public required GetEmailAccountQuery Sender { get; init; }
+
+ public required IEnumerable Recipients { get; init; }
+
+ public required string Subject { get; init; }
+
+ public required string Body { get; init; }
+
+ public bool IsHtml { get; init; } = true;
+
+ ///
+ /// IMAP folder to which the sent message will be appended (default: "Sent").
+ ///
+ public string SentFolder { get; init; } = "Sent";
+
+ [JsonIgnore]
+ internal IEnumerable Attachments { get; private init; } = [];
+
+ public PublishEmailViaImapCommand WithAttachments(IEnumerable attachments)
+ => this with { Attachments = attachments };
+}
+
+public class PublishEmailViaImapCommandHandler(
+ IRepository Repo,
+ ISendingEmailPublisher Publisher,
+ IMapper Mapper,
+ ILogger Logger) : IRequestHandler
+{
+ public async Task Handle(PublishEmailViaImapCommand request, CancellationToken cancellationToken)
+ {
+ var senderAccounts = await Repo.FindAsync(
+ request.Sender.Id is int id ? x => x.Id == id : x => x.Username == request.Sender.Username,
+ cancellationToken: cancellationToken);
+
+ if (senderAccounts.Count() > 1)
+ Logger.LogWarning("Multiple email accounts found ({Criteria}). Using first.",
+ request.Sender.Id is not null ? $"Id: {request.Sender.Id}" : $"Username: {request.Sender.Username}");
+
+ var senderAccount = senderAccounts.FirstOrDefault()
+ ?? throw new NotFoundException($"No email account found (Id: {request.Sender.Id}, Username: {request.Sender.Username}).");
+
+ if (string.IsNullOrWhiteSpace(senderAccount.ImapServer))
+ throw new BadRequestException(
+ $"IMAP is not configured for account '{senderAccount.Username}' (Id: {senderAccount.Id}). Set ImapServer in EmailAccounts configuration.");
+
+ var emailContext = Mapper.Map(request) with { Sender = senderAccount };
+
+ var sendingEmailEvent = new SendingEmailEvent
+ {
+ Id = Guid.NewGuid(),
+ Mail = emailContext,
+ QueuedAt = DateTime.Now,
+ SentFolder = request.SentFolder,
+ UseImapAppend = true
+ };
+
+ await Publisher.EnqueueAsync(sendingEmailEvent, cancellationToken);
+ return sendingEmailEvent.Id;
+ }
+}
+#endif
diff --git a/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailViaOAuth2Command.cs b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailViaOAuth2Command.cs
new file mode 100644
index 0000000..4804dcd
--- /dev/null
+++ b/src/core/DigitalData.MessagingService.Application/EmailSending/Commands/PublishEmailViaOAuth2Command.cs
@@ -0,0 +1,79 @@
+#if NET
+using AutoMapper;
+using DigitalData.MessagingService.Application.Common.Dto;
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
+using DigitalData.MessagingService.Application.EmailAccounts.Queries;
+using DigitalData.MessagingService.Domain.Entities;
+using DigitalData.MessagingService.Domain.Exceptions;
+using MediatR;
+using Microsoft.Extensions.Logging;
+using System.Text.Json.Serialization;
+
+namespace DigitalData.MessagingService.Application.EmailSending.Commands;
+
+///
+/// Command to send an email via SMTP using OAuth2 authentication (queued via RabbitMQ).
+/// The account must have UseOAuth2 = true and valid OAuth2 credentials configured.
+///
+public record PublishEmailViaOAuth2Command : IRequest
+{
+ public required GetEmailAccountQuery Sender { get; init; }
+
+ public required IEnumerable Recipients { get; init; }
+
+ public required string Subject { get; init; }
+
+ public required string Body { get; init; }
+
+ public bool IsHtml { get; init; } = true;
+
+ [JsonIgnore]
+ internal IEnumerable Attachments { get; private init; } = [];
+
+ public PublishEmailViaOAuth2Command WithAttachments(IEnumerable attachments)
+ => this with { Attachments = attachments };
+}
+
+public class PublishEmailViaOAuth2CommandHandler(
+ IRepository Repo,
+ ISendingEmailPublisher Publisher,
+ IMapper Mapper,
+ ILogger Logger) : IRequestHandler
+{
+ public async Task Handle(PublishEmailViaOAuth2Command request, CancellationToken cancellationToken)
+ {
+ var senderAccounts = await Repo.FindAsync(
+ request.Sender.Id is int id ? x => x.Id == id : x => x.Username == request.Sender.Username,
+ cancellationToken: cancellationToken);
+
+ if (senderAccounts.Count() > 1)
+ Logger.LogWarning("Multiple email accounts found ({Criteria}). Using first.",
+ request.Sender.Id is not null ? $"Id: {request.Sender.Id}" : $"Username: {request.Sender.Username}");
+
+ var senderAccount = senderAccounts.FirstOrDefault()
+ ?? throw new NotFoundException($"No email account found (Id: {request.Sender.Id}, Username: {request.Sender.Username}).");
+
+ if (!senderAccount.UseOAuth2)
+ throw new BadRequestException(
+ $"Account '{senderAccount.Username}' (Id: {senderAccount.Id}) is not configured for OAuth2. Set UseOAuth2 = true.");
+
+ if (string.IsNullOrWhiteSpace(senderAccount.OAuth2ClientId) ||
+ string.IsNullOrWhiteSpace(senderAccount.OAuth2ClientSecret))
+ throw new BadRequestException(
+ $"OAuth2 credentials (ClientId, ClientSecret) are not configured for account '{senderAccount.Username}'.");
+
+ var emailContext = Mapper.Map(request) with { Sender = senderAccount };
+
+ var sendingEmailEvent = new SendingEmailEvent
+ {
+ Id = Guid.NewGuid(),
+ Mail = emailContext,
+ QueuedAt = DateTime.Now
+ };
+
+ await Publisher.EnqueueAsync(sendingEmailEvent, cancellationToken);
+ return sendingEmailEvent.Id;
+ }
+}
+#endif