【C#】NetOfficeを使用してOutlookの全メールをさらう

# 前書き①：メール多過ぎ問題

Outlookユーザーです。
メール多過ぎです。遅いです。なんだかきちんと検索されているか不安です。何とかしたいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138460/55c70472-7882-ab32-98e0-006effeedfc1.png)

# 前書き②：抜いてしまえばなんとかなるだろう

「データぶっこ抜き」さえすれば、あとはデータベースに突っ込むなりして
- 速く
- 検索条件が明らか

に過去のメールが引けるはず。

※直近メールはとりあえず気にせず、過去を振り返ることに注力。

# 前書き③：道具の選定

「OutlookのCOMを操作できればなんでもいい」になってしまうわけですが、
- 自分的に一番手が速いC#
- できるだけラクにやりたい（間接的にはコスト削減かもですが、直接私の口座に二兆円振り込まれるわけではないので手抜きしたい）

ということでNetOfficeを使うことにしました。
なんとなくCOM操作の感覚があれば、それなりに勘で使えるっぽいという杜撰な理由です。

https://netoffice.io/

https://github.com/NetOfficeFw/NetOffice

# 使い方

ここにサンプルがあるのでCloneしてビルドしてソースを見てみればいいですよ・・・と言ってしまうとここで終わってしまうので、続きます。

https://github.com/NetOfficeFw/Samples

# 今回の環境

