惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Stack Overflow Blog
Stack Overflow Blog
云风的 BLOG
云风的 BLOG
G
Google Developers Blog
J
Java Code Geeks
C
Check Point Blog
Last Week in AI
Last Week in AI
Microsoft Azure Blog
Microsoft Azure Blog
Blog — PlanetScale
Blog — PlanetScale
月光博客
月光博客
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
L
LangChain Blog
有赞技术团队
有赞技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 司徒正美
IT之家
IT之家
Martin Fowler
Martin Fowler
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
U
Unit 42
Jina AI
Jina AI
Microsoft Security Blog
Microsoft Security Blog
I
InfoQ

博客园 - 冠军

Keep alive 分析 SSRS Reporting Service 日志 [翻译] 感谢 Avalonia, .NET MAUI 即将来到 Linux 和浏览器端 多区域一致性:鱼与熊掌兼得! Microsoft Agent Framework (预览) 入门:让所有的开发者轻松创建 AI Agents 使用 AI app 模板扩展来创建基于订制数据进行聊天的 .NET AI 应用 真正的 .NET 单文件发布与构建 Microsoft.Extensions.AI 库 OfficeRuntime.storage 适配 oidc-client.js 中的 WebStorageStateStore Microsoft.Extensions.VectorData.Abstractions 入门 基于本地 Llama 生成 .NET AI 矢量搜索应用 使用 PdfPig 处理 PDF 文档 使用 Any CPU构建 .NET 8 应用会支持 32 位吗? 使用 NATS CLI NATS: 使用 work-queue Stream NATS: Pull Consumers in JetStream .NET 单文件执行程序拆解器 SingleFileExtractor .NET Aspire Apps 集成测试 ASCII 与 Unicode 中的引号
翻译:基于 OAuth2 在 Web 服务中使用 MailKit
冠军 · 2025-07-22 · via 博客园 - 冠军

MailKit 基于 OAuth2 操作 Exchange (IMAP, POP3 和 SMTP)

原文地址:https://github.com/jstedfast/MailKit/blob/master/ExchangeOAuth2.md#using-oauth2-with-exchange-imap-pop3-or-smtp

1. 注册应用

不管你是准备在桌面应用、移动应用,还是在 Web 服务中基于 MailKit 来访问 Exchange,你需要做的第一件事,就是在微软的 Identity Platform 中注册你的应用。

你需要按照 快速入门 中的要求来完成这一步。

注册之后,你可以得到你所注册的应用 Id, 或者在 ids 中被称为 client id.

在上图中,client id 为:11111111-1111-1111-1111-11111111, 同时,还可以看到相应的 Object ID: 00000000-0000-0000-0000-00000000

2. 为应用配置正确的 API 权限

基于你的应用希望支持的邮件协议,有许多种不同的 API 权限需要配置。见:为你的 Extra AD 应用准备权限:POP, IMAP, SMTP

  1. 在 Azure 门户种,通过 Microsoft Extra appliation 的管理视图,选择 API Permissions 权限
  2. 选择我的组织使用的 API 权限 (APSs my organization uses),然后,选择 "Office 365 Exchange Online"
  3. 点击应用权限 (Application permissions)
    1. 对于 POP 访问协议,选择 POP.AccessAsApp 权限
    2. 对于 IMAP 访问协议,选择 IMAP.AccessAsApp
    3. 对于 SMTP 访问协议,选择 SMTP.SendAsApp
  4. 选择正确的协议之后,点击增加权限 Add permissions

现在,你应该在你的 Entra application 权限种添加了 SMTP, POP 或者 IMAP 权限。

3. 基于 OAuth2 在 Web 服务中使用 MailKit

3.1 为您的 Web 服务注册服务主体

Web 应用代理用户访问 Exchange 需要先进行授权的,这需要到 Exchange 服务器上进行授权。

您的 Web 服务注册完成后,租户管理员需要注册您的服务主体。这是在 Exchange 中配置 Web 服务的访问权限。

要使用 New-ServicePrincipal 命令,请打开 Azure Powershell 终端,安装 ExchangeOnlineManagement 并连接到您的租户,如下所示:

Install-Module -Name ExchangeOnlineManagement -allowprerelease
Import-module ExchangeOnlineManagement 
Connect-ExchangeOnline -Organization <tenantId>

