前回はPythonの標準ライブラリを使って、Gmailからメールを送信しました。
Python3.5 + smtplib.SMTP_SSL.send_message()で、Gmailからメールを送信する - メモ的な思考的な
ただ、Googleアカウント名とパスワードをスクリプト上に書いておく必要があったため、何となく嫌な感じでした。
調べてみたところ、GmailのGmail APIを使うことで、Googleアカウントのパスワードの記載が不要になることがわかったため、今回試してみます。
目次
環境
Google製のライブラリgoogle-api-python-client
は現在Python3をサポートしているものの、
- 現時点の公式ドキュメントにはPython3.5の文字が無い
- GitHubのREADMEには以下の記載あり
Python 3.3+ is also now supported! However, this library has not yet been used as thoroughly with Python 3, so we'd recommend testing before deploying with Python 3 in production.
という点に注意します。
事前準備
Python Quickstartを参考に、事前準備を行います。
Python Quickstart | Gmail API | Google Developers
Google Developers Console で Gmail API を有効化
Quickstartのリンク(リンク先:API を有効にする)より、以下の流れで登録を行います。
認証情報の作成ではどの形を選択すれば良いのか悩みましたが、Quickstartを参考にウィザードで選択する形を取りました。Webアプリ化する場合などは別の方法になるかと思います。
新しいプロジェクトを作成
で続行
、My Project
プロジェクトが作成認証情報に進む
をクリック認証情報を作成
をクリックウィザードで選択
をクリック- OAuth 2.0 クライアント ID を作成
- 名前:
MyGmailSender
クライアントIDの作成
をクリック
- 名前:
- OAuth 2.0 同意画面を設定
- メールアドレス: 自分のGmailアドレス
- ユーザーに表示するサービス名:
MyGmailSender Auth
次へ
をクリック
- 認証情報をダウンロード
ダウンロード
をクリック、client_id.json
ファイルをダウンロード- 画面に表示されているClient IDは、jsonファイルの中にも記載あり
完了
をクリック
アプリケーション環境の準備
Googleのドキュメントでは、ユーザのディレクトリにclient_secret.json
ファイルを保管していましたが、今回はアプリのディレクトリに保管します。
# Python + google-api-python-client環境の構築 >virtualenv -p c:\python35-32\python.exe env >env\Scripts\activate (env) >pip install google-api-python-client # ダウンロードしたJSONファイルをコピーしてリネーム (env) >copy %USERPROFILE%\Downloads\client_id.json .\ 1 個のファイルをコピーしました。 (env) >ren client_id.json client_secret.json
実装
認証部分
認証部分については、Python Quickstartにあるget_credentials()
関数を参考にします。
Python Quickstart | Gmail API | Google Developers
今回はclient_secret.json
ファイルをスクリプトと同じディレクトリに置いたため、その部分は変更しておきます。
また、今回はメールを送信するだけなので、SCOPE定数の値をhttps://www.googleapis.com/auth/gmail.send
へと変更しておきます。
Choose Auth Scopes | Gmail API | Google Developers
SCOPES = "https://www.googleapis.com/auth/gmail.send" def get_credentials(): script_dir =os.path.abspath(os.path.dirname(__file__)) credential_dir = os.path.join(script_dir, ".credentials") if not os.path.exists(credential_dir): os.makedirs(credential_dir) credential_path = os.path.join(credential_dir, "my-gmail-sender.json") store = oauth2client.file.Storage(credential_path) credentials = store.get() if not credentials or credentials.invalid: flow = oauth2client.client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) flow.user_agent = APPLICATION_NAME credentials = oauth2client.tools.run_flow(flow, store, flags) print("Storing credentials to " + credential_path) return credentials
メッセージの作成
公式ドキュメントを参考に作成します。
Users.messages: send | Gmail API | Google Developers
Request bodyのrawプロパティへ設定する際、エンコードする必要がありますが、公式ドキュメントでは2種類の方法が記載されていました。
base64.urlsafe_b64encode()
を使う- base64.b64encode()
エンコードされる内容が微妙に違うため、どちらが正しいのか調べましたが、後者のドキュメントを読んだ際に、
The entire email message in an RFC 2822 formatted and base64url encoded string.
https://developers.google.com/gmail/api/v1/reference/users/messages/send#auth
と、その後のサンプルコードと異なった解説が書かれていたことと、ライブラリのリファレンスや他の言語でもbodyはbase64urlでエンコードすると書かれていたことからも、base64.urlsafe_b64encode()
を使うことにしました。
そこで、
def create_message(): message = MIMEText("Gmail body: Hello world!") message['from'] = MAIL_FROM message['to'] = MAIL_TO message['subject'] = "gmail api test" message["Date"] = formatdate(localtime=True) return {'raw': base64.urlsafe_b64encode(message.as_string())}
としたところ、実行時に
TypeError: a bytes-like object is required, not 'str'
というエラーになりました。
Python3のドキュメントでは、bytes-like objectを引数に取ると書かれており、それが今回の原因と考えられました。
base64.urlsafe_b64encode(s) - 19.6. base64 — Base16, Base32, Base64, Base85 データの符号化 — Python 3.5.1 ドキュメント
message.as_string()
の結果が文字列型となるため、これをバイト列型へと変換する必要があるようです。
変換するには、str.encode()を使います。
- Best way to convert string to bytes in Python 3? - Stack Overflow
- Python の Unicode サポート - Unicode HOWTO — Python 3.5.1 ドキュメント
そこで、
def create_message(): ... message["Date"] = formatdate(localtime=True) byte_msg = message.as_string().encode(encoding='UTF-8') return {'raw': base64.urlsafe_b64encode(byte_msg)}
のように変更し、
result = service.users().messages().send( userId=MAIL_FROM, body=create_message() ).execute()
とAPIを叩いたところ、
TypeError: b'Q29...==' is not JSON serializable
というエラーになりました。JSONのシリアライズのところで失敗しているようです。
エラーとなったのは先ほどエンコードしたバイト列でした。バイト列はシリアライズできないようです。
- Python データ構造に対する拡張可能な JSON エンコーダ - 19.2. json — JSON エンコーダおよびデコーダ — Python 3.5.1 ドキュメント
- 13.9 Pythonのデータ型をjsonにマッピングする - Pythonオブジェクトをシリアライズする - Dive Into Python 3 日本語版
Dive Into Python 3 日本語版には、バイト列をシリアライズさせる方法が記載されていますが、シリアライズはライブラリのこのあたりで行われているため手が出せません。
そこで、
message.as_string().encode(encoding="UTF-8")
でバイト列にエンコードbase64.urlsafe_b64encode()
でbase64urlでエンコード- エンコード結果はバイト列のため、
str.decode(encoding="UTF-8")
で文字列にデコード - 上記3.の結果、base64urlエンコードされた文字列が得られたので、rawプロパティへセット
の実装へと変更し、動作を確認しました。
ソースコード全体は以下の通りです。
import httplib2 import os import apiclient import oauth2client import argparse flags = argparse.ArgumentParser( parents=[oauth2client.tools.argparser] ).parse_args() import base64 from email.mime.text import MIMEText from email.utils import formatdate import traceback # If modifying these scopes, delete your previously saved credentials # at ~/.credentials/gmail-python-quickstart.json SCOPES = "https://www.googleapis.com/auth/gmail.send" CLIENT_SECRET_FILE = "client_secret.json" APPLICATION_NAME = "MyGmailSender" MAIL_FROM = "example@gmail.com" MAIL_TO = "example+alias@gmail.com" def get_credentials(): script_dir =os.path.abspath(os.path.dirname(__file__)) credential_dir = os.path.join(script_dir, ".credentials") if not os.path.exists(credential_dir): os.makedirs(credential_dir) credential_path = os.path.join(credential_dir, "my-gmail-sender.json") store = oauth2client.file.Storage(credential_path) credentials = store.get() if not credentials or credentials.invalid: flow = oauth2client.client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES) flow.user_agent = APPLICATION_NAME credentials = oauth2client.tools.run_flow(flow, store, flags) print("Storing credentials to " + credential_path) return credentials def create_message(): message = MIMEText("Gmail body: Hello world!") message["from"] = MAIL_FROM message["to"] = MAIL_TO message["subject"] = "gmail api test" message["Date"] = formatdate(localtime=True) byte_msg = message.as_string().encode(encoding="UTF-8") byte_msg_b64encoded = base64.urlsafe_b64encode(byte_msg) str_msg_b64encoded = byte_msg_b64encoded.decode(encoding="UTF-8") return {"raw": str_msg_b64encoded} def main(): credentials = get_credentials() http = credentials.authorize(httplib2.Http()) service = apiclient.discovery.build("gmail", "v1", http=http) try: result = service.users().messages().send( userId=MAIL_FROM, body=create_message() ).execute() print("Message Id: {}".format(result["id"])) except apiclient.errors.HttpError: print("------start trace------") traceback.print_exc() print("------end trace------") if __name__ == "__main__": main()
ブラウザでの許可
コマンドラインからの初回実行時、途中でブラウザが起動し、
MyGmailSender Auth が次の許可をリクエストしています:
ユーザーに代わるメールの送信
という画面が表示されます。
ここで許可
をクリックすると、The authentication flow has completed.
が表示されて使えるようになります。
また、コマンドラインにも以下のような結果が表示されます。
(env) >python gmail_sender.py Your browser has been opened to visit: https://accounts.google.com/o/oauth2/auth?access_type=offline... If your browser is on a different machine then exit and re-run this application with the command-line parameter --noauth_local_webserver Authentication successful. Storing credentials to path\to\.credentials\my-gmail-sender.json
ソースコード
GitHubに上げておきました。gmail_sender.py
が今回のファイルです。
thinkAmi-sandbox/google-api-python-client-sample
その他
Gmail APIのリミットについて
以下に記載がありました。
Usage Limits | Gmail API | Google Developers
GoogleのOAuth2.0について
以下にまとまっていました。
Using OAuth 2.0 to Access Google APIs | Google Identity Platform | Google Developers