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

これは [ユニキャストアドベントカレンダー](https://qiita.com/advent-calendar/2017/unicast) の２日目の記事です。
１日目の [Python の標準ライブラリで POP3 を操ってみる](https://qiita.com/jsaito/items/89ea1679a9f2b99e7889) の続きになります。

# Python で電子メールを読む

前回は標準ライブラリの `poplib` を使って POP3 でメールを受信したり削除したりすることをしました。
今回もまた標準ライブラリの [email](http://docs.python.jp/3.3/library/email.html) パッケージを使って、メールの本文を受信してみます。

## 電子メールの表現形式

電子メールは IMF (Internet Message Format) という形式でやりとりされています。
メールソフトはこのIMF形式を人間が読める形に変換して表示しています。

IMFは、ヘッダとボディの２つのパートから構成されています。
宛先や差出人はヘッダに、本文や添付ファイルはボディに入っています。
ボディはさらにMIMEという形式でエンコードされているのが一般的です。

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

Python では poplib で受信したメールを、 `email.message_from_bytes()` で処理しやすい形に変換することができます。
またヘッダは `email.header.decode_header()` を使って変換します。
通常メールは歴史的な事情により 7-bit ASCII の文字コードで送信するため、日本語などのマルチバイト文字は ISO-2022-JP (いわゆるJISコード) に変換したり、MIME形式にエンコードしてから送信されています。
なのでメールをプログラムで処理するためには、文字コードを適切に変換する必要があります。
（メールが文字化けするのは、上記の取り扱いに失敗したため発生しています。）

### サンプルコード

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

- メールの受信
- ヘッダのデコード
- 本文のデコード

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

`poplib` で取ってきたメッセージは一行ずつのリスト形式になっているため、`email.message_from_bytes()` に渡す前に `join()` しています。
IMFでは文字列の区切りはCRLFと規定されているため、`\r\n` で結合します。

```python3
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()` でデコードしたヘッダは `(ヘッダ, 文字コード)` のタプルのリストで返ってくるため、タプルを展開して適切な文字コードでデコードするようにします。

```python3
# ヘッダを取得
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()` メソッドで取得できます。
これも適切な文字コードでデコードするようにします。

```python3
# 本文を取得
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()` の呼び出し方は以下のようになります。

```python3
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` に対応する必要が出てきます。

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