Document Actions
moblog奮闘記番外編 - emailモジュール
moblog.pyを読んで、さらにいじれるようになることを目標にがんばるためには、とりあえずemailモジュールその他について整理しておかないといけないと思いました。
Eメールの構造
emailモジュールを見てみる前に、そもそもEメールはどのような構造をしているのでしょうか。
Eメールは大きく分けてヘッダと本文という二つのパーツからなっています。ヘッダには送信者のメールアドレスや送信先のメールアドレスなど、メールを送るために必要な情報が含まれています。そして本文はもちろんそのメールの内容です。ヘッダと本文は改行で区切られています。
Return-Path: <nyusuke@example.com>
Delivered-To: nagosui@example.com
From: nyusuke@example.com
To: nagosui@example.com
Subject: =?iso-2022-jp?B?GyRCJF8kcyRKJE4bKEJQeXRob24=?=
Message-ID: <E8142EF2F595FEAEFC0@example.com>
Date: Mon, 6 Nov 2006 01:13:12 +0900
Mime-Version: 1.0
Content-Type: text/plain; charset=iso-2022-jp; ←ここまでヘッダ
テストメールです。 ←ここから本文
テストです。
テストなんです。
画像等の添付ファイルがあった場合には構造が少し違ってきます。上記の本文のパーツの部分が複数にわかれ、本文のデータや画像のデータ等が個々のパーツに分けられます。このような構造を「マルチパート」と呼びます。マルチパート形式のメールの本文や画像データ等のパーツは「boundary」(つまり境界線)と呼ばれる文字列によって区切られています。
次の例ではヘッダの最後の部分において、このメールがマルチパートであること、そして境界線として「-------=_NextPart_57559_22154_52285」という文字列を使用していることが述べられています。そして実際その後に、「-------=_NextPart_57559_22154_52285」で区切られた本文と画像データが続いています。
Return-Path: <nyusuke@example.com>
Delivered-To: nagosui@example.com
From: nyusuke@example.com
To: nagosui@example.com
Subject: =?iso-2022-jp?B?GyRCJF8kcyRKJE4bKEJQeXRob24=?=
Message-ID: <E8142EF2F595FEAEFC0@example.com>
Date: Mon, 6 Nov 2006 01:13:12 +0900
Mime-Version: 1.0
Content-Type: multipart/mixed; boundary="-----=_NextPart_57559_22154_52285"
-------=_NextPart_57559_22154_52285
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: 7bit
添付ファイルのあるテストメール。
-------=_NextPart_57559_22154_52285
Content-Type: image/jpeg; name="test.jpg"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test.jpg"
/9j/4QJqRXhpZgAATU0AKgAAAAgACwEOAAIAAAAPAAAAvgEPAAIAAAAIAAAA3AEQAAIAAAAFAAAA
-------=_NextPart_57559_22154_52285--
マルチパートか否かで構造が変わってくる点に注意ですね。
Messageクラス
emailモジュールはメールをほげほげするのにとても便利なMessageクラスを提供してくれるようです。すなわち基本的な流れとしては、poplibによってゲッツしたメールだとかファイルやら文字列やらからMessageオブジェクトへ変換し、それをほげほげする、という形になるのではないでしょうか。
Messageオブジェクトはヘッダとペイロードというパーツを持っています。ペイロードというのは「ヘッダ以外の部分」を指す言葉で、まぁ本文と考えてだいたい合っているのではないでしょうか(マルチパートのEメールの場合は本文以外にも画像データなどが含まれます)。
試しにファイルからMessageオブジェクトを作ってみます。
>>> import email
>>> message = email.message_from_file(open('message.txt'))
>>> message
<email.Message.Message instance at 0x00CA4918>
2行目のmessage_from_file()はファイルオブジェクトからMessageオブジェクトに変えてくれるメソッドです。ファイルシステム上にあるmessage.txtというテキストファイルをopen()メソッドによってファイルオブジェクトとして開き、これをMessageオブジェクトへ変換、messageという変数に代入しました。中身はas_string()メソッドを使えば文字列として見ることができるでしょう。
>>> message.as_string()
'Return-Path: <nyusuke@example.com>\nDelivered-To: nagosui@example.com\nFrom: ny
usuke@example.com\nTo: nagosui@example.com\nSubject: =?iso-2022-jp?B?GyRCJF8kcyR
KJE4bKEJQeXRob24=?=\nMessage-ID: <E8142EF2F595FEAEFC0@example.com>\nDate: Mon, 6
Nov 2006 01:13:12 +0900\nMime-Version: 1.0\nContent-Type: text/plain; charset=i
so-2022-jp;\n\n\x83e\x83X\x83g\x83\x81\x81[\x83\x8b\x82\xc5\x82\xb7\x81B\n\x83e\
x83X\x83g\x82\xc5\x82\xb7\x81B\n\n\x83e\x83X\x83g\x82\xc8\x82\xf1\x82\xc5\x82\xb
7\x81B'
ヘッダをいじる
Messageオブジェクトのヘッダは辞書形式でいじることができますので、has_key()、keys()、values()、items()、get()などおなじみのメソッドが利用できます。
# keys()はヘッダフィールドをリストとして返します
>>> message.keys()
['Return-Path', 'Delivered-To', 'From', 'To', 'Subject', 'Message-ID', 'Date', '
Mime-Version', 'Content-Type']
# items()はフィールドとその値のタプルをリストとして返します
>>> message.items()
[('Return-Path', '<nyusuke@example.com>'), ('Delivered-To', 'nagosui@example.com
'), ('From', 'nyusuke@example.com'), ('To', 'nagosui@example.com'), ('Subject',
'=?iso-2022-jp?B?GyRCJF8kcyRKJE4bKEJQeXRob24=?='), ('Message-ID', '<E8142EF2F595
FEAEFC0@example.com>'), ('Date', 'Mon, 6 Nov 2006 01:13:12 +0900'), ('Mime-Versi
on', '1.0'), ('Content-Type', 'text/plain; charset=iso-2022-jp;')]
# get()は指定したフィールドの値を返します
>>> message.get('From')
'nyusuke@example.com'
ヘッダをもっといじる
例えば件名には日本語がよく利用されます。しかしget()などで得られる値はそのままでは意味不明なので加工してやる必要があります。
>>> message.get('Subject')
'=?iso-2022-jp?B?GyRCJF8kcyRKJE4bKEJQeXRob24=?=' ←意味不明これを何とかするのに便利なのが、email.Headerモジュールのdecode_header()メソッドです。decode_header()は文字セットを変更せずにヘッダをデコードし、デコードされた文字列とエンコード名からなるタプルを返します。
>>> import email.Header
>>> subject = message.get('Subject')
>>> email.Header.decode_header(subject)
[('\x1b$B$_$s$J$N\x1b(BPython', 'iso-2022-jp')] ←タプルが入ったリスト
ということは以下のようにすればデコードされた文字列とエンコードを取り出すことができます。
# リストの最初の要素であるタプルの、最初の要素を取り出す
>>> d_subject = email.Header.decode_header(subject)[0][0]
>>> d_subject
'\x1b$B$_$s$J$N\x1b(BPython'
# リストの最初の要素であるタプルの、1番目の要素を取り出す
>>> enc = email.Header.decode_header(subject)[0][1]
>>> enc
'iso-2022-jp'
ということはこれらをもとにして意味のある文字列としての件名を得ることができます。
# ユニコード文字列に変換
>>> u_subject = unicode(d_subject, enc)
>>> u_subject
u'\u307f\u3093\u306a\u306ePython'
# エスケープされるのを回避するためにprintableで出力
>>> print u_subject
みんなのPython
ペイロードをいじる
ペイロードの構造はマルチパートか否かで変わってきますので注意が必要です。ペイロードを得るためにはget_payload()メソッドを使います。非マルチパートなメールの場合ペイロードは文字列となりますが、マルチパートメッセージの場合は「Messageオブジェクトのリスト」になります。
# 非マルチパートメッセージの場合は文字列
>>> msg_not_multi = email.message_from_file(open('message2.txt'))
>>> msg_not_multi.get_payload()
'\x1b$B%F%9%H%a!<%k$G$9!#\x1b(B\n\x1b$B%F%9%H$G$9!#\x1b(B\n\n\x1b$B%F%9%H$J$s$G$
9!#\x1b(B'
# マルチパートメッセージの場合はMessageオブジェクトのリスト
>>> msg_multi = email.message_from_file(open('message3.txt'))
>>> msg_multi.get_payload()
[<email.Message.Message instance at 0x00CE7F08>, <email.Message.Message instance
at 0x00CE7F30>]
非マルチパートメッセージのペイロード
非マルチパートの場合は文字列が返ってきますので扱いはさほど面倒ではないように見えます。問題はエンコードですが、get_content_charset()というメソッドがありますので、これを利用してユニコード文字列に変換したりすれば扱いもらくちんなのではないでしょうか。
# エンコードを得る
>>> msg_not_multi.get_content_charset()
'iso-2022-jp'
# ユニコード文字列に変換して表示してみる
>>> u_body_nm = unicode(msg_not_multi.get_payload(), msg_not_multi.get_content_charset())
>>> print u_body_nm
テストメールです。
テストです。
テストなんです。
マルチパートメッセージのペイロード
一般的なマルチパートを考えるときりがないので、moblogの場合だけに範囲をしぼって考えたいと思います。
携帯電話から添付画像付きのメールを送った場合にはメールのサブパートは
- メール本文
- 画像1
- 画像2
- 画像n
という感じになるはずです。そしてこれらのサブパートそれぞれがMessageオブジェクトとして存在しています。
マルチパートメッセージのMessageオブジェクトに対してget_payload()すると、これらのリストが返ってくるわけですね。ということは、
- msg_multi.get_payload[0]→Messageオブジェクト(メール本文)
- msg_multi.get_payload[1]→Messageオブジェクト(画像1)
- msg_multi.get_payload[2]→Messageオブジェクト(画像2)
のようにそれぞれを取り出すことができます。
各サブパートはMessageオブジェクトですから、例えばメール本文部分は以下のような構造を持つMessageオブジェクトです。
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: 7bit ←ここまでヘッダ
添付ファイルのあるテストメール。 ←ここからペイロード
画像は以下のような構造になります。
Content-Type: image/jpeg; name="test.jpg"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test.jpg" ←ここまでヘッダ
/9j/4QJqRXhpZgAATU0AKgAAAAgACwEOAAIAAAAPAAAAvgEPAAIAAAAIAAAA3AEQAAIAAAAFAAAA ←ここからペイロード
AAAIAAAA3AEQAAIAAA Base64でエンコードされた画像データが続く AgACwEOAAIAAAAPA
EQAAIAAAAF/9j/4QJqRXhpZgAATU0AKgAAIAAAAPAAAAvgEPAAIAAAAIAAAA3AEQAAIAAAAFAAAA
本文の部分のペイロードを得るにはmsg_multi.get_payload[0]に対してget_payload()してやればいいですよね。
>>> msg_multi.get_payload()[0].get_payload()
'\x1b$BE:IU%U%!%$%k$N$"$k%F%9%H%a!<%k!#\x1b(B\n'
扱いやすくするためにエンコードをゲッツして、ユニコード文字列に変換します。
>>> body_multi = msg_multi.get_payload()[0].get_payload()
>>> enc_multi = msg_multi.get_payload()[0].get_content_charset()
>>> u_body_m = unicode(body_multi, enc_multi)
>>> print u_body_m
添付ファイルのあるテストメール。
画像の方はBase64でエンコードされていますが、get_payload()にはdecodeというオプションがあります。これは12.2.1 電子メールメッセージの表現によれば
オプションの decode はそのペイロードが Content-Transfer-Encoding: ヘッダに従って デコードされるべきかどうかを指示するフラグです。
ということです。
添付画像をほげほげする場合には画像の生データが欲しいわけで、画像のMessageオブジェクトには(おそらく)「Content-Transfer-Encoding: base64」があるので、これを信用してしまってdecodeオプションをつけてやれば生データを得ることができるのではないでしょうか。
# decode=1を指定してget_payload()する
>>> msg_multi.get_payload()[1].get_payload(decode=1)
'\xff\xd8\xff\xe1\x02jExif\x00\x00MM\x00*\x00\x00\x00\x08\x00\x0b\x01\x0e\x00\x0
2\x00\x00\x00\x0f\x00\x00\x00\xbe\x01\x0f\x00\x02\x00\x00\x00\x08\x00\x00\x00\xd
c\x01\x10\x00\x02\x00\x00\x00\x05\x00\x00\x00(以下ずーっと続く)'
マルチパートメッセージのサブパートを表示してみる
以上をふまえて、マルチパートメッセージのサブパートであるメール本文と画像のデータを表示してみます。
マルチパートメッセージの構造をまとめておきましょう。下図では画像が1枚添付されているメールを想定しています。
ここで取り出したいのは、「ペイロード:メール本文」と「ペイロード:画像データ」です。図は画像が1枚だけ添付された状態ですが、実際には2枚、3枚と画像を添付したくなってしまう場合もあると思います。こういった状況に柔軟に対応するためにはwalk()メソッドを使うと便利です。
walk()メソッド
walk()メソッドはメッセージオブジェクトツリー中のすべてのパートおよびサブパートを渡り歩き、forループと一緒に使うと便利なメソッドです。上図のメッセージオブジェクトツリーは以下のようになります。
- Messageオブジェクト(全体)
- Messageオブジェクト(メール本文)
- Messageオブジェクト(画像データ)
# マルチパートメッセージのファイルを開いてMessageオブジェクトを作る
msg_multi = email.message_from_file(open('msg_multi.txt'))
# メッセージの各部分に対して処理を行う
for part in msg_multi.walk():
part.処理
などとすると、まずオブジェクトツリーのトップにある「Messageオブジェクト(全体)」に対して処理が行われ、次にその下層の一番上にある「Messageオブジェクト(メール本文)」に対して処理が行われ、最後に「Messageオブジェクト(画像データ)」に対して処理が行われます。
例えば
for part in msg_multi.walk():
print part.get_payload()
とすれば、それぞれのMessageオブジェクトのペイロードを順番に表示します。結果は以下のようになります。
# Messageオブジェクト(全体)のペイロードを表示
# マルチパートメッセージなのでMessageオブジェクトのリストが返ってくる
[<email.Message.Message instance at 0x00C07AF8>, <email.Message.Message instance
at 0x00C07A80>]
# Messageオブジェクト(メール本文)のペイロードを表示
$BE:IU%U%!%$%k$N$"$k%F%9%H%a!<%k!#(B
# Messageオブジェクト(画像データ)のペイロードを表示
/9j/4QJqRXhpZgAATU0AKgAAAAgACwEOAAIAAAAPAAAAvgEPAAIAAAAIAAAA3AEQAAIAAAAFAAAA
(以下画像データが続く)
もちろん添付画像が複数あればそれらに対して順に処理を行います。
実際の処理を考える
さてサブパートを表示させることを考えると、forループとifを使えばうまくいきそうです。
- Messageオブジェクト(全体)は特に表示させるものもないのでスルーする
- Messageオブジェクト(メール本文)のペイロードをゲッツして、ユニコード文字列に変換して表示する
- Messageオブジェクト(画像データ)のペイロードをゲッツして、生データに変換して表示する
という流れにしてみます。
ループ中の当該Messageオブジェクトが全体か、メール本文か、画像データかは、当該オブジェクトのコンテンツタイプをチェックして判別します。
- Messageオブジェクト(全体)→'multipart/mixed'
- Messageオブジェクト(メール本文)→'text/plain'
- Messageオブジェクト(画像データ)→'image/jpg'
となるはずですので、それぞれのコンテンツタイプのメインタイプ(スラッシュの前の部分)で分岐させてます。メインタイプはget_content_maintype()メソッドから得ることができます。
for part in msg_multi.walk():
# Messageオブジェクト(全体)に対しては何もしないのでcontinue
if part.get_content_maintype() == 'multipart':
continue
# Messageオブジェクト(メール本文)に対しては
# ペイロード(メール本文)をユニコード文字列にして表示
elif part.get_content_maintype() == 'text':
body = subpart.get_payload()
enc = subpart.get_content_charset()
u_body = unicode(body, enc)
print u_body
# Messageオブジェクト(画像データ)に対しては
# 生データに変換してから表示
elif part.get_content_maintype() == 'image':
r_img = subpart.get_payload(decode=1)
print r_img
結果は以下のような感じ。
# メール本文
添付ファイルのあるテストメール。
# 画像データ
リ・jExif MM
(以下画像データが続く)
さてこれほどPythonをいじったのは初めてなんですが、moblog.pyを読めるようになったでしょうか…。
- The URL to Trackback this entry is:
- http://nagosui.org/Nagosui/COREBlog2/fight-for-moblog-extra-email-module/tbping
- バベルの塔
- ¦
- Main
- ¦
- moblog奮闘記2-1

