使用 OAuth 認證存取 Office 365 雲端 Exhange 收發信
4 | 6,158 |
前情提要:之前寫程式存取 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 授權模式有兩種:
- Delegagted Permission
需使用者或管理者在同意網頁完成授權,讓應用程式代表「登入中的使用者」呼叫 API 進行動作 - 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
# by Frank
大大您好: 我有看過您之前 基本驗證 使用 EWS 收發 Office 365 雲端 Exhange 信箱,https://blog.darkthread.net/blog/ews-office365/,幫助我滿多,最近 基本驗證 被棄用了,馬上來查就發現大大的文章了 收益良多。 想請問使用 本文這個oauth2.0 方法 會有另外的費用嗎 ,或是額度費用之類的? 謝謝 我有看到 另一個 microsoft graph api 是需要費用的,這兩個應該不一樣,只是想確認一下 謝謝 受益良多 再次謝謝
# by Jeffrey
to Frank,依我查到的結果,只是讀寫 Office 365 信箱內容,即使是用 Graph API 也是不收費的,它涵蓋在 O365 授權中。 Read/Write operations on O365 mailboxes using Graph APIs are not chargeable. 出處 https://learn.microsoft.com/en-us/answers/questions/993835/graph-api-pricing.html
# by Frank
Jeffrey 大好 了解 謝謝,看起來是。 抱歉 ,Graph API 的部分我不熟悉,想請問哪裡可以看到比較細節,或比較完整的收費方式嗎? 謝謝 我是查到這個,只有寫到 "每 1,000 個擷取的物件 " https://azure.microsoft.com/zh-tw/pricing/details/graph-data-connect/ 感謝回覆
# by 小胖
To 黑大: 感謝你在另一篇文章的回覆 我整個爬完官網的設定,實作完後才發現你這篇文章XD 如果早一點發現就好了XD