然后,为你的 Web 服务注册服务主体, 这需要使用 New-ServicePrincipal 命令。

此 cmdlet 仅适用于基于云的服务。

使用 New-ServicePrincipal cmdlet 可在基于云的组织中创建服务主体。

Microsoft Entra ID 中的服务主体用于定义应用程序可以执行的操作、谁可以访问应用程序以及应用程序可以访问的资源。在 Exchange Online 中,服务主体是对 Microsoft Entra ID 中服务主体的引用。要将 Exchange Online 基于角色的访问控制 (RBAC) 角色分配给 Microsoft Entra ID 中的服务主体,请使用 Exchange Online 中的服务主体引用。Exchange Online PowerShell 中的 *-ServicePrincipal cmdlet 允许您查看、创建和删除这些服务主体引用。

有关更多信息,请参阅 Microsoft Entra ID 中的应用程序和服务主体对象。

您需要先获得权限才能运行此 cmdlet。虽然本主题列出了此 cmdlet 的所有参数,但如果某些参数未包含在分配给您的权限中,您可能无法访问它们。要查找在您的组织中运行任何 cmdlet 或参数所需的权限,请参阅查找运行任何 Exchange cmdlet 所需的权限。

ObjectId 参数指定服务主体的唯一 ObjectId GUID 值。例如,7c7599b2-23af-45e3-99ff-0025d148e929。

此参数的有效值可在以下位置获取:

  • Microsoft Graph PowerShell 中 Get-MgServicePrincipal cmdlet 输出中的 Id 属性。
  • Microsoft Entra 管理中心中企业应用程序中的 Object ID 属性
New-ServicePrincipal -AppId <APPLICATION_ID> -ObjectId <OBJECT_ID> [-Organization <ORGANIZATION_ID>]

3.2 为你的 Web 服务进行授权

为了授予您的 Web 服务访问 Office365 和/或 Exchange 帐户的权限,您需要首先使用以下命令获取上一步中注册的服务主体 ID:

Get-ServicePrincipal | fl

一旦你的 Web 服务拥有了服务主体标识 Service Principal ID,使用下面的命令,来为你 Web 服务种使用的邮箱账号添加完整的邮箱权限。

Add-MailboxPermission -Identity "john.smith@example.com" -User <SERVICE_PRINCIPAL_ID> -AccessRights FullAccess

3.3 使用 OAuth2 认证 Web 服务

现在,已经有了 Client ID 和 Tenant ID,需要把他们使用到你的应用中。

在下面的示例代码中,使用 Microsoft.Identity.Client NuGet 包来获得访问令牌 Access token, MailKit 需要将它传递给 Exchange Server.

// protocal 是准备使用的邮件访问协议,例如 "SMTP"
// 不同的协议,需要使用的 scope 不同
static async Task<AuthenticationResult> GetConfidentialClientOAuth2CredentialsAsync (string protocol, CancellationToken cancellationToken = default)
{
    var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create (clientId)
        .WithAuthority ($"https://login.microsoftonline.com/{tenantId}/v2.0")
        .WithCertificate (certificate) // or .WithClientSecret (clientSecret)
        .Build ();

    string[] scopes;

    if (protocol.Equals ("SMTP", StringComparison.OrdinalIgnoreCase)) {
        scopes = new string[] {
            // For SMTP, use the following scope
            "https://outlook.office365.com/.default"
        };
    } else {
        scopes = new string[] {
            // For IMAP and POP3, use the following scope
            "https://ps.outlook.com/.default"
        };
    }

    return await confidentialClientApplication.AcquireTokenForClient (scopes).ExecuteAsync (cancellationToken);
}

3.3.1 SMTP

使用通过 GetConfidentialClientOAuth2CredentialsAsync() 方法获得的令牌访问邮箱。

var result = await GetConfidentialClientOAuth2CredentialsAsync ("SMTP");
var oauth2 = new SaslMechanismOAuth2 ("username@outlook.com", result.AccessToken);

using (var client = new SmtpClient ()) {
    await client.ConnectAsync ("smtp.office365.com", 587, SecureSocketOptions.StartTls);
    await client.AuthenticateAsync (oauth2);
    await client.DisconnectAsync (true);
}

