PythonでGmailにIMAP経由でアップロードしてやんよ

経緯

いまさらだが、ローカルにためてたメールをGmailにアップロードしてみた。最初はThunderbirdでIMAP4の設定をして、Thunderbirdで選択+移動でアップロードしていたのだが、(1)ときどきConnectionが切れる、(2)時々Gmailがエラーを返す、ために移動が止まってしまう。しかも、複数選択した状態で移動に失敗すると、アップロードに成功したメールも移動元に残ってしまい、どのメールをアップロードしたのかわからなくなってしまう。
なので、一回あたり数十通だけを選択してチマチマアップロードしていたのだが、一つ目のディレクトリをアップロードした時点残りのメールの量に絶望した。

下調べ

とりあえず、imapとmboxを扱うライブラリが無いかとcheese shopを物色してみるも、殆ど無い。そういえば標準ライブラリにあった気がすると思い出して、標準ライブラリの法を調べてみると、imaplib, mailbox といったモジュールがあった。さすがPython、バッテリ同梱を特徴に挙げるだけのことはある。
Pythonのインタラクティブシェルでしばらく試してみた感じ、かなり簡単にアップロードできた。簡単に使い方で言うと、こんな感じ。

>> mb = mailbox.mbox('mboxファイル名')
>> gmail = imaplib.IMAP4_SSL('imap.gmail.com')
>> gmail.login('アカウント名@gmail.com', 'パスワード')
>> for msg in mb:
..   gmail.append('フォルダ名', [], None, str(msg))
..

実践

試行錯誤しながら、下記のようなことをしながらアップロードするスクリプトを組んでみた。

  • 送信に失敗したメールは別メールボックスを作ってそこに保存
  • imap.append()の第3引数に、メッセージのDateフィールドから持ってきたDateTimeを入れる。(From - で始まる行の日付を入れたほうが良いと後で気づいたが、無視)
  • コネクションが切れたら再接続

コネクション再接続したときに、メッセージを再送信しないで失敗に入れちゃうとか問題あるけど、自分のメールで送信できるものは全部送信したのでもういいや。現時点でのソースはこんな感じ。

from time import sleep, mktime
from email.utils import parsedate
from email.header import decode_header
from imaplib import IMAP4_SSL
from mailbox import mbox
import sys

# user setting
host = 'imap.gmail.com'
user = 'username@gmail.com'
passwd = 'password'
folder = 'tagname'

class Gmail:

    def __init__(self):
        self.conn = None

    def __prepare(self):
        if not self.conn:
            sleep(3)
            self.conn = IMAP4_SSL(host)
            self.conn.login(user, passwd)
            print "Connect"

    def create_mailbox(self, name):
        self.__prepare()
        try:
            return self.conn.create(name)
        except:
            self.conn = None
            return None

    def append(self, msg):
        datetime_tuple = parsedate(msg['Date'])
        if len(datetime_tuple) >= 9:
            dt = mktime(datetime_tuple)
        else:
            print >>sys.stderr, "Error - Date: " + msg['Date']
            return None
        self.__prepare()
        try:
            return self.conn.append(folder, [], dt, str(msg))
        except:
            self.conn = None
            return None

def main(to_send, sent, fail):
    gmail = Gmail()
    gmail.create_mailbox(folder)
    mb_rest = mbox(to_send)
    mb_sent = mbox(sent)
    mb_fail = mbox(fail)

    try:
        for key, msg in mb_rest.iteritems():
            if key not in mb_sent:
                res1, res2 = gmail.append(msg)
                if res1 == 'OK':
                    print res2, msg['Date'], msg['Subject']
                    mb_sent.add(msg)
                else:
                    mb_fail.add(msg)
                    # What's wrong?
                    print >>sys.stderr, res1, res2
        else:
            print "No more messege in %s." % to_send
    except:
        mb_fail.add(msg)
    finally:
        mb_rest.close()
        mb_sent.close()
        mb_fail.close()

if __name__ == '__main__':
    main(sys.argv[1], sys.argv[2], sys.argv[3])

気になった点

Pythonのdictでは、if key in dict: でkeyがdictのキーに含まれているかどうかを真偽値で返し、 for key in dict: でdictから一つ一つキーを取り出してループする。for key, val in dict: になっていないのは if文との対象性を取るためであり、key, valが欲しいなら for key, val in dict.items(): を使う。
それに対して、messageboxオブジェクトは、if key in mbox: for msg in mbox: となっており、dictでは確保されている対象性が保たれていない。ドキュメントを読めば一応判るが、Pythonに慣れた人は for key in msg: do_something(msg[key]) とかやっちゃうので、これはdictと同じ仕様にするべきだと思う。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。