Gmailの添付ファイルをpythonで取得する

課題

データ連携のツールとしてmailのケースがたまにあります
それを予測データとして他のツールにimportしたりすると思いますが

  • ローカルにダウンロード
  • ダウンロードしたデータをDBにimport と人作業の部分が発生してしまうのでそれをどうにかしたい

その課題から、mailに添付されたファイルを自動で取得できるようした時のメモ

事前準備

今回Gmailアカウントを対象としてます Gmailは自前で作成したコードから接続を試みると"安全性の低いアプリ"と判断されてrejectするので まずは安全性の低いアプリの許可を有効にする必要がある

Google安全性の低いアプリのアクセスページに入りログインして許可にします

f:id:gri-blog:20210707125630p:plain

使用する時だけこれをONにしたいのですが、この有効化手順の自動はstackoverflowのここ見る限りでは難しそうです

今回実行したpythonのバージョンは3.9.1となります

実行

必要なモジュール

import imaplib
import base64
import os
import email
import datetime as dt
  • 全て標準ライブラリになります。pipなどでの追加インストール作業は不要です
  • imaplibはemailプロトコルimap standardのpackage
  • base64はASCIIへの変換、逆変換するものとしてimportしてます

次にmailのプロトコルを使った設定

mail = imaplib.IMAP4_SSL('imap.gmail.com', 993)

ログイン

email_user = 'email-address@gmail.com'
email_pass = 'email-password'

mail.login(email_user, email_pass)

エラーが起きなければ接続ができています Inbox内のメールから対象ファイルを取得したいので場所を指定します

mail.select('Inbox')

Inbox内の対象ファイルを検索します
今回は送信元のアドレス、期間で絞り込みます

t_addr = '送信元メールアドレス'

t_date = '2021-07-01'
t_date_format = dt.datetime.strptime(t_date, '%Y-%m-%d')

search_option = f'(FROM "{t_addr}" SENTSINCE "{t_date_format.strftime("%d-%b-%Y")}")'
type, data = mail.search(None, search_option)

t_date, t_date_formatは対象となる日付を指定し、それを日付型にしてます
また、searchが認識する日付型に再度変換してます('2021-07-01' -> '01-Jul-2021')
RFC-822に沿った日付型だとエラー。年月日までの型じゃないとダメみたいです

searchの最初の引数はcharsetとなり、特に指定する文字形式がなければNoneにします
次の引数でフィルタをかけます。最低1つの条件が必要となってます
imaplib.IMAP4.search

受け取ったtype, dataはこんな感じ

OK [b'477 1149 1522 1724 1954']

typeは成功したかどうか、成功した場合dataにはlist型でメッセージ番号がスペース区切りで入っています

これを直近のメッセージだけ取得する場合は下記のように書きます

t_number = data[0].split()[-1]  # b'1954'

で、今後は受け取った番号のメッセージ内容を取得するためfetchを使います

type, data = mail.fetch(t_number, '(RFC822)')

受け取ったdataはlist型内にtupleとしてメッセージのコンテンツが入っています
またデータはbyte型なのでここでemailモジュールを使ってパースします

email_message = email.message_from_bytes(data[0][1])

パースしたメッセージを順に読み込んでいき、添付されたfileを検索
取得したらそれをローカルファイルに書き込みます

for part in email_message.walk():
    file_name = part.get_filename()
    if not file_name:
        continue
    fns = file_name.split('?')
    output_file_name = base64.b64decode(fns[-2]).decode(fns[1])
    with open(f'{os.getcwd()}/{output_file_name}', 'wb') as f:
        f.write(part.get_payload(decode=True))

get_filename()で添付ファイル名を取得 file_nameは場合によってこんな感じになっていると思います

file_name: =?ISO-2022-JP?B?nanikashiranomojiretsu=?=

これは下記の形式になっており

=?文字セット?エンコード方式?エンコード文字列?=

文字セット、エンコード方式から文字列をデコードする必要があるんですね

?で区切ったリストから文字セットとエンコード文字列(ファイル名)を取得したあと人が読める文字にデコード
これをアウトプットのファイル名(添付ファイルと同名)にし、payloadからファイルコンテンツを取得して書き込んでます

エンコードされていないファイルの場合はfile_nameをそのまま使用します

いかがでしたでしょうか?メールのプロトコルって歴史があるためか、今のapiとかと比べると取得が大変なんですねー
ちなみに今回は受信メールを対象にしてますが、送信になるとまたプロトコルが変わりますよー

参照

IMAP4 プロトコルクライアント
Gmailで「安全性の低いアプリ」がブロックされた場合の対処方法

higashi kunimitsu