ascii と iso-2022-jp 混在の Subject がうまくデコードされない
新刊.net のアラートメールの Subject は、
Subject: =?ISO-2022-JP?B?GyRCPzc0KRsoQg==?=.net =?ISO-2022-JP?B?GyRCJSIlaSE8JUgbKEI=?=
みたいになっているのだが(メーラ等で見ると "新刊.net アラート" と表示される)、これを Python 2.7.6 でデコードしようとすると、
>>> value = '=?ISO-2022-JP?B?GyRCPzc0KRsoQg==?=.net =?ISO-2022-JP?B?GyRCJSIlaSE8JUgbKEI=?=' >>> >>> from email.header import decode_header, make_header >>> dc_list = decode_header(value) >>> dc_list [('\x1b$B?74)\x1b(B', 'iso-2022-jp')] >>> print unicode(make_header(dc_list)) 新刊 >>>
のようになって、".net アラート" が消えてしまう。
iso-2022-jp→ascii→iso-2022-jpの文字列なので、dc_list((文字列, コード)のペアリスト)は3組でないとおかしいが、1組しかない。
${PYTHONHOME}/lib/python2.7/email/header.py を調べてみると、エンコードされている文字列を抜き出すための正規表現が、
# Match encoded-word strings in the form =?charset?q?Hello_World?= ecre = re.compile(r''' =\? # literal =? (?P<charset>[^?]*?) # non-greedy up to the next ? is the charset \? # literal ? (?P<encoding>[qb]) # either a "q" or a "b", case insensitive \? # literal ? (?P<encoded>.*?) # non-greedy up to the next ?= is the encoded string \?= # literal ?= (?=[ \t]|$) # whitespace or the end of the string ''', re.VERBOSE | re.IGNORECASE | re.MULTILINE)
のように定義されていた。
これだと、"=?ISO-2022-JP?B?<エンコード文字列>?="の直後は半角スペースもしくはタブでないといけないが、上記例だとすぐに".net"と続いているためにマッチせず、文字列の終端である"...KEI=?="の部分までが一連のものとみなされてしまう。
一方で、デコードの処理では、最初の"?="までしか処理されないため、その後の部分がとりこぼされてしまう。
試しに、
(?=[ \t]|$) # whitespace or the end of the string
の部分だけをコメントアウトして試してみると、
>>> dc_list = decode_header(value) >>> dc_list [('\x1b$B?74)\x1b(B', 'iso-2022-jp'), ('.net', None), ('\x1b$B%"%i!<%H\x1b(B', 'iso-2022-jp')] >>> print unicode(make_header(dc_list)) 新刊 .net アラート >>>
のように、とりあえず ascii 部と iso-2022-jp 部とできちんと分割されているように見える。
ただし、
- "新刊"と".net" の間に半角スペースが入っている。
下記の __unicode__() によって付加される。 - ".net"の後の半角スペースが消えてしまっている。
decode_header() 内の "unenc = parts.pop(0).strip()" の箇所で消されている。
ちなみに、".net"と"アラート"の間にある半角スペースは、__unicode__() によって付加されたもの。
${PYTHONHOME}/lib/python2.7/email/header.py を流し読みしていると、class Header 定義で、
def __unicode__(self): """Helper for the built-in unicode function.""" uchunks = [] lastcs = None for s, charset in self._chunks: # We must preserve spaces between encoded and non-encoded word # boundaries, which means for us we need to add a space when we go # from a charset to None/us-ascii, or from None/us-ascii to a # charset. Only do this for the second and subsequent chunks. nextcs = charset
のようなコメントがあった。
要は、Non/us-ascii とそれ以外のエンコードされた文字列が続いている場合には、間にスペースを挟むような仕様が前提となっている模様。
となると、そもそも新刊.netの Subject のエンコード方法がおかしい可能性もある、のか?
もっとも、多くのメーラで意図通りにデコードされているところを見ると、Python の email.header が余計なことをしている可能性も…悩むくらいなら、正しい仕様を調べろという話もあるが。
ちなみに、Python でエンコード→デコードすると、
>>> from email.Header import Header >>> value = str(Header(u'新刊.net アラート', 'ISO-2022-JP')) >>> value '=?iso-2022-jp?b?GyRCPzc0KRsoQi5uZXQgGyRCJSIlaSE8JUgbKEI=?=' >>> >>> from email.header import decode_header, make_header >>> dc_list = decode_header(value) >>> dc_list [('\x1b$B?74)\x1b(B.net \x1b$B%"%i!<%H\x1b(B', 'iso-2022-jp')] >>> print unicode(make_header(dc_list)) 新刊.net アラート >>>
こんな感じになる。
ペアは一組だけで、iso-2022-jp として一括で処理される。
追記
参考までに、上記問題に対応した ${PYTHONHOME}/lib/python2.7/email/header.py の差分(2行修正・3行追加)。
$ diff -cr header-orig.py ./header.py *** header-orig.py 2014-01-02 22:30:22.084789888 +0900 --- ./header.py 2014-01-16 07:07:25.258016571 +0900 *************** *** 39,45 **** \? # literal ? (?P<encoded>.*?) # non-greedy up to the next ?= is the encoded string \?= # literal ?= ! (?=[ \t]|$) # whitespace or the end of the string ''', re.VERBOSE | re.IGNORECASE | re.MULTILINE) # Field name regexp, including trailing colon, but not separating whitespace, --- 39,45 ---- \? # literal ? (?P<encoded>.*?) # non-greedy up to the next ?= is the encoded string \?= # literal ?= ! #(?=[ \t]|$) # whitespace or the end of the string ''', re.VERBOSE | re.IGNORECASE | re.MULTILINE) # Field name regexp, including trailing colon, but not separating whitespace, *************** *** 82,88 **** continue parts = ecre.split(line) while parts: ! unenc = parts.pop(0).strip() if unenc: # Should we continue a long line? if decoded and decoded[-1][1] is None: --- 82,88 ---- continue parts = ecre.split(line) while parts: ! unenc = parts.pop(0) if unenc: # Should we continue a long line? if decoded and decoded[-1][1] is None: *************** *** 202,207 **** --- 202,208 ---- def __unicode__(self): """Helper for the built-in unicode function.""" uchunks = [] + ''' lastcs = None for s, charset in self._chunks: # We must preserve spaces between encoded and non-encoded word *************** *** 218,223 **** --- 219,227 ---- uchunks.append(USPACE) lastcs = nextcs uchunks.append(unicode(s, str(charset))) + ''' + for s, charset in self._chunks: + uchunks.append(unicode(s, str(charset))) return UEMPTYSTRING.join(uchunks) # Rich comparison operators for equality only. BAW: does it make sense to
Python で送信→スマートフォンで受信したメール本文の最後に「�」(U+FFFD・REPLACEMENT CHARACTER)が表示されてしまう
PCのメールソフトだと別に普通に見えるのに、スマートフォン(Android:自分の場合は WX04K)で見ると、本文の最後に�が表示されてしまっている。
今のところ、Python で送信する際のエンコード済み文字列に、最後にEOF('\x1a')を付加することで改善されたので、様子見中。
正しい解決方法が知りたい。
2014/01/16 追記
だめだ、最後に EOF を付けると、今度は PC メールで受信したときに、そのまま EOF が表示されてしまう…うーむ。