sgcWebSockets · Technical Document

Mail OAuth2

TsgcMailOAuth2: OAuth2 tokens and SASL XOAUTH2 / OAUTHBEARER strings for SMTP, IMAP and POP3 with Microsoft 365 and Gmail.

Overview

Microsoft 365 and Gmail no longer accept a plain password for most mailboxes. Get the OAuth2 tokens, keep them fresh, and hand your SMTP, IMAP or POP3 client the exact SASL string it needs.

The TsgcMailOAuth2 component obtains and refreshes the OAuth 2.0 access tokens needed to send and read mail with Microsoft 365 (Exchange Online) and Gmail, and builds the SASL XOAUTH2 and OAUTHBEARER (RFC 7628) strings used by SMTP, IMAP and POP3. Both providers have disabled basic authentication with a password for most accounts, so a mail client needs OAuth2 to sign in.

The component is transport agnostic, it never talks to the mail server. Use it with Indy TIdSMTP, TIdIMAP4, TIdPOP3 or any other mail library which can send a raw SASL command.

At a glance

Component class
TsgcMailOAuth2
Standards / spec
SASL OAUTHBEARER, RFC 7628 · XOAUTH2
Transports
HTTPS for the token requests; SMTP, IMAP and POP3 through your mail library
Platforms
Windows, macOS, Linux, iOS, Android
Frameworks
VCL, FireMonkey
Edition
Enterprise (also sgcAuth pack)

Features

Technical specification

Standards & specsRFC 7628 · XOAUTH2 SASL mechanism · OAuth for IMAP, POP and SMTP · RFC 7636 · RFC 8628
Component classTsgcMailOAuth2 (unit sgcAuth_Mail_OAuth2)
FrameworksVCL, FireMonkey
PlatformsWindows, macOS, Linux, iOS, Android
EditionsgcWebSockets Enterprise, also sold in the sgcAuth pack

Main properties

The published and public properties used to configure and drive the component.

AccessTokenCurrent access token, read-only.
ClientIdClient ID of the application registered with the provider.
ClientSecretClient secret of the application, when the provider issued one.
CustomAuthURLAuthorization endpoint used when Provider is mopCustom.
CustomDeviceAuthorizationURLDevice authorization endpoint used when Provider is mopCustom and Flow is mofDeviceCode.
CustomScopeSpace separated scopes requested when Provider is mopCustom.
CustomTokenURLToken endpoint used when Provider is mopCustom.
ExpiresAtUTC date and time when the access token expires, 0 when unknown.
FlowOAuth2 flow used by Start: authorization code with PKCE or device code.
HTTPClientOptionsOptions of the internal HTTP client which calls the token endpoint.
LocalServerOptionsLocal HTTP server which receives the redirect of the authorization code flow.
ProtocolsMail protocols the access token is requested for (Microsoft 365).
ProviderMail provider: Microsoft 365, Gmail or a custom OAuth2 server.
RefreshTokenRefresh token, assign the stored value before calling Refresh.
TenantIdMicrosoft Entra tenant used in the Microsoft 365 endpoints.
VersionRead-only string exposing the sgcWebSockets library version.

Main methods

The public methods exposed by the component.

GetAuthURLReturns the authorization endpoint for the current Provider.
GetDeviceAuthorizationURLReturns the device authorization endpoint for the current Provider.
GetOAuthBearerReturns the Base64 SASL OAUTHBEARER (RFC 7628) initial response.
GetOAuthBearerRawReturns the SASL OAUTHBEARER initial response without Base64 encoding.
GetScopeReturns the scope requested for the current Provider and Protocols.
GetTokenURLReturns the token endpoint for the current Provider.
GetXOAuth2Returns the Base64 SASL XOAUTH2 initial response for a user and the current access token.
GetXOAuth2RawReturns the SASL XOAUTH2 initial response without Base64 encoding.
RefreshRequests a new access token with the RefreshToken, synchronously.
StartStarts the configured flow: opens the browser (PKCE) or requests a device code.
StopStops a running flow and the local HTTP server.

Events

The events fired by the component.

OnDeviceCodeFired by the device code flow with the code the user must enter on the verification page.
OnErrorFired when the provider returns an OAuth2 error or the device code expires.
OnTokensChangedFired when new tokens are received, after Start and after every Refresh.

Quick Start

