Outlook REST API : 通知 (Webhook) と同期 (Synchronization) を処理する


Office 365 API
(※ 現在、統一エンドポイントとして Microsoft Graph がご利用いただけます)

(Skype API は こちら)

以降、Microsoft Graph で Webhook を使う場合は、下記を使用してください。(現在、同期は対応していません。)
/subscriptions

こんにちは。

これまで、Exchange のアイテム (メール など) に対する通知・同期の処理には SOAP の EWS (Exchange Web Services) や ActiveSync API を使う必要がありましたが、Office 365 の REST API (Outlook REST API) でも扱えるようになりました。
(//BUILD 2015 のタイミングで Preview として提供されました。)

通知 (Notification) は Mail (Messages) / Calendar (Events) / Contacts / Groups に対して使用可能で、同期 (Synchronization) は Calendar (Events) に対して使用可能で、将来的に Mail (Messages), Contacts など他のアイテムでも提供される予定です。

2015/11 追記 : 最新の Outlook Rest API (beta) では、Mail の Messsage, Folder の同期 (Synchronization) も可能になりました。

また、Outlook REST API については、将来、従来 EWS でしか扱えなかった Free / Busy, MailTips なども扱えるようにし、EWS 同等の Outlook レベルの機能性を提供していく計画です。また、これまで OAuth + SMTP などを駆使して面倒なプログラミングを強いられてきた Outlook.com (旧 Hotmail) も この Outlook REST API で使用可能とし、オンプレミス環境 (Exchange Server) でも使用できるようにするなど、ますます充実されていく計画ですので、今後の機能拡張に是非注目しておいてください。

 

Webhook による通知 (Notification)

デスクトップ クライアントなどにありがちな、「Mail を受信したら通知をおこなう」といった類のアプリケーションを、Outlook REST API で実装できます。(この通知は  Mail (Messages) だけでなく、Calendar (Events) / Contacts / Groups に対しても設定可能です。)

SOAP EWS の Notification では long polling による connected な Notification Experience を提供していましたが、Outlook REST API の Notification では、購読 (Subscription) を開始すると (購読終了まで) Listner の HTTP Endpoint に通知されるという Stateless な (REST らしい) 通知の概念で実装します。
このため、スマホなどユーザー端末に通知する際は、Web 側 (HTTP Endpoint) から Notification Service に通知をおこなうなど、追加の仕掛けが必要です。

今回は、ログインしたユーザーの Mail (Messages) の着信を感知する簡単な Application を例に紹介しましょう。

まず Developer は、「Office 365 API 入門」で解説した手順で、あらかじめ Application を Azure AD に登録して Permission を設定します。(利用者は何もする必要はありません。)
なお、今回のサンプルでは、ログインしているユーザーのメール (Message) の着信のみを確認しますので、[Read user mail] の Permission を設定しておけば充分です。

さらに、「Office 365 API 入門」で解説した手順で HTTP Flow を実装して access token を取得します。(詳細の流れは「Office 365 API 入門」を参照してください。この際、利用者には Login 画面が表示されます。)

access token が取得できたら、ここからが今日の本題です !

Notification (通知) を開始するには、まず、下記の HTTP Request を発行して購読 (subscription) を開始します。(下記の Authorization Header には、上記で取得した access token を設定します。)
下記で、ResourceURL には Mail (Messages), Calendar (Events) などの区別を、ChangeType には作成時 (Created)、変更時 (Updated) などの取得したいイベントの種類を (論理和を設定することも可能)、CallbackURL には通知先の URL を設定します。
また、下記の ClientState には、通知先のエンドポイント (CallbackURL) に渡したい任意のデータを指定できます。(Subscription の種類に応じて Callback 先で動作を変えたい場合などには、この ClientState に目印となる情報を設定しておくと良いでしょう。)

補足 : 後述の通り、Callback 側で Webhook Validation Request の応答処理を実装しないと、下記の Request はエラーになります。

POST https://outlook.office365.com/api/beta/me/subscriptions
Accept: application/json
Authorization: Bearer eyJ0eXAiOiJKV...
Content-Type: application/json; charset=utf-8

{
  "@odata.type": "#Microsoft.OutlookServices.PushSubscription",
  "ResourceURL": "https://outlook.office365.com/api/beta/me/messages",
  "CallbackURL": "https://callbackendpoint.azurewebsites.net/test.php",
  "ChangeType": "Created",
  "ClientState": "testdata01"
}

上記の結果返される HTTP Response は下記の通りです。
Subscription Id (下記の Id) は、この Subscription を終了する際などに必要になるので、アプリケーションで保持しておいてください。

HTTP/1.1 201 Created
Content-Type: application/json;...
Location: https://outlook.office365.com/api/beta/Users('tsmatsuz@o365demo01.onmicrosoft.com')/Subscriptions('NDA5MDM4Q0QtMjQ3Qi00Mjg3LUFGQzUtNjJGMDIwOEZERjI0X0QyQkJFOUJFLTYwQzItNEQyQS1COUM0LTY1NUQwNjA3RDBDNQ==')

{
  "@odata.context": "https://outlook.office365.com/api/beta/$metadata#Me/Subscriptions/$entity",
  "@odata.type": "#Microsoft.OutlookServices.PushSubscription",
  "@odata.id": "https://outlook.office365.com/api/beta/Users('tsmatsuz@o365demo01.onmicrosoft.com')/Subscriptions('NDA5MDM4Q0QtMjQ3Qi00...')",
  "Id": "NDA5MDM4Q0QtMjQ3Qi00...",
  "ResourceURL": "https://outlook.office365.com/api/beta/me/messages",
  "ChangeType": "Created, Acknowledgment, Missed",
  "ClientState": "testdata01",
  "CallbackURL": "https://callbackendpoint.azurewebsites.net/test.php",
  "ExpirationTime": "2015-06-13T05:14:18.903711Z"
}

Listner となる CallbackURL 側 (上記の https://callbackendpoint.azurewebsites.net/test.php) では、イベントを受信した際の処理 (下記の else 以降) と、Webhook Validation Requests の応答処理を実装しておきます。

Webhook Validation Requests は、上述の Subscription 開始の際に Office 365 側がおこなうもので、Callback 側が受け入れを許可しているか否かを https://{callback url}?validationtoken=xxxxx の形式で呼び出すことで確認します。下記の通り、Callback 側では、受け取った validationtoken をそのまま Response Body に設定して、Status 200 を返しておきます。

下記の else 以降は、実際に Notification が来た際の処理であり、今回は、受信した内容 (Body) をそのまま標準出力に Output しています。(受信に成功したことを相手に知らせるため、下記の通り 200 の HTTP Status を返しておいてください。)

補足 : 現実の開発では、可能な限り、Webhook の Callback 内で重い処理は実行しないでください。(すぐに Webhook に応答してください。) もし、通知を受けて Exchange Online への接続などが必要な場合は、処理をキューしておき、別スレッドで処理するなど、設計上の工夫をします。

<?php
  $validToken = $_GET['validationtoken'];
  if($validToken) {
    // Respond to Webhook Validation Requests
    header('Content-Type: text/plain');
    print($validToken);
    http_response_code(200); 
  }
  else {
    // Monitor Notification
    $bodytxt = file_get_contents('php://input');
    error_log($bodytxt);
    http_response_code(200);
  }
?>

例えば、この Callback アプリを Azure の App Service Editor のブラウザー エディターを使って Ctrl-F5 で Run すると、下図の通り、出力画面 (右側) に受信した内容が表示されます。(ログ出力している場合には、サーバーの D:\home\LogFiles\php_errors.log に結果が出力されます。)
下記は、メールを受信した際の出力サンプルです。

補足 : こうした Webhook 開発では、ngrok を使った Debug や HTTP Capture も可能です。

受信データ (HTTP Payload) は下記の通りです。
ヘッダーに上述の ClientState (前述) が付与されているのがわかります。また、受信した Message の詳細 (Subject, Body, etc) を取得したいなら、@odata.id の URI を使って詳細情報を取得します。

補足 : CallbackURL 側の Listner アプリから Office 365 API (Outlook REST API など) を呼び出す場合は、「Azure AD : Backend Server-Side アプリの開発 (Deamon, Service など)」で紹介している方法で Backend 連携 (server-to-server 連携) をおこなうか、あるいは、ユーザーのデバイスに通知などをおこない、ユーザー権限でログインして処理を進めます。

Content-Type: application/json;...
X-ClientState: testdata01

{
  "value": [
    {
      "@odata.type": "#Microsoft.OutlookServices.Notification",
      "Id": null,
      "SubscriptionId": "NDA5MDM4Q0QtMjQ3Qi00...",
      "SubscriptionExpirationTime": "2015-06-13T05:14:18.903711Z",
      "SequenceNumber": 1,
      "ChangeType": "Created",
      "Entity": {
        "@odata.type": "#Microsoft.OutlookServices.Message",
        "@odata.id": "https://outlook.office365.com/api/beta/Users('tsmatsuz@o365demo01.onmicrosoft.com')/Messages('AAMkAGQy...')",
        "@odata.etag": "W/\"CQAAABYAAAC1O9XIP0YERprleNO9Rgi4AAIH/itV\"",
        "Id": "AAMkAGQyYmJlOWJlLTYwYzItNGQy..."
      }
    }
  ]
}

さいごに、Subscription を終了するには、上記で取得した Subscription Id を使用して下記の HTTP Request を出します。
Status 204 (No Content) が返れば、成功しています。

DELETE https://outlook.office365.com/api/beta/me/subscriptions('NDA5MDM4Q0QtMjQ3Qi00...')
Accept: application/json
Authorization: Bearer eyJ0eXAiOiJKV...

なお、Subscription 開始の際、この通知の期限 (ExpiraitonTime) が下記の通り Response で送信されています。既定の有効期限は 3 日です。

{
  "@odata.context": "https://outlook.office365.com/api/beta/$metadata#Me/Subscriptions/$entity",
  "@odata.type": "#Microsoft.OutlookServices.PushSubscription",
  "@odata.id": "https://outlook.office365.com/api/beta/Users('tsmatsuz@o365demo01.onmicrosoft.com')/Subscriptions('NDA5MDM4Q0QtMjQ3Qi00...')",
  "Id": "NDA5MDM4Q0QtMjQ3Qi00...",
  "ResourceURL": "https://outlook.office365.com/api/beta/me/messages",
  "ChangeType": "Created, Acknowledgment, Missed",
  "ClientState": "testdata01",
  "CallbackURL": "https://callbackendpoint.azurewebsites.net/test.php",
  "ExpirationTime": "2015-06-13T05:14:18.903711Z"
}

この Subscription に対し、下記の通り renew を呼び出すことで、さらに 3 日間通知を延長させることができます。(成功すると Status 202 (Accepted) が返ります。)

POST https://outlook.office365.com/api/beta/me/subscriptions('NDA5MDM4Q0QtMjQ3Qi00...')/renew

補足 : バッチ処理などの場合、通知が一気に多数到着するケースもあるので、こうしたケースにも対応して設計してください。

 

同期 (Synchronization)

皆さんがお使いの Outlook (Client) では、立ち上げるたびに Mail (Messages) や Calendar (Events) の全件を再検索するのではなく、同期 (Synchronization) を使用し、前回同期した時点からの変更点のみを取得して、その内容を Outlook (Client) に反映します。
こうした処理を皆さんが作成するカスタム アプリに実装するには、ここで紹介する手法を使います。
なお、今回は、Calendar (Events) の同期を例に紹介します。

同期 (Synchronization) のプログラミングでは、Server 側と Client 側で同期の目印となる token (後述の「deltaToken」) を共有し、Client がこの token を付与して処理を要求 (GET) するこで、前回同期した時点以降の変更情報のみを取得します。(以降も同様に、この token を使用して変更情報を取得します。)
では、早速 HTTP Flow を見てみましょう。

まず、いつものように、「Office 365 API 入門」で解説した手順で、Application を Azure AD に登録して Permission を設定してください。
今回のサンプルでは、Permission として [Read user calendar] を付与すれば充分です。

今回は、2015/06/01 - 2015/06/30 までの予定 (Events) を同期するサンプルを構築します。

最初に同期を開始するには、通常の予定 (Events) 取得の HTTP Request に加え、下記の通り Prefer Header に odata.track-changes を設定します。

GET https://outlook.office365.com/api/v1.0/me/calendarview?startDateTime=2015-06-01T00%3a00%3a00Z&endDateTime=2015-06-30T00%3a00%3a00Z
Accept: application/json
Authorization: Bearer eyJ0eXAiOiJKV1...
Prefer: odata.track-changes, odata.maxpagesize=3

上記の要求をおこなうと、下記の通り、Event のリスト (一覧) に加え、@odata.deltaLink 属性を伴って HTTP Response が返ってきます。(なお、下記の各 Event の Id 属性は重要ですので、おぼえておいてください。後述します。)

HTTP/1.1 200 OK
Content-Type: application/json;...
Preference-Applied: odata.track-changes

{
  "@odata.context": "https://outlook.office365.com/api/v1.0/$metadata#Me/CalendarView",
  "value": [
    {
      "@odata.id": "https://outlook.office365.com/api/v1.0/Users('demouser01@o365demo01.onmicrosoft.com')/Events('...')",
      "@odata.etag": "W/\"iL/fTnhkuEedc...\"",
      "Id": "AAMkADhhNjUyNDlkLWFmYzQtND...",
      "ChangeKey": "iL/fTnhkuEedcEJW+V4MGAABwmwRlA==",
      "DateTimeCreated": "2015-06-11T03:25:37.4705379Z",
      "DateTimeLastModified": "2015-06-11T03:25:37.4861635Z",
      "Subject": "Ignite Report to our team",
      "Body": {
        "ContentType": "HTML",
        "Content": "..."
      },
      . . .

    },
    {
      "@odata.id": "https://outlook.office365.com/api/v1.0/Users('demouser01@o365demo01.onmicrosoft.com')/Events('...')",
      "@odata.etag": "W/\"iL/fTnhkuEedc...\"",
      "Id": "BBMkADhhNjUyNDlkLWFmYzQtND...",
      "ChangeKey": "iL/fTnhkuEedcEJW+V4MGAABwmwRlQ==",
      "DateTimeCreated": "2015-06-11T03:26:06.2832515Z",
      "DateTimeLastModified": "2015-06-11T03:26:06.3145014Z",
      "Subject": "Twilio Hands-on",
      "Body": {
        "ContentType": "HTML",
        "Content": "..."
      },
      . . .

    }
    . . .

  ],
  "@odata.deltaLink": "https://outlook.office365.com/api/v1.0/me/calendarview/
    ?startDateTime=2015-06-01T00%3a00%3a00Z
    &endDateTime=2015-06-30T00%3a00%3a00Z
    &%24deltatoken=b9a7df04eba944f5bb1a7706d58bf830"
}

この @odata.deltaLink に記述されている $deltaToken が、次回の同期の際のキーとなる token です。
次回、変更された Event のみを取得する際には、以下の通り $deltaToken を付与して HTTP Request をおこないます。

GET https://outlook.office365.com/api/v1.0/me/calendarview
  ?startDateTime=2015-06-01T00%3a00%3a00Z
  &endDateTime=2015-06-30T00%3a00%3a00Z
  &%24deltatoken=b9a7df04eba944f5bb1a7706d58bf830
Accept: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJh...
Prefer: odata.track-changes, odata.maxpagesize=3

変更された Event の一覧として、下記の通り結果 (HTTP Response) が返ってきます。
下記は、 2 件の新規 Event が追加された場合のサンプルです。

HTTP/1.1 200 OK
Content-Type: application/json;...

{
  "@odata.context": "https://outlook.office365.com/api/v1.0/$metadata#Me/CalendarView/$delta",
  "value": [
    {
      "@odata.id": "https://outlook.office365.com/api/v1.0/Users('demouser01@o365demo01.onmicrosoft.com')/Events('...')",
      "@odata.etag": "W/\"iL/fTnhkuEedc...\"",
      "Id": "CCMkADhhNjUyNDlkLWFmYzQtND...",
      "ChangeKey": "iL/fTnhkuEedcEJW+V4MGAABwmwRlg==",
      "DateTimeCreated": "2015-06-11T04:21:09.0889853Z",
      "DateTimeLastModified": "2015-06-11T04:21:09.1202356Z",
      "Subject": "Event report for customers",
      "Body": {
        "ContentType": "HTML",
        "Content": "..."
      },
      . . .

    },
    {
      "@odata.id": "https://outlook.office365.com/api/v1.0/Users('demouser01@o365demo01.onmicrosoft.com')/Events('...')",
      "@odata.etag": "W/\"iL/fTnhkuEedc...\"",
      "Id": "DDMkADhhNjUyNDlkLWFmYzQtND...",
      "ChangeKey": "iL/fTnhkuEedcEJW+V4MGAABwmwRlw==",
      "DateTimeCreated": "2015-06-11T04:21:42.9798612Z",
      "DateTimeLastModified": "2015-06-11T04:21:42.9954874Z",
      "Subject": "Office camp",
      "Body": {
        "ContentType": "HTML",
        "Content": "..."
      },
      . . .

    }
  ],
  "@odata.deltaLink": "https://outlook.office365.com/api/v1.0/me/calendarview/?startDateTime=2015-06-01T00%3a00%3a00Z&endDateTime=2015-06-30T00%3a00%3a00Z&%24deltaToken=de765404bc944a5ba25a29c2ecab84b0"
}

では、Event が変更 (Update), 削除 (Delete) された場合は、どう返されるでしょうか ?

まず、変更 (Update) された場合は、同様に変更された Event の Entity が返されるので、Id 属性を元に対象の Event を取得して必要な処理 (変更処理など) をおこないます。
また、削除 (Delete) の際は下記のような Body が返されます。同様に、Id 属性によって、どの Event が削除されたか識別できます。

{
  "@odata.context": "https://outlook.office365.com/api/v1.0/$metadata#Me/CalendarView/$delta",
  "value": [
    {
      "@odata.context": "https://outlook.office365.com/api/v1.0/$metadata#Me/CalendarView/$deletedEntity",
      "Id": "AAMkADhhNjUyNDlkLWFmYzQtND...",
      "reason": "deleted"
    }
  ],
  "@odata.deltaLink": "https://outlook.office365.com/api/v1.0/me/calendarview/?startDateTime=2015-06-01T00%3a00%3a00Z&endDateTime=2015-06-30T00%3a00%3a00Z&%24deltaToken=1ff13804e0f2424085d3745d746a7ea4"
}

なお、結果が多数あり、続きがある場合は、下記の通り $skipToken の付与された URI が返されるので、この URI にアクセスして続きのデータを取得できます。

{
  "@odata.context": "https://outlook.office365.com/api/v1.0/$metadata#Me/CalendarView/$delta",
  "value": [
    . . .
  ],
  "@odata.nextLink": "https://outlook.office365.com/api/v1.0/me/calendarview/
    ?startDateTime=2015-06-01T00%3a00%3a00Z
    &endDateTime=2015-06-30T00%3a00%3a00Z
    &%24skipToken=a1e5b10261804221aceb856143b8af19"
}

 

参考情報 :
Office Dev Center - Outlook Notifications REST API reference (preview)
https://msdn.microsoft.com/en-us/office/office365/api/notify-rest-operations

Office Dev Center - Synchronize events in an Outlook calendar view
https://msdn.microsoft.com/en-us/office/office365/howto/sync-calendar-view

 

※ 変更履歴 :

2017/05  新 Azure Portal (https://portal.azure.com/) に画面を変更

Comments (0)

Skip to main content