4. 桌面和移动应用

现在您已获得客户端 ID 和租户 ID 字符串,您需要将这些值插入到您的应用程序中。

以下示例代码使用 Microsoft.Identity.Client Nuget 包来获取访问令牌,MailKit 需要该令牌才能将其传递给 Exchange 服务器。

// 获得指定用户的访问令牌,protocol 为邮件访问协议,emailAddress 为用户邮箱
static async Task<AuthenticationResult> GetPublicClientOAuth2CredentialsAsync (string protocol, string emailAddress, CancellationToken cancellationToken = default)
{
    var options = new PublicClientApplicationOptions {
        ClientId = "Application (client) ID",
        TenantId = "Directory (tenant) ID",

        // Use "https://login.microsoftonline.com/common/oauth2/nativeclient" for apps using
        // embedded browsers or "http://localhost" for apps that use system browsers.
        RedirectUri = "https://login.microsoftonline.com/common/oauth2/nativeclient"
    };
 
    var publicClientApplication = PublicClientApplicationBuilder
        .CreateWithApplicationOptions (options)
        .Build ();

    string[] scopes;
 
    if (protocol.Equals ("IMAP", StringComparison.OrdinalIgnoreCase)) {
        scopes = new string[] {
            "email",
            "offline_access",
            "https://outlook.office.com/IMAP.AccessAsUser.All"
        };
    } else if (protocol.Equals ("POP", StringComparison.OrdinalIgnoreCase)) {
        scopes = new string[] {
            "email",
            "offline_access",
            "https://outlook.office.com/POP.AccessAsUser.All"
        };
    } else {
        scopes = new string[] {
            "email",
            "offline_access",
            "https://outlook.office.com/SMTP.Send"
        };
    }

    try {
        // First, check the cache for an auth token.
        return await publicClientApplication.AcquireTokenSilent (scopes, emailAddress).ExecuteAsync (cancellationToken);
    } catch (MsalUiRequiredException) {
        // If that fails, then try getting an auth token interactively.
        return await publicClientApplication.AcquireTokenInteractive (scopes).WithLoginHint (emailAddress).ExecuteAsync (cancellationToken);
    }
}

4.1 简单邮件发送协议 SMTP

var result = await GetPublicClientOAuth2CredentialsAsync ("SMTP", "username@outlook.com");

// Note: We always use result.Account.Username instead of `Username` because the user may have selected an alternative account.
var oauth2 = new SaslMechanismOAuth2 (result.Account.Username, result.AccessToken);

using (var client = new SmtpClient ()) {
    await client.ConnectAsync ("smtp.office365.com", 587, SecureSocketOptions.StartTls);
    await client.AuthenticateAsync (oauth2);
    await client.DisconnectAsync (true);
}

注意:一旦使用上述交互式方法获取了身份验证令牌,您就可以避免提示用户,如果您缓存结果。帐户信息,然后使用以下代码在将来静默地重新获取身份验证令牌:

var result = await publicClientApplication.AcquireTokenSilent(scopes, account).ExecuteAsync(cancellationToken);

注意:有关缓存令牌的信息,请参阅 Microsoft 有关如何实现 跨平台令牌缓存 的文档。

4.2 IMAP

var result = await GetPublicClientOAuth2CredentialsAsync ("IMAP", "username@outlook.com");

// Note: We always use result.Account.Username instead of `Username` because the user may have selected an alternative account.
var oauth2 = new SaslMechanismOAuth2 (result.Account.Username, result.AccessToken);

using (var client = new ImapClient ()) {
    await client.ConnectAsync ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect);
    await client.AuthenticateAsync (oauth2);
    await client.DisconnectAsync (true);
}

5 Web 应用

独立用户通过微软认证之后,发送或者接收邮件。

5.1 使用 OAuth2 认证 Web 应用

如果你希望代理用户进行邮件收/发,请使用如下代码:

// Common Code
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Kiota.Abstractions.Authentication;

public static class OAuthMicrosoft
{
    public static readonly string[] RegistrationScopes = new string[] {
        "offline_access",
        "User.Read",
        "Mail.Send",
        "https://outlook.office.com/SMTP.Send",
        "https://outlook.office.com/IMAP.AccessAsUser.All",
    };

