PythonでGmail APIに触れてみる: メールにある添付ファイルや画像を収集してみよう

PythonでGmail APIに触れてみる: メールにある添付ファイルや画像を収集してみよう

メールにあるファイルや画像を扱ってみよう

前回の続きです。Gmail APIでメール本文を取得する方法を紹介しました。

今回は、前回にも書いていたメールに添付されたファイルを収集する方法を紹介します。

会社では依頼案件のメールを頂くことが多いのですが、その際に添付されたファイルを収集するのが面倒でした。今はGmail APIで添付ファイルを収集してプロジェクトごとに展開するようにしています。添付ファイルをひとつづつダウンロードしてコピペする手間はこれで解消されました。

(検索についてはまた次回以降にて!)

過去シリーズはこちら

メールの添付ファイルや画像はどこにあるか

Gmail APIでメールの添付ファイルを扱うとき、添付ファイル自体は別のリソースになっています。ドキュメントではuser.messages.attachmentsリソースとして扱われています。詳しくは後ほどの章で解説します。

そのリソースを扱う前にMessageリソース>MessagePartリソースを取得しておく必要があります。

これは冒頭にも書いた前回の記事を見てください。

Gmailの添付ファイルはMessagePartリソースの中から見ていきます。メールに添付ファイルがある場合のMimeTypeはmultipart/mixedです。MessagePartのpartsに添付ファイルについての情報があります。

REST Resource: users.messages  |  Gmail  |  Google for Developers

{
  "partId": string,
  "mimeType": string,
  "filename": string,
  "headers": [
    {
      object (Header)
    }
  ],
  "body": {
    object (MessagePartBody)
  },
  "parts": [
    {
      object (MessagePart)
    }
  ]
}

