0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-03-23

GmailAPIを利用してPythonistaにメールマガジンを読ませたい

前回、GmailのAPIを使えば良いものをGoogle App ScriptとGoogle Drive APIを使って実装してしまったもののリベンジ投稿。

Gmail APIを利用する

  • こちらを参考にコードを丸コピしてまずはラベルを取得できるか確認した。

  • 前回の記事でも触れたが、pythonista上でパッケージをダウンロードすると下記のエラーが発生するかもしれない。

ModuleNotFoundError: No module named 'google.api_core'

僕にはさっぱり原因がわからなかったので、PC上Anaconda環境でquickstart.pyを作り、動作することを確認してPC上のGoogle関連のライブラリを丸ごとPythonistaの Python Modules/site-package-3内にコピーペーストしたら無事動いた。

  • こちらも前回触れたが、GmailのAPIを利用するにはまずGoogle Cloud Platformでアプリケーションの登録、Gmail APIの登録が必要。同様にcredential.json(名前はなんでも良いが、google.cloudの認証情報(クライアントIDとクライアントシークレットが入ったjsonファイル)が必要なのでダウンロードしておく。このページを参考にした。

以下、quickstart.pyをほぼ丸コピしたコード。

# 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
from google.oauth2.credentials import Credentials

# If modifying these scopes, delete the file token.json.
SCOPES = ['https://www.googleapis.com/auth/gmail.readonly',
          'https://www.googleapis.com/auth/gmail.modify']

def main(): 
"""Shows basic usage of the Gmail API.
    Lists the user's Gmail labels.
    """
    creds = None
    # The file token.json stores the user's access and refresh tokens, and is
    # created automatically when the authorization flow completes for the first
    # time.
    if os.path.exists('token_gmail.json'):
        creds = Credentials.from_authorized_user_file('token_gmail.json', SCOPES)
    # 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_gmail.json', SCOPES)
            creds = flow.run_local_server(port=0)
        # Save the credentials for the next run
        with open('token_gmail.json', 'w') as token:
            token.write(creds.to_json())

    service = build('gmail', 'v1', credentials=creds)
  • credentials_gmail.jsonはgoogle cloud platformからダウンロードしてきたファイル。こいつをpythonファイルと同階層に入れておくと、そこからtokenをjson形式で作成してくれる。(ここでは、'token_gmail.json'という名前をつけた。)
  • SCOPEはgmailAPIで何をしたいかによって変更する。僕の場合は閲覧とラベルの編集ができれば良いのでこのようなスコープになった。こちら参照。
  • tokenがもう作成されている状態でSCOPEを変更することはできない。SCOPEを変更した場合は、jsonファイル("token_gmail.json")を一旦削除してから実行すると変更を反映したtokenが作られる。

機能の実装

ラベルを無事取得したのを確認したので、自分が実践したい機能(gmail内特定のラベルをしたメールを、日付でソートして読み上げる)を実装していく。

1) threadsを取得する。

 # Call the Gmail API and get threads
    threads = service.users().threads().list(userId='me', q="is:unread label:Magazines").execute().get('threads', [])
  • service.users().threads().list(条件).execute().get('threads',[])で条件にあうthreadsをリスト形式で取得できる。
  • 条件には自分のuserIdと、queryを渡した。q="is:unread label:Magazines"は未読の、ラベルが"Magazines"で条件を絞っている。
  • 他にも、誰からのメールか、誰へのメールか、メールボックスなど指定できる。
  • threadは一連のメールのやり取りのこと。AさんがBさんにメールしてそれにBさんが返事したら2通のメールは同じthread内にあるということ。

- threadsの中身はこうなっている。

print(threads)
[{'id': 'xxx', 'snippet': 'メール本文の最初100文字くらいがここに格納される', 'historyId': 'xxx'},..., {'id': 'yyy', 'snippet': 'メール本文の最初100文字くらいがここに格納される', 'historyId': 'yyy'}]
  • threadオブジェクトのリストになっており、一つ一つのthreadオブジェクトがid, snippet, historyIdを持っている。
  • historyIdをソートすることで時系列にメールを並び替えることができた。(たまに順番が狂うらしき情報もググってて発見したが、今のところそういう事態には遭遇していない。

2) threadsをsortしたものから順番にidを取り出して、各スレッドの情報、tdataにアクセスする。

def get_history_id(e):
    return e['historyId']

    # HistoryIdでソートしたthreadsのリストを取得
    threads = sorted(service.users().threads().list(userId='me', q="is:unread label:Magazines", maxResults=300).execute().get('threads',[]),key=get_date)
      
    for thread in threads:
        tdata = service.users().threads().get(userId='me', id=thread['id']).execute()
        print(tdata)
  

- thread一つのデータの中身は下記のようになっている。

{'id': 'xxx', 'historyId': 'xxx', 'messages': [{'id': 'xxx', 'threadId': 'xxx', 'labelIds': ['UNREAD', 'CATEGORY_UPDATES', 'INBOX', 'Label_8560577985261429618'], 'snippet': 'メール本文の最初100文字くらいがここに格納される', 'payload': {'partId': '', 'mimeType': 'text/plain', 'filename': '', 'headers': [{'name': 'fuga', 'value': '931shinotaku@gmail.com'},{'name': 'hoge'}], 'body': {'size': 5244, 'data': 'M-aciDE45.....'}}, 'sizeEstimate': 9908, 'historyId': 'yyy', 'internalDate': '1616023800000'}]}
  • 目的の本文は、tdata['messages'][0]['payload']['body']['data']に格納されていた。base64形式でエンコードされていたのでデコードして本文を取得する。
import base64

def decode_body(encoded):
  """Decode message body."""
  decoded = base64.urlsafe_b64decode(encoded).decode()
  return decoded

encoded = tdata['messages'][0]['payload']['body']['data']
body = decoded_body(encoded)

3) あとはこいつを読み上げるだけ

import speech
import time

def read_msg(body):
    speech.say(body,'ja-JP',0.75)

def finish_speaking():
    # Block until speech synthesis has finished
    while speech.is_speaking():
        time.sleep(0.1)
        
def main():
    #(省略)
    for thread in threads:
        tdata = service.users().threads().get(userId='me', id=thread['id']).execute()
        body = tdata['messages'][0]['payload']['body']['data']
        read_msg(decode_body(body))
        finish_speaking()

  • speech.is_speaking()はスピーチ機能が喋っている間trueを返すので、その間は他のメールを読み上げ始めないように、whileループを回した。

次回やりたいこと

今回も期待通りに動いてくれたし、コードもpythonistaのものだけとなり、すっきりした!が、まだ使い勝手が悪い部分があるので、あんまり実用的ではなかった。

  • 途中で読み終わりたい。(メルマガは後ろの方に広告やら解約の案内やら何やら色々定型文がついてくるので、そこを飛ばして次に行く機能をつけたい)
  • UIをつけて本文を表示する。
  • 読み終わったメールはアーカイブラベルをつけるか、そのまま既読にするか、もしくはもう一度読むか、選べるようにしたい。読み上げだけだと、メールに貼り付けてある気になるリンク等は開けないし、後で確認したい時はアーカイブに保存して置けると便利。
0
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?