程式範例 - 使用 EWS 收發 Office 365 / Exchange 信件
| | 9 | | ![]() |
我想透過 Email 觸發某些批次作業,構想是定期跑程式從 Exchange 或 Office 365 收信,由主旨跟內容決定作業內容,執行完成再透過 Email 回報結果,這篇將示範使用 C# + Managed EWS API 的程式寫法。
開始前先提一下,用程式整合 Exchange 的方法不只 EWS 一種,微軟有篇完整介紹:Exchange Online and Exchange development,包含 Graph REST APIs (Office 365/Exchange Online)、EWS、Outlook VBA、Exchange PowerShell 指令... 等。若連線對象為 Exchange Online,微軟強烈建議改用 Microsoft Graph,EWS 雖然還是可存取,但已不會再發展擴充新功能,至於 EWS 對企業 Exchange 的支援策略則會不變。考量我想通吃 Exchage Service 跟 Office 365,故仍選擇使用 EWS。
之前試過用 EWS 連 Exchange Server 發送紅色主旨信件跟讀取共用資料夾,這篇將著重整合 Office 365 跟收信的部分。
使用 Managed EWS 程式庫收發信的範例我主要是參考 MSDocs 文件:
- Get started with EWS Managed API client applications
- Work with Exchange mailbox items by using EWS in Exchange
廢話少說,放碼過來!
using Microsoft.Exchange.WebServices.Data;
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()
{
var ews = new ExchangeService(ExchangeVersion.Exchange2010_SP2);
//公司內部Exchange比較簡單,可慱入帳密登入
//ews.Credentials = new NetworkCredential(userName, password, domain);
//或使用執行程式的AD帳號自動登入
//ews.UseDefaultCredentials = true;
//Office 365 登入方式
//AppContants.Account 即為 Email 地址,如 "email@xxx.onmicrosoft.com"
ews.Credentials = new WebCredentials(AppContants.Account, AppContants.Password);
var sw = new Stopwatch();
sw.Start();
ews.AutodiscoverUrl(AppContants.Account, RedirectUrlVaidationCallback);
sw.Stop();
Console.WriteLine($"AutodiscoverUrl() in {sw.ElapsedMilliseconds:n0}ms");
return ews;
}
private static bool RedirectUrlVaidationCallback(string redirectionUrl)
{
return (new Uri(redirectionUrl).Scheme == "https");
}
static void Main(string[] args)
{
var ews = InitEws();
CheckInbox(ews);
SendEmail(ews);
}
static void CheckInbox(ExchangeService ews)
{
// 繫結到收件匣
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(AppContants.TestRecipient);
mail.Subject = $"發信測試: {DateTime.Now:HHmmss}";
mail.Body = new MessageBody(BodyType.Text, "Sent from Office 365");
mail.Send(); //或是 mail.SendAndSaveCopy();
}
}
}
登入部分 Exchange Server 或 Office 365 做法有點不同,Exchange Server 用 AD 帳號,通常設 UseDefaultCredentials = true 就好;使用 Office 365 信箱登入的話要用 WebCredential。AutodiscoverUrl() 可依據 Email 自動找到對應的 EWS 服務,使用時需提供一個 RedirectUrlVaidationCallback 檢查憑證,一般直接只檢查回傳網址為 https 並 return true 即可。參考
實測收信成功:
發信也沒問題:
但有個地方不理想。由收信擷圖第一行顯示,AutodiscoverUrl() 花了 14 秒才完成。關於這點,MSDocs 有篇 - Improving performance when using Autodiscover for Exchange 提到:Autodisover 背後的動作蠻複雜,涉及與 AD Domain Service 溝通、尋找 SCP (Service Connection Point)、嘗試多個 EndPoint... 等程序。文件提到 EnableScpLookup = false 可加速,經實測差異不大。回到根本問題,AutodiscoverUrl() 原本的設計想法是呼叫取得設定資訊後 Cache 起來重複利用,不該頻繁執行,我想到一個解法是將收發信功能寫成服務取代定期執行 EXE,如此服務啟動時執行一次即可。
不過進一步研究,我發現如果已知 EWS 服務網址,直接指定 ews.Url 就好了,大可省略耗時的 AutodiscoverUrl() 程序。所以,開發時執行一次 AutodiscoverUrl() 找出 Url 存成設定檔,未來直接讀取設定就好了。
還有一個確認 EWS Url 的做法是從網頁版 Outlook (https://outlook.office365.com/mail) 找右上角齒輪的設定 / 檢視所有 Outlook 設定:
再找郵件 / 同步電子郵件,查到 outlook.office365.com:
加上 ews/exchange.asmx 變成 https://outlook.office365.com/ews/exchange.asmx 就是 EWS 位址。
故簡化版如下,實測可省下十幾秒。
static ExchangeService InitEws()
{
var ews = new ExchangeService(ExchangeVersion.Exchange2010_SP2);
//實際運用時建議由設定檔讀取,需調整時才不用改程式
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 WebCredentials(AppContants.Account, AppContants.Password);
return ews;
}
寫死 EWS Url 有個風險是當 EWS Url 變更時程式會壞掉,需人工校正。EWS 服務修改網址的頻率不高,事前應該也會通知,到時再手工調整就好。若想做到無懈可擊,可以加入一段 try catch 呼叫簡單 EWS,若發生錯誤再補上 AutodiscoverUrl()。
Example of using Managed EWS API to receive and send Emails for Exchange Online.
Comments
# by Ike
想問… EWS 可以取得 Email Group 的 成員 有哪些嗎?
# by Jeffrey
tp lke, 像這樣嗎?https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-2010/hh532557(v%3Dexchg.80)
# by Ike
有像,來試試… 感謝~
# by Ike
傷心 竟然 Exchange 2007 以上才能用
# by Samuel
不好意思, 問個很菜的問題, AppContants 和 RedirectUrlVaidationCallback 不存在, 我使用Microsoft.Exchange.WebServices 2.2版本, 是否缺了什麼?
# by Samuel
抱歉, 看懂了, 原來AppContants 和 RedirectUrlVaidationCallback 另有定義
# by Max
不好意思, 想請教一個問題,我依照上述的範例程式嘗試發送一封信件,但是都一直收到此訊息 : Microsoft.Exchange.WebServices.Data.ServiceRequestException: 'The request failed. 遠端伺服器傳回一個錯誤: (401) 未經授權。' 我已經確認帳號與密碼沒有錯誤,可能是哪一個部分錯了。
# by Jeffrey
to Max, 會是因為帳號啟用了多重因素認證?
# by Eddie
我也遇到Max的問題,請問是什麼問題呢