PythonでGmail APIに触れてみる: メールの下書きを作る

PythonでGmail APIに触れてみる: メールの下書きを作る


久々の更新になります。Gmail APIを触れてみるシリーズ。このシリーズの最後としてメールの下書きを作成してみます。

下書きを作ってメール送付までができると、メールを受け取って処理する流れの自動化ができ上がります。

GMail APIでメールを作成する

スコープを変更する

今までのシリーズでは、gmail.readonlyスコープでAPIを使ってきましたが、今回は下書きを作るためにgmail.composeスコープを追加します。こう言った作業が面倒と思ってすべての権限を持つスコープを渡すこともできますが、セキュリティ的にも必要ない権限は渡さないようにするのが良いでしょう。

まずコード上のスコープのリストに、gmail.composeを追加します。

SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly", # 今まではこれだけだった
    "https://www.googleapis.com/auth/gmail.compose", # 下書きの追加変更はこのスコープが必須
]

Gmail APIの認証画面の設定も変更が必要です。Google Cloud Console > OAuth同意画面の2番目 > スコープの追加または削除でhttps://www.googleapis.com/auth/gmail.composeを追加してください。

APIで認証した後に保存されるtokenのjsonファイルは一度削除して再度OAuth認証をしましょう。

この先のコード上ではすでにスコープのリストにgmail.composeを追加しています。

下書きを作る

下書きは drafts.createメソッドを使います。この時のmessageは、RFC 2822の形式に沿ったものを用意します。Pythonだとemail.messageモジュールのEmailMessageクラスを使うと楽です。 また添付ファイルも、EmailMessageクラスで作成したオブジェクトに添付ファイルを追加することで、添付ファイルを追加できます。

# create_draft_new_message.py
import base64
from email.message import EmailMessage

# こちらを参考: https://hr-sano.net/blog/gmail-api-read-attachment/#get_cledential
from googleauth_util import get_cledential

from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# readonlyに、下書きを作成するための権限を追加
SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.compose",
]

# 利用する際に入力します
SUBJECT = "タイトル"
BODY = "本文"
TO_ADDRESS = ""  # 送信先
ATTACHMENT_PATH = ""  # 添付ファイルのパス


def main():
    creds = get_cledential(SCOPES)
    try:
        service = build("gmail", "v1", credentials=creds)

        # 下書きを作成
        message = EmailMessage()
        message.set_content(BODY)
        message["subject"] = SUBJECT
        message["to"] = TO_ADDRESS

        # 添付ファイルがある場合は追加
        # TODO: ここでは一つのファイルしか扱っていない。複数の場合はループ処理で登録する
        if ATTACHMENT_PATH:
            with open(ATTACHMENT_PATH, "rb") as f:
                attachment = f.read()
            message.add_attachment(
                attachment,
                maintype="application",
                subtype="octet-stream",
                filename=ATTACHMENT_PATH,
            )

        # apiで下書きを作成する
        draft = (
            service.users()
            .drafts()
            .create(
                userId="me",
                body={
                    "message": {
                        "raw": base64.urlsafe_b64encode(message.as_bytes()).decode()
                    }
                },
            )
            .execute()
        )
        print(f"Create draft. Draft id: {draft['id']}")

    # エラーハンドリング
    except HttpError as error:
        print(f"An error occurred: {error}")


if __name__ == "__main__":
    main()

返信として扱う(スレッドを指定する)

作成するメールをとあるメールの返信としたい場合、スレッドを使うことで返信として扱うことができます。

スレッドIDが必要で、かつスレッド内のメッセージIDが必要になりますが、APIでスレッドIDからスレッド内のメール一覧を取得し、一番最後のメッセージのIDを使えば良い形です。そのほか、Gmailのスレッドとして扱う際には、References, In-Reply-Toヘッダーの指示も必要になります。

その他はコードを見てもらえたらと思ってます。とくに返信として扱うので、引用元のメッセージを取得して引用形式で返信するようにしていますが、これですべてのメールで動くようなものではないので注意が必要です。

# create_draft_thread.py
import base64
from email.message import EmailMessage

# こちらを参考: https://hr-sano.net/blog/gmail-api-read-attachment/#get_cledential
from googleauth_util import get_cledential

from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

# readonlyに、下書きを作成するための権限を追加
SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.compose",
]

# 利用する際に入力します
THREAD_ID = ""  # スレッドIDを指定
MSG_BODY = "返信本文"
ATTACHMENT_PATH = ""  # 添付ファイルのパス


