Outlook や OWA の予定表のショートカットを EWS で取得する

Outlook や OWA で開いた他人の予定表は画面左側にリスト表示されますが、この情報を EWS で取得したいという話をよく聞きます。
正しい呼び方がわからないのでここでは説明の都合上「予定表のショートカット」と呼びますが、残念ながら EWS にはこの内容を取得する サポートされた API がありません。

2016080501

それでも情報はメールボックスに保存されているので、情報を取得できないこともないです。
サポートされる API がない以上、今後実装が変わる可能性はありますが、まずはどのように情報が保存されているのかを確認する必要があります。

予定表のショートカットは、メールボックスのルートの中の Common Views フォルダーの隠しアイテムとして存在しています。
また、予定表のショートカットは必ず何らかのグループに所属していますが、グループも同じく Common Views フォルダーの隠しアイテムで、アイテムのクラスまで一緒です。
プロパティを見ることでグループかどうか判断がつきますが、詳しくは MSDN に記載があります。

TITLE: [MS-OXOCFG]: PidTagWlinkType Property
URL: https://msdn.microsoft.com/en-us/library/ee157753(v=exchg.80).aspx

TITLE: [MS-OXOCFG]: PidTagWlinkFolderType Property
URL: https://msdn.microsoft.com/en-us/library/ee218711(v=exchg.80).aspx

TITLE: [MS-OXOCFG]: PidTagWlinkGroupName Property
URL: https://msdn.microsoft.com/en-us/library/ee178417(v=exchg.80).aspx

さらに、誰の予定表に対するショートカットなのかは PidTagWlinkAddressBookEID プロパティから判断できます。

TITLE: [MS-OXOCFG]: PidTagWlinkAddressBookEID Property
URL: https://msdn.microsoft.com/en-us/library/ee159163(v=exchg.80).aspx

この情報をもとにコードを書きます。
初めに、PidTagWlinkAddressBookEID に格納されている EntryID を表すクラスを定義します。

        internal struct AddressEntryID
        {
            public int Flags { get; private set; }
            public Guid ProviderUID { get; private set; }
            public int Version { get; private set; }
            public int Type { get; private set; }
            public string X500DN { get; private set; }

            // バイナリ データから値を取得
            // 各情報のサイズは以下を参照
            // https://msdn.microsoft.com/en-us/library/ee160588(v=exchg.80).aspx
            public AddressEntryID(byte[] bytes)
            {
                using (var reader = new BinaryReader(new MemoryStream(bytes)))
                {
                    Flags = reader.ReadInt32();

                    byte[] buff = new byte[16];
                    reader.Read(buff, 0, buff.Length);
                    ProviderUID = new Guid(buff);

                    Version = reader.ReadInt32();
                    Type = reader.ReadInt32();

                    X500DN = Encoding.UTF8.GetString(reader.ReadBytes(bytes.Length - 29));
                }
            }
        }

続いて、メインの処理です。

        private void GetSharedCalendars(ExchangeService service)
        {
            // プロパティ定義
            ExtendedPropertyDefinition PidTagWlinkType = new ExtendedPropertyDefinition(0x6849, MapiPropertyType.Long);
            ExtendedPropertyDefinition PidTagWlinkFolderType = new ExtendedPropertyDefinition(0x684f, MapiPropertyType.Binary);
            ExtendedPropertyDefinition PidTagWlinkGroupName = new ExtendedPropertyDefinition(0x6851, MapiPropertyType.String);
            ExtendedPropertyDefinition PidTagWlinkAddressBookEID = new ExtendedPropertyDefinition(0x6854, MapiPropertyType.Binary);

            // "Common Views" フォルダーの取得
            FolderId rootFolder = new FolderId(WellKnownFolderName.Root);
            SearchFilter commonViewsfilter = new SearchFilter.IsEqualTo(FolderSchema.DisplayName, "Common Views");
            var commonViewsFolder = service.FindFolders(rootFolder, commonViewsfilter, new FolderView(1));

            // "予定表グループの取得"
            // PidTagWlinkType = 4 はグループ
            // https://msdn.microsoft.com/en-us/library/ee157753(v=exchg.80).aspx
            // PidTagWlinkFolderType = "AngGAAAAAADAAAAAAAAARg==" は予定表フォルダー (バイナリ データを Convert.ToBase64String で変換した値)
            // https://msdn.microsoft.com/en-us/library/ee218711(v=exchg.80).aspx
            PropertySet groupPropertySet = new PropertySet(BasePropertySet.FirstClassProperties, PidTagWlinkType, PidTagWlinkAddressBookEID, PidTagWlinkFolderType);
            SearchFilter groupsFilter = new SearchFilter.SearchFilterCollection(LogicalOperator.And,
                new SearchFilter.IsEqualTo(PidTagWlinkType, 4),
                new SearchFilter.IsEqualTo(PidTagWlinkFolderType, "AngGAAAAAADAAAAAAAAARg=="));
            ItemView groupsView = new ItemView(100) { PropertySet = groupPropertySet, Traversal = ItemTraversal.Associated };
            var groups = commonViewsFolder.Folders[0].FindItems(groupsFilter, groupsView);

            foreach (var group in groups)
            {
                textBox1.Text += "Group : " + group.Subject + "\r\n";

                // グループ内の予定表を取得
                // PidTagWlinkType = 2 は別ユーザーの共有フォルダー
                PropertySet calendarPropertySet = new PropertySet(BasePropertySet.FirstClassProperties, PidTagWlinkAddressBookEID);
                SearchFilter calendarSearchFilter = new SearchFilter.SearchFilterCollection(LogicalOperator.And,
                    new SearchFilter.IsEqualTo(PidTagWlinkType, 2),
                    new SearchFilter.IsEqualTo(PidTagWlinkGroupName, group.Subject));
                ItemView calendarView = new ItemView(100) { PropertySet = calendarPropertySet, Traversal = ItemTraversal.Associated };
                var calendars = commonViewsFolder.Folders[0].FindItems(calendarSearchFilter, calendarView);

                foreach (var calendar in calendars)
                {
                    textBox1.Text += "  Calendar : " + calendar.Subject + "\r\n";

                    // 予定表を開くために LegacyExchangeDN を取得し、アドレスを出力
                    byte[] wlinkAddressBookEID;
                    if (calendar.TryGetProperty(PidTagWlinkAddressBookEID, out wlinkAddressBookEID))
                    {
                        AddressEntryID entryID = new AddressEntryID(wlinkAddressBookEID);
                        NameResolutionCollection resolveResult = service.ResolveName(entryID.X500DN, ResolveNameSearchLocation.DirectoryOnly, false);
                        textBox1.Text += "    Address = " + resolveResult[0].Mailbox.Address + "\r\n\r\n";
                    }
                }
            }
        }

これでグループとそこに所属する予定表のショートカットが取得できます。

2016080502

このように [個人用の予定表] に入るべき既定で用意されている予定表やフル アクセス権を持つユーザーの予定表は、この方法では取得することができません。
PidTagWlinkType が 2 であることを指定しているためです。
この部分も取得する必要があるのであれば、さらに別の条件を指定して情報を取ってくる必要がありますが、PidTagStoreEntryId の解析が必要になるようです。