0
5

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-18

きっかけ

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

やりたいこと

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

実装方法

Google Drive API を使うやり方を考えた。
順番としては

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

序. Google Drive APIの設定

1. .eml形式でgoole drive に保存する機能

Google App Scriptで実装。

  1. Google Spread Sheetを起動し、新しいスプレッドシートを作成
  2. ツール>スクリプトエディタを起動
    Screen Shot 2021-03-16 at 10.29.18 AM.png
    )
  3. 下記、記述したコード

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


//メッセージを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()
    });
  }
}
  1. 定期実行する。

App Script内からトリガーのタブを開き、新しいトリガーを設定するだけ。時間帯で毎日定期実行できて便利。
Screen Shot 2021-03-17 at 6.56.10 AM.png

Screen Shot 2021-03-17 at 6.57.46 AM.png

2. 読み上げ機能

こちらはpythonistaで実装。

  1. Pythonistaにstashを入れる → こちらを参考にした: Pythonista3のStash導入〜pip installまでの手順
  2. これでpipコマンドが使えるので、必要なパッケージをインストール → 参考: Python quick start
    なお、Quickstartには
pip install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib

をしろと書いてあるが、Pythonista,Stash上ではこれができなかった。とりあえず無視して進めてみた。

案の定エラーが出た。

ModuleNotFoundError: No module named 'google.api_core'

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

  1. これが全体のコード
# 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を既読にするかどうかを選ぶ
  • 未読にする場合、アーカイブフォルダに保存
    ググるの下手なばっかりに、まどろっこしいやり方を選んでしまった笑
0
5
0

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
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?