Pythonistaにメールマガジンを読ませる①

# きっかけ
移動時間に購読しているメールマガジンを音声読み上げ機能を使って読んでもらっているが、メールマガジンは比較的短いので、iphoneの純正メールアプリでいちいち本文を選択し、読み上げ機能を選択して読ませるということを2-3分置きにしたくなかった。

## やりたいこと
- Gmailで特定ラベルがついた未読メール（メールマガジン）を日付順で読ませたい
- 読み終わった後は既読に。（できれば、特定のメールマガジンが非常に有用だと感じた場合、未読のままで留めて、他のフォルダに移動するオプションも実装したい。ここも音声で指示できたら最高)

## 実装方法
Google Drive API を使うやり方を考えた。
順番としては
1. ラベルのついた未読メールを.eml形式でgoogle drive 所定のフォルダに保存
2. Google Drive APIにアクセスし、日付をソートして本文を順番に読み上げさせる。読み終わったらファイルを削除。

### 序. Google Drive APIの設定
- 参考にしたページ
[APIキー、OAuthクライアントID、サービスアカウントキーの違い:Google APIs](https://messefor.hatenablog.com/entry/2020/10/08/080414)
[google drive の Quickstart（サービスアカウント編）](https://playwithgoogleapi.hatenablog.com/entry/2019/06/30/133415)

### 1. .eml形式でgoole drive に保存する機能
Google App Scriptで実装。
1. Google Spread Sheetを起動し、新しいスプレッドシートを作成
2. ツール>スクリプトエディタを起動
![Screen Shot 2021-03-16 at 10.29.18 AM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1212890/22809575-47b4-7b57-36e9-5382f99b67b8.png)
)
3. 下記、記述したコード

これで、"Magazines"のラベルを貼ったメールが.emlファイルになって Google Document内 "Magazines" Folderに入り、処理したメールは既読にすることができた。ただしこのやり方だと、未読にするオプションは付けれないことに後から気づいた...

```javascript

//メッセージをeml(blob)として取得
function msgToEml(msg) {
  var fn = Utilities.formatDate(msg.getDate(), "Asia/Tokyo", "yyyyMMddHHmmss");
  //Blob形式でファイルを保存。
  var blob = Utilities.newBlob(msg.getRawContent())
    .setName(fn + ".eml")
    .setContentTypeFromExtension();
  return blob;
}



function saveEmail() {
  var blob, fol;
  var fol_name = "Magazines";
   
  // Saved folder == label name
  //　フォルダーがない場合作成、ある場合そのフォルダーを変数folに格納
  if (DriveApp.getFoldersByName(fol_name).hasNext() === false) {
    fol = DriveApp.createFolder(fol_name);
  } else {
    fol = DriveApp.getFoldersByName(fol_name).next();
  }
   
 
 //Gmail操作
  var q = "label:" + fol_name + ' ' + 'is:unread';
  var threads = GmailApp.search(q);
  if (threads.length > 0) {
    threads.forEach(function(thread) {
      thread.getMessages().forEach(function(msg) {
        blob = msgToEml(msg);
        fol.createFile(blob);
      });
      thread.markRead()
    });
  }
}
```
4. 定期実行する。

App Script内からトリガーのタブを開き、新しいトリガーを設定するだけ。時間帯で毎日定期実行できて便利。
![Screen Shot 2021-03-17 at 6.56.10 AM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1212890/c73e71d1-b08f-63c8-3668-cd0a1fda9dec.png)


![Screen Shot 2021-03-17 at 6.57.46 AM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1212890/f9df5c74-11c1-5485-a67a-c74d27c21790.png)



### 2. 読み上げ機能
こちらはpythonistaで実装。
1. Pythonistaにstashを入れる　→　こちらを参考にした: [Pythonista3のStash導入〜pip installまでの手順](https://note.com/junyaaa/n/n1b6861038047)
2. これでpipコマンドが使えるので、必要なパッケージをインストール → 参考: [Python quick start](https://developers.google.com/drive/api/v3/quickstart/python)
なお、Quickstartには

```shell
pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
```
をしろと書いてあるが、Pythonista,Stash上ではこれができなかった。とりあえず無視して進めてみた。

案の定エラーが出た。

```shell
ModuleNotFoundError: No module named 'google.api_core'
```

ググってみると、やはりpythonistaでパッケージをインストールした時に問題が発生しているみたい。。
とりあえずPCで同じようにパッケージをインストールし実行、その後Icloud経由でPythonistaにGoogle関連のパッケージを全部移してこの問題は解決した。


3. これが全体のコード

```python
# google-related
from __future__ import print_function
import pickle
import os.path
from apiclient import errors
from apiclient import http
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request

# email imports
import sys
import email
from email.header import decode_header

# speech function
import speech
import time

# datetime
import datetime



# If modifying these scopes, delete the file token.pickle.
SCOPES = ['https://www.googleapis.com/auth/drive']

def main():
    
    creds = None
    # The file token.pickle stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    # credentials.jsonはGoogle APIで登録して取得する。
    if os.path.exists('token.pickle'):
        with open('token.pickle', 'rb') as token:
            creds = pickle.load(token)
    # If there are no (valid) credentials available, let the user log in.
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token.pickle', 'wb') as token:
            pickle.dump(creds, token)
 
    service = build('drive', 'v3', credentials=creds)

    # Get parents id
    parentResult = service.files().list(
        pageSize=1,fields="nextPageToken, files(id, name)",q="name='Magazines'").execute()
    
    parent = parentResult.get('files',[])
    parentId = parent[0]['id']

    # Get eml files and sort
    def get_date(e):
        return datetime.datetime.strptime(os.path.splitext(os.path.basename(e['name']))[0], '%Y%m%d%H%M%S')
        
    results = service.files().list(
        pageSize=100, fields="nextPageToken, files(id, name)",q=f"'{parentId}' in parents").execute()
    items = results.get('files', [])
    items.sort(key=get_date)


    # Read and delete
    if not items:
        print('No files found.')
    else:
        for item in items:
            read_msg_in_eml(get_file_content(service, item['id']))
            time.sleep(10)
            finish_speaking()
            service.files().delete(fileId=item['id']).execute()
            

def get_file_content(service, file_id):
  """Get a file's content.

  Args:
    service: Drive API service instance.
    file_id: ID of the file to print content for.
  """
  try:
    file = service.files().get_media(fileId=file_id).execute()
    return file
  except errors.HttpError as error:
    print('An error occurred: %s' % error)

def read_msg_in_eml(file):
    msg = email.message_from_bytes(file)
    # 本文を取得する
    for part in msg.walk():
        if part.get_content_maintype() == "multipart":
            continue
        if part.get_filename() is None:
            charset = str(part.get_content_charset())
            if charset:
                body = part.get_payload(decode=True).decode(charset, errors="replace")
            else:
                body = part.get_payload(decode=True)
                
　　# 文字コードとスピードを設定。
    speech.say(body,'ja-JP',0.75)

def finish_speaking():
    # Block until speech synthesis has finished
    while speech.is_speaking():
        time.sleep(0.1)

if __name__ == '__main__':
    main()

```

## 使ってみて

実際に順番にEmailを読んでくれて感動！しかしそのあとでgoogle mailのAPIもあることを発見。直接Emailにアクセスした方が良いじゃん。。
このやり方だと以下の機能が実装できない
- Gmailを既読にするかどうかを選ぶ
- 未読にする場合、アーカイブフォルダに保存
ググるの下手なばっかりに、まどろっこしいやり方を選んでしまった笑