Microsoft Graph で Microsoft 365 Enterprise 組織内のメールを取得する

Microsoft Graph で Microsoft 365 Enterprise 組織内のメールを取得する方法を試してみました。これを実現させるためには、アプリケーションを許可（サインインしたユーザー無しで、バックグランドサービスまたはデーモンとして実行）するように、Azure Active Directory にアプリケーションを登録する必要があります。

NLPやGraph等と組み合わせると、組織の効率化に繋がる分析に使えそうですね。

# Azure Active Directory にアプリケーション登録
まずは、組織の Azure Active Directory にアプリケーションを登録します。Microsoft Azureのポータルから、Azure Active Directory に移動してください。
![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32580/c2aeee2d-cfda-3467-1209-f6c4262ac7e4.png)
左側のメニューの「アプリの登録」から、「新規登録」を選びます。
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32580/2b955a80-0ccd-6cad-fc40-478c23321ddf.png)
登録するアプリの名前を入力し、今回は「シングルテナント」を選択します。入力が終わったら「登録」します。
![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32580/00280070-6802-87e0-e8e9-60cc8a94aa90.png)
登録したアプリ（office-app）の画面になるので、「アプリケーション（クライアント）ID」、「ディレクトリ（テナント）ID」をメモしておいてください。
![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32580/d29d0a28-b86a-a22c-927e-f3d09474d69f.png)
「APIのアクセス許可」から「アクセス許可の追加」をします。
![06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32580/af15891f-dc25-fd00-81df-a1e70455e483.png)
今回は、ディレクトリの読み込みと、ユーザの読み込み、メールの読み込みのアクセスに関して許可を追加します。まずは、「Microsoft Graph」を選択します。
![07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32580/e8694045-2e2f-ae95-f9a4-867d36667ef6.png)
「アプリケーションの許可」を選択して、「User.Read.All」「Directory.Read.All」「Mail.Read.All」のアクセス許可を追加します。検索すると見つけやすいです。（今回のメールとは関係ありませんが、Files.Read.Allも追加しました）
![08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32580/ec0a54e4-d8cb-5bce-0d70-ab6ab8b9d2cb.png)
追加した後は、組織のテナントに管理者の同意を与えます。（今回のメールとは関係ありませんが、Files.Read.Allも追加してます）
![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32580/34da3453-5dc0-57c3-164c-22f5fa68ea6a.png)
最後に、アプリからのアクセスに使用するクライアントシークレットを作成します。シークレット（値）は一度しか表示されないので、必ずメモしておいてください。
![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32580/9435bf9c-c5fc-dfef-34b5-0019e3947303.png)

# バックグランドサービスとして動くアプリを作成
まずは、サインインしたユーザー無しで、バックグランドサービスまたはデーモンとして実行できるように、認証プロバイダを作成します。

```c#:ClientSecretAuthProvider.cs
using Microsoft.Graph;
using Microsoft.Identity.Client;
using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace GraphMessagesSample
{
    public class ClientSecretAuthProvider : IAuthenticationProvider
    {
        private IConfidentialClientApplication msalClient;
        private string appId;
        private string clientSecret;
        private string[] scopes;
        private string tenantId;

        public ClientSecretAuthProvider(string appId, string[] scopes, string tenantId, string clientSecret)
        {
            this.appId = appId;
            this.clientSecret = clientSecret;
            this.scopes = scopes;
            this.tenantId = tenantId;

            this.msalClient = ConfidentialClientApplicationBuilder.Create(this.appId)
                .WithAuthority(AzureCloudInstance.AzurePublic, this.tenantId)
                .WithClientSecret(this.clientSecret)
                .Build();
        }

        public async Task<string> GetAuthorizationHeader()
        {
            try
            {
                var result = await this.msalClient
                    .AcquireTokenForClient(this.scopes)
                    .ExecuteAsync();
                return result.CreateAuthorizationHeader();
            }
            catch (Exception exception)
            {
                Console.WriteLine($"Error getting access token: {exception.Message}");
                return null;
            }
        }

        public async Task AuthenticateRequestAsync(HttpRequestMessage requestMessage)
        {
            requestMessage.Headers.Add("Authorization", await GetAuthorizationHeader());
        }
    }
}
```

次に、Graph API から、セキュリティグループ（GetGroupsAsync）、グループのユーザー（GetGroupMembersAsync）、メール（GetMessagesAsync）を取得するプログラムを作成します。

```c#:GraphHelper.cs
using Microsoft.Graph;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace GraphMessagesSample
{
    public class GraphHelper
    {
        private static GraphServiceClient graphClient;
        public static void Initialize(IAuthenticationProvider authProvider)
        {
            graphClient = new GraphServiceClient(authProvider);
        }

        public static async Task<IEnumerable<Group>> GetGroupsAsync()
        {
            try
            {
                List<Group> groups = new List<Group>();
                var resultPage = await graphClient.Groups.Request().GetAsync();
                while (true)
                {
                    groups.AddRange(resultPage.CurrentPage);
                    if (resultPage.NextPageRequest == null)
                    {
                        break;
                    }
                    resultPage = resultPage.NextPageRequest.GetAsync().Result;
                }
                return groups;
            }
            catch (ServiceException ex)
            {
                Console.WriteLine($"Error getting events: {ex.Message}");
                return null;
            }
        }
        public static async Task<IEnumerable<DirectoryObject>> GetGroupMembersAsync(string groupId)
        {
            try
            {
                List<DirectoryObject> dirobjects = new List<DirectoryObject>();
                var resultPage = await graphClient.Groups[groupId].Members.Request().GetAsync();
                while (true)
                {
                    dirobjects.AddRange(resultPage.CurrentPage);
                    if (resultPage.NextPageRequest == null)
                    {
                        break;
                    }
                    resultPage = resultPage.NextPageRequest.GetAsync().Result;
                }
                return dirobjects;
            }
            catch (ServiceException ex)
            {
                Console.WriteLine($"Error getting events: {ex.Message}");
                return null;
            }
        }
        public static async Task<IEnumerable<Message>> GetMessagesAsync(string userId)
        {
            try
            {
                List<Message> dirobjects = new List<Message>();
                var resultPage = await graphClient.Users[userId].Messages.Request().GetAsync();
                while (true)
                {
                    dirobjects.AddRange(resultPage.CurrentPage);
                    if (resultPage.NextPageRequest == null)
                    {
                        break;
                    }
                    resultPage = resultPage.NextPageRequest.GetAsync().Result;
                }
                return dirobjects;
            }
            catch (ServiceException ex)
            {
                Console.WriteLine($"Error getting events: {ex.Message}");
                return null;
            }
        }
    }
}
```

あとは、これらを組み合わせてメールを表示するプログラムを作成します。組織のグループに属しているユーザのメールの件名を全て表示するので、特定グループだけ表示したい場合などはプログラムを変更してください。

```c#:Program.cs
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using Microsoft.Graph;

namespace GraphMessagesSample
{
    class Program
    {
        static Dictionary<string, string> LoadClientSecretAppSettings()
        {
            Dictionary<string, string> result = null;
            // Get config ftom AppSettings
            var appConfig = new ConfigurationBuilder()
                .AddUserSecrets<Program>()
                .Build();
            var appId = appConfig["appId"];
            var scopes = appConfig["scopes"];
            var tenantId = appConfig["tenantId"];
            var clientSecret = appConfig["clientSecret"];
            if (string.IsNullOrEmpty(appId) == false &&
                string.IsNullOrEmpty(scopes) == false &&
                string.IsNullOrEmpty(tenantId) == false &&
                string.IsNullOrEmpty(clientSecret) == false)
            {
                result = new Dictionary<string, string>()
                {
                    {"appId", appId},
                    {"scopes", scopes},
                    {"tenantId", tenantId},
                    {"clientSecret", clientSecret}
                };
            }
            return result;
        }

        static void GetMessages()
        {
            var groups = GraphHelper.GetGroupsAsync().Result;
            foreach (var group in groups)
            {
                Console.WriteLine($"group.Id: {group.Id}");
                Console.WriteLine($"group.DisplayName: {group.DisplayName}");
                var members = GraphHelper.GetGroupMembersAsync(group.Id).Result;
                foreach (Microsoft.Graph.User member in members)
                {
                    Console.WriteLine($"member.Id: {member.Id}");
                    Console.WriteLine($"member.DisplayName: {member.DisplayName}");
                    Console.WriteLine($"member.UserPrincipalName: {member.UserPrincipalName}");
                    var messages = GraphHelper.GetMessagesAsync(member.Id).Result;
                    foreach (Microsoft.Graph.Message message in messages)
                    {
                        Console.WriteLine($"message.Subject: {message.Subject}");
                    }
                }
            }
        }

        static void Main(string[] args)
        {
            IAuthenticationProvider authProvider = null;

            var appConfig = LoadClientSecretAppSettings();
            if (appConfig == null)
            {
                Console.WriteLine("Missing or invalid AppSettings");
                return;
            }
            var appId = appConfig["appId"];
            var scopesString = appConfig["scopes"];
            var scopes = scopesString.Split(';');
            var tenantId = appConfig["tenantId"];
            var clientSecret = appConfig["clientSecret"];
            // Initialize the auth provider
            authProvider = new ClientSecretAuthProvider(appId, scopes, tenantId, clientSecret);
            // Initialize Graph client
            GraphHelper.Initialize(authProvider);
            // Get messages
            GetMessages();
        }
    }
}
```

# 実行する
上で書いたコードは、githubにおいてあるので、cloneします。

```sh
git clone https://github.com/KentaroAOKI/GraphMessagesSample.git
cd GraphMessagesSample
```

Azure Active Directory でアプリケーション登録をした際にメモした値を使って設定します。

```sh
dotnet user-secrets init
dotnet user-secrets set appId "アプリケーション（クライアント）ID"
dotnet user-secrets set scopes "https://graph.microsoft.com/.default"
dotnet user-secrets set tenantId "ディレクトリ（テナント）ID"
dotnet user-secrets set clientSecret "クライアントシークレット"
```

ビルドして実行します。

```
dotnet build
dotnet run
```

# さいごに

メール件名だけでなく、宛先や本文も取得できます。以下のGraph APIドキュメントを参考にしてください。

https://docs.microsoft.com/ja-jp/graph/api/resources/message?view=graph-rest-1.0

メールは正しくパースされているし、件名、本文、宛先など、エンコードが統一されて取得できるので、すごく便利ですね。Graph APIの凄さに改めて驚きました。