OAuth 2.0 web flow with Office365/Exchange IMAP/POP3/SMTP

This article shows how to implement OAuth 2.0 web 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

RedirectUri

Add an authentication redirect uri to your application:

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.

Create an app secret and remember its value:

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";
string clientSecret = "Client secret value";

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

var app = ConfidentialClientApplicationBuilder
    .Create(clientId)
    .WithTenantId(tenantId)
    .WithClientSecret(clientSecret)
    .WithRedirectUri("http://localhost/myapp/")
    .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 try finding account by an identifier (it will be null on first access) in MSAL cache:

string userName;
string accessToken;

string identifier = null;

var account = await app.GetAccountAsync(identifier);

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

    userName = refresh.Account.Username;
    accessToken = refresh.AccessToken;

}
catch (MsalUiRequiredException e)
{
    // no token cache entry - perform authentication:

    Uri msUri = await app
        .GetAuthorizationRequestUrl(scopes)
        .ExecuteAsync();

    // Add a redirect code to the above 
    // Microsoft authentication uri and end this request.
}

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

After successful authentication Microsoft will redirect user’s browser back to your application – to the app’s RedirectUri (in our case http://localhost/MyApp/):

http://localhost/myapp/?code=0.Aa…AA&client_info=ey…I0In0&session_state=4dd….4488c8#

Controller responsible for handling this request should retrieve code parameter

string code = "get from url after redirect";

AuthenticationResult result = await app
    .AcquireTokenByAuthorizationCode(scopes, code)
    .ExecuteAsync();

string identifier = result.Account.HomeAccountId.Identifier;
string userName = result.Account.Username;
string accessToken = result.AccessToken;

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();
} 

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.