データサイエンス

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