diff --git a/src/presentation/DigitalData.MessagingService.API/Controllers/OAuth2Controller.cs b/src/presentation/DigitalData.MessagingService.API/Controllers/OAuth2Controller.cs
new file mode 100644
index 0000000..13a9a7d
--- /dev/null
+++ b/src/presentation/DigitalData.MessagingService.API/Controllers/OAuth2Controller.cs
@@ -0,0 +1,95 @@
+using DigitalData.MessagingService.Application.OAuth2.Commands;
+using DigitalData.MessagingService.Application.OAuth2.Queries;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DigitalData.MessagingService.API.Controllers;
+
+///
+/// Manages the OAuth2 authorization code flow for email accounts.
+/// Use these endpoints to authorize Google accounts without manually
+/// obtaining refresh tokens via external tools.
+///
+[ApiController]
+[Route("api/[controller]")]
+public class OAuth2Controller(IMediator mediator, IHttpContextAccessor httpContextAccessor) : ControllerBase
+{
+ #region Google Authorization Flow
+
+ ///
+ /// Step 1: Redirects the user to Google's consent screen for the specified email account.
+ /// After consent, Google redirects to /api/oauth2/google/callback with an authorization code.
+ ///
+ /// ID of the email account to authorize.
+ /// Cancellation token.
+ /// HTTP 302 redirect to Google consent screen.
+ [HttpGet("google/authorize/{accountId:int}")]
+ [ProducesResponseType(StatusCodes.Status302Found)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task AuthorizeGoogle(int accountId, CancellationToken cancellationToken)
+ {
+ var redirectUri = BuildCallbackUri();
+
+ var authUrl = await mediator.Send(new GetOAuth2AuthorizationUrlQuery
+ {
+ AccountId = accountId,
+ RedirectUri = redirectUri
+ }, cancellationToken);
+
+ return Redirect(authUrl);
+ }
+
+ ///
+ /// Step 2: Google callback endpoint. Exchanges the authorization code for a refresh token
+ /// and saves it to the email account. This endpoint is called automatically by Google
+ /// after the user grants consent — do not call it directly.
+ ///
+ /// Authorization code provided by Google.
+ /// Account ID passed as state in the authorization request.
+ /// Error message if the user denied access.
+ /// Cancellation token.
+ /// HTTP 200 on success, HTTP 400 if access was denied.
+ [HttpGet("google/callback")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task GoogleCallback(
+ [FromQuery] string? code,
+ [FromQuery] string? state,
+ [FromQuery] string? error,
+ CancellationToken cancellationToken)
+ {
+ if (!string.IsNullOrWhiteSpace(error))
+ return BadRequest(new { Error = error, Message = "User denied access or an error occurred during Google OAuth2 authorization." });
+
+ if (string.IsNullOrWhiteSpace(code))
+ return BadRequest(new { Error = "missing_code", Message = "Authorization code not received from Google." });
+
+ if (!int.TryParse(state, out var accountId))
+ return BadRequest(new { Error = "invalid_state", Message = "Invalid state parameter — could not determine account ID." });
+
+ var redirectUri = BuildCallbackUri();
+
+ var result = await mediator.Send(new CompleteOAuth2AuthorizationCommand
+ {
+ AccountId = accountId,
+ Code = code,
+ RedirectUri = redirectUri
+ }, cancellationToken);
+
+ return Ok(new
+ {
+ result.Success,
+ result.Username,
+ Message = $"Google OAuth2 authorization completed. Refresh token saved for account '{result.Username}'."
+ });
+ }
+
+ #endregion
+
+ private string BuildCallbackUri()
+ {
+ var request = httpContextAccessor.HttpContext!.Request;
+ return $"{request.Scheme}://{request.Host}/api/oauth2/google/callback";
+ }
+}
diff --git a/src/presentation/DigitalData.MessagingService.API/Controllers/SyncController.cs b/src/presentation/DigitalData.MessagingService.API/Controllers/SyncController.cs
new file mode 100644
index 0000000..c5e59be
--- /dev/null
+++ b/src/presentation/DigitalData.MessagingService.API/Controllers/SyncController.cs
@@ -0,0 +1,35 @@
+using DigitalData.MessagingService.Infrastructure.Services.Background;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Hosting;
+
+namespace DigitalData.MessagingService.API.Controllers;
+
+///
+/// Controls email synchronization operations.
+///
+[ApiController]
+[Route("api/[controller]")]
+public class SyncController(IEnumerable hostedServices) : ControllerBase
+{
+ ///
+ /// Triggers an immediate email sync cycle for all configured accounts,
+ /// skipping the remaining interval wait.
+ /// If a sync is already in progress, the next cycle will start immediately after it completes.
+ ///
+ /// HTTP 202 Accepted.
+ [HttpPost("trigger")]
+ [ProducesResponseType(StatusCodes.Status202Accepted)]
+ [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
+ public IActionResult TriggerSync()
+ {
+ var syncWorker = hostedServices.OfType().FirstOrDefault();
+
+ if (syncWorker is null)
+ return StatusCode(StatusCodes.Status503ServiceUnavailable,
+ new { Message = "EmailSyncWorker is not running." });
+
+ syncWorker.TriggerSync();
+
+ return Accepted(new { Message = "Sync triggered. The next cycle will start immediately." });
+ }
+}
diff --git a/src/presentation/DigitalData.MessagingService.API/Program.cs b/src/presentation/DigitalData.MessagingService.API/Program.cs
index e59f69c..2e37be9 100644
--- a/src/presentation/DigitalData.MessagingService.API/Program.cs
+++ b/src/presentation/DigitalData.MessagingService.API/Program.cs
@@ -71,6 +71,9 @@ try
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
+ // Required by OAuth2Controller to build callback URIs
+ builder.Services.AddHttpContextAccessor();
+
// Register Serilog.UI with SQLite provider for web log viewer
builder.Services.AddSerilogUi(options =>
{