Pick the Provider and the Protocols, set ClientId, reuse the stored refresh token or call Start, then authenticate the mail session with GetXOAuth2.

About this scenario. Sign in once, send mail for months. The same code ships in the demo Demos\26.Authentication\06.Mail_OAuth2.

Delphi (VCL / FireMonkey)

uses
  IdSMTP, sgcAuth_Mail_OAuth2;

// Mail is a form field: Mail: TsgcMailOAuth2;
procedure TForm1.FormCreate(Sender: TObject);
begin
  Mail := TsgcMailOAuth2.Create(Self);
  Mail.Provider := mopMicrosoft365;
  Mail.Protocols := [mpSMTP];
  Mail.TenantId := 'contoso.onmicrosoft.com';
  Mail.ClientId := 'your-application-id';
  Mail.OnTokensChanged := OnMailTokensChanged;

  // reuse the stored refresh token, sign in only when it no longer works
  Mail.RefreshToken := LoadRefreshToken;
  if (Mail.RefreshToken = '') or not Mail.Refresh then
    Mail.Start; // opens the browser, PKCE and a loopback redirect
end;

procedure TForm1.OnMailTokensChanged(Sender: TObject; const AccessToken,
  RefreshToken: String; const ExpiresAt: TDateTime);
begin
  SaveRefreshToken(RefreshToken); // encrypt it in your store
end;

// TIdSMTP with UseTLS = utUseExplicitTLS and AuthType = satNone
procedure TForm1.Authenticate(aSMTP: TIdSMTP);
begin
  aSMTP.Connect; // EHLO and STARTTLS
  aSMTP.SendCmd('AUTH XOAUTH2 ' + Mail.GetXOAuth2('user@contoso.com'), 235);
end;

C++ Builder

// uses: IdSMTP, sgcAuth_Mail_OAuth2
TsgcMailOAuth2 *Mail = new TsgcMailOAuth2(this);
Mail->Provider = mopMicrosoft365;
Mail->Protocols = TsgcMailOAuth2Protocols() << mpSMTP;
Mail->TenantId = "contoso.onmicrosoft.com";
Mail->ClientId = "your-application-id";
Mail->OnTokensChanged = OnMailTokensChanged;

Mail->RefreshToken = LoadRefreshToken();
if (Mail->RefreshToken.IsEmpty() || !Mail->Refresh())
  Mail->Start();

IdSMTP1->Connect();
IdSMTP1->SendCmd("AUTH XOAUTH2 " + Mail->GetXOAuth2("user@contoso.com"), 235);

Common scenarios

The following topics come from the online help. Each one explains a part of the component and shows the Delphi code that drives it.