def main():
    creds = get_cledential(SCOPES)
    try:
        service = build("gmail", "v1", credentials=creds)

        # スレッドIDを指定してスレッド情報を取得 -> スレッドの最下部=最新メッセージを取得
        thread = service.users().threads().get(userId="me", id=THREAD_ID).execute()
        # print(thread)
        quote_message = thread["messages"][-1]
        print(
            f'Quoted message id:{quote_message["id"] } snippet:{quote_message["snippet"][:30]}'
        )

        message = EmailMessage()

        # 引用する形で返信する
        # メッセージの種類によっては引用しづらい時がある。引用元のメッセージはtext/planeを使うと良い
        # partsがない場合 = シンプルなテキストベースの場合
        if not quote_message["payload"].get("parts"):
            reply_info = "--- quoted_message ---"
            # 元のメッセージのボディを引用符で囲む
            quoted_body = f'> {base64.urlsafe_b64decode(quote_message["payload"]["body"]["data"]).decode("utf8")}'
            quoted_body = quoted_body.replace("\\", "\\\\").replace("\n", "\n> ")
            reply_body = f"{MSG_BODY}\n{reply_info}\n{quoted_body}"

            message.set_content(reply_body)
        # payloadにpartがある場合は返信元のメッセージを引用しないようにしています。
        # htmlメールの場合はtext/planeのメール本文もあるのですが探すコードをここに書くと長くなるので割愛してます。
        else:
            message.set_content(MSG_BODY)

        # 送信先を指定: 返信元のメッセージのfromを取得して送信先に設定
        # headersの構造は、以下のようになっている
        # [
        #     {
        #     "name": "To",
        #     "value": "送信先アドレス"
        #     },
        # ...]
        # このようになっているので、nameのtoを探す必要があるので、リスト内包表記で探し、取得したvalueの最初の要素next関数で取得
        message["to"] = next(
            (
                i.get("value", "")
                for i in quote_message["payload"]["headers"]
                if i.get("name").lower() == "from"
            )
        )
        quoted_subject = next(
            (
                i.get("value", "")
                for i in quote_message["payload"]["headers"]
                if i.get("name").lower() == "subject"
            )
        )
        message["subject"] = f"Re: {quoted_subject}"

        # References,In-Reply-Toを設定する
        # 返信先のメッセージのMessage-IDを取得して、それを設定する
        message_id = next(
            (
                i.get("value", "")
                for i in quote_message["payload"]["headers"]
                if i.get("name").lower() == "message-id"
            )
        )
        message["reply-to"] = message_id
        message["references"] = message_id

        # 添付ファイルがある場合は追加
        # TODO: ここでは一つのファイルしか扱っていない。複数の場合はループ処理で登録する
        if ATTACHMENT_PATH:
            with open(ATTACHMENT_PATH, "rb") as f:
                attachment = f.read()
            message.add_attachment(
                attachment,
                maintype="application",
                subtype="octet-stream",
                filename=ATTACHMENT_PATH,
            )

        # apiで下書きを作成する
        draft = (
            service.users()
            .drafts()
            .create(
                userId="me",
                body={
                    "message": {
                        "raw": base64.urlsafe_b64encode(message.as_bytes()).decode()
                    }
                },
            )
            .execute()
        )
        print(f"Create draft. Draft id: {draft['id']}")

    # エラーハンドリング
    except HttpError as error:
        print(f"An error occurred: {error}")


if __name__ == "__main__":
    main()

プログラムから下書きメールを送る時

下書きメール自体はGmailアプリ側でも確認できるので、あとはそこから手動送信することも可能です。

もし自動で送信したい場合は、drafts.sendメソッドを使うことで送信されます。

メールの送信  |  Gmail  |  Google for Developers

このメソッドを使う際の判断は、自動で送るか手動で人が送るかはシチュエーションによると思います。

メールを自動的に送信したいシチュエーションは、大量にメールを送信する必要がある場合は行っても良いと思います。または本当に自動化しても問題ないやり取りだけに絞る方が良いと思われます。

メールの事故はよくあるトラブルなので、確認や送信は人が行うプロセスを挟む、レビュープロセスの1つとして下書きメールを作り、中身を確認して送る方法も有効です。たとえばGoogleChatでメール内容のレビューをして承認をした上で送付するような機能実装も良いアイデアと思います。

まとめ

メール本体が作成できるとメールでやり取りしている作業の多くが自動化できると思います。

ここまでで、Gmail APIのシリーズになりました。今後もGoogle WorkspaceのAPIについては扱ったものを載せていきます。次は最近一番使ってるGoogle ChatのAPIについて書いていこうと思います。

おまけ: メールのデータ構造どうするか問題

しかしここまで書いていて思うこととして、PythonでGmail APIのメールデータを扱うのは結構骨が折れます。自家製でGmail APIのJSONデータをクラス化して見てますが、継ぎ足し的に作った結果独自なデータ構造にしてしまったので、PythonのEmailMessageを使ったものに変更しようかなと思いつつ、EmailMessageで扱えないAPI独自のデータをどうするかなど悩みどころでした。

何か良い方法 or ライブラリがあればなーと思ってます。ご存知な方がいれば教えてください!

参考

宣伝

所属会社の宣伝です! 自動車プレス金型の機械設計を行う設計事務所で、製品や試作品の3Dモデリングのお仕事も受け付けています。

また製造業で上記記事で扱う作業の自動化やその先に繋がるデジタル化、DXに取り組みたい方もご相談承っております。

今回の記事で扱ったGmail等のメールの扱いや、データを抽出したり、円滑な作業自動化を行いたい方はぜひお気軽にお問い合わせください!

お問い合わせは設計事務所のWEBサイトにて!

付録1: get_cledential関数について

サンプルコートにあるget_cledential関数はGmail APIを使うための認証情報を取得する自作関数です。詳しくは前回の記事をご覧ください。