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 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 uri = await app
        .GetAuthorizationRequestUrl(scopes)
        .ExecuteAsync();

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

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

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

Office365: Client Credential Flow support for POP/IMAP is coming in June 2022

Microsoft is working on bringing OAuth Client Credential Flow support for POP/IMAP for Office365/Exchange. It’s planned to be released in June 2022:

https://www.microsoft.com/en-us/microsoft-365/roadmap?filters=&searchterms=70577

Check out currently supported OAuth 2.0 flows

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

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

Device flow allows operator/administrator to authenticate your application on a different machine than your application is installed.

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

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";

IPublicClientApplication app = PublicClientApplicationBuilder
    .Create(clientId)
    .WithTenantId(tenantId)
    .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",
};

Now acquire an access token and a user name:

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 acquire = await app.AcquireTokenWithDeviceCode(
        scopes, 
        callback=>
    {
        // Write url and code to logs so the operator can react:
        Console.WriteLine(callback.VerificationUrl);
        Console.WriteLine(callback.UserCode);

        // This happens on the first run, manually,
        //  on the operator machine.
        // The code below code is only to illustrate 
        // the operator opening browser on his machine,
        // opening the url and using the code 
        // (extracted from the application logs)
        // to authenticate the app.
        System.Diagnostics.Process.Start(
            new ProcessStartInfo(result.VerificationUrl) 
                        { UseShellExecute = true }
            );

        return Task.CompletedTask;
    }).ExecuteAsync();

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

AcquireTokenWithDeviceCode call waits until operator/administrator gives consent by going to VerificationUrl, entering UserCode and authenticating – this usually happens on a different machine than the application is installed.

Finally your app will exit AcquireTokenWithDeviceCode method and connect using IMAP/POP3/SMTP, authenticate and download emails:

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

    // ...

    client.Close();
} 

You can find more details on this flow here:

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code

Token serialization

Below is a simple implementation that saves MSAL token cache to file. Please note that most likely you should store this cache in an encrypted form:

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

More details on MSAL token serialization are available here:

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

Extending Sign-in frequency with policies

You can extend how often operator needs to re-authenticate the application up to 1 year:

Side note: Have in mind that similarly a client credential flow requires a client secret which is valid for 2 years maximum.

Office 365 enable IMAP/POP3 and SMTP access

First log in to Microsoft 365 admin portal at https://admin.microsoft.com/ as an administrator, go to Org settings screen and find Modern authentication entry:

Check ‘Turn on modern authentication…‘ for OAuth flows.

Check IMAP, POP3 and SMTP for App passwords flows.

Then go to Users screen:

Select an user and on the Mail tab click Manage email apps

Check IMAP, Pop and Authenticated SMTP to turn on the protocols for this account

Have in mind it takes 20-30 minutes for the changes to take effect.

AD configuration

In your Active Directory, make sure Enable Security defaults is set to No:

Make sure there are no Conditional Access | Policies defined in your AD:

For MFA enabled/enforced accounts you must create and use App passwords or OAuth flows.

For other accounts – you should be able to log in using your primary password (remember MFA needs to be disabled):

using (Imap imap = new Imap())
{
    imap.ConnectSSL("outlook.office365.com");
 
    imap.UseBestLogin(
        "AdeleV@limilabs.onmicrosoft.com",  
        "password");
 
    imap.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;
    }
 
    imap.Close();
}

Exchange administration

You can find the same mailbox/user settings through Exchange administration screens:

Office 365 App passwords

First make sure IMAP/POP3/SMTP are turned on properly: enable IMAP / POP3 and SMTP in Office 365

To use App passwords, Multi-factor Authentication (MFA) must be turned on for the account.

On the other had, if you have MFA turned on for your account, App passwords are the simplest way to receive your emails – your primary password won’t work.

First log in to Microsoft 365 admin portal at https://admin.microsoft.com/ as an administrator. Go to Users screen and click Mulit-factor authentication:

Select a user and change Mulit-factor Auth Status to Enforced:

Log in as this user to his My Account at https://myaccount.microsoft.com/, then go My sign-ins and create a new App password on the Security info screen:

Write down the created password (you won’t be able to see it again).

Have in mind it takes 20-30 minutes for the changes to take effect.

Remember to turn IMAP on for those Office 365 accounts as well, now you can now use it as a standard password:

using (Imap imap = new Imap())
{
    imap.ConnectSSL("outlook.office365.com");

    imap.UseBestLogin(
        "AlexW@limilabs.onmicrosoft.com", 
        "qfhnlzsdaqew");

    imap.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;
    }

    imap.Close();
}

You can find more details here as well:
https://support.microsoft.com/en-us/account-billing/using-app-passwords-with-apps-that-don-t-support-two-step-verification-5896ed9b-4263-e681-128a-a6f2979a7944