1 · Microsoft 365 setup

  1. In the Azure portal open Microsoft Entra IDApp registrationsNew registration and register the application.
  2. In Authentication add the platform Mobile and desktop applications with the loopback redirect URI of LocalServerOptions (for example http://localhost). For the device code flow enable Allow public client flows.
  3. In API permissions add the delegated permissions for the protocols you use: SMTP.Send, IMAP.AccessAsUser.All, POP.AccessAsUser.All, plus offline_access.
  4. Copy the Application (client) ID to ClientId. For a single tenant application copy the Directory (tenant) ID to TenantId, otherwise keep common.
  5. SMTP AUTH must be enabled for the mailbox, for example with the Exchange Online PowerShell command Set-CASMailbox -Identity user@contoso.com -SmtpClientAuthenticationDisabled $false.

Servers: SMTP smtp.office365.com:587 (STARTTLS), IMAP outlook.office365.com:993, POP3 outlook.office365.com:995.

2 · Gmail setup

  1. In the Google Cloud Console create a project and configure the OAuth consent screen with the scope https://mail.google.com/. While the application is in testing mode, add the accounts which will sign in as test users.
  2. In Credentials create an OAuth client ID of type Desktop app.
  3. Copy the client ID to ClientId and the client secret to ClientSecret.
  4. Use the authorization code flow with PKCE. Google does not allow the mail scope with the device code flow, Start raises an exception for that combination.

Servers: SMTP smtp.gmail.com:587 (STARTTLS), IMAP imap.gmail.com:993, POP3 pop.gmail.com:995.

3 · Flows

With mopCustom the endpoints and the scope come from CustomAuthURL, CustomTokenURL, CustomDeviceAuthorizationURL and CustomScope.

4 · Token persistence and OnTokensChanged

The user signs in only once. OnTokensChanged is fired every time new tokens are received, after Start and after every Refresh, with the access token, the refresh token and the UTC expiry time. Save the RefreshToken encrypted: it gives access to the mailbox. In the next session assign it to the RefreshToken property and call Refresh, which is synchronous, to get a new access token without user interaction. Access tokens are short lived (about one hour), refresh before ExpiresAt.

The token events can be fired in a secondary thread (the thread of the local HTTP server), synchronize any access to the user interface.

5 · SASL XOAUTH2 and OAUTHBEARER

GetXOAuth2 returns the Base64 initial client response of the XOAUTH2 mechanism, user={user}^Aauth=Bearer {token}^A^A, supported by Microsoft 365 and Gmail. GetOAuthBearer returns the standard OAUTHBEARER response of RFC 7628, n,a={user},^Ahost={host}^Aport={port}^Aauth=Bearer {token}^A^A, supported by Gmail. The Raw variants return the same strings without Base64 encoding, for libraries which encode them. Send them as:

The functions sgcGetXOAuth2, sgcGetXOAuth2Raw, sgcGetOAuthBearer and sgcGetOAuthBearerRaw of the same unit build the strings from an access token obtained in any other way.

6 · Example: sending mail with Indy TIdSMTP

Delphi (VCL / FireMonkey)
uses
  IdSMTP, IdMessage, IdSSLOpenSSL, IdExplicitTLSClientServerBase, sgcAuth_Mail_OAuth2;

procedure TForm1.FormCreate(Sender: TObject);
begin
  oMail := TsgcMailOAuth2.Create(Self);
  oMail.Provider := mopMicrosoft365;
  oMail.Protocols := [mpSMTP];
  oMail.TenantId := 'contoso.onmicrosoft.com';
  oMail.ClientId := '00000000-0000-0000-0000-000000000000';
  oMail.OnTokensChanged := OnMailTokensChanged;
  oMail.OnError := OnMailError;
  // next sessions: reuse the stored refresh token
  oMail.RefreshToken := LoadRefreshToken; // decrypted from your store
  if (oMail.RefreshToken = '') or not oMail.Refresh then
    oMail.Start; // opens the browser, the tokens arrive in OnTokensChanged
end;

procedure TForm1.OnMailTokensChanged(Sender: TObject; const AccessToken,
  RefreshToken: String; const ExpiresAt: TDateTime);
begin
  SaveRefreshToken(RefreshToken); // encrypt it
end;

procedure TForm1.OnMailError(Sender: TObject; const Error, ErrorDescription: String);
begin
  Log('OAuth2 error: ' + Error + ' ' + ErrorDescription);
end;

procedure TForm1.SendMail;
var
  oSMTP: TIdSMTP;
  oSSL: TIdSSLIOHandlerSocketOpenSSL;
  oMessage: TIdMessage;
begin
  oSMTP := TIdSMTP.Create(nil);
  oMessage := TIdMessage.Create(nil);
  try
    oSSL := TIdSSLIOHandlerSocketOpenSSL.Create(oSMTP);
    oSMTP.IOHandler := oSSL;
    oSMTP.Host := 'smtp.office365.com';
    oSMTP.Port := 587;
    oSMTP.UseTLS := utUseExplicitTLS;
    oSMTP.AuthType := satNone;
    oSMTP.Connect; // sends EHLO and STARTTLS
    oSMTP.SendCmd('AUTH XOAUTH2 ' + oMail.GetXOAuth2('user@contoso.com'), 235);
    oMessage.From.Address := 'user@contoso.com';
    oMessage.Recipients.EMailAddresses := 'john@example.com';
    oMessage.Subject := 'Hello';
    oMessage.Body.Text := 'Sent with OAuth2.';
    oSMTP.Send(oMessage);
    oSMTP.Disconnect;
  finally
    oMessage.Free;
    oSMTP.Free;
  end;
end;

The strings are built from the current AccessToken. When there is no access token, GetXOAuth2 and GetOAuthBearer raise "There is no access token, call Start or Refresh first.".

Sources used to build this document

Every external claim links back to a primary source. The online help references are the canonical pages the company maintains for this component.

Document scope. This document covers the publicly documented surface of the Mail OAuth2 component shipped with sgcWebSockets Enterprise and the sgcAuth pack. For the full property, method and event reference consult the online help linked above.