Office 365(というか Exchange Online)に POP3 や IMAP、SMTP 接続しようとすると、いまのところは ID/PW による基本認証が使えますが、今年中にもこの機能が削除される予定*1になっているので、今後のことを考えると先進認証と呼ばれるSASL XOAUTH2 に対応させる必要があります。
Outlook や Thunderbird などのメーラーは設定を変更するだけですんなり対応できるので良いのですが、fetchmail などの非対話のコマンドラインツールなどを使おうとすると一気に面倒なことになります。基本的なやり方は以下にあるのですが、OAuth2 の設定するためには Azure AD などの仕組みについて理解する必要が出てきます。
† パスワードの代わりになるものはアクセストークン
そして、今回はパスワードの代わりに利用されるアクセストークンの取得処理を以下のライブラリと Python を使って実装してみました。
モジュール自体はいつものごとくpip install O365
でインストールできます。
† アクセストークンを取得するスクリプト
最もシンプルに書いてみたところ、こんな感じになりました。
get_token.py
#!/usr/bin/env python# -*- coding: utf-8 -*-# BSD 2-clausefrom O365 import Account, MSGraphProtocol, FileSystemTokenBackend# TENANT_ID等はダミーなので適宜読み替えてくださいTENANT_ID = '99999999-9999-9999-99999999999999999'CLIENT_ID = '99999999-9999-9999-99999999999999999'CLIENT_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'TOKEN_FILENAME='token.json'TOKEN_PATH='.'AUTH_FLOW='authorization'SCOPES = ['offline_access', 'https://outlook.office365.com/POP.AccessAsUser.All', 'https://outlook.office365.com/IMAP.AccessAsUser.All', 'https://outlook.office365.com/SMTP.AccessAsUser.All']token_backend = FileSystemTokenBackend(token_path=TOKEN_PATH, token_filename=TOKEN_FILENAME)credentials=(CLIENT_ID, CLIENT_SECRET)account = Account(credentials=credentials, scopes=SCOPES, token_backend=token_backend, auth_flow_type=AUTH_FLOW, tenant_id=TENANT_ID)if not account.is_authenticated: account.authenticate() print('Authenticated!')else: account.connection.refresh_token() print('Refreshed!')with open('access_token', mode='w') as f: f.write(token_backend.get_token()['access_token'])
比較的簡単なモジュールですが、いくつか落とし穴があります。
まず、SCOPES の部分ですが、Azure AD でのアプリ登録の画面ではhttps://graph.microsoft.com/POP.AccessAsUser.All となっていますが、Outlook にアクセスするためにはhttps://outlook.office365.com/POP.AccessAsUser.All のように読み替えないと、アクセストークンが取得できても認証が通りません*2。
また、python-o365 のバグなのか、以下のような感じで怒られるので、メッセージに従って無理やりパッチを当てました。
Unable to fetch auth token. Error: (invalid_client) AADSTS700025: Client is public so neither 'client_assertion' nor 'client_secret' should be presented.Trace ID: 99999999-9999-9999-99999999999999999Correlation ID: 99999999-9999-9999-99999999999999999Timestamp: 0000-00-00 00:00:00ZSomething go wrong. Please try again.
O365.diff
--- a/O365/connection.py+++ b/O365/connection.py@@ -511,7 +511,8 @@ class Connection: token_url=self._oauth2_token_url, authorization_response=authorization_url, include_client_id=True,- client_secret=self.auth[1]))+ )) elif self.auth_flow_type == 'public': self.token_backend.token = Token(self.session.fetch_token( token_url=self._oauth2_token_url,@@ -633,7 +634,8 @@ class Connection: self.session.refresh_token( self._oauth2_token_url, client_id=client_id,- client_secret=client_secret)+ ) ) elif self.auth_flow_type == 'public': client_id = self.auth[0]
上手く認証ができるとAuthenticated!
やRefreshed!
のメッセージが表示され、token.json
ファイルにトークンの情報が、access_token
ファイルにアクセストークンが書込まれるようになっています。アクセストークンの有効期限は1時間*3なので、cron 等でこのスクリプトを定期的に実行し続ける必要があります。