Microsoft Graphを使ってアプリケーションからメールを送る その2

## 目的
前回の記事は[こちら](https://qiita.com/utsuki_protein/items/159e13f113cf0fb9396e)

タスクスケジューラで動かしているスクリプトの通知メールを個人Googleアカウントから会社公式メールアカウントに切り替えたい。

<font color="LightSteelBlue">
真の目的
Redmine用メールサーバを立てることは社内規定上禁止。
そのためチケット変更等をポーリングするスクリプトからメールを送信する。
</font>
##### 前回上がった課題について対策が打てたのでその備忘録となります。
>トークンの有効期限が1時間という問題があります。
>今回手動でブラウザを使って認証コードをとってきましたが毎時間ブラウザ開くのも・・・・
>スクレイピングかwebアプリを作るしかなさそうです。
> 何かいい方法があれば教えてください。


## 使用する言語、技術
* Python3
* Selenium
    * [Selenium入門](https://app.codegrid.net/entry/selenium-1)
    * ブラウザの自動化テストなどに用いられるブラウザを操作するツールになります。
    * BeautifulSoupではフォームへの入力やボタンクリックができないので採用しました。
* MicroSoft Edge
    * トークンの取得に使用します。ブラウザであれば何でもよいと思います。

## やったこと
* Seleniumの準備
* ログインページの調査
* コード実装
* トライ&エラー結果
* 対策

## Seleniumの準備
#### SeleniumのPythonライブラリのインストール
```
pip install selenium
```
で入ります。

#### WebDriverのダウンロード
参考にしたのが[こちらのページ](https://uitspitss.hatenablog.com/entry/2016/04/15/193446)のため、PhantomJSを使用します。
動作環境がWin10のため、Windows用のexeをダウンロードしています。

#### 
#### 
#### 

## ログインページの調査
#### 入力フォームのid確認
![login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169001/26632e6d-72aa-b90e-847b-68ad399ee39a.png)
前回使用した認証コード取得URLにアクセスし、EdgeブラウザでF12を押してHTMLの要素を確認します。

```html
<input name="loginfmt" id="i0116" />
``` 
このあたりが使えそうです。今回はidで処理を進めていきます。

#### 「次へ」ボタンのid確認
```html
<input class="btn btn-block btn-primary" id="idSIButton9" />
``` 
ボタンはこのへんですかね、html,cssまったくわからないのですがclassは複数のオブジェクト(呼び方あってるのかな？)に設定できるはずなのでユニークなidのほうが良いと判断してid=idSIButton9で処理します。


## コード実装
実際のコードは下記のとおりです。
URLへのアクセス、フォームへ入力、次へボタン押下、最後に404で表示される予定の遷移後のURLを取得しようとしています。

```python:get_authcode.py
# -*- coding:utf-8 -*-
from selenium import webdriver

# PhantomJSのドライバーを得る
browser = webdriver.PhantomJS(
        executable_path='C:\\path\\phantomjs.exe'
    )

# ドライバが読み込まれるまで待機,3秒でタイムアウトによる例外発生
browser.implicitly_wait(3)

# 認証コード取得URLにアクセス
url_login = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?XXXXXXXXXXXX"
browser.get(url_login)
print("ログインページにアクセスしました")
browser.save_screenshot('./login.png')

# 入力フォームのオブジェクト取得
e = browser.find_element_by_id("i0116")
# ゴミが入っていた場合に備えてクリア
e.clear()
# フォームにアカウント名を入力
e.send_keys('xxxxxxxxxx@xxxxxxxx')
browser.save_screenshot('./send_keys.png')

# ボタンを押してフォームを送信
frm = browser.find_element_by_id("idSIButton9")
frm.submit()
print("情報を入力してログインボタンを押しました")
browser.save_screenshot('./get_authcode.png')

print('https://XXXXXX?XXXX=YYYY&xxxx=yyyy')
print('が表示されるはず')
print(browser.current_url)
```


## 一回目結果
ログイン後うまくいっていないようです。

login.png　:relaxed:
![login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169001/4af8e770-7b54-07d9-495c-17d9b39fbc62.png)

send_keys.png　:relaxed::relaxed:
![send_keys.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169001/ba6a1d81-a1e2-f0ad-4fb2-0ae0fd5a155f.png)

get_authcode.png　:rage:
![get_authcode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169001/9c359bc2-1996-08ce-c6b5-4898e48427db.png)

InPrivateモードで手動ログインを試してみるとシステムレベル？でのダイアログが表示されました。
![windows_form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169001/1c466476-c332-cab5-dc46-9f145b13a6e2.png)

F12で開発者モードにもできないため、他の方法を検討します。


## 対策
かなり端折りますが、InPrivateモードではないEdgeならダイアログが表示されないことが分かりました。
キャッシュ、Cookieが原因だと思われますがそちらの知識にも疎いため今回はEdgeDriverとすでにEdgeに保存されているCookieを使用することで解決に至りました。

↓の状態でSeleniumを実行します。
![login_cookie_enable.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169001/6485d0e9-4642-8d89-4a04-019046b2bc3d.png)


下記の課題もいつかは解決できればと思いますが今回は認証コードを取得することが目的のため、飛ばします。
* ダイアログへの入力
    * OSレベル？ブラウザレベル？
* Cookieの操作など


## 実装(修正後)
```python:get_authcode2.py
# -*- coding:utf-8 -*-
import requests
import time
from bs4 import BeautifulSoup
from selenium import webdriver

# EdgeDriverはEdgeがすでに起動中の場合エラーになるので
# 終了後必ずbqowser.quit()でEdgeを閉じる
try:
    browser = webdriver.Edge(
            executable_path='C:\\path\\edgedriver.exe'
        )

    browser.implicitly_wait(3)

    url_login = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=XXXX"
    browser.get(url_login)
    print("ログインページにアクセスしました")

    # 「Windowsに接続済み」のオブジェクトclass名は"table-row"
    frm = browser.find_element_by_class_name("table-row")
    frm.click()
    print("情報を入力してログインボタンを押しました")

    # コールバックの画面に切り替わるまでループ
    while 1:
        time.sleep(3)
        url = browser.current_url
        print('------------URLを確認する------------')
        print(url)
        # 前回コールバックURLをlocalhostにしたので判定条件を"http://localhost"に
        if 'http://localhost:10101' in url:
            break
    print('終了')
except e:
    print('Error')

browser.quit()
```
## 二回目結果
実行後のログになります。
codeを取得することが出来ました!

```
ログインページにアクセスしました
情報を入力してログインボタンを押しました
------------URLを確認する------------
https://login.microsoftonline.com/hogehoge/この時点ではまだ変わっていません
------------URLを確認する------------
ms-appx-web://microsoft.microsoftedge/assets/errorpages/dnserror.html?ErrorStatus=0x800C0005&DNSError=0#http://localhost:10101/authorized?code="とれました！！！"&state=1234&session_state="XXXXX"
終了
```

## さいごに
これで個人メールを使わない＆メールサーバを立てずにRedmineのデータをフルオートでメール送信することが出来ます。
メールだけでなくOffice365の機能であればどれにでも有効だと思うので社内システムがOffice365でかゆいところに手が届かないって問題があれば今回の記事を参考にしていただければと思います。


# **言い忘れてましたがRestAPIでメール送るだけならFlowでノンコーディングでできます。**