きっかけ
移動時間に購読しているメールマガジンを音声読み上げ機能を使って読んでもらっているが、メールマガジンは比較的短いので、iphoneの純正メールアプリでいちいち本文を選択し、読み上げ機能を選択して読ませるということを2-3分置きにしたくなかった。
やりたいこと
- Gmailで特定ラベルがついた未読メール(メールマガジン)を日付順で読ませたい
- 読み終わった後は既読に。(できれば、特定のメールマガジンが非常に有用だと感じた場合、未読のままで留めて、他のフォルダに移動するオプションも実装したい。ここも音声で指示できたら最高)
実装方法
Google Drive API を使うやり方を考えた。
順番としては
- ラベルのついた未読メールを.eml形式でgoogle drive 所定のフォルダに保存
- Google Drive APIにアクセスし、日付をソートして本文を順番に読み上げさせる。読み終わったらファイルを削除。
序. Google Drive APIの設定
1. .eml形式でgoole drive に保存する機能
Google App Scriptで実装。
これで、"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()
});
}
}
- 定期実行する。
App Script内からトリガーのタブを開き、新しいトリガーを設定するだけ。時間帯で毎日定期実行できて便利。
2. 読み上げ機能
こちらはpythonistaで実装。
- Pythonistaにstashを入れる → こちらを参考にした: Pythonista3のStash導入〜pip installまでの手順
- これで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関連のパッケージを全部移してこの問題は解決した。
- これが全体のコード
# 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を既読にするかどうかを選ぶ
- 未読にする場合、アーカイブフォルダに保存
ググるの下手なばっかりに、まどろっこしいやり方を選んでしまった笑