Exchange Online 開発 : 通知 (Notification) の実装


環境 : Microsoft Online Services, Visual Studio 2008

Exchange Online 開発

こんにちは。

GMail 用の Notifier アプリケーションや Mailer アプリケーションなど、世の中には、便利なツールがいくつか存在します。これと同様の仕組みを Exchange Online でも、もちろん、実装できます。
今日は、昨日に続き、EWS (Exchange Web Services) を使った通知 (Notification) の実装方法について説明しましょう。

通知 (Notification) の概念

通知の仕組みは、例えば、Inbox のメール一覧を定期的に取得して、 この内容を前回と比較すれば実装できるかもしれません。しかし、今やメールは、日常業務における主要アプリケーションの 1 つです。例えば、数百、数千というメールアイテムを対象に、毎回こうした処理をおこなうのは現実的ではありません。
このため、EWS (Exchange Web Services) では、こうした通知 (Notification) に対応した専用の API が提供されています。ここでは、この通知の概念について説明します。

補足 :  Mailer や、他アプリケーションと Exchange とのアイテムの同期など、アプリケーションの終了後も状態を維持 (連携) したい場合には、同期 (Synchronization) の API が使用できます。これについては、この投稿の「さいごに」で軽く説明します。

まず、EWS を使用した通知には、以下の 2 つのモデルがあることを理解しておいてください。(さいごに説明しますが、実は、Exchange 2010 SP1 以降だと、別のモデルを使用することもできます。)

  • Push Notification :
    クライアント側にエンドポイントを構成しておき、Exchange サーバーに対してイベント ハンドラーを登録 (Subscribe) することで、作成、変更、削除などのイベントがサーバー上に発生した際に、サーバー側からクライアントのエンドポイントに対して通知がおこなわれます。
  • Pull Notification :
    クライアントから、CAS (Client Access Server) に対してイベント通知を登録することで、Exchange サーバーでイベントが発生するたびに、この CAS に対してそのイベントが通知され、CAS に蓄積されます。クライアントからは、定期的に、この CAS へのイベント確認をおこなって、もしイベントが存在していたら、その内容を取得します。
    いわゆる、Polling のモデルです。

このアーキテクチャの詳細は、以下に、図を使って説明されています。

MSDN : Exchange Server – Event Notification

http://msdn.microsoft.com/en-us/library/aa579128(EXCHG.140).aspx

皆さんの会社でも、インターネットへの接続では、ファイウォールや、プロキシー サーバーなどが介在している場合が多いでしょう。Push Notification では、上記のように、サーバーからクライアントへの逆向きの接続がおこなわれるため、企業内 (On-Premise) の Exchange Server との連携では現実的ですが、Exchange Online で、この Push Notification を使用するのは現実的とは言えません。
このため、ここでは、Pull Notification を使用して、Exchange Online と接続した通知アプリケーションを構築したいと思います。

通知 (Notification) の実装

Visual Studio プロジェクトの新規作成と、設定方法については、前回 の記事を参照してください。ここでは、プログラム コードの内容を説明します。

まず、以下に、通知 (Notification) の処理の大まかな流れを説明します。

上述の通り、まず、クライアント側から CAS に対して、Subscribe の要求 (Subscribe メソッドの実行) をおこなうことで、Exchange Server で発生したイベントの内容が、CAS 上に通知され、蓄積されます。
クライアントからは、この Subscribe の際に取得した Subscription Id を使用して、定期的に、GetEvents メソッドを使用して差分のイベント情報を取得し、必要な処理をおこないます。
そして、クライアントの終了時には、Unsubscribe メソッドを使って、購読の終了を CAS に通知します。

この際、ウォーターマーク (Watermatk) という概念に注意してください。
GetEvents によるイベントの取得の際は、どこまでイベントの取得をおこなったのか (次回は、どこから取得するか)、毎回 目印をつけておき、続きからイベントを取得します。この目印が、Watermark になります。(最初の Subscribe 時にも、Watermark を取得しておきます。)

以下に、少々長めですが、サンプル コードを掲載します。
今回は、サンプルを簡単にするため、単一スレッドで通知 (Notification) の確認をおこなっていますが、実際の開発では、勿論、Timer などを使用して実装してください。(例によって、分かりやすさ重視のため、エラー処理、リソース管理などは、かなりさぼって書いています。)

補足 : Managed API を使用したサンプルコードは、この投稿の最後に掲載します。(2010/11/20 追記)

using MailTestApplication.ServiceReference1;
. . .

// 接続
ExchangeServicePortTypeClient cl = new ExchangeServicePortTypeClient("MyBPOSEndpoint");
cl.ClientCredentials.UserName.UserName = "demouser1@tsmatsuz.apac.microsoftonline.com";
cl.ClientCredentials.UserName.Password = "XXXXXXX";
cl.Open();

// Pull Notification の購読 (Subscribe) リクエストの作成
PullSubscriptionRequestType subscribe1 = new PullSubscriptionRequestType();
SubscribeType subscribeReq = new SubscribeType
{
    Item = subscribe1
};

