Python3 + google-api-python-clientで、Gmail APIを使ってメールを送信する

前回はPythonの標準ライブラリを使って、Gmailからメールを送信しました。
Python3.5 + smtplib.SMTP_SSL.send_message()で、Gmailからメールを送信する - メモ的な思考的な

 
ただ、Googleアカウント名とパスワードをスクリプト上に書いておく必要があったため、何となく嫌な感じでした。

調べてみたところ、GmailGmail APIを使うことで、Googleアカウントのパスワードの記載が不要になることがわかったため、今回試してみます。

 
目次

 

環境

 
Google製のライブラリgoogle-api-python-clientは現在Python3をサポートしているものの、

という点に注意します。

 

事前準備

Python Quickstartを参考に、事前準備を行います。
Python Quickstart  |  Gmail API  |  Google Developers

 

Google Developers Console で Gmail API を有効化

Quickstartのリンク(リンク先:API を有効にする)より、以下の流れで登録を行います。

認証情報の作成ではどの形を選択すれば良いのか悩みましたが、Quickstartを参考にウィザードで選択する形を取りました。Webアプリ化する場合などは別の方法になるかと思います。

  • 新しいプロジェクトを作成続行My Projectプロジェクトが作成
    • しばらく待つと「プロジェクトが作成され Gmail API が有効化されました。」
  • 認証情報に進むをクリック
  • 認証情報を作成をクリック
  • ウィザードで選択をクリック
    • 使用するAPI: Gmail API
    • APIを呼び出す場所: その他のUI (Windows、CLIツールなど)
    • アクセスするデータの種類: ユーザーデータ
    • 必要な認証情報をクリック
  • 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種類の方法が記載されていました。

エンコードされる内容が微妙に違うため、どちらが正しいのか調べましたが、後者のドキュメントを読んだ際に、

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()を使います。

 
そこで、

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シリアライズのところで失敗しているようです。

 
エラーとなったのは先ほどエンコードしたバイト列でした。バイト列はシリアライズできないようです。

Dive Into Python 3 日本語版には、バイト列をシリアライズさせる方法が記載されていますが、シリアライズはライブラリのこのあたりで行われているため手が出せません。

 
そこで、

  1. message.as_string().encode(encoding="UTF-8")でバイト列にエンコード
  2. base64.urlsafe_b64encode()でbase64urlでエンコード
  3. エンコード結果はバイト列のため、str.decode(encoding="UTF-8")で文字列にデコード
  4. 上記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