リソース内のpartsにはメール本文以外にも添付ファイルやhtmlメールで入れられている画像もあります。これらもすべてMessagePartリソースのmimeTypeで判断できます。添付ファイルだと大抵はapplication/*で、PDFならapplication/pdf、Zipファイルならapplication/x-zip-compressed、画像ならimage/*です。

MessagePartリソースの例はこんな感じです。

{
  "partId": "1",
  "mimeType": "multipart/mixed",
  "filename": "",
  "headers": ...,
  "parts": [
    {
      "partId": "0.0",
      "mimeType": "text/plain",
      "filename": "",
      "headers": [
        {
          "name": "Content-Type",
          "value": "text/plain; charset=\"UTF-8\""
        }
      ],
      "body": {
        "size": 0
      }
    },...
    {
      "partId": "1",
      "mimeType": "application/x-zip-compressed",
      "filename": "tenpu-file.zip",
      "headers": ...,
      "body": {
        "attachmentId": "....",
        "size": 123456
      }
    },
    {
      "partId": "2",
      "mimeType": "image/png",
      "filename": "image.png",
      "headers": ...,
      "body": {
        "attachmentId": "....",
        "size": 123456
      }
    }
  ]
}

ここで、mimeTypesapplication/*image/*のものを収集できたらヨシ!ということになります。

メールにある画像や添付ファイルを収集

冒頭に書いた通り画像や添付ファイルはuser.messages.attachmentsリソースとして扱われます。これはMessagePartリソースのbodyの中にあるattachmentIdを使って取得します。

REST Resource: users.messages.attachments  |  Gmail  |  Google for Developers

{
  "attachmentId": string,
  "size": integer,
  "data": string
}

このattachmentIdがない場合はbody>dataの中に入っているようです。これはMimeTypeがtext/plane等のメール本文(MessagePartBodyリソース)の時に当たるようです。(最初このリソースの意味が理解できずにbody>attachmentIdをBASE64でエンコードして悩んでいたのは内緒です)

最後にuser.messages.attachments.getメソッドを使いBASE64でエンコードされた添付ファイルのデータを取得します。ファイル保存はデコードをしてバイナリデータとして保存します。

サンプルコードはこちらです。

import base64
from pathlib import Path

# 認証用の関数を新設しました。付録を参照してください。
from googleauth_util import get_cledential

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

SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]

SAVE_AS_DIR_PATH = Path("mail_files")

# 画像や添付ファイルがあるメールメッセージID
MESSAGE_ID = "[ここに添付ファイルや画像の埋め込みがあるhtmlメールのIDを入れる]"


def decode_base64url(data):
    """Base64URLでエンコードされたデータをデコードする"""
    if data:
        return base64.urlsafe_b64decode(data)
    return None


def download_attachment(service, userId, messageId, attachment_id, filepath):
    """
    添付ファイルをダウンロードして保存する関数
    """
    attachment = (
        service.users()
        .messages()
        .attachments()
        .get(userId=userId, messageId=messageId, id=attachment_id)
        .execute()
    )
    file_data = decode_base64url(attachment["data"])
    with open(filepath, "wb") as f:
        f.write(file_data)
    print(f"Saved attachment to {str(filepath)}")


def find_and_download_attachments(
    service, message_id: str, message_payload: dict, userId: str, directory: Path
):
    """
    メッセージから添付ファイルを探索し、ダウンロードする関数
    """
    directory.mkdir(exist_ok=True)

    for part in message_payload.get("parts", []):
        filename = part.get("filename")
        body = part.get("body", {})
        attachment_id = body.get("attachmentId")

        if filename and attachment_id:
            # 添付ファイルが見つかった場合、ダウンロード
            print(f"filename: {filename}, attachment_id: {attachment_id}")
            filepath = directory / filename
            download_attachment(service, userId, message_id, attachment_id, filepath)

        if "parts" in part:
            # 入れ子になっている場合は再帰的に探索
            find_and_download_attachments(service, message_id, part, userId, directory)


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

        # メッセージの取得
        message = service.users().messages().get(userId="me", id=MESSAGE_ID).execute()

        print(f"Message ID: {message['id']}")
        print(f"Message snippet: {message['snippet']}")

        # 添付ファイルの取得
        message_payload = message["payload"]
        find_and_download_attachments(
            service, MESSAGE_ID, message_payload, "me", SAVE_AS_DIR_PATH
        )

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


if __name__ == "__main__":
    main()

(今回もChatGPTと一緒にコード生成してました。やっぱり便利ですね)

まとめ

最近はストレージサービスやファイル共有する手段も多数あり、より大容量ファイルの共有もしやすくなっていますが、添付ファイルや画像ファイルは今なおメールでも扱う大事なデータです。

メールの本文と同様、見落としたりすることもあるので、自動的に必要なファイルを収集することはとても便利ですね。

次回はメールの検索について扱います。

サンプルコード: gmail-api-example-python/src/gmail_api_example_python/read_attachment at main · hrsano645/gmail-api-example-python

宣伝

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

また製造業の業務、バックオフィスの自動化やその先に繋がるデジタル化、DXに取り組みたい方もご相談承っております。 今回の記事で扱ったGmail等のメールの扱いや、データを抽出して作業の自動化を行いたい方は、ぜひお気軽にお問い合わせください!

付録: get_cledential関数について

get_cledential関数はGmail APIを使うための認証情報を取得する関数です。前回はソースコードへべた書きしていましたが、別ファイルに分離しました。 同じ位置やインポート可能な位置へgoogleauth_util.pyというファイル名で保存してください。今後はこのモジュールを利用したコードを前提にします。

# googleauth_util.py
from pathlib import Path

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow

# トークンとクレデンシャルの保存先
token_save_path = Path("google_api_access_token.json")
cred_json = Path("credentials.json")


# クレデンシャルの取得用の関数
def get_cledential(scopes: list[str]) -> Credentials:
    """
    Google APIの認証情報をOAuth2の認証フロー(クライアントシークレット)で取得します。
    既に認証情報があればそれを返します。
    リフレッシュトークンに対応しています。
    なければ認証情報を取得し、google_api_access_token.jsonに保存します。
    args:
        scopes: 認証情報を取得する際に必要なスコープ
    return:
        認証情報
    """
    creds = None
    if token_save_path.exists():
        creds = Credentials.from_authorized_user_file(token_save_path, scopes)
    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(cred_json, scopes)
            creds = flow.run_local_server(port=8081)
        with token_save_path.open("w") as token:
            token.write(creds.to_json())
    return creds

About Me

買ったり作ったり考えたり試したの日々の記録です