This is using ROPC.
First, register an Azure Active Directory app:
- single tenant (I haven't tried the other options)
- Authentication / Allow public client flows (not sure that's required but that's what I have)
- create a secret
- API permissions: use delegated permissions and have an admin grant consent for them
- email
- offline_access
- openid
- IMAP.AccessAsUser.All
- SMTP.Send
- User.Read (not sure that's needed)
Even though this is a daemon-like application, we're using delegated permissions because we're using the ROPC grant.
Then you can use this code which uses the following nuget packages:
using MailKit;
using MailKit.Net.Imap;
using MailKit.Net.Smtp;
using MailKit.Search;
using MailKit.Security;
using MimeKit;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace MailKitExchangeDaemon
{
class Program
{
const string ScopeEmail = "email";
const string ScopeOpenId = "openid";
const string ScopeOfflineAccess = "offline_access";
const string ScopeImap = "https://outlook.office.com/IMAP.AccessAsUser.All";
const string ScopeSmtp = "https://outlook.office.com/SMTP.Send";
const string SmtpHost = "smtp.office365.com";
const string ImapHost = "outlook.office365.com";
const string TenantId = "<GUID>";
const string AppId = "<GUID>";
const string AppSecret = "<secret value>";
const string Username = "<email address>";
const string Password = "<password>";
static async Task Main(string[] args)
{
Console.WriteLine($"Sending an email to {Username}...");
await sendEmail();
System.Threading.Thread.Sleep(2000);
Console.WriteLine($"Printing {Username} inbox...");
await printInbox();
Console.Write("Press ENTER to end this program");
Console.ReadLine();
}
static async Task printInbox()
{
var accessToken = await getAccessToken(ScopeEmail, ScopeOpenId, ScopeOfflineAccess, ScopeImap);
using (var client = new ImapClient(/*new MailKit.ProtocolLogger(Console.OpenStandardOutput())*/))
{
try
{
await client.ConnectAsync(ImapHost, 993, true);
await client.AuthenticateAsync(accessToken);
client.Inbox.Open(FolderAccess.ReadOnly);
var emailUIDs = client.Inbox.Search(SearchQuery.New);
Console.WriteLine($"Found {emailUIDs.Count} new emails in the {Username} inbox");
foreach (var emailUID in emailUIDs)
{
var email = client.Inbox.GetMessage(emailUID);
Console.WriteLine($"Got email from {email.From[0]} on {email.Date}: {email.Subject}");
}
}
catch (Exception e)
{
Console.Error.WriteLine($"Error in 'print inbox': {e.GetType().Name} {e.Message}");
}
}
}
static async Task sendEmail()
{
var accessToken = await getAccessToken(ScopeEmail, ScopeOpenId, ScopeOfflineAccess, ScopeSmtp);
using (var client = new SmtpClient(/*new MailKit.ProtocolLogger(Console.OpenStandardOutput())*/))
{
try
{
client.Connect(SmtpHost, 587, SecureSocketOptions.Auto);
client.Authenticate(accessToken);
var email = new MimeMessage();
email.From.Add(MailboxAddress.Parse(Username));
email.To.Add(MailboxAddress.Parse(Username));
email.Subject = "SMTP Test";
email.Body = new TextPart("plain") { Text = "This is a test" };
client.Send(email);
}
catch (Exception e)
{
Console.Error.WriteLine($"Error in 'send email': {e.GetType().Name} {e.Message}");
}
}
}
/// <summary>
/// Get the access token using the ROPC grant (<see cref="https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc"/>).
/// </summary>
/// <param name="scopes">The scopes/permissions the app requires</param>
/// <returns>An access token that can be used to authenticate using MailKit.</returns>
private static async Task<SaslMechanismOAuth2> getAccessToken(params string[] scopes)
{
if (scopes == null || scopes.Length == 0) throw new ArgumentException("At least one scope is required", nameof(scopes));
var scopesStr = String.Join(" ", scopes.Select(x => x?.Trim()).Where(x => !String.IsNullOrEmpty(x)));
var content = new FormUrlEncodedContent(new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("username", Username),
new KeyValuePair<string, string>("password", Password),
new KeyValuePair<string, string>("client_id", AppId),
new KeyValuePair<string, string>("client_secret", AppSecret),
new KeyValuePair<string, string>("scope", scopesStr),
});
using (var client = new HttpClient())
{
var response = await client.PostAsync($"https://login.microsoftonline.com/{TenantId}/oauth2/v2.0/token", content).ConfigureAwait(continueOnCapturedContext: false);
var responseString = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(responseString);
var token = json["access_token"];
return token != null
? new SaslMechanismOAuth2(Username, token.ToString())
: null;
}
}
}
}