前回は、PythonでGmailのAPIにアクセスし、メールの要約を確認するところまで紹介した。今回は、もう一歩進めて、受信したメールを指定したフォルダに保存していくプログラムを作ってみよう。メールをファイルに保存するなら、個人的なメールバックアップとして使うこともできるし、また、様々な用途にも使えるだろう。
前回までのあらすじ
さて、今回は、前回の続きとなる。既に、OAuth認証を行って、GmailのAPIが自由に使えるようになっているところから始める。もし、認証が終わってない人は、前回の内容を参考にして、作業を一通り終えておこう。
前回の補足 - APIの利用権限について
ちなみに、前回、一つ目に実行したプログラム「gmail_auth.py」は、ユーザー認証のために必要なプログラムだ。このプログラムでは、冒頭で、以下のようにスコープ(利用可能権限の範囲)を指定していた。
# Gmail権限のスコープを指定
SCOPES = 'https://www.googleapis.com/auth/gmail.readonly'
上記のようにSCOPEを指定すると、Gmailを読込専用で利用するという意味になる。もし、メールの送信なども行いたい場合には、SCOPEの値を「https://mail.google.com/」と変更しなくてはならない。より詳しくSCOPEを指定したい場合は、こちらを参考にして書き換える必要がある。
今回は、受信専用のプログラムを作るので、そのままの設定で大丈夫だ。
メールを受信するプログラム
ここから、前回認証した情報をそのまま利用して、メールを受信するプログラムを改良してみよう。前回と同じように、認証情報のファイル「client_id.json」と「credentials-gmail.json」をJupyter Notebookと同じディレクトリに配置しよう。
それでは、Jupyter Notebookでプログラムを実行していこう。ここでは、前回のように、完全なプログラムを一度に全部実行するのではなく、少しずつ、意味のある塊ごとに実行して、プログラムの仕組みを理解しこう。
まず、以下のプログラムは、Google APIを有効にするものだ。
import httplib2, os
from apiclient import discovery
from oauth2client import client, tools
from oauth2client.file import Storage
# Gmail権限のスコープを指定
SCOPES = 'https://www.googleapis.com/auth/gmail.readonly'
# 認証ファイル
CLIENT_SECRET_FILE = 'client_id.json'
USER_SECRET_FILE = 'credentials-gmail.json'
# ------------------------------------
# ユーザ認証データの取得
def gmail_user_auth():
store = Storage(USER_SECRET_FILE)
credentials = store.get()
if not credentials or credentials.invalid:
flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
flow.user_agent = 'Python Gmail API'
credentials = tools.run_flow(flow, store, None)
print('認証結果を保存しました:' + USER_SECRET_FILE)
return credentials
# Gmailのサービスを取得
def gmail_get_service():
credentials = gmail_user_auth()
http = credentials.authorize(httplib2.Http())
service = discovery.build('gmail', 'v1', http=http)
return service
# ------------------------------------
# GmailのAPIが使えるようにする
service = gmail_get_service()
このプログラムを実行すると、何も表示されないが、エラーがでなければ、変数serviceを通して、GoogleのAPIが使えるようになっている。ここまでのプログラムは、ほぼ定型文と言って良いだろう。
続けて、Gmailのメール一覧を取得してみよう。以下のように記述すると、メッセージの一覧(メッセージIDと、スレッドID)を取得できる。
# メッセージを扱うAPI
messages = service.users().messages()
# 自分のメッセージ一覧を100件得る
msg_list = messages.list(userId='me', maxResults=100).execute()
msg_list
実行すると以下のような画面が表示される。
もちろん、このメッセージ一覧は、実際のメッセージではなく、メッセージを識別するためのID一覧だ。実際のメッセージを得るには、さらに以下のように実行される。例えば、取得したメッセージIDを利用してメッセージを表示するには、以下のようにする。以下は、最新のメールを一件、取得してその内容を表示する。
# 先頭のメッセージ情報を得る
msg = msg_list['messages'][0]
# idを得る
id = msg['id']
threadid = msg['threadId']
# メッセージの本体を取得する
data = messages.get(userId='me', id=id).execute()
print(data)
プログラムを実行すると、以下のようになる。
メッセージの本体を取得するためには、messages.get()メソッドを使う。その際に、idを指定すると、idに対応するメッセージが取得されるという具合だ。
手順をまとめると、以下のようになる。
- (1) messages.list()でidの一覧を得る
- (2) messages.get()でidを指定してメッセージの詳細を得る
また、取得したメッセージの詳細を見てみよう。以下を実行して見ると、送信者情報やメールの件名を得ることができる。
data['payload']['headers']
実行すると以下のように表示される。実は、このメール、勉強のために自分自身に対して毎日メールしている、英単語の勉強メールだ。その点を踏まえて内容を見ると、分かりやすいだろうか。Subject(メールの件名)の項目に「[単語] 17日目」という文字列を見つけることができるだろう。
続けて、メール本文の要約データを見てみよう。
data['snippet']
プログラムを実行すると、以下のように表示される。
詳細な本文データを取得する方法
上記の方法では、本文の要約が取得した辞書型データのキー「snippet」に設定されているので、この項目を参照すれば、メールの本文が分かる。とは言え、正確にメールの本文や添付ファイルなどが知りたい場合には、生のメールデータから取得する必要がある。
しかし、ハッキリ言って、電子メールのフォーマットは、歴史的な事情から、非常に複雑なものとなっている。素人が独自にメールフォーマットを解析しようとしても難しい。しかし、Pythonには、電子メールを解析するためのモジュールemailがあるので、これを利用することで、比較的手軽にメールから本文を取り出すことができる。
実際のメール本文を得るためには、まず、rawフォーマット(Emailそのままの形式)でデータを取得し、Emailモジュールを使って、メールを解析するという手順を踏む。
実際のプログラムを見てみよう。
import base64, email
msg = msg_list['messages'][0]
# (1) rawフォーマットでメールを取得
data = messages.get(userId='me', id=msg['id'], format='raw').execute()
raw_data = base64.urlsafe_b64decode(data['raw'])
# (2) Emailを解析する
eml = email.message_from_bytes(raw_data)
# (3) 本文を取得
body = ""
for part in eml.walk(): # (4)
if part.get_content_type() != 'text/plain': # (5)
continue
s = part.get_payload(decode=True)
if isinstance(s, bytes):
charset = part.get_content_charset() or 'iso-2022-jp' # (6)
s = s.decode(str(charset), errors="replace")
body += s
# (7) 本文を表示
print(body)
実行すると、以下のように実際の本文が表示される。
プログラムの(1)の部分では、raw形式でメールを取得する。ただし、この時点でメールは、Base64でエンコードされている。そこで、Base64でデコードする。すると、Googleがメールを取得したそのままの状態のメールデータが得られる。次に、(2)の部分では、emailモジュールを使って、メールの解析を行う。
そして、(3)の部分で、メールの本文を取り出す。ただし、ここで注意が必要なのは、メールというは、複数のパートに分けられているものがるということだ。なぜ、複数のパートに分けられるのかというと、添付ファイルやHTMLメールを実現するためだ。一つのメールの中に、複数のデータを持たせるために、このような仕組みとなっている。
それで、(4)にあるように、walk()メソッドを使うと、複数パートを持つメールの各パートを一つずつ処理することができる。(5)では、複数のパートを一つずつ調べていって、コンテンツの種類が「text/plain」(テキストデータ)であれば、それを本文と判定して処理を行うようにしている。そして、(6)の部分で、取り出したテキストデータを文字コードごとにデコードする。
最終的なプログラム
以上、ここまでの部分をまとめて、最新のメール100件を特定のフォルダに保存するプログラムを作ってみよう。少々長くなるがまずはソースコードを表示する。
import httplib2, os
from apiclient import discovery
from oauth2client import client, tools
from oauth2client.file import Storage
import base64, email
import dateutil.parser
# 保存先
save_dir = os.path.join(os.curdir, 'gmaildata')
if not os.path.exists(save_dir):
os.mkdir(save_dir)
# Gmail権限のスコープを指定
SCOPES = 'https://www.googleapis.com/auth/gmail.readonly'
# 認証ファイル
CLIENT_SECRET_FILE = 'client_id.json'
USER_SECRET_FILE = 'credentials-gmail.json'
# ------------------------------------
# ユーザ認証データの取得
def gmail_user_auth():
store = Storage(USER_SECRET_FILE)
credentials = store.get()
if not credentials or credentials.invalid:
flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
flow.user_agent = 'Python Gmail API'
credentials = tools.run_flow(flow, store, None)
print('認証結果を保存しました:' + USER_SECRET_FILE)
return credentials
# Gmailのサービスを取得
def gmail_get_service():
credentials = gmail_user_auth()
http = credentials.authorize(httplib2.Http())
service = discovery.build('gmail', 'v1', http=http)
return service
# ------------------------------------
def email_extract_text(raw):
# Emailを解析する
eml = email.message_from_bytes(raw)
# 件名を取得
subject = ''
lines = email.header.decode_header(eml.get('Subject'))
for frag, encoding in lines:
if encoding:
sub = frag.decode(encoding)
subject += sub
else:
if isinstance(frag, bytes):
sub = frag.decode('iso-2022-jp')
else:
sub = frag
subject += sub
# 差出人を取得
addr = ''
lines = email.header.decode_header(eml.get('From'))
for frag, encoding in lines:
if encoding:
sub = frag.decode(encoding)
addr += sub
else:
if isinstance(frag, bytes):
addr = frag.decode('iso-2022-jp')
else:
sub = frag
addr += sub
print("-----------")
print("From: " + addr)
# 本文を取得
body = ""
for part in eml.walk():
if part.get_content_type() != 'text/plain':
continue
# ヘッダを辞書型に落とす
head = {}
for k,v in part.items():
head[k] = v
s = part.get_payload(decode=True)
# 文字コード
if isinstance(s, bytes):
charset = part.get_content_charset() or 'iso-2022-jp'
s = s.decode(str(charset), errors="replace")
body += s
print("Body: " + body)
# 日付
date = dateutil.parser.parse(eml.get('Date')).strftime("%Y/%m/%d %H:%M:%S")
# 件名と本文を結果とする
return \
'From: ' + addr + "\n" + \
'Date: ' + date + "\n" + \
'Subject:' + subject + "\n\n" + \
'Body:\n' + body
def receive_gmail(count):
# GmailのAPIが使えるようにする
service = gmail_get_service()
# メッセージを扱うAPI
messages = service.users().messages()
# 自分のメッセージ一覧を100件得る
msg_list = messages.list(userId='me', maxResults=count).execute()
for i, msg in enumerate(msg_list['messages']):
msg_id = msg['id']
print(i, "=", msg_id)
m = messages.get(userId='me', id=msg_id, format='raw').execute()
raw = base64.urlsafe_b64decode(m['raw'])
# メールデータを保存
text = email_extract_text(raw)
fname = os.path.join(save_dir, msg_id + '.txt')
with open(fname, 'wt', encoding='utf-8') as f:
f.write(text)
if __name__ == '__main__':
receive_gmail(100)
処理の流れとしては、今回紹介したプログラムをつなぎ合わせたもので、Gmail APIからメッセージ一覧を取得し、一件ずつrawデータを取り出し、emailモジュールを使ってメールを解析してファイルへ保存するというものになっている。
プログラムを実行するには、以下のようにコマンドラインから実行する。
$ python gmail-to-file.py
すると、プログラムと同じディレクトリに、「gamildata」というディレクトリを作成し、そのディレクトリ以下にメールを100件保存する。
まとめ
Gmail APIを使うことで、Pythonを使って気軽にメール一覧を取得できることが分かっただろう。しかし、生の電子メールデータから本文や添付ファイルを取り出すためには、Pythonのemailモジュールおよび、メールフォーマットに対するそれなりの理解が必要となるだろう。とにかく、Gmailをテキスト形式で保存したいという方は、本稿の最後のプログラムをダウンロードして、内容を確認してみよう。
自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。技術書も多く執筆している。