我想透過 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 文件:

廢話少說,放碼過來!

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 另有定義

Post a comment


92 + 8 =