python3 で pop3 を使う。

#初めに
##ここでやったこと
やったこと
python + POP3 で

- pop3 サーバーへのログイン
- メールの uid の取得
- メールのヘッダーを取得し、 Sbuject と From を Decode

やってないこと

- メールの本文を読み取る。

##必要になった理由

僕は @nifty を利用している。
メールボックスの容量は 5GB で、利用率が 80% を超えていた。
で、サイズの大きいメールを優先して消すことを考えた。

さて、どうやってやろう。

・・・で、python3 ( + sqlite3) でやることにした。
なお、@nifty のメールボックスへのアクセスは pop3 を利用している。

# python で poplib の利用

で、 python で pop3 を利用するには [poplib](https://docs.python.org/3/library/poplib.html)  を用いる。

## パスワードとアカウント
ここでは予め、「nifpass.py」という、パスワードとアカウントの情報を記述したモジュールを用意しておく。
セキュアにするには適宜暗号化する、あるいは getpass を利用するなどする。

```nifpass.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

ACCOUNT = 'xxxxxxxx@nifty.ne.jp'
PASSWORD = 'password'
POPSERVER = 'pop.nifty.com'
```

##接続・切断

サーバーに接続し、アカウントとパスワードを接続する。

pop3 オブジェクトのインスタンスを生成し、メソッド usr() と pass_() を呼び出す。

```
>>> import poplib
>>> import nifpass
>>> M = poplib.POP3(nifpass.POPSERVER)
>>> M.user(nifpass.ACCOUNT)
b'+OK Password required for xxxxxx@nifty.ne.jp.'
>>> M.pass_(nifpass.PASSWORD)
b'+OK xxxxxx@nifty.ne.jp has 148769 messages in 4117347295 octets.'
```



切断は quit() を用いる。

```
>>> M.quit()
b'+OK Pop server at conbox-046 signing off.'
```

関数の場合は try - finally ブロックにする。

```
import poplib
import nifpass


def XXXXX():
    try:
        M = poplib.POP3(nifpass.POPSERVER)
        M.user(nifpass.ACCOUNT)
        M.pass_(nifpass.PASSWORD)

        　　　　・・・

    finally:
        M.quit()
```

## メッセージ番号と uid

pop3オブジェクトのメソッド list() を引数無しで使うとメールのメッセージ番号とサイズのリストを取得できる。
また、メソッド uidl() を引数無しで使うとメールのメッセージ番号と uid のリストを取得できる。

メッセージ番号は現在メール番号にあるメールに対して 1から件数まで順に番号を振ったものである。これはメールの削除が発生した場合には番号がずれる。
一方、uid は個々のメールに対して unique に設定される。
ローカルに情報を保存する場合には uid をキーとする必要があるが、
pop3 の操作はメッセージ番号に基づいて行うため、
ローカルに保存した情報に基づいて削除等の操作を行う場合には uid からそのメールのそのときのメッセージ番号を取得してから処理を行う必要がある。

なお、今回はメールの件数が多い(15万件)ので 引数無しの list()、uidl() は使わず、メソッド stat() で件数を取得し、 order_num を 1 から件数まで回し、 list(order_num), poplib.uidl(order_num) を取得した。

list(order_num), uidl(order_num) は次の形式で値を返す。

```
>>> order_num = xxx
>>> M.list(order_num)
b'+OK 1 12966'
>>> M.uidl(order_num)
b'+OK 1 1178399386.25537.mailbox'
```

型がバイナリ型になっている。
python2 だとそのまま文字列としても利用できるようだが python3 では文字列とバイナリ型は区別されているので文字列型に変換する必要がある。
list(order_num) の結果からサイズを取得するにはつぎのようにする。

```
>>> int(M.list(order_num).decode().split()[2])
12966
```

uidl(order_num) の結果からの uid の取り出しも同様にする。

```
>>> order_num = 1
>>> M.uidl(order_num).decode().split()[2]
'1178399386.25537.mailbox'
```

##メッセージヘッダの読み取りと subject, from の取り出し

メール全体を取得するには retr(order_num) メソッドを呼び出す。
このメソッドで呼び出すとメッセージを取得するとともにサーバー側で既読フラグが設定される。

ただし、今回はサイズの大きいメールを削除するのが目的で、決して構わないかを Subject と From から判定するつもりなので本文はいらない。
そこで top(order_num, 0) メソッドを呼び出す。
第2引数は読み込む本文の行数を指定する。ヘッダーだけなので 0 となる。

メールのヘッダーは top(order_num, 0) の返値のタプルの 2番目に行ごとに分割されたバイナリ配列の形で返される。これを変数に格納しておく

```
>>> mail_header_raw = M.top(order_num, 0)[1]
```

この受信したヘッダーから subject と From をデコードするのに試行錯誤したが、次のようにした。

- email module をインポートして ByteFeedParser のインスタンスを生成し、ヘッダーのバイナリの配列の各要素の末尾に '\r\n' をつけて食わせる(feedさせる)。
- ByteFeedParser のインスタンスを閉じ、message オブジェクトを取得する。
- message オブジェクトから subject要素、from要素を取り出し、email.header.decode_header に渡し、さらにその結果を email.header.make_header に渡して文字列にキャストする。

下記では subject のみ取り出しているが、 From も同様に取得できる。

```
>>> import email.parser
>>> fp = email.parser.BytesFeedParser()
>>> [fp.feed(x + b'\r\n') for x in mail_header_raw]
[None, None, None, None, None, None, None, None, None, 
None, None, None, None, None, None, None, None, None, 
None, None, None, None, None, None, None, None, None, 
None, None, None, None, None, None, None, None, None, 
None, None, None, None, None, None]
>>> msg = fp.close()
>>> msg['subject']
'=?ISO-2022-JP?B?GyRCJUYlOSVIJWEhPCVrGyhC?='
>>> str(email.header.make_header(email.header.decode_header(msg['subject'])))
'テストメール'
```

関数等にする場合は壊れていて decode に失敗するものもあるので例外処理が必要である。

なお、Date など　Encode されていない Field　に関しては message オブジェクトから取り出してそのまま利用できる。(ただし、Date の値を扱うには日時として parse する必要があるがここでは割愛する)

```
>>> msg['Date']
'Sun, 29 Oct 2017 11:05:06 +0900 (JST)'
```

#終わりに
実際にはローカルの sqlite3 の DB に格納し、そこから削除するものを選択して削除するようなものにした。削除されたメールがある場合、order_num は切断(quit()) 後再度接続したときに変わっているので uid を突き合わせてローカルの order_num を更新するものも作った。

そのうち github に上げるつもり。

#リンク

|module|document|
|:--|:--|
|email.parser|https://docs.python.org/3/library/email.parser.html|
|poplib|https://docs.python.org/3/library/poplib.html|