IMAP4と日本語のメールボックス
少し前に Yahoo!メールが IMAP4 から読めるようになったので、Python の標準ライブラリに収録されている imaplib を試しているが日本語で問題が発生した。
普通のメールソフトでフォルダに相当するものを IMAP4 はメールボックスと呼んでいる。メールボックスの名前として使える文字は ASCII の一部だけで、残りは Modified UTF-7 という方法で符号化しなければならない。しかし現在の Python にはこの実装がついてこない。
「事案5305: imaplib は内部メールボックス名をサポートすべき」という文書から
- Twisted がすでに Modified UTF-7 を実装しているが、
- それには軽度なバグがある(ということを日本人らしき人が指摘している)
ということがわかった。自分は Python3 の実装が欲しかったので、前述のバグの改修も含めて移植してみた。
# Python 3.2.2 import codecs import base64 def fix_base64(s): pad = b'=' * (((~len(s)) + 1) & 3) return s + pad def modified_unbase64(s): s = fix_base64(s) return base64.b64decode(s, b'+,').decode('utf_16_be') def modified_base64(s): enc = base64.b64encode(s.encode('utf_16_be'), b'+,') return enc.rstrip(b'=') def decoder(s, errors=None): assert isinstance(s, bytes) or isinstance(s, memoryview) r = [] decode = [] for i in bytes(s): if i == ord(b'&') and not decode: decode.append(i) elif i == ord(b'-') and decode: if len(decode) == 1: r.append('&') else: r.append(modified_unbase64(bytes(decode[1:]))) del decode[:] elif decode: decode.append(i) else: r.append(chr(i)) if decode: r.append(modified_unbase64(bytes(r[1:]))) return ''.join(r), len(s) def encoder(s, erros=None): ret = bytearray() _in = [] for c in s: if '\x20' <= c <= '\x7E': if _in: ret.extend(b'&' + modified_base64(''.join(_in)) + b'-') del _in[:] if c == '&': ret += b'&-' else: ret += c.encode() else: _in.append(c) if _in: ret.extend(b'&' + modified_base64(''.join(_in)) + b'-') return bytes(ret), len(ret) class StreamReader(codecs.StreamReader): def decode(self, s, errors='strict'): return decoder(s) class StreamWriter(codecs.StreamWriter): def encode(self, s, errors='strict'): return encoder(s) _codecInfo = codecs.CodecInfo(encoder, decoder, StreamReader, StreamWriter) def imap4_utf_7(name): if name == 'imap4-utf-7': return _codecInfo codecs.register(imap4_utf_7)
以上のコードを実行すると、かなり簡易ではあるものの一応 imap4-utf-7 というエンコーディングが使用可能になる。以下は適当なメールボックスを作って削除する例。
import imaplib import getpass import re import sys def test(): conn = imaplib.IMAP4_SSL('imap.mail.yahoo.co.jp') password = getpass.getpass('password:') try: conn.login('/* ここにアカウント名を入れる */', password) create_mailbox(conn, 'やっほーyahoo') list_mailbox(conn) print('-' * 20) delete_mailbox(conn, 'やっほーyahoo') list_mailbox(conn) except conn.error as err: print('IMAP Error:', err, file=sys.stderr) finally: conn.logout() def list_mailbox(conn): re_response = re.compile(br'\((?P<flags>.*?)\) "(?P<delimiter>.*)" (?P<name>.*)') typ, lines = conn.list() for i in lines: m = re_response.match(i) if m: flags, delim, name = m.groups() if name.startswith(b'"') and name.endswith(b'"'): name = name[1:-1] print('-', name.decode('imap4-utf-7')) else: print('Response Parsing Error:', i, file=sys.stderr) def create_mailbox(conn, name): conn.create(name.encode('imap4-utf-7')) def delete_mailbox(conn, name): conn.delete(name.encode('imap4-utf-7')) if __name__ == '__main__': test() """ 結果: - Bulk Mail - Draft - Inbox - Sent - Trash - やっほーyahoo - 保存 -------------------- - Bulk Mail - Draft - Inbox - Sent - Trash - 保存 """
参考
- モバイル時代の優等生「IMAP4〜前編」
- Modified UTF-7のエンコード・デコード - Programming/Tips - 総武ソフトウェア推進所
- RFC 3501 — 5.1.3. Mailbox International Naming Convention
- Python 3.0 Hacks:第4回 「俺様プチencoding」を実装して理解するPython3.0のioとcodec,encodingの機構
- UTF-7 — SuikaWiki
- 7.8. codecs ― codec レジストリと基底クラス — Python 2.7ja1 documentation
- imaplib - IMAP4 client library - Python Module of the Week