- OS:Windows10 バージョン21H2
- 開発環境:Visual Studio 2022 Enterprise
 (※大したことはしてないのでCommunity Editionでも動くと思います。）
- .Net Framework4.8
- コンソールアプリ
- Outlook2016 

# やりたいこと

- Outlook内のメールをさらいたい
    - フォルダーが複数ある
    - フォルダーが入れ子になっている
    - アカウントも複数登録されている
    - とりあえず今回は「フォルダー名」「受信時間」「題名」「添付ファイル名」を標準出力に出すところまで。

ここまでできれ後は好きなようにできると思います。

# おことわり

- （どのようなデータにも言えることですが）抜き出したデータは丸裸なので取り扱いはご注意を。何があっても責任は負えません。
- 分かりやすさ優先で最低限の実装です（エラー処理が無いなど）。

# ただ既定ユーザーの受信フォルダーの中身を抜くだけであればとてもシンプル

- NuGetでNetOffice取得
- Outlookインスタンス作成
- MAPI取得
- 受信フォルダー取得
- 受信フォルダー内メールを取得

の手順で行えます。

``` C#:説明用（Disposeも書いてないので実用はNG）
using NetOffice;
using Outlook = NetOffice.OutlookApi;
using NetOffice.OutlookApi.Enums;
using NetOffice.OutlookApi;
~~~
// Outlookインスタンス作成
var outlookApplication = new Outlook.Application();

// MAPI取得
var outlookNS = outlookApplication.GetNamespace("MAPI");

// アカウントの受信フォルダー取得
var inboxFolder = outlookNS.GetDefaultFolder(OlDefaultFolders.olFolderInbox);

// フォルダー内のメール情報出力
foreach (var item in inboxFolder.Items)
{
    var mailitem = item as Outlook.MailItem;
    System.Console.WriteLine($"受信時刻[{mailitem.ReceivedTime}]:題名[{mailitem.Subject}]");
}
```

「デフォルトユーザーの受信フォルダー」の中身を取得しているだけなので

- アカウントが複数ある場合
- 受信フォルダー以下にフォルダーがある場合
- さらには受信フォルダーが入れ子になっている場合

複数アカウントあって、しこたまフォルダー作って振り分けてる私はこれだと救われないわけです。

# ということで料理開始

## プロジェクト一式

GitHubに置いてあります。

https://github.com/fu-foo/NetOfficeOutlookGetAllMailsSample

## ソース全景

<details><summary>ソースコードを表示（折りたたみ）</summary><div>

```C#:Program.cs
using Outlook = NetOffice.OutlookApi;
using NetOffice.OutlookApi.Enums;
using NetOffice.OutlookApi;
using System.Linq.Expressions;

namespace Fu.NetOfficeOutlookGetAllMailsSample
{
    internal class Program
    {
        /// <summary>
        /// 保存フォルダー
        /// </summary>
        private static string _folder = ROOT_FOLDER;
        private const string ROOT_FOLDER = "受信フォルダー";

        /// <summary>
        /// Entry Point
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            // Outlookインスタンス作成
            var outlookApplication = new Outlook.Application();

            // MAPI取得
            var outlookNS = outlookApplication.GetNamespace("MAPI");

            // 存在するアカウントで繰り返し
            foreach (var account in outlookNS.Session.Accounts)
            {
                // アカウント取得
                var acc = account as Outlook.Account;

                // アカウントの受信フォルダー取得
                var inboxFolder = account.DeliveryStore.GetDefaultFolder(OlDefaultFolders.olFolderInbox);

                // メール数取得
                System.Console.WriteLine($"アカウント[{acc.DisplayName}]:フォルダー内メール数:{inboxFolder.Items.Count}");

                // メール内容取得
                GetMails(inboxFolder);

                // 出力用のフォルダー階層クリア
                _folder = ROOT_FOLDER;

                // Dispose
                inboxFolder.Dispose();
                acc.Dispose();
                account.Dispose();
            }

            // 破棄
            outlookNS.Session.Accounts.Dispose();
            outlookNS.Session.Dispose();
            outlookNS.Dispose();

            // 破棄
            outlookApplication.Quit();
            outlookApplication.Dispose();
        }

        /// <summary>
        /// メール取得
        /// </summary>
        /// <param name="folder"></param>
        static void GetMails(MAPIFolder folder)
        {
            // フォルダー内のメール情報出力
            foreach (var item in folder.Items)
            {
                // 通常のメールと会議をとりあえずサポート
                var mailitem = item as Outlook.MailItem;
                var meetingitem =  item as MeetingItem;

                // メールの場合
                if (mailitem != null)
                {
                    System.Console.WriteLine($"受信時刻[{mailitem.ReceivedTime}]:題名[{mailitem.Subject}]");
                    foreach (var attachments in mailitem.Attachments)
                    {
                        System.Console.WriteLine($"添付ファイル:{attachments.DisplayName}");
                        attachments.Dispose();
                    }
                    // 破棄
                    mailitem.Attachments.Dispose();
                    mailitem.Dispose();
                }

                // 会議の場合
                if (meetingitem != null)
                {
                    System.Console.WriteLine($"受信時刻[{meetingitem.ReceivedTime}]:題名[{meetingitem.Subject}]:本文[{meetingitem.Body}]");

                    // 添付ファイル取得
                    foreach (var attachments in mailitem.Attachments)
                    {
                        System.Console.WriteLine($"添付ファイル:{attachments.DisplayName}");
                        attachments.Dispose();
                    }
                    // 破棄
                    mailitem.Attachments.Dispose();
                    meetingitem.Dispose();
                }
            }

            // 下位のフォルダー取得→再帰実行
            foreach (var nowDirFolder in folder.Folders)
            {
                var folderitem = nowDirFolder as MAPIFolder;

                // 今いる階層と今回のフォルダーを連結
                _folder += "/" + folderitem.Name;
                System.Console.WriteLine($"フォルダー[{_folder}]メール数:{folderitem.Items.Count}");

                // 再帰
                GetMails(folderitem as MAPIFolder);

                // 破棄
                folderitem.Dispose();
            }

            // 破棄
            folder.Dispose();

            // フォルダーがなければクリア
            _folder = ROOT_FOLDER;
        }
    }
}
```
</div></details>

## NuGet

NuGetでNetOfficeFw.Outlookを入れましょう。（NetOfficeFw.OfficeとNetOfficeFw.Coreは一緒に入ってくれます。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138460/36e4fe3b-b9d6-8450-076e-59dca4676b10.png)

