Python

Python の標準ライブラリでメールをデコードする

More than 1 year has passed since last update.

これは ユニキャストアドベントカレンダー の2日目の記事です。

1日目の Python の標準ライブラリで POP3 を操ってみる の続きになります。


Python で電子メールを読む

前回は標準ライブラリの poplib を使って POP3 でメールを受信したり削除したりすることをしました。

今回もまた標準ライブラリの email パッケージを使って、メールの本文を受信してみます。


電子メールの表現形式

電子メールは IMF (Internet Message Format) という形式でやりとりされています。

メールソフトはこのIMF形式を人間が読める形に変換して表示しています。

IMFは、ヘッダとボディの2つのパートから構成されています。

宛先や差出人はヘッダに、本文や添付ファイルはボディに入っています。

ボディはさらにMIMEという形式でエンコードされているのが一般的です。


email と email.header パッケージを使う

Python では poplib で受信したメールを、 email.message_from_bytes() で処理しやすい形に変換することができます。

またヘッダは email.header.decode_header() を使って変換します。

通常メールは歴史的な事情により 7-bit ASCII の文字コードで送信するため、日本語などのマルチバイト文字は ISO-2022-JP (いわゆるJISコード) に変換したり、MIME形式にエンコードしてから送信されています。

なのでメールをプログラムで処理するためには、文字コードを適切に変換する必要があります。

(メールが文字化けするのは、上記の取り扱いに失敗したため発生しています。)


サンプルコード

ちょっと長くなったので、


  • メールの受信

  • ヘッダのデコード

  • 本文のデコード

の3つの関数に分けて書いています。

poplib で取ってきたメッセージは一行ずつのリスト形式になっているため、email.message_from_bytes() に渡す前に join() しています。

IMFでは文字列の区切りはCRLFと規定されているため、\r\n で結合します。

import poplib

import email
from email.header import decode_header
from email.utils import parsedate_to_datetime

# メールを受信する
def fetchmail(cli, msg_no):
content = cli.retr(msg_no)[1]
msg = email.message_from_bytes(b'\r\n'.join(content))
# From ヘッダ(差出人)
from_ = get_header(msg, 'From')
# Date ヘッダ(送信日時)
date = get_header(msg, 'Date')
# Subject ヘッダ(件名)
subject = get_header(msg, 'Subject')
# 本文
content = get_content(msg)
return (subject, content, from_, date)

ヘッダのデコードのサンプルです。

email.header.decode_header() でデコードしたヘッダは (ヘッダ, 文字コード) のタプルのリストで返ってくるため、タプルを展開して適切な文字コードでデコードするようにします。

# ヘッダを取得

def get_header(msg, name):
header = ''
if msg[name]:
for tup in decode_header(str(msg[name])):
if type(tup[0]) is bytes:
charset = tup[1]
if charset:
header += tup[0].decode(tup[1])
else:
header += tup[0].decode()
elif type(tup[0]) is str:
header += tup[0]
return header

本文は get_payload() メソッドで取得できます。

これも適切な文字コードでデコードするようにします。

# 本文を取得

def get_content(msg):
charset = msg.get_content_charset()
payload = msg.get_payload(decode=True)
try:
if payload:
if charset:
return payload.decode(charset)
else:
return payload.decode()
else:
return ""
except:
return payload # デコードできない場合は生データにフォールバック

fetchmail() の呼び出し方は以下のようになります。

cli = poplib.POP3('mail.example.com')

cli.user('jsaito@example.com')
cli.pass_('password')

msg = fetchmail(cli, 1)
print(msg)

注意したいのは、このコードでは実社会の有象無象のメールに100%対応することはできないということです。

メールソフトが正しくIMF形式やMIMEエンコードを正しく実装出来ていなかったり、文字コードの指定と実際のエンコーディングが違っていたり、文字コードの名称が標準と異なっていたりするため、様々なメールを正しく表示させようとするためには、もっとたくさんの例外処理が必要になります。(つらい)


まとめ

Python の標準ライブラリで電子メールを読み込むサンプルコードを例示しました。

ここまで来ると、新着メールを何かのデバイスに通知したり、受信したメールをDBに取り込んでアレコレするプログラムが書けそうです。

そのためには UIDL に対応する必要が出てきます。

次回予告みたいな終わり方になりましたが、以上よろしくお願いします。