// 監視対象のフォルダーとして Inbox フォルダー, Deleted フォルダーを設定
subscribeReq.Item.FolderIds = new BaseFolderIdType[2]
{
    new DistinguishedFolderIdType()
    {
        Id = DistinguishedFolderIdNameType.inbox
    },
    new DistinguishedFolderIdType()
    {
        Id = DistinguishedFolderIdNameType.deleteditems
    }
};

// 作成、削除、移動のイベントを取得
subscribe1.EventTypes = new NotificationEventTypeType[3]
{
    NotificationEventTypeType.NewMailEvent,
    NotificationEventTypeType.MovedEvent,
    NotificationEventTypeType.DeletedEvent
};

// GetEvent の待機時間を 1 分に設定 (1 分以上 応答がなければ、無効)
subscribe1.Timeout = 1;

// 通知 (Notification) の購読実行 !
SubscribeResponseType subscribeRes;
cl.Subscribe(
    null,
    null,
    "ja-JP",
    null,
    subscribeReq,
    out subscribeRes);

// エラーチェック (失敗したら終了。細かな解析は、今回は省略しています . . .)
if (subscribeRes.ResponseMessages.Items.Length <= 0 ||
    subscribeRes.ResponseMessages.Items[0].ResponseClass != ResponseClassType.Success)
{
    MessageBox.Show("Subscription のエラー !");
    return;
}

// 初回の Watermark を取得
SubscribeResponseMessageType subscribeResMsg =
    (SubscribeResponseMessageType)subscribeRes.ResponseMessages.Items[0];
string currentMark = subscribeResMsg.Watermark;

// 20 秒ごとに、GetEvents でイベントを確認
for(int i = 0; i < 3; i++) // 60 秒間確認する
{
    // GetEvents の実行 !
    GetEventsType eventReq = new GetEventsType()
    {
        SubscriptionId = subscribeResMsg.SubscriptionId,
        Watermark = currentMark
    };
    GetEventsResponseType eventRes = new GetEventsResponseType();
    cl.GetEvents(
        null,
        null,
        "ja-JP",
        null,
        eventReq,
        out eventRes);

    // ここでも、ちゃんとエラーの確認が必要です !
    // (今回は省略します . . .)

    // 受け取った答え (イベント情報) を処理
    GetEventsResponseMessageType eventResMeg =
        (GetEventsResponseMessageType)eventRes.ResponseMessages.Items[0];
    for (int j = 0; j < eventResMeg.Notification.ItemsElementName.Length; j++)
    {
        BaseNotificationEventType notificationEvent =
            eventResMeg.Notification.Items[j];
        ItemsChoiceType choiceType =
            eventResMeg.Notification.ItemsElementName[j];
        if (notificationEvent is BaseObjectChangedEventType)
        {
            string eventType = string.Empty;
            if (choiceType == ItemsChoiceType.NewMailEvent)
                eventType = "新規";
            else if (choiceType == ItemsChoiceType.DeletedEvent)
                eventType = "削除";
            else if (choiceType == ItemsChoiceType.MovedEvent)
                eventType = "移動";
            BaseObjectChangedEventType objectEvent =
                (BaseObjectChangedEventType)notificationEvent;
            ItemIdType itemId = (ItemIdType)objectEvent.Item;
            listBox1.Items.Add(eventType + " : " + itemId.Id);
            listBox1.Update();
        }

        // Watermark を更新
        currentMark = notificationEvent.Watermark;
    }

    // 20 秒待機
    System.Threading.Thread.Sleep(20000);
}

// サブスクリプションを終了
UnsubscribeType unsubscribeReq = new UnsubscribeType()
{
    SubscriptionId = subscribeResMsg.SubscriptionId
};
UnsubscribeResponseType unsubscribeRes = new UnsubscribeResponseType();
cl.Unsubscribe(
    null,
    null,
    "ja-JP",
    null,
    unsubscribeReq,
    out unsubscribeRes);

// ここでも、ちゃんとエラーの確認が必要です !
// また、さぼります . . .

// 切断
cl.Close();

この実行結果は、下図のようになります。

上記のコードを見てお分かりのように、GetEvents の戻り値に、更新されたメール アイテムのアイテム Id が入っているので、メールの件名、本文などの詳細は、この Id を元に取得をおこなって処理してください。(今回は、これらの処理は省略しています。)

また、アイテム移動時に、移動元と移動先のフォルダを取得するには、下記の太字の通り記述します。

. . .

FolderIdType oldFolderId, newFolderId;
if (choiceType == ItemsChoiceType.NewMailEvent)
    eventType = "新規";
else if (choiceType == ItemsChoiceType.DeletedEvent)
    eventType = "削除";
else if (choiceType == ItemsChoiceType.MovedEvent)
{
    eventType = "移動";
    MovedCopiedEventType movedEvent = (MovedCopiedEventType)notificationEvent;
    oldFolderId = movedEvent.OldParentFolderId;
    newFolderId = movedEvent.ParentFolderId;
}
. . .

