Microsoft Outlook API で遊ぶ

会社の予定表として Outloook にお世話になっているが、プログラムで操作する方法を調べた。普通こういうの curl 使うもんだと思うが、最近気に入っている Jupytor 使用。

Outlook の操作には [Graph API](https://developer.microsoft.com/en-us/graph/docs/concepts/overview) を使う

## アプリを作る

Outlook を操作するにはまずアプリを登録する。

* [Application Registration Portal](https://apps.dev.microsoft.com/#/appList) に行く
* Application Name を決める。
* Generate New Password で client_secret を取得。Azure AD 1 を使う場合無くても使える。
* Add Platform
  * https://developer.microsoft.com/en-us/graph/docs/concepts/auth_overview に説明があるが、良くわからなかった。自分で調べた結果は以下。
  * Web: Web アプリ向け
    * Redirect URLs に OAuth2 の authorization code を受ける URL を指定する。
    * token の受け取りに client_secret が必要。
  * Native Application
    * Custom Redirect URIs hogehoge://auth のようなカスタム URL を指定。携帯アプリではこの URL で authorization code を受ける。
    * token の受け取りに client_secret は不要な時がある。
* Delegated Permissions
  * Calendars.Read を追加 (Azure AD のみ)
  * 一旦アプリにログインすると後から追加した Permission は無効のままなので、変更したい場合は https://portal.office.com/account/#apps で一旦 Revoke する。このアドレスは work or school 用。フリーアカウントで revoke する方法は見つからなかった。

## 実際に認証をやってみる

詳しい話は後にして、実際に認証してみる。アプリを作ったら結局以下の三つのパラメータが手に入るので、jupytor で試すときは次のように保存しておく。

```python
APP = {
    'client_id': 'YOUR_CLIENT_ID', # 自動生成されたもの
    'redirect_uri': 'http://localhost:10101/authorized', # 自分で決める。例として http://localhost:10101/authorized を使う。
    'client_secret': 'YOUR_SECRET' # 自動生成されたもの
}
```

このパラメータを使い、次のような URL をブラウザに入れて Outlook アカウントにログインする。

    https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=(client_id)&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A10101%2Fauthorized&response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2Fcalendars.read+offline_access&state=1234

ブラウザで承認すると、http://localhost:10101/authorized にリダイレクトする。この URL は適当なのでエラーになるが、そのときブラウザのアドレスバーにある URL の code パラメータから authorization code を抜き出す。例えば

    http://localhost:10101/authorized?code=HOGEHOGE&state=1234&session_state=6dbc8b19-2b4e-4f74-bb28-a095f37b3a60

ならば authorization code は HOGEHOGE となる。authorization code から refresh_token を取得する。

```python
def get_refresh_token(code, app):
    data = {
        'grant_type': 'authorization_code',
        'client_id' : app['client_id'],
        'code': code,
        'redirect_uri' : app['redirect_uri'],
        'scope': 'https://graph.microsoft.com/calendars.read offline_access',
        'client_secret': app['client_secret'],
    }
    print('data: %s' % data)
    global response
    response = requests.post(
        'https://login.microsoftonline.com/common/oauth2/v2.0/token',
        data=data,
    )
    if response.status_code == 200:
        token = response.json()['refresh_token']
        return response, token
    return response, None

refresh_token = get_refresh_token(APP, 'HOGEHOGE')
```

取得した refresh_token を使って、今度は access_token を取得する。

```python
def get_access_token(app, token):
    params = {
        'grant_type': 'refresh_token',
        'client_id' : app['client_id'],
        'refresh_token': token,
        'redirect_uri' : app['redirect_uri'],
        'client_secret' : app['client_secret'],
        'scope': 'https://graph.microsoft.com/calendars.read offline_access',
    }

    response = requests.post(
        'https://login.microsoftonline.com/common/oauth2/v2.0/token',
        data=params,
    )
    return response.json()['access_token']

access_token = get_access_token(APP, refresh_token)
access_token
```

access_token が手に入ったら認証は完了。以下に何をやったか書く。

## 認証

認証には OAuth2 という仕組みが使われる。ザクっと次の流れになる。

1. アプリはユーザに Office 認証サーバへのリンクを用意する。リンクにあらかじめ決めて置いた redirect_url を埋め込む。
2. ユーザはブラウザで Office アカウントにログインする。
3. Office 認証サーバは redirect_url にリダイレクトする。その際 URL に authorization code 付加される。
4. 普通は URL にあるアプリサーバで認証処理を継続する。アプリサーバが無いときはブラウザがエラーになるので手で authorization code をコピペして使う。
5. authorization code を token API に POST して access_token をもらう。出来たら refresh_token ももらう。
  * authorization code はもう使わないので忘れる。
6. token API は refresh_token から access_token を発行する事も出来る。
7. 認証終わり、access_token をヘッダに入れて API にアクセスする。

## 二つの認証エンドポイント: Azure AD と Azure AD 2.0

ややこしい事に、認証のためのエンドポイントが二つある。試行錯誤して次の事がわかった。上の例では Azure AD 2.0 を使った。

* Azure AD
  * 会社や学校のアカウント専用。
  * Native Aplication を有効にすると client_secret 無しでも refresh_token を取得出来る。
    * 面白い事に Native Aplication を有効にするだけで Web の redirect_uri を使っても client_secret 無しの refresh_token 取得が出来る。
  * 認証時のパラメータには特に権限を入れない。Application Registration Portal の Delegated Permissions で設定してある。
* Azure AD 2.0
  * https://login.live.com/ で作ったフリーアカウントでも使える。
  * 何をするにも client_secret 必須。
  * 認証時のパラメータに権限を表す scope が必須。空白区切の文字列。
    * refresh_token を使うときは `offline_access` を指定
    * 例えばカレンダーを読むなら `https://graph.microsoft.com/calendars.read` を指定
    * Application Registration Portal の Delegated Permissions の設定は不要らしい。
    * https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-scopes

## Azure AD 2.0 の scope 指定方法

[Requesting individual user consent
](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-scopes#requesting-individual-user-consent)
によると、スコープは Application ID URI と Scope value を連結した物。

* Microsoft Graph の場合 Application ID URI は `https://graph.microsoft.com`
* カレンダーを読む Scope value は `Calendars.Read`
  * 他の例: https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference
* 足して `https://graph.microsoft.com/calendars.read` となる。
* 小文字にするとか、細かい事はわからなかった。

## カレンダーを取得

https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_list_events

単純にイベントの内容を取得するには [List events](https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_list_events) API を使う。

```python
def graph(path, params={}):
    headers = {
        'Authorization': 'Bearer %s' % access_token,
        'Prefer': 'outlook.timezone="Asia/Tokyo", outlook.body-content-type="text"'
    }
    response = requests.get(
        path,
        params=params,
        headers=headers,
    )
    if response.status_code != 200:
        print(response.json())
    return response

response = graph('https://graph.microsoft.com/v1.0/me/events')
response.json()
```

これで最近追加した順に 10 件イベントが返る。ただ、これでは繰り返しの予定が一つのイベントとして返されるので役に立たない。

例えば、繰り返すかどうかに関わらず先の二週間分を取得するには 代わりに [CalendarView](https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_list_calendarview) を使う。

```python
from datetime import datetime, timedelta

start = datetime.today()
end = start + timedelta(days=14)

response = graph("https://graph.microsoft.com/v1.0/me/calendar/calendarView",
                 {
                     'startDateTime': start.isoformat(),
                     'endDateTime': end.isoformat(),
                     '$orderby': 'start/dateTime',
                 })
response.json()
```

上で、クエリの中にある `$ordereby` は [OData](https://developer.microsoft.com/en-us/graph/docs/concepts/query_parameters) というクエリ言語。OData を使うと共同のやり方でデータの検索や、フィルタ、並び替えなどを行える。

## 参考

日本語約へのリンクもある。

* [Graph API](https://developer.microsoft.com/en-us/graph/docs/concepts/overview)
* [CalendarView](https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/user_list_calendarview)
* [OData](https://developer.microsoft.com/en-us/graph/docs/concepts/query_parameters)