課題
データ連携のツールとしてmailのケースがたまにあります。
それを予測データとして他のツールにimportしたりすると思いますが、
- ローカルにダウンロード
- ダウンロードしたデータをDBにimport
と人作業の部分が発生してしまうのでそれをどうにかしたい
その課題から、mailに添付されたファイルを自動で取得できるようした時のメモです。
事前準備
今回Gmailアカウントを対象としてます
Gmailは自前で作成したコードから接続を試みると”安全性の低いアプリ”と判断されてrejectするので、まずは安全性の低いアプリの許可を有効にする必要があります。
Googleの安全性の低いアプリのアクセスページに入りログインして許可にします。
使用する時だけこれを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)
- imap.gmail.comはホスト名、993はポート番号でimap4用ポートとして登録されているwell known portになります
ログイン
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