前情提要:之前寫程式存取 Exchange 收發信,我都是用 EWS API,用 AD 整合式驗證(企業環境)或應用程式密碼(Internet 線上版)執行作業,但 2022/10/1 起微軟停用了雲端 Exchange (Office 365 信箱)的 HTTP 基本驗證,後者已不能再用密碼存取 EWS,需改用 OAuth,本篇將介紹如何使用註冊應用程式、取得 Token 存取 EWS API。

OAuth 對我是陌生領域,所幸微軟有給說明:Authenticate an EWS application by using OAuth,就摸著石頭過河吧。

第一步是要在 Azure AD 註冊你的應用程式,用管理者身分登入Azure AD 管理中心,進入 Azure Active Dicretory 介面點選「應用程式註冊」:

有了上回設定 AzureAD 的經驗,這次倒沒茫然不知所措。IT 這行就是這樣,一點一點累積,路會愈走愈順... 直到下次發現他 X 的做法/版本/平台/工具怎麼又換了! (補聲暗)

新增應用程式時輸入名稱、帳戶類型、重導 URL 輸入 https://login.microsoftonline.com/common/oauth2/nativeclient

建立後有兩個識別碼要記下來,等下會用到。「應用程式(用戶端)識別碼」是 AppId、「目錄(租用戶)識別碼」是 TenantId:

OAuth 授權模式有兩種:

  1. Delegagted Permission
    需使用者或管理者在同意網頁完成授權,讓應用程式代表「登入中的使用者」呼叫 API 進行動作
  2. Application Permission
    即使使用者未登入能執行動作,適用於排程或服務程式。我要跑排程收發信,要選這個

點開建立好的應用程式,點「資訊設定」(Manifest),找到 requiredResourceAccess,改成 Exchage 服務的 Application Permission 設定:

{
    "resourceAppId": "00000002-0000-0ff1-ce00-000000000000",
    "resourceAccess": [
        {
            "id": "dc890d15-9560-4a4c-9b7f-a736ec74ec40",
            "type": "Role"
        }
    ]
}

點「API 權限」,左側選單點「API 權限」,應可看到 Office 365 Exchange / full_access_as_app,點「代表XXX授與管理者同意」:

接著選「憑證與祕密」,按「新增用戶端密碼」以建立密碼:

建好密碼後,記得複製密碼值,它就是 Client Secret

到這裡設定就算完成了,呼! 再來要改程式,這部分反而簡單。

專案要參照 Microsoft.Identity.Client (MSAL.NET):

將 EWS 範例改寫如下,ConfidentialClientApplicationBuilder 輸入 AppId、TenantId、Client,執行 ExecuteAsyc() 取得 AccessToken,用來建立 OAuthCredentials 物件取代 WebCredentials,在 ImpersonatedUserId 指定存取信箱,程式終於又能收發信了。

using Microsoft.Exchange.WebServices.Data;
using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace RcvAndSendMail
{
    class Program
    {
        static ExchangeService InitEws()
        {
            // 取得 Token
            var cca = ConfidentialClientApplicationBuilder
                .Create(AppConstants.AppId)
                .WithClientSecret(AppConstants.ClientSecret)
                .WithTenantId(AppConstants.TenantId)
                .Build();
            var ewsScopes = new string[] { "https://outlook.office365.com/.default" };
            var authResult = cca.AcquireTokenForClient(ewsScopes).ExecuteAsync().Result;

            var ews = new ExchangeService(ExchangeVersion.Exchange2013_SP1);
            ews.Url = new Uri("https://outlook.office365.com/ews/exchange.asmx");

            //企業地端Exchange比較簡單,可傳入帳密登入
            //ews.Credentials  = new NetworkCredential(userName, password, domain);
            //或使用執行程式的AD帳號自動登入
            //ews.UseDefaultCredentials = true;

            //Office 365 登入方式
            ews.Credentials = new OAuthCredentials(authResult.AccessToken);
            //ews.TraceEnabled = true;
            ews.ImpersonatedUserId = new ImpersonatedUserId(
                ConnectingIdType.SmtpAddress, AppConstants.Email);
            
            return ews;
        }

        static void Main(string[] args)
        {
            var ews = InitEws();
            CheckInbox(ews);
            SendEmail(ews);
            Console.ReadLine();
        }

        static void CheckInbox(ExchangeService ews)
        {
            
            //https://docs.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-work-with-exchange-mailbox-items-by-using-ews-in-exchange
            // 繫結到收件匣
            Folder inbox = Folder.Bind(ews, WellKnownFolderName.Inbox);
            // 篩選未讀信件
            SearchFilter sf = new SearchFilter.SearchFilterCollection(LogicalOperator.And, 
                new SearchFilter.IsEqualTo(EmailMessageSchema.IsRead, false));

            ItemView view = new ItemView(10);
            // Fire the query for the unread items.
            // This method call results in a FindItem call to EWS.
            FindItemsResults<Item> findResults = ews.FindItems(WellKnownFolderName.Inbox, sf, view);
            foreach (var item in findResults.Items)
            {
                string subject = item.Subject;
                if (item is EmailMessage && subject.StartsWith("#BOT"))
                {
                    //信件內容及附件要 Load() 才會載入
                    item.Load();
                    var mail = item as EmailMessage;
                    var text = mail.Body.Text;
                    Console.WriteLine($"收件時間:{item.DateTimeReceived:yyyy/MM/dd HH:mm}");
                    Console.WriteLine($"寄件者:{mail.From}");
                    Console.WriteLine($"主旨:{item.Subject}");
                    Console.WriteLine($"內容:{text}");
                    //TODO 執行特定作業
                    //刪除有 HardDelete/SoftDelete/MoveToDeletedItems 三種模式
                    item.Delete(DeleteMode.HardDelete);
                }
            }

        }

        static void SendEmail(ExchangeService ews)
        {
            var mail = new EmailMessage(ews);
            mail.ToRecipients.Add(AppConstants.TestRecipient);
            mail.Subject = $"發信測試: {DateTime.Now:HHmmss}";
            mail.Body = new MessageBody(BodyType.Text, "Sent from Office 365");
            mail.Send(); //或是 mail.SendAndSaveCopy();
        }
    }
}

提醒,以上做法應用程式可以存取組織的所有信箱,如要限制個別信箱存取權限,需使用 PowerShell 設定 ApplicationAccessPolicy,詳情可參考文件:Limiting application permissions to specific Exchange Online mailboxes

Tutorial of registering application and creating client script in AzureAD and use it to access EWS API.


Comments

Be the first to post a comment

Post a comment


1 - 0 =