## 「該当するインターフェイスを使用してください」

NuGetで取得してさぁ使うぞ・・・なんですが無慈悲に「該当するインターフェイスを使用してください。」とエラーが出ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138460/7cd57978-7e5f-e633-2728-c45faa247073.png)

NuGetで取得した対象の
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138460/99f78109-3c3c-5bae-0a9d-adae20177ef5.png)

「相互運用型の埋め込み」はFalseに変更してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138460/b344bcb6-34f3-fcf1-8fc4-e7c9e1594357.png)


## ソース解説①:最初のお約束部分

Outlookのインスタンス作成、MAPI取得を最初に行います。


```C#:Program.cs
// Outlookインスタンス作成
var outlookApplication = new Outlook.Application();

// MAPI取得
// 環境によっては事前にOutlook起動が必要？
var outlookNS = outlookApplication.GetNamespace("MAPI");
```

## ソース解説②：アカウントの取得

Session.AccountsでOutlook内のアカウント全てが取得できます。
各アカウント逐一処理で、受信フォルダー取得～メール取得を行います。

```C#:Program.cs
// 存在するアカウントで繰り返し
foreach (var account in outlookNS.Session.Accounts)
 {
    // アカウント取得
    var acc = account as Outlook.Account;

    // アカウントの受信フォルダー取得
    var inboxFolder = account.DeliveryStore.GetDefaultFolder(OlDefaultFolders.olFolderInbox);

    // メール数取得
    System.Console.WriteLine($"アカウント[{acc.DisplayName}]:フォルダー内メール数:{inboxFolder.Items.Count}");

    // メール内容取得
    GetMails(inboxFolder);

    // 出力用のフォルダー階層クリア
    _folder = ROOT_FOLDER;

    // Dispose
    inboxFolder.Dispose();
    acc.Dispose();
    account.Dispose();
}
```

## ソース解説③：メール取得

※ソース全景のほうはメールだけでなくタスクの取得と添付ファイルの取得を書いてますが、ここの説明はメール取得に絞ります。

フォルダー内フォルダーのような構造の場合・・・再帰の出番ですね。
フォルダー内にフォルダーがあれば再度GetMails呼び出しでフォルダー内にフォルダーが無くなるまで繰り返します。

```C#:Sample.cs
static void GetMails(MAPIFolder folder)
{
	// フォルダー内のメール情報出力
	foreach (var item in folder.Items)
	{
        var mailitem = item as Outlook.MailItem;

        // メールの場合
        if (mailitem != null)
        {
            System.Console.WriteLine($"受信時刻[{mailitem.ReceivedTime}]:題名[{mailitem.Subject}]");

            // 破棄
            mailitem.Dispose();
        }
    }

    // 下位のフォルダー取得→再帰実行
    foreach (var nowDirFolder in folder.Folders)
    {
        var folderitem = nowDirFolder as MAPIFolder;

        // 今いる階層と今回のフォルダーを連結
        _folder += "/" + folderitem.Name;
        System.Console.WriteLine($"フォルダー[{_folder}]メール数:{folderitem.Items.Count}");

        // 再帰
        GetMails(folderitem as MAPIFolder);

        // 破棄
        folderitem.Dispose();
    }

    // 破棄
    folder.Dispose();

    // フォルダーがなければクリア
    _folder = ROOT_FOLDER;
}
```

### 実行結果

伏せだらけですが、こんな感じになります。
フォルダー内部を一回で読むので件数があると待たされますが、Outlookさんが頑張っているので待ちましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138460/e7f90606-932a-4b58-1dc1-da72f43941cf.png)

## 終わりに

「とにかく全てのメール」に触れればこっちのもの、あとはOutlook.MailItemの中を見て色々拾えます。

個人的には簡素なUI＋SQLiteで多量のメールの中からサクっと検索できるツールでも作ろうかなーとボンヤリ考えています。