diff --git a/src/core/DigitalData.MessagingService.Application/OAuth2/Commands/CompleteOAuth2AuthorizationCommand.cs b/src/core/DigitalData.MessagingService.Application/OAuth2/Commands/CompleteOAuth2AuthorizationCommand.cs
new file mode 100644
index 0000000..f1ac0cb
--- /dev/null
+++ b/src/core/DigitalData.MessagingService.Application/OAuth2/Commands/CompleteOAuth2AuthorizationCommand.cs
@@ -0,0 +1,72 @@
+#if NET
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
+using DigitalData.MessagingService.Domain.Entities;
+using DigitalData.MessagingService.Domain.Exceptions;
+using MediatR;
+using Microsoft.Extensions.Logging;
+
+namespace DigitalData.MessagingService.Application.OAuth2.Commands;
+
+///
+/// Exchanges a Google OAuth2 authorization code for a refresh token
+/// and persists it on the email account.
+/// Call this from the OAuth2 callback endpoint after the user grants consent.
+///
+public record CompleteOAuth2AuthorizationCommand : IRequest
+{
+ ///
+ /// ID of the email account being authorized.
+ ///
+ public required int AccountId { get; init; }
+
+ ///
+ /// The authorization code received from the OAuth2 provider callback.
+ ///
+ public required string Code { get; init; }
+
+ ///
+ /// The redirect URI used in the original authorization request.
+ ///
+ public required string RedirectUri { get; init; }
+}
+
+public record CompleteOAuth2AuthorizationResult
+{
+ public required string Username { get; init; }
+ public required bool Success { get; init; }
+ public string? ErrorMessage { get; init; }
+}
+
+public class CompleteOAuth2AuthorizationCommandHandler(
+ IRepository Repo,
+ IOAuth2AuthorizationService AuthService,
+ ILogger Logger) : IRequestHandler
+{
+ public async Task Handle(CompleteOAuth2AuthorizationCommand request, CancellationToken cancellationToken)
+ {
+ var account = await Repo.GetByIdAsync(request.AccountId, cancellationToken)
+ ?? throw new NotFoundException($"No email account found with Id: {request.AccountId}.");
+
+ Logger.LogInformation("Exchanging OAuth2 authorization code for account '{Username}' (Id: {Id}).",
+ account.Username, account.Id);
+
+ var refreshToken = await AuthService.ExchangeCodeForRefreshTokenAsync(
+ account, request.Code, request.RedirectUri, cancellationToken);
+
+ // Set the refresh token directly on the tracked entity — no AutoMapper needed
+ account.OAuth2RefreshToken = refreshToken;
+ await Repo.UpdateSingleAsync(a => a.Id == account.Id, account, cancellationToken);
+
+ Logger.LogInformation("OAuth2 refresh token saved for account '{Username}' (Id: {Id}).",
+ account.Username, account.Id);
+
+ return new CompleteOAuth2AuthorizationResult
+ {
+ Username = account.Username,
+ Success = true
+ };
+ }
+}
+#endif
+
diff --git a/src/core/DigitalData.MessagingService.Application/OAuth2/Queries/GetOAuth2AuthorizationUrlQuery.cs b/src/core/DigitalData.MessagingService.Application/OAuth2/Queries/GetOAuth2AuthorizationUrlQuery.cs
new file mode 100644
index 0000000..c07d760
--- /dev/null
+++ b/src/core/DigitalData.MessagingService.Application/OAuth2/Queries/GetOAuth2AuthorizationUrlQuery.cs
@@ -0,0 +1,46 @@
+#if NET
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Application.Common.Interfaces.Repositories;
+using DigitalData.MessagingService.Domain.Entities;
+using DigitalData.MessagingService.Domain.Enums;
+using DigitalData.MessagingService.Domain.Exceptions;
+using MediatR;
+
+namespace DigitalData.MessagingService.Application.OAuth2.Queries;
+
+///
+/// Returns the authorization URL the user must visit to grant OAuth2 access.
+/// Supported for providers that require user-delegated access (e.g. Google).
+///
+public record GetOAuth2AuthorizationUrlQuery : IRequest
+{
+ ///
+ /// ID of the email account to authorize.
+ ///
+ public required int AccountId { get; init; }
+
+ ///
+ /// The redirect URI registered with the OAuth2 provider.
+ /// Must exactly match the URI configured in the provider's developer console.
+ ///
+ public required string RedirectUri { get; init; }
+}
+
+public class GetOAuth2AuthorizationUrlQueryHandler(
+ IRepository Repo,
+ IOAuth2AuthorizationService AuthService) : IRequestHandler
+{
+ public async Task Handle(GetOAuth2AuthorizationUrlQuery request, CancellationToken cancellationToken)
+ {
+ var account = await Repo.GetByIdAsync(request.AccountId, cancellationToken)
+ ?? throw new NotFoundException($"No email account found with Id: {request.AccountId}.");
+
+ if (account.OAuth2Provider == OAuth2Provider.None)
+ throw new BadRequestException(
+ $"Account '{account.Username}' (Id: {account.Id}) has no OAuth2 provider configured. " +
+ $"Set OAuth2Provider to a supported value (e.g. Google).");
+
+ return AuthService.GetAuthorizationUrl(account, request.RedirectUri);
+ }
+}
+#endif