這些年 RPA(Robotic Process Automation) 是個熱門話題,日常瑣事的機械化動作丟給機器人處理,讓人類脫離手工作業地獄,怎麼想都是個好主意。不過,業界在談的 RPA 多指採購廠商開發的軟體,強調介面友善功能豐富又容易上手(甚至具備機器學習等 AI 功能),讓不會寫程式或的使用者也能將日常作業自動化,實現 Low-Code 甚至 No-Code 目標,擺脫對程式開發人員的依賴。

不過,身為程式開發老人,我有不一樣的看法,認為若真要充分發揮 RPA 精神,很難不走上寫程式這條路。所以程式開發人員倒也不用擔心被取代,自己寫機器人程式做自動化(對照 Low-Code/No-Code ,就叫它 Full-Code RPA 吧)在功能及效率上能輕易擊敗通用 RPA 軟體,還是有機會專攻講求整合性、客製化及執行效率的高端市場。 (延伸閱讀:關於 RPA (機器人流程自動化),我說的其實是...)

信件處理是很常見的人工作業瓶頸,而 Outlook 是許多企業常用的信件軟體,這篇就來研究如何寫支 .NET 6+ 程式搜尋 Outlook 的收信匣,找尋特定郵件,相信在許多 RPA 情境中能派上用場。

.NET 6 程式在桌面執行,存取 Outlook 最簡便的管道是透過 COM+ 介面。.NET Core/.NET 5+ 開始跨平台,但在 Windows 執行仍能存取 COM+ 元件。做法是在 Visual Studio 的 Solution Exploere 點選 Add COM Reference:

找到並參考 Microsoft Outlook 16.0 Object Library:

.csproj 將新增以下內容:

  <ItemGroup>
    <COMReference Include="Microsoft.Office.Interop.Outlook">
      <WrapperTool>tlbimp</WrapperTool>
      <VersionMinor>6</VersionMinor>
      <VersionMajor>9</VersionMajor>
      <Guid>00062fff-0000-0000-c000-000000000046</Guid>
      <Lcid>0</Lcid>
      <Isolated>false</Isolated>
      <EmbedInteropTypes>true</EmbedInteropTypes>
    </COMReference>
  </ItemGroup>

然後 .NET 6+ 程式中,先 using Outlook = Microsoft.Office.Interop.Outlook,之後使能 new Outlook.Application() 連上執行中的 Outlook,存取其 DOM 模型。

想在 Outlook 搜尋郵件、連絡人,需要一些背景知識,官方文章的 Filtering Items 是不錯的入門。以下是我整理搜尋收件匣的原理:

  • 先取得收件匣 Folder 物件,使用類似 SQL 的查詢語法對 Items 項目集合進行篩選,若筆數少可使用 Items.Find()/FindNext() 逐筆取回,Items.Restrict() 則可一次傳回符合條件的集合。
  • 要找到特定信件,還有個笨方法是對 Items 跑迴圈一筆一筆撈出來讀屬性比對(若查詢邏輯很特殊,這是唯一解),篩選功能可用類 SQL 語法查資料夾,更簡便且有效率。
  • Find()/Restrict() 用的類 SQL 查詢語法有兩種格式:Jet Query Language 及 DAV Searching and Locating(DASL),Jet 格式為 [Subject] = '...'、DASL 則為 @SQL=urn:schemas:httpmail:subject = '...',兩種查詢都支援 AND/OR 及一些簡單運算,但二者不能混用,而且只有 DASL 才支援 LIKE 查詢,要做到關鍵字查詢,只能用 DASL。
  • DASL 語法(@SQL=urn:schemas...) 會用到欄位對映 urn,可參考文件 Exchage Store Schema 取得。
  • 找到 MailItem 物件後,可由 Subject、Body、Attachments 讀取主旨、內文及附件,也可呼叫 Delete()、Move()、Reply()、Forward() 進行刪除、搬移、回覆及轉寄等動作,做出各種花式應用。

我寫了一個簡單範例,展示用寄件者名稱、主旨關鍵字、收件時間區間... 等條件搜尋 Outlook 收件匣:

using System.Diagnostics;
using System.Globalization;
using System.Text;
using Outlook = Microsoft.Office.Interop.Outlook;

Action<string> printTitle = (s) =>
{
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine();
    Console.WriteLine(s);
    Console.ResetColor();
};

printTitle("Test 1. Sender = 'Windows'");

SearchInbox(senderName: "Windows");

printTitle("Test 2. Subject LIKE '%Microsoft Learn%'");

SearchInbox(subjectKeywd: "Microsoft Learn");

printTitle("Test 3: Sender = 'Microsoft Azure', Subject LIKE '%Azure%', Date > 2023/1/1");

SearchInbox("Microsoft Azure", "Azure", new DateTime(2023, 1, 1));

printTitle("Test 4: Subject LIKE '%Azure%', Date > 2023/1/1 AND < '2023/1/20");

SearchInbox(null, "Azure", new DateTime(2023, 1, 1), new DateTime(2023, 1, 20));

void SearchInbox(string senderName = null, string subjectKeywd = null, DateTime? startTime = null, DateTime? endTime = null)
{
    if (Process.GetProcessesByName("OUTLOOK").Length > 0)
    {
        var app = new Outlook.Application();
        var ns = app.GetNamespace("MAPI");
        var inbox = ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
        var items = inbox.Items;
        var conds = new List<string>();
        Func<string, string> escape = (s) => s.Replace("'", "''");
        if (!string.IsNullOrEmpty(senderName))
        {
            conds.Add(@$"(""urn:schemas:httpmail:sendername"" = '{escape(senderName)}')");
        }
        if (!string.IsNullOrEmpty(subjectKeywd))
        {
            conds.Add(@$"(""urn:schemas:httpmail:subject"" LIKE '%{escape(subjectKeywd)}%')");
        }
        if (startTime != null)
        {
            conds.Add(@$"(""urn:schemas:httpmail:datereceived"" > '{startTime.Value:yyyy-MM-dd HH:mm:ss}')");
        }
        if (endTime != null)
        {
            conds.Add(@$"(""urn:schemas:httpmail:datereceived"" < '{endTime.Value:yyyy-MM-dd HH:mm:ss}')");
        }
        var filterString = "@SQL=" + string.Join(" AND ", conds.ToArray());
        var filterd = items.Restrict(filterString);
        if (filterd.Count == 0)
        {
            Console.WriteLine("Not Found");
        }
        else
        {
            foreach (var item in items.Restrict(filterString))
            {
                var mailItem = item as Outlook.MailItem;
                if (mailItem != null)
                {
                    Console.WriteLine($"{mailItem.SentOn:yyyy-MM-dd HH:mm:ss} / {mailItem.SenderName} / {mailItem.Subject}");
                }
            }
        }
    }
    else
    {
        Console.WriteLine("Outlook is not running");
    }
}

實測成功!

掌握以上技巧,我們就能寫程式在 Outlook 快速找到特定郵件、連絡人、行事曆,玩出更多有趣的應用。

Tutorial of using .NET 6 and Outlook COM reference to search Outlook inbox folder.


Comments

# by Jackson2847

995 老板聽了 RPA 感覺很神,希望行政人員也能用 RPA 減輕工作!! 讓行政人員學 RPA 倒不如叫 MIS 振作點...怎麼可能不寫 code 想要網路爬蟲,怎麼可能完全不懂 HTML 跟 JS

Post a comment