diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/GoogleOAuth2AuthorizationService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/GoogleOAuth2AuthorizationService.cs
new file mode 100644
index 0000000..ca58c02
--- /dev/null
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/GoogleOAuth2AuthorizationService.cs
@@ -0,0 +1,119 @@
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Domain.Entities;
+using DigitalData.MessagingService.Domain.Enums;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Text.Json.Serialization;
+using System.Web;
+
+namespace DigitalData.MessagingService.Infrastructure.Services;
+
+///
+/// Implements the Google OAuth2 authorization code flow.
+/// Generates consent URLs and exchanges authorization codes for refresh tokens.
+/// This is a one-time setup operation per account — the resulting refresh token
+/// is stored in and reused by
+/// for all subsequent token acquisitions.
+///
+public class GoogleOAuth2AuthorizationService(
+ ILogger Logger,
+ IHttpClientFactory HttpClientFactory) : IOAuth2AuthorizationService
+{
+ private const string AuthEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
+ private const string TokenEndpoint = "https://oauth2.googleapis.com/token";
+ private const string Scope = "https://mail.google.com/";
+
+ public string GetAuthorizationUrl(EmailAccount account, string redirectUri)
+ {
+ if (account.OAuth2Provider != OAuth2Provider.Google)
+ throw new InvalidOperationException(
+ $"GoogleOAuth2AuthorizationService cannot handle provider '{account.OAuth2Provider}'. Expected '{OAuth2Provider.Google}'.");
+
+ if (string.IsNullOrWhiteSpace(account.OAuth2ClientId))
+ throw new InvalidOperationException($"OAuth2ClientId is not configured for account '{account.Username}' (Id: {account.Id}).");
+
+ var query = HttpUtility.ParseQueryString(string.Empty);
+ query["client_id"] = account.OAuth2ClientId;
+ query["redirect_uri"] = redirectUri;
+ query["response_type"] = "code";
+ query["scope"] = Scope;
+ query["access_type"] = "offline"; // ensures refresh_token is returned
+ query["prompt"] = "consent"; // forces refresh_token even if already authorized
+ query["state"] = account.Id.ToString();
+
+ var url = $"{AuthEndpoint}?{query}";
+
+ Logger.LogDebug("Generated Google OAuth2 authorization URL for account '{Username}' (Id: {Id}).",
+ account.Username, account.Id);
+
+ return url;
+ }
+
+ public async Task ExchangeCodeForRefreshTokenAsync(
+ EmailAccount account,
+ string code,
+ string redirectUri,
+ CancellationToken cancellationToken = default)
+ {
+ if (account.OAuth2Provider != OAuth2Provider.Google)
+ throw new InvalidOperationException(
+ $"GoogleOAuth2AuthorizationService cannot handle provider '{account.OAuth2Provider}'. Expected '{OAuth2Provider.Google}'.");
+
+ 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}).");
+
+ Logger.LogDebug("Exchanging authorization code for refresh token. Account='{Username}' (Id: {Id}).",
+ account.Username, account.Id);
+
+ var httpClient = HttpClientFactory.CreateClient(nameof(GoogleOAuth2AuthorizationService));
+
+ var requestBody = new FormUrlEncodedContent([
+ new("client_id", account.OAuth2ClientId),
+ new("client_secret", account.OAuth2ClientSecret),
+ new("code", code),
+ new("redirect_uri", redirectUri),
+ new("grant_type", "authorization_code"),
+ ]);
+
+ var response = await httpClient.PostAsync(TokenEndpoint, requestBody, cancellationToken);
+ var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ throw new InvalidOperationException(
+ $"Google token endpoint returned {(int)response.StatusCode} while exchanging authorization code " +
+ $"for account '{account.Username}'. Response: {responseBody}");
+
+ var tokenResponse = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken)
+ ?? throw new InvalidOperationException($"Failed to deserialize Google token response for account '{account.Username}'.");
+
+ if (string.IsNullOrWhiteSpace(tokenResponse.RefreshToken))
+ throw new InvalidOperationException(
+ $"Google did not return a refresh_token for account '{account.Username}'. " +
+ "Ensure 'access_type=offline' and 'prompt=consent' are set in the authorization URL, " +
+ "and that the user has not previously authorized this app without revoking access.");
+
+ Logger.LogInformation("Successfully obtained Google refresh token for account '{Username}' (Id: {Id}).",
+ account.Username, account.Id);
+
+ return tokenResponse.RefreshToken;
+ }
+
+ private sealed class GoogleTokenResponse
+ {
+ [JsonPropertyName("access_token")]
+ public string AccessToken { get; init; } = string.Empty;
+
+ [JsonPropertyName("refresh_token")]
+ public string? RefreshToken { get; init; }
+
+ [JsonPropertyName("expires_in")]
+ public int ExpiresIn { get; init; }
+
+ [JsonPropertyName("token_type")]
+ public string TokenType { get; init; } = string.Empty;
+ }
+}
diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/GoogleOAuth2TokenService.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/GoogleOAuth2TokenService.cs
new file mode 100644
index 0000000..9f45853
--- /dev/null
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/GoogleOAuth2TokenService.cs
@@ -0,0 +1,105 @@
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Domain.Entities;
+using DigitalData.MessagingService.Domain.Enums;
+using Microsoft.Extensions.Logging;
+using System.Collections.Concurrent;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Text.Json.Serialization;
+
+namespace DigitalData.MessagingService.Infrastructure.Services;
+
+///
+/// Acquires OAuth2 access tokens for Google accounts (Gmail / Google Workspace)
+/// using the OAuth2 client credentials flow against https://oauth2.googleapis.com/token.
+/// Tokens are cached in-memory and reused until 5 minutes before expiry.
+///
+/// Required Google Cloud configuration:
+///
+/// - Create a project in .
+/// - Enable the Gmail API.
+/// - Create an OAuth 2.0 Client ID (type: Web application or Desktop).
+/// - Set OAuth2ClientId and OAuth2ClientSecret in configuration.
+/// - Leave OAuth2TenantId empty — Google does not use tenant IDs.
+///
+///
+///
+/// Note: Google's OAuth2 for IMAP/SMTP requires user-level access (Delegated),
+/// not application-level (Client Credentials). A valid refresh token must
+/// be stored in OAuth2ClientSecret after the initial user authorization flow.
+/// The token endpoint is used here to exchange the refresh token for an access token.
+///
+///
+public class GoogleOAuth2TokenService(
+ ILogger Logger,
+ IHttpClientFactory HttpClientFactory) : IOAuth2TokenService
+{
+ private const string TokenEndpoint = "https://oauth2.googleapis.com/token";
+
+ private readonly ConcurrentDictionary _cache = new();
+
+ public async Task GetAccessTokenAsync(EmailAccount account, CancellationToken cancellationToken = default)
+ {
+ if (account.OAuth2Provider != OAuth2Provider.Google)
+ throw new InvalidOperationException(
+ $"GoogleOAuth2TokenService cannot handle provider '{account.OAuth2Provider}' " +
+ $"for account '{account.Username}'. Expected '{OAuth2Provider.Google}'.");
+
+ if (_cache.TryGetValue(account.Id, out var cached) && cached.Expiry > DateTimeOffset.UtcNow.AddMinutes(5))
+ {
+ Logger.LogDebug("Returning cached Google 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}).");
+
+ if (string.IsNullOrWhiteSpace(account.OAuth2RefreshToken))
+ throw new InvalidOperationException(
+ $"OAuth2RefreshToken is not configured for account '{account.Username}' (Id: {account.Id}). " +
+ "Obtain a refresh token via https://developers.google.com/oauthplayground " +
+ "using scope 'https://mail.google.com/' and set it in OAuth2RefreshToken.");
+
+ Logger.LogDebug("Acquiring new Google OAuth2 token for account {Username} (Id: {Id}).", account.Username, account.Id);
+
+ var httpClient = HttpClientFactory.CreateClient(nameof(GoogleOAuth2TokenService));
+
+ var requestBody = new FormUrlEncodedContent([
+ new("client_id", account.OAuth2ClientId),
+ new("client_secret", account.OAuth2ClientSecret),
+ new("grant_type", "refresh_token"),
+ new("refresh_token", account.OAuth2RefreshToken),
+ ]);
+
+ var response = await httpClient.PostAsync(TokenEndpoint, requestBody, cancellationToken);
+ var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
+
+ if (!response.IsSuccessStatusCode)
+ throw new InvalidOperationException(
+ $"Google token endpoint returned {(int)response.StatusCode} for account '{account.Username}'. Response: {responseBody}");
+
+ var tokenResponse = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken)
+ ?? throw new InvalidOperationException($"Failed to deserialize Google token response for account '{account.Username}'.");
+
+ var expiry = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn);
+ _cache[account.Id] = (tokenResponse.AccessToken, expiry);
+
+ return tokenResponse.AccessToken;
+ }
+
+ private sealed class GoogleTokenResponse
+ {
+ [JsonPropertyName("access_token")]
+ public string AccessToken { get; init; } = string.Empty;
+
+ [JsonPropertyName("expires_in")]
+ public int ExpiresIn { get; init; }
+
+ [JsonPropertyName("token_type")]
+ public string TokenType { get; init; } = string.Empty;
+ }
+}
diff --git a/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/OAuth2TokenServiceDispatcher.cs b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/OAuth2TokenServiceDispatcher.cs
new file mode 100644
index 0000000..7542286
--- /dev/null
+++ b/src/infrastructure/DigitalData.MessagingService.Infrastructure/Services/OAuth2TokenServiceDispatcher.cs
@@ -0,0 +1,34 @@
+using DigitalData.MessagingService.Application.Common.Interfaces;
+using DigitalData.MessagingService.Domain.Entities;
+using DigitalData.MessagingService.Domain.Enums;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace DigitalData.MessagingService.Infrastructure.Services;
+
+///
+/// Routes OAuth2 token requests to the correct provider-specific implementation
+/// based on .
+/// Registered as the single in DI — all other
+/// services depend on this dispatcher rather than on a concrete provider directly.
+///
+public class OAuth2TokenServiceDispatcher(IServiceProvider ServiceProvider) : IOAuth2TokenService
+{
+ public Task GetAccessTokenAsync(EmailAccount account, CancellationToken cancellationToken = default)
+ {
+ var service = account.OAuth2Provider switch
+ {
+ OAuth2Provider.Microsoft => ServiceProvider.GetRequiredService(),
+ OAuth2Provider.Google => (IOAuth2TokenService)ServiceProvider.GetRequiredService(),
+
+ OAuth2Provider.None => throw new InvalidOperationException(
+ $"Account '{account.Username}' (Id: {account.Id}) has OAuth2Provider = None. " +
+ "Set UseOAuth2 = false or configure a valid OAuth2Provider."),
+
+ _ => throw new NotSupportedException(
+ $"OAuth2Provider '{account.OAuth2Provider}' is not supported. " +
+ $"Supported providers: {string.Join(", ", Enum.GetNames())}")
+ };
+
+ return service.GetAccessTokenAsync(account, cancellationToken);
+ }
+}