    public static readonly string[] SmtpScopes = new string[] {
        "email",
        "offline_access",
        "https://outlook.office.com/SMTP.Send"
    };

    public static readonly string[] ImapScopes = new string[] {
        "email",
        "offline_access",
        "https://outlook.office.com/IMAP.AccessAsUser.All",
    };

    // 获得访问令牌
    public static IConfidentialClientApplication CreateConfidentialClient ()
    {
        var clientId = "Application (client) ID";
        var tenantId = "common"; // common = anybody with microsoft account personal or organization; other options see https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols#endpoints
        var clientSecret = "client secret";

        var redirectURL = "https://example.com/oauth/microsoft/callback";

        var confidentialClientApplication = ConfidentialClientApplicationBuilder.Create (clientId)
            .WithAuthority ($"https://login.microsoftonline.com/{tenantId}/v2.0")
            .WithClientSecret (clientSecret)
            .WithRedirectUri (redirectURL)
            .Build ();

        // You also need to configure an MSAL token cache. so that token are remembered.
        return confidentialClientApplication;
    }
}

注册页面,将用户重定向到微软页面进行认证

// Registration page - redirect user to Microsoft to get authorization 
// 注册页面,重定向用户到微软认证页面
public async Task<IActionResult> OnPostAsync ()
{
    var client = OAuthMicrosoft.CreateConfidentialClient ();

    // Note: When getting authorization, specify all of the scopes that your application will ever need (eg. SMTP /and/ IMAP).
    // Later, when requesting an access token, you will only ask for the specific scopes that you need (e.g. SMTP).
    var authurlbuilder = client.GetAuthorizationRequestUrl (OAuthMicrosoft.RegistrationScopes);
    var authurl = await authurlbuilder.ExecuteAsync ();

    return this.Redirect (authurl.ToString ());
}

// Callback page = https://example.com/oauth/microsoft/callback in this example
// 回调页面
public async Task<IActionResult> OnGet ([FromQuery] string code)
{
    var confidentialClientApplication = OAuthMicrosoft.CreateConfidentialClient ();
    var scopes = OAuthMicrosoft.SmtpScopes;

    var auth = await confidentialClientApplication.AcquireTokenByAuthorizationCode (scopes, code).ExecuteAsync (); //this saves the token in msal cache

    var ident = auth.Account.HomeAccountId.Identifier;
    // Note: you will need to persist the ident to refer to later.
}

// Use the credentials
// 发邮件
public async Task SendEmailAsync (string ident)
{
    var confidentialClientApplication = OAuthMicrosoft.CreateConfidentialClient ();
    var account = await confidentialClientApplication.GetAccountAsync (ident);
    var scopes = OAuthMicrosoft.SmtpScopes;

    try {
        var auth = await confidentialClientApplication.AcquireTokenSilent (scopes, account).ExecuteAsync ();

        using (var client = new SmtpClient ()) {
            await client.ConnectAsync ("smtp-mail.outlook.com", 587, SecureSocketOptions.StartTls);

            var oauth2 = new SaslMechanismOAuth2 (auth.Account.Username, auth.AccessToken);

            await client.AuthenticateAsync (oauth2);

            var serverfeedback = await client.SendAsync (message);
            await client.DisconnectAsync (true);
        }
    } catch (MsalUiRequiredException) {
        throw new Exception ("Need to get authorization again");
    }
}

public async Task TestImapAsync (string ident)
{
    var confidentialClientApplication = OAuthMicrosoft.CreateConfidentialClient ();
    var account = await confidentialClientApplication.GetAccountAsync (ident);
    var scopes = OAuthMicrosoft.ImapScopes;

    var auth = await confidentialClientApplication.AcquireTokenSilent (scopes, account).ExecuteAsync ();    

    var oauth2 = new SaslMechanismOAuth2 (auth.Account.Username, auth.AccessToken);

    using (var client = new ImapClient ()) {
        await client.ConnectAsync ("outlook.office365.com", 993, SecureSocketOptions.SslOnConnect);
        await client.AuthenticateAsync (oauth2);
        await client.DisconnectAsync (true);
    }
}

参考资料