如何将 MailKit 与 IMAP for Exchange 结合使用,将 OAuth2 用于守护程序/非交互式应用程序

lau*_*ian 11 imap exchange-server azure oauth-2.0 mailkit

我有一个守护程序,可以读取电子邮件地址的收件箱并对电子邮件执行操作。我正在使用 MailKit 通过 IMAP 连接到交换服务器,但 Microsoft 已关闭我们的基本身份验证(凌晨 4 点,没有警告......)。所以我需要一种新的方式来连接到我的邮箱。

使用图表需要对我的应用程序进行重大重写。我打算这样做,但与此同时,我需要一个中间解决方案来保留 MailKit。

lau*_*ian 18

这是使用ROPC

首先,注册 Azure Active Directory 应用程序:

  • 单一租户(我没有尝试过其他选项)
  • 身份验证/允许公共客户端流(不确定是否需要,但这就是我所拥有的)
  • 创造一个秘密
  • API 权限:使用委派权限并获得管理员同意
    • 电子邮件
    • 离线访问
    • 开放ID
    • IMAP.AccessAsUser.All
    • SMTP发送
    • User.Read(不确定是否需要)

尽管这是一个类似守护程序的应用程序,但我们正在使用委派权限,因为我们正在使用 ROPC 授予。

然后您可以使用此代码,该代码使用以下 nuget 包:

  • 邮件套件
  • Newtonsoft.Json
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;
            }
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我做错的是没有传递范围 **https://outlook.office.com/IMAP.AccessAsUser.All** (2认同)