HOME Python 書き込む

imaplib と email ライブラリーを使ったメールの処理


1. 初めに

imaplib を使ってサーバーからメールを取得し、それを解析する方法について解説します。 e-mail の解析には、email ライブラリにある関数を使うと簡単にできます。

紫藤は、領収書のない支出の記録と、体重の記録に e-mail を使っています。 以下にそれらのスクリプトを例に挙げながら、imaplib と email ライブラリのユーティリティを 簡単に解説します。

2. imaplib の使い方

imaplib を使ってメールをサーバーから取得するには以下の手順で行います。
  1. サーバーを指定して IMAP4 (または IMAP_SSL) オブジェクトを作成する。
  2. サーバーにログインする。
  3. メールボックスを指定する。('INBOX', 'INBOX.important' など)
  4. 条件を指定し、メールの番号のリストを取得する。
  5. 番号を指定し、メールを取得する。
以下が、対話モードで実行したときの例です。
Python 3.2.2 (default, Sep  4 2011, 09:51:08) [MSC v.1500 32 bit (Intel)] on win
32
Type "help", "copyright", "credits" or "license" for more information.
>>> import imaplib
>>> c=imaplib.IMAP4_SSL('gmail.com') (オブジェクトの作成)
>>> c.login('jhon_smith', '__hello-world__')  (ログイン)
('OK', [b'[CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE SORT
SORT=DISPLAY THREAD=REFERENCES THREAD=REFS MULTIAPPEND UNSELECT IDLE CHILDREN NA
MESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCH
RES WITHIN CONTEXT=SEARCH LIST-STATUS] Logged in'])
>>> c.select('INBOX')   (メールボックスの選択)
('OK', [b'1106'])
>>> c.search(None, '(SENTON "10-Nov-2011")')  (メールの検索。ここでは 10-Nov-2011 に受信したメールを検索している。)
('OK', [b'1102 1103 1104 1105'])
>>> c.fetch(b'1102', '(RFC822.HEADER)') (1102 の番号のメールを取得)
('OK', [(b'1102 (RFC822.HEADER {1106}', b'Return-Path: <20111110020231989c6dfa88
e44e28b208f5d27dad271b@bounces.amazon.com>\r\nX-Original-To: jhon_smith@gmail.com
\r\nDelivered-To: jhon_smith@gmail.com\r\nReceived: from smtp-out-188-18.amazon.c
om (smtp-out-188-18.amazon.com [207.171.188.18])\r\n\tby gmail.com (Postfix
) with ESMTP id 8E231694048\r\n\tfor <jhon_smith@gmail.com>; Thu, 10 Nov 2011 11:
02:42 +0900 (JST)\r\nDate: Thu, 10 Nov 2011 02:02:39 +0000 (UTC)\r\nFrom: "Amazo
n.co.jp" <ship-confirm@amazon.co.jp>\r\nReply-To: "ship-confirm@amazon.co.jp" <s
hip-confirm@amazon.co.jp>\r\nTo: "jhon_smith@gmail.com" <jhon_smith@gmail.com>\r\n
Message-ID: <urn.correios.msg.20111110020231989c6dfa88e44e28b208f5d27dad271b@132
0890559990.rte-svc-fe-31001.sea31.amazon.com>\r\nSubject: =?ISO-2022-JP?Q?Amazon
.co.jp_=1B$B$4CmJ8$NH/Aw=1B(B_(503-7809728-0738205)?=\r\nMIME-Version: 1.0\r\nCo
ntent-Type: text/plain; charset=ISO-2022-JP\r\nContent-Transfer-Encoding: 7bit\r
\nBounces-to: 20111110020231989c6dfa88e44e28b208f5d27dad271b@bounces.amazon.com\
r\nX-AMAZON-MAIL-RELAY-TYPE: notification\r\nX-AMAZON-RTE-VERSION: 2.0\r\nX-Bogo
sity: Ham, tests=bogofilter, spamicity=0.000000, version=1.0.2\r\n\r\n'), b')'])

>>> c.close()  (クローズ)
('OK', [b'Close completed.'])
>>> c.logout()  (ログアウト)
('BYE', [b'Logging out'])
メールの検索は search() メソッドで行います。 このメソッドの使い方は 20.13. imaplib - IMAP4 protocol client、 また IMAP4 の検索条件は IMAP4のSEARCH条件の一覧 を見てください。

メールの取得は fetch() メソッドで行います。 引数に、メールの番号と、取得する情報を指定します。 取得する情報については、 IMAP4のFETCHオプションの一覧 を参照してください。

3. メールを解析するためのユーティリティ

3.1. email.parser

以下は email を解析するためのクラスです。
HeaderParser
ヘッダーのみを解析するには このクラスを使います。parsestr() メソッドで文字列を解析し、デフォルトで email.message.Message オブジェクトを生成します。
BytesParser
メール全体を解析するにはこのクラスを使います。parsebytes() メソッドでバイト列を解析し、デフォルトで email.message.Message オブジェクトを生成します。(Python 3.2 から)
email.message.Message については 18.1.1. email: Representing an email message を見てください。このオブジェクトにはヘッダ情報が辞書として登録されているので、 ヘッダ情報の取り扱いが容易になります。

3.2. MIME フォーマットのヘッダの解析

MIME フォーマットのヘッダを解析するには email.header.decode_header() 関数を用います。 この関数は (decoded_string, charset) の対からなるリストを返すので、文字列全体を返すためには 次のような関数を定義します。
def decode_mime_header(s0):
    return ''.join([str(s,c) if c else s for s,c in  email.header.decode_header(s0)])

3.3. Date 文字列の解析

メールヘッダの Date フィールドの文字列を解析し、time.mktime() の引数として渡せる9要素のタプルを 返します。
>>> from email.utils import parsedate
>>> parsedate('Sun, 13 Nov 2011 00:24:16 +0900 (JST)')
(2011, 11, 13, 0, 24, 16, 0, 1, -1)
>>>

文献

個々であげた機能はほんの一例です。他にもいろいろな機能がありますので、 詳しくは Python3.x のドキュメントを見てください。

4. 例


以下に、体重や経費の記録の集計、加工するスクリプトを例に示します。

4.1. 体重を記録するスクリプト

紫藤は、専用のアドレスにメールを送信することで体重を記録しています。 Subject に体重と体脂肪率をスペースで区切って書いて (例えば "73.3 24.1") それを送信して、日々の体重を記録しています。
以下のスクリプトは記録した体重のデータを表にまとめるためのスクリプトです。
以下のような処理をします。
  1. 体重記録用メールアドレス (例えば jhon_smith_weight@gmail.com) の メールボックス (例えば INOBX) を検索し、 体重の記録者 (例えば jhon_smith@gmail.com ) が送信元で、未読のメールを探します。
  2. それらのメールから体重のデータを読み、体重の変動を記録したファイル (例えば weight.dat) に送信時刻と体重を書き加えます。 その際、読み取ったメールには既読のマークを付けます。 weight.dat の書式は、日付、エポック秒、体重 (kg)、体脂肪率 (%) です。 エポック秒はグラフの横軸に使います。
メモ:
01:     #! /usr/bin/python3
02:     import imaplib, email.parser, email.header, email.utils, time, re
03:     
04:     HOST='mail.gmail.com'
05:     USER='jhon_smith_weight'
06:     PW='__hello-world__'
07:     
08:     MBOX='INBOX'
09:     FROM_EMAIL='jhon_smith@gmail.com'
10:     
11:     #FOUT='/home/jhon/weight/weight.dat'
12:     FOUT='weight.dat'
13:     
14:     def fetch_headers(host, user, passwd, mbox, _from):
15:     
16:         con = imaplib.IMAP4_SSL(host)
17:         con.login(user,passwd)
18:         con.select(mbox)
19:         p=email.parser.HeaderParser()
20:         typ, data = con.search(None, 'UNSEEN', '(FROM "{0}")'.format(_from))
21:     
22:         if not data[0]: return
23:     
24:         def _parse(num):
25:             typ, data = con.fetch(num, '(RFC822)')
26:             con.store(num, '+FLAGS', '\\Seen')
27:             return p.parsestr(str(data[0][1], 'ascii'))
28:         
29:         ls = [ _parse(num) for num in data[0].split() ]
30:         con.expunge()
31:         con.close()
32:         con.logout()
33:         return ls
34:     
35:     def extract_weight_data(fout, ls0):
36:     
37:         re_d = re.compile(r'^\d{2}(\.\d)? +\d{2}(\.\d)?$')
38:     
39:         def wconv(e):
40:             dt = email.utils.parsedate(e['Date'])
41:             dat=''.join([str(s,c) if c else s for s,c in  email.header.decode_header(e['Subject'])])
42:             return "{flag}{year}/{month}/{date} {sec} {dat}\n".format( \
43:                          flag='' if re_d.match(dat) else '# ', \
44:                          year=dt[0], \
45:                          month=dt[1], \
46:                          date=dt[2], \
47:                          sec=time.mktime(dt), \
48:                          dat=dat
49:                          )
50:         
51:         if ls0:
52:             with open(fout, 'a', encoding='ascii') as f:
53:                 f.write(''.join([wconv(e) for e in ls0]))
54:     
55:     
56:     
57:     
58:     if __name__=='__main__':
59:         extract_weight_data(FOUT,  fetch_headers(HOST, USER, PW, MBOX, FROM_EMAIL))
以下が集計したデータの例です。
#date, sec, weight(kg), fat contents(%)
2011/11/5 1320446330.0 72.8 23.2
2011/11/6 1320530436.0 73.7 23.8
2011/11/7 1320616449.0 74.0 23.6
2011/11/8 1320701737.0 73.3 23.8
2011/11/9 1320789021.0 73.5 23.3
2011/11/10 1320875695.0 72.3 23.3
2011/11/11 1320962043.0 73.0 23.6
2011/11/12 1321048826.0 71.8 23.6
2011/11/13 1321134392.0 71.8 23.2
2011/11/14 1321220334.0 71.5 23.1
2011/11/15 1321306494.0 72.1 23.1

4.2. 領収書のない経費を記録するスクリプト

紫藤は領収書のない経費 (例えば自動販売機で購入した飲料など) を記録用アドレスにメールを送信することによって管理しています。 Subject には金額と用途をスペースで区切って書きます。 毎月 1日に cron で以下のスクリプトを走らせて、前月の領収書がない経費の合計を計算します。 処理は以下のとおりです。
  1. 先月1日と今月1日の日付を取得する。
  2. jhon_smith_expence@gmail.com の INBOX を検索し、jhon_smith@gmail.com から先月中に送られてきたメールを検索する。
  3. レポートファイルにヒットしたメールのそれぞれの Message-Id, Date, Subject 及び先月の合計金額を出力する。
メモ:
01:     #! /usr/bin/python3
02:     import imaplib, email.parser, email.header, email.utils, re, datetime, os.path
03:     
04:     HOST='mail.gmail.com'
05:     USER='jhon_smith_expence'
06:     PW='__hello-world__'
07:     
08:     MBOX='INBOX'
09:     FROM_EMAIL='jhon_smith@gmail.com'
10:     
11:     OUTDIR=''
12:     #OUTDIR='/home/jhon/expence/'
13:     
14:     
15:     
16:     def this_and_last_month():
17:         dt = datetime.date.today()
18:         y, m =dt.year, dt.month
19:         y_last, m_last = (y-1, 12) if m==1 else (y, m-1)
20:         return datetime.date(y,m,1), datetime.date(y_last, m_last, 1)
21:     
22:     
23:     def fetch_headers(host, user, passwd, mbox, _from, since_date, before_date):
24:     
25:         con = imaplib.IMAP4_SSL(host)
26:         con.login(user,passwd)
27:         con.select(mbox)
28:         p=email.parser.HeaderParser()
29:     
30:         typ, data = con.search(None, \
31:                              '(FROM "{0}")'.format(_from),                       \
32:                               since_date.strftime('(SENTSINCE "1-%b-%Y")'),      \
33:                               before_date.strftime('(SENTBEFORE "1-%b-%Y")'))
34:     
35:         if not data[0]: return
36:     
37:         def _parse(num):
38:             typ, data = con.fetch(num, '(RFC822.HEADER)')
39:             con.store(num, '+FLAGS', '\\Seen')
40:             return p.parsestr(str(data[0][1], 'ascii'))
41:         
42:         ls = [ _parse(num) for num in data[0].split() ]
43:         con.expunge()
44:         con.close()
45:         con.logout()
46:         return ls
47:     
48:     def decode_mime_header(s0):
49:         return ''.join([str(s,c) if c else s for s,c in  email.header.decode_header(s0)])
50:     
51:     def calc_total(ls):
52:         ls1=[decode_mime_header(e['Subject']).split()[0] for e in ls]
53:         return sum([int(x) for x in ls1 if x.isdigit()])
54:     
55:     
56:     
57:     
58:     def write_keihi(since_date,ls):
59:     
60:         def _each(e):
61:             return '\n'.join(['{0}: {1}'.format(x, decode_mime_header(e[x])) for x \
62:               in  ['Message-Id', 'Date', 'Subject']])
63:     
64:         fout = os.path.join(OUTDIR, since_date.strftime('keihi%y%m.txt'))
65:         with open(fout, 'w') as f:
66:             f.write(since_date.strftime('payment for miscs in %b %Y\n\n'))
67:             f.write('\n\n'.join([_each(e) for e in ls]))
68:             f.write('\n\n\n---------------------------\ntotal: {0}\n'.format(calc_total(ls)))
69:     
70:     
71:     if __name__=='__main__':
72:         before_date, since_date = this_and_last_month()
73:         ls=fetch_headers(HOST, USER, PW, MBOX, FROM_EMAIL, since_date, before_date)
74:         if ls:
75:             write_keihi(since_date, ls)
以下が出力ファイルの例です。
payment for miscs in Sep 2011


Message-Id: <20110901060053.924E5694049@gmail.com>
Date: Thu, 01 Sep 2011 15:00:51 +0900
Subject: 1000 昼食


Message-Id: <20110906043215.CA87F694049@gmail.com>
Date: Tue, 06 Sep 2011 13:32:14 +0900
Subject: 1000 昼食


Message-Id: <20110928041038.1EB44694049@gmail.com>
Date: Wed, 28 Sep 2011 13:10:35 +0900
Subject: 800 昼食


Message-Id: <20110926224824.06949694049@gmail.com>
Date: Tue, 27 Sep 2011 07:48:21 +0900
Subject: 800 昼食


Message-Id: <20110924041729.89CD4694049@gmail.com>
Date: Sat, 24 Sep 2011 13:17:27 +0900
Subject: 130 コーラ


Message-Id: <20110923042115.17A45694049@gmail.com>
Date: Fri, 23 Sep 2011 13:21:13 +0900
Subject: 220 切符



-----------------------------
total: 5000

4. 終わりに

専用のメールアドレスにデータを送信するという方法は、出先で、携帯電話やスマートフォンを使ってデータを入力しサーバーに送信する簡便な方法です。 日付や、送信者は自動で入力されるので、ユーザは必要最小限のデータを入力するだけですみます。

また、ごく簡単なスクリプトでデータの集計や加工ができます。

さらに、http を使ったデータの送信と比較して、 web ページや専用クライアントを作成する必要もなく、認証もメール送信サーバーの認証機能が使えるので、 準備に手間がかからず、気軽に始められます。