また、メール アイテムの場合、特に、削除のイベント (NotificationEventTypeType.DeletedEvent) には注意が必要です。
Inbox から削除をおこなって Deleted フォルダに移動した場合は、実は、DeletedEvent ではなく、MovedEvent が発生するので、このイベントを取得する必要があります。(Deleted フォルダから恒久的に削除された場合に、DeletedEvent が発生します。) また、Deleted フォルダから恒久的に削除されたアイテムを取得したい場合には、Inbox フォルダ (DistinguishedFolderIdNameType.inbox) ではなく、上記のように Deleted フォルダ (DistinguishedFolderIdNameType.deleteditems) も監視しておく必要があるので注意してください。

さいごに

上述したように、Push Notification では、ファイアウォールやプロキシー サーバーの構成などに強く影響されるため、今回のような Exchange Online との連携 (loosely coupled) では、Pull Notification を使用します。
しかし、Pull Notification であっても、構成に影響を受ける場合があります。例えば、自社内で、Exchange の CAS (Client Access Server) を負荷分散 (Load Balance) させている場合を想像してください。この場合、クライアントから GetEvents をおこなう際に、イベント購読をしているマシン (CAS のサーバー) とは異なるマシン (CAS のサーバー) にリクエストをおこなう可能性があります。こうした場合には、クライアントごとに CAS のアフィニティ (affinity) をスティッキー (sticky) に割り当てておく必要があるでしょう。(Exchange Online の場合は、問題なく動くようです . . .)
実は、Exchange Server 2010 SP1 以降では、こうしたケースに対応して、Streaming Notification と呼ばれる もう 1 つの通知 (Notification) のモデル (3 番目の方法) が使用可能です。

また、例えば、メーラー (Mailer) や、Exchange と他アプリケーションにおけるアイテム連携などでは、アプリケーション終了後や、アプリケーションが計画外で停止した場合でも、アイテム間の連携が必要になります。このような場合には、通知 (Notification) ではなく、同期 (Synchronization) の API も使用できます。
同期では、ここで述べた Watermark の代わりに、SyncState という文字列を使用します。SyncState をファイルやデータベースなどに保持 (永続化) しておき、次回起動時にこの SyncState を使って同期をおこなうと、停止していた間の更新情報も含めた変更情報の一覧を取得できます。(初回の同期をおこなう場合には、SyncState を null にして呼び出します。)

補足 : 同期では、ActiveSync と同様の基盤が使用されています。Managed API では、SyncFolderItems メソッド、SyncFolderHierarchy メソッドを使用します。
なお、Form の Load イベントなどでは、初回の同期はできないので注意してください。

これらの詳細については、いずれ、セミナー、イベントなどでご紹介したいと思いますので、お楽しみに。

 

Managed API を使用した場合のコード (補足)

参考のため、以下に、Managed API を使用した通知 (Notification) のサンプルコードを掲載しておきます。(非常に簡単ですね。一度 Managed API を使ったら、離れられません。)
Managed API については、第 3 回 で紹介します。

// EWS の接続設定
ExchangeVersion ver = new ExchangeVersion();
ver = ExchangeVersion.Exchange2007_SP1;
ExchangeService sv = new ExchangeService(ver);
sv.Credentials = new System.Net.NetworkCredential(
    @"demouser1@tsmatsuz.apac.microsoftonline.com",
    "XXXXXXX");
sv.Url = new Uri(@https://red003.mail.apac.microsoftonline.com/EWS/Exchange.asmx);

// 通知の Subscribe
// 第 1 引数 : 対象のフォルダーID
// 第 2 引数 : タイムアウト (分)
// 第 3 引数 : Watermark (初回は null)
// 第 4 引数以降 : 購読するイベント (複数可能)
PullSubscription subscription = sv.SubscribeToPullNotifications(
    new FolderId[] { new FolderId(WellKnownFolderName.Inbox), new FolderId(WellKnownFolderName.DeletedItems) },
    1, null, EventType.NewMail, EventType.Moved, EventType.Deleted);

// 60 秒間確認する
for (int i = 0; i < 3; i++)
{
    GetEventsResults eventRes = subscription.GetEvents();

    foreach (ItemEvent eventItem in eventRes.ItemEvents)
    {
        string eventType = String.Empty;
        if (eventItem.EventType == EventType.NewMail)
            eventType = "新規";
        else if (eventItem.EventType == EventType.Deleted)
            eventType = "削除";
        else if (eventItem.EventType == EventType.Moved)
            eventType = "移動";
        listBox1.Items.Add(eventType + " : " + eventItem.ItemId.ToString());
        listBox1.Update();
    }

    System.Threading.Thread.Sleep(20000); // 20 秒待機
}

ItemId からメールのオブジェクト本体 (Subject, Body など) も EmailMessage.Bind メソッドを使って簡単に (たった 1 行で) 取得できます。

 

Comments (0)

Skip to main content