経緯
いまさらだが、ローカルにためてたメールを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と同じ仕様にするべきだと思う。