調整不要：EWS Managed APIで会議調整を自動化


この記事は、BrainPad Advent Calendar 2018 4日目の記事です。
今回は、Exchange Serverユーザーに贈る、会議自動調整プログラムについて紹介します。


# 会議の日程調整って面倒...
業務時間が限られている中、会議参加者間の日程調整はとても面倒ですよね。
時間をかけたくない作業の代表格と言っても過言ではありません。

そこで、会議調整にかける時間を削減すべく、
入力情報を元に会議候補日を返してくれるプログラムを作成してみようと思います。

# 準備
まずは以下のツールをダウンロードをします。
開発環境は、Visual Studio 2015を使っています。
開発言語はC#を使用しています。（Javaでも可能らしい）

- EWSアセンブリ
    - https://github.com/OfficeDev/ews-managed-api
- Visual Studio
    - https://visualstudio.microsoft.com/ja/vs/older-downloads/

# 利用する仕組みたち
####**・Exchange Server アーキテクチャ**
　⇒詳しくは[こちら](https://docs.microsoft.com/ja-jp/exchange/client-developer/exchange-web-services/ews-applications-and-the-exchange-architecture)を参照
  
####**・EWS Managed API**
　⇒詳しくは[こちら](https://docs.microsoft.com/ja-jp/exchange/client-developer/exchange-web-services/get-started-with-ews-managed-api-client-applications)を参照

# 自動調整するまでの流れ
**0. シナリオ**
**1. Exchange Server証明書の検証**
**2. ユーザー認証**
**3. 空き時間の検索**
**4. 会議予約作成**
**5. コンパイル**

---
### **0. シナリオ**
会議の「タイトル」、「時間」、「参加者」、「調整可能期間」を渡すと、
候補日を返却した上で、スケジュール登録してくれるプログラム作成する。

動作としては、今回はコマンドラインから引数を渡した形でプログラムを実行しようと思います。
※実際は画面を作ってラップした方が使い勝手が良いですが、取り急ぎ動くものを作ろうと思います。

実行文：
`MeetingArrangement.exe {会議名} {会議時間} {調整可能開始日} {調整可能終了日} {[参加者]}`

【引数解説】
args[0]  : 今回作成するプログラム（コンパイル済み）
args[1]  : 設定したい会議のタイトル
args[2]  : 会議の所要時間（minutes形式）
args[3]  : 調整期間の開始日(yyyyy/mm/dd)から今日の日付を引いた日数
args[4]  : 調整期間の終了日(yyyyy/mm/dd)から今日の日付を引いた日数
args[5-] : 参加者のメールアドレス


※args[3],args[4]については、少々まどろっこしいのですが、
"今日から何日後か"という情報をint型で使用したいので、上記のように記載してあります。
こちらは素直に日付を渡してプログラムの方で対応しても問題ない部分です。


さて、プログラム実行時に以下のようにコマンドラインから引数を受け取ります。

```
            string[] mail = new string[args.Length - 4];

            for (int i = 0; i < args.Length; ++i)
            {
                if (i == 0)
                {
                    subject = args[i];
                } else if (i == 1)
                {
                    meetingtime = Int32.Parse(args[i]);
                }
                else if (i == 2)
                {
                    startday = Int32.Parse(args[i]);
                }
                else if (i == 3)
                {
                    endday = Int32.Parse(args[i]);
                }
                else
                {
                    mail[i - 4] = args[i];
                }
            }
```

### **1. Exchange Server証明書の検証**
既定では、Exchange 2007 SP1 以降のバージョンの Exchange は、自己署名付き X509 証明書を使用して、EWS からの呼び出しを認証することになっております。
そのため、アプリから呼び出す場合は、EWSがExchangeに問合せする際に証明書検証をさせるための仕組み（コールバックメソッド）を組み込む必要があるようです。（詳細は[こちら](https://docs.microsoft.com/ja-jp/exchange/client-developer/exchange-web-services/how-to-validate-a-server-certificate-for-the-ews-managed-api)参照）

```
        private static bool CertificateValidationCallBack(
         object sender,
         System.Security.Cryptography.X509Certificates.X509Certificate certificate,
         System.Security.Cryptography.X509Certificates.X509Chain chain,
         System.Net.Security.SslPolicyErrors sslPolicyErrors)
        {
            // If the certificate is a valid, signed certificate, return true.
            if (sslPolicyErrors == System.Net.Security.SslPolicyErrors.None)
            {
                return true;
            }
            // If there are errors in the certificate chain, look at each error to determine the cause.
            if ((sslPolicyErrors & System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) != 0)
      {
                if (chain != null && chain.ChainStatus != null)
        {
                    foreach (System.Security.Cryptography.X509Certificates.X509ChainStatus status in chain.ChainStatus)
                    {
                        if ((certificate.Subject == certificate.Issuer) &&
                        (status.Status == System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.UntrustedRoot))
            {
                            // Self-signed certificates with an untrusted root are valid. 
                            continue;
                        }
            else
            {
                            if (status.Status != System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.NoError)
                            {
                                // If there are any other errors in the certificate chain, the certificate is invalid,
                                // so the method returns false.
                                return false;
                            }
                        }
                    }
                }
                // When processing reaches this line, the only errors in the certificate chain are 
                // untrusted root errors for self-signed certificates. These certificates are valid
                // for default Exchange server installations, so return true.
                return true;
            }
      else
      {
                // In all other cases, return false.
                return false;
            }
        }
```

### **2. ユーザー認証**
上記の証明書検証用のコールバック関数を記載した上で、ExchangeのURLに対して接続を実施します。
下記の例では、現在のログインユーザーで認証しております。
ドメイン名の部分はExchange Serverが配置されているドメイン名を指定します。
※当然、実行環境からExchange Server所属のドメインに対するアクセス権は必須となります。

デバックして、ここまで上手くいけばあとはしめたものです。

```
            ServicePointManager.ServerCertificateValidationCallback = CertificateValidationCallBack;

            ExchangeService service = new ExchangeService(ExchangeVersion.Exchange2010);
            service.UseDefaultCredentials = true;
            
            string mailaddress = @"https://{ドメイン名}/EWS/Exchange.asmx";

            service.Url = new Uri(mailaddress);
```

### **3. 空き時間の検索**
以下の関数で指定したメンバー間での空き時間を検索する部分を実装します。
下記の例では、3つの候補を出させて一番直近の1日について、
MakeMeetingScheduleでスケジュール登録するようにしています。
※MakeMeetingScheduleは次で説明します。

参加メンバーの属性（主催者、メンバー、必須の有無など）や、
空き時間検索ロジックのオプションについては、色々と指定できるようですので、
ご興味ある方は確認してみてください。


・[AteendeeInfo](https://msdn.microsoft.com/en-us/library/microsoft.exchange.webservices.data.attendeeinfo_members(v=exchg.80).aspx)
・[AvailabilityOptions](https://msdn.microsoft.com/ja-jp/library/microsoft.exchange.webservices.data.availabilityoptions_members(v=exchg.80).aspx)

```
 private static void GetSuggestedMeetingTimesAndFreeBusyInfo(ExchangeService service, string[] args, int meetingtime, int startday, int endday, int percentage)

        {
            // Create a collection of attendees. 
            List<AttendeeInfo> attendees = new List<AttendeeInfo>();

            string[] mail = new string[args.Length];
            for (int j = 0; j < args.Length; ++j)
            {
                mail[j] = args[j];

                attendees.Add(new AttendeeInfo()
                {
                    SmtpAddress = mail[j],
                    AttendeeType = MeetingAttendeeType.Required
                });
            }

            int k = 0;

            EmailAddressCollection myRoomLists = service.GetRoomLists();

            foreach (EmailAddress address in myRoomLists)
            {
                EmailAddress myRoomList = address.Address;
                System.Collections.ObjectModel.Collection<EmailAddress> myRoomAddresses = service.GetRooms(myRoomList);

                foreach (EmailAddress eachaddress in myRoomAddresses)
                {
                                // Specify options to request free/busy information and suggested meeting times.
                                AvailabilityOptions availabilityOptions = new AvailabilityOptions();
                                availabilityOptions.GoodSuggestionThreshold = percentage - 51;
                                availabilityOptions.MaximumNonWorkHoursSuggestionsPerDay = 0;
                                availabilityOptions.MaximumSuggestionsPerDay = 1;
                                // Note that 60 minutes is the default value for MeetingDuration, but setting it explicitly for demonstration purposes.
                                availabilityOptions.MeetingDuration = meetingtime;
                                //availabilityOptions.MergedFreeBusyInterval = 0;
                                availabilityOptions.MinimumSuggestionQuality = SuggestionQuality.Good;
                                availabilityOptions.DetailedSuggestionsWindow = new TimeWindow(DateTime.Now.AddDays(startday), DateTime.Now.AddDays(endday));
                                availabilityOptions.RequestedFreeBusyView = FreeBusyViewType.FreeBusy;

                                // Return free/busy information and a set of suggested meeting times. 
                                // This method results in a GetUserAvailabilityRequest call to EWS.
                                GetUserAvailabilityResults results = service.GetUserAvailability(attendees,
                                                                                             availabilityOptions.DetailedSuggestionsWindow,
                                                                                             AvailabilityData.FreeBusyAndSuggestions,
                                                                                             availabilityOptions);

                                DateTime dt;
                                DateTime st;
                                DateTime et;

                                foreach (Suggestion suggestion in results.Suggestions)
                                {
                                    foreach (TimeSuggestion timeSuggestion in suggestion.TimeSuggestions)
                                    {
                                        if (k == 0)
                                        {
                                            dt = suggestion.Date;
                                            st = timeSuggestion.MeetingTime;
                                            et = timeSuggestion.MeetingTime.Add(TimeSpan.FromMinutes(meetingtime));
                                            //                     Console.WriteLine("{0}, {1} - {2}", dt, st, et);
                                            MakeMeetingSchedule(service, mail, subject, st, meetingtime);
                                        }
                                        Console.WriteLine("{0}, {1} - {2}, [recommend]Room:{3}", suggestion.Date.ToShortDateString(), timeSuggestion.MeetingTime.ToShortTimeString(), timeSuggestion.MeetingTime.Add(TimeSpan.FromMinutes(meetingtime)).ToShortTimeString(), eachaddress.Address);
                                        k++;
                                        if (k == 3) break;
                                    }
                                    if (k == 3) break;
                                }
                }
                if (k == 3) break;
            }
        }

```

### **4. 会議予約作成**
3で取得したメンバーと空き時間情報をそのまま渡して、スケジュール登録を実施します。

```
private static void MakeMeetingSchedule(ExchangeService service, string[] mail, string subject, DateTime st, int meetingtime)
        {
            Appointment meeting = new Appointment(service);
            string[] attendee = new string[mail.Length];
            for (int i = 0; i < mail.Length; ++i)
            {
              attendee[i] = mail[i];
            }
            // Set the properties on the meeting object to create the meeting.
            meeting.Subject = subject;
            // meeting.Body = "Let's learn to really work as a team and then have lunch!";
            meeting.Start = st;            
            meeting.End = st.AddMinutes(meetingtime);
            // meeting.Location = "Conference Room 12";
            for (int i = 0; i < mail.Length; ++i) {
                meeting.RequiredAttendees.Add(attendee[i]);
            }
            // meeting.ReminderMinutesBeforeStart = 60;
            // Save the meeting to the Calendar folder and send the meeting request.
            meeting.Save(SendInvitationsMode.SendOnlyToAll);
        }
```

### **5. コンパイル**
最後に作成したプログラムをコンパイルしてexeファイルにします。
最新のコンパイラをダウンロードし、そちらで実施してください。
私が実施した際は以下からダウンロードしました。
https://github.com/dotnet/roslyn

使い方はページの下の方に書いてあるのでその通りに従えば実行できます。
コンパイルの際は、EWSのdllを[reference](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/compiler-options/reference-compiler-option)とする必要があるので、要注意です。

#まとめ
パーツ化するとさほど難しい内容ではないかと思います。
その他にも役立つ関数が用意されているので、面倒な作業は自動化していきましょう！