OAuth 2.0 with Office365/Exchange IMAP/POP3/SMTP

This article shows how to implement OAuth 2.0 desktop flow to access Office365 via IMAP, POP3 or SMTP using Mail.dll .net email client.

Make sure IMAP/POP3/SMTP is enabled for your organization and mailbox:
Enable IMAP/POP3/SMTP in Office 365

Register your application in Azure Portal, here’s a detailed guide how to do that:
https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app

Remember to add authentication entries (localhost is needed for .net core):

RedirectUri

.NET desktop: https://login.microsoftonline.com/common/oauth2/nativeclient
.NET core/.NET 5,6,7+: http://localhost
ASP.NET: your application custom url

Then you need to apply correct API permissions and grant the admin consent for your domain.

In the API permissions / Add a permission wizard, select Microsoft Graph and then Delegated permissions to find the following permission scopes listed:

  • offline_access
  • email
  • IMAP.AccessAsUser.All
  • POP.AccessAsUser.All
  • SMTP.Send

Remember to Grant admin consent..

Use Microsoft Authentication Library for .NET (MSAL.NET) nuget package to obtain an access token:
https://www.nuget.org/packages/Microsoft.Identity.Client/

string clientId = "Application (client) ID";
string tenantId = "Directory (tenant) ID";

// for @outlook.com/@hotmail accounts instead of setting .WithTenantId use:
// .WithAuthority(AadAuthorityAudience.PersonalMicrosoftAccount)

var app = PublicClientApplicationBuilder
                .Create(clientId)
                .WithTenantId(tenantId)
                .WithDefaultRedirectUri()
                .Build();
// This allows saving access/refresh tokens to some storage
TokenCacheHelper.EnableSerialization(app.UserTokenCache);

var scopes = new string[] 
{
    "offline_access",
    "email",
    "https://outlook.office.com/IMAP.AccessAsUser.All",
    "https://outlook.office.com/POP.AccessAsUser.All",
    "https://outlook.office.com/SMTP.Send",
};

In addition, you should request offline_access scope. When a user approves the offline_access scope, your app can receive refresh tokens from the Microsoft identity platform token endpoint. Refresh tokens are long-lived. Your app can get new access tokens as older ones expire.

Now acquire the access token and user email address:

string userName;
string accessToken;

var account = (await app.GetAccountsAsync()).FirstOrDefault();

try
{
    AuthenticationResult refresh = await app
        .AcquireTokenSilent(scopes, account)
        .ExecuteAsync();

    userName = refresh.Account.Username;
    accessToken = refresh.AccessToken;
}
catch (MsalUiRequiredException e)
{
    var result = await app.AcquireTokenInteractive(scopes)
        .ExecuteAsync();

    userName = result.Account.Username;
    accessToken = result.AccessToken;
}

On the first run user will see a Microsoft login screen, with option to log-in, using a known account and granting access to the app (if needed):

Finally you can connect using IMAP/POP3/SMTP, authenticate and download user’s emails:

using (Imap client = new Imap())
{
    client.ConnectSSL("outlook.office365.com");
    client.LoginOAUTH2(userName, accessToken);
 
    client.SelectInbox();

    List<long> uids = imap.Search(Flag.Unseen);
    foreach (long uid in uids)
    {
        IMail email = new MailBuilder()
                .CreateFromEml(imap.GetMessageByUID(uid));
        string subject = email.Subject;
   }

    client.Close();
} 

As this is fairly new feature for Exchange/Office365, here are some useful links:

https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth

https://stackoverflow.com/questions/29747477/imap-auth-in-office-365-using-oauth2

https://stackoverflow.com/questions/43473858/connect-to-outlook-office-365-imap-using-oauth2

https://stackoverflow.com/questions/61597263/office-365-xoauth2-for-imap-and-smtp-authentication-fails

Any organization and personal accounts

To access accounts from any organization and personal accounts as well, you need to specify correct account types when you create the App in your AD:

Additionally you need to use:    

    .WithAuthority(
        AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount
        )

instead of

    .WithTenantId(tenantId)

when creating the app:

 var app = PublicClientApplicationBuilder
    .Create(clientId)
    .WithAuthority(
        AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount
        )
    .WithDefaultRedirectUri()
    .Build();

Token serialization

Below is a simple implementation that saves MSAL token cache to file:

static class TokenCacheHelper
{
    public static void EnableSerialization(ITokenCache tokenCache)
    {
        tokenCache.SetBeforeAccess(BeforeAccessNotification);
        tokenCache.SetAfterAccess(AfterAccessNotification);
    }

    private static readonly string _fileName = "msalcache.bin3";

    private static readonly object _fileLock = new object();


    private static void BeforeAccessNotification(TokenCacheNotificationArgs args)
    {
        lock (_fileLock)
        {
            byte[] data = null;
            if (File.Exists(_fileName))
                data = File.ReadAllBytes(_fileName);
            args.TokenCache.DeserializeMsalV3(data);
        }
    }

    private static void AfterAccessNotification(TokenCacheNotificationArgs args)
    {
        if (args.HasStateChanged)
        {
            lock (_fileLock)
            {
                byte[] data = args.TokenCache.SerializeMsalV3();
                File.WriteAllBytes(_fileName, data);
            }
        }
    }
};

Please note that most likely you should store this cache in an encrypted form in some kind of a database.
Consider using MSAL token serialization implementations available here:

https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-token-cache-serialization

Tags:     

Questions?

Consider using our Q&A forum for asking any questions.