MIMEヘッダエンコーディングは複雑すぎてつらい

これは NSEG Advent Calender の7日目の記事です(内容は NSEG とも長野とも関係ありませんが…)。

www.adventar.org

メールの送信者(From)や件名(Subject)は本来ASCII(の一部の文字)しか書くことができないんですが、MIME(RFC2047)の登場によって日本語等の非ASCII文字を記述することができるようになりました。

とは言ってもメールアプリから見て日本語が表示できているだけで、内部的にはASCII文字にエンコードされています。MIMEヘッダエンコーディングと呼ばれています。

たとえば、「日本語」という文字列は =?utf-8?b?5pel5pys6Kqe?==?iso-2022-jp?b?GyRCRnxLXDhsGyhC?= に変換されています。

この処理が実は非常に複雑で、正しくエンコードされてない場合がかなりあります。

件名の「MIMEヘッダエンコーディングは複雑すぎてつらい」をエンコードしてみます。

文字列を Base64 化して =?utf-8?b??= で括るだけです。

Subject: =?utf-8?b?TUlNReODmOODg+ODgOOCqOODs+OCs+ODvOODh+OCo+ODs+OCsOOBr+ikh+mbkeOBmeOBjuOBpuOBpOOCieOBhA==?=

これだけであればとても簡単なのですが、実はここからが大変です。

まず、メールヘッダの物理的な1行の長さは78文字であるべきという制約があります(RFC5322 2.1.1)。 これより長い文字列は、空白文字のところで折り返すことができます(RFC5322 2.2.3)。

が、エンコードされた文字列には空白文字は含まれていません。むりやり次のようにしてはいけません。

Subject: =?utf-8?b?TUlNReODmOODg+ODgOOCqOODs+OCs+ODvOODh+OCo+ODs+OCsOOBr+ikh+m
 bkeOBmeOBjuOBpuOBpOOCieOBhA==?=

どうすればいいかというと複数のエンコード文字列に分割します。空白区切りで連続したエンコード文字列はデコード時に空白を無視して結合するためです(RFC2047 6.2)。

Subject: =?utf-8?b?TUlNReODmOODg+ODgOOCqOODs+OCs+ODvOODh+OCo+ODs+OCsOOBr+ik?=
 =?utf-8?b?h+mbkeOBmeOBjuOBpuOBpOOCieOBhA==?=

でもこれも実は正しくありません。

1行目の文字列をデコードすると「MIMEヘッダエンコーディングは\xE8\xA4」となります。「複(\xE8\xA4\x87)」が先頭2バイトで切られてしまっていますが、文字列をエンコードする際はマルチバイト文字を途中で区切ってはいけないためです(RFC2047 5)。

正しくは次のようになります。

Subject: =?utf-8?b?TUlNReODmOODg+ODgOOCqOODs+OCs+ODvOODh+OCo+ODs+OCsOOBr+==?=
 =?utf-8?b?6KSH6ZuR44GZ44GO44Gm44Gk44KJ44GE?=

エンコードの際には文字単位を意識する必要があります。

ISO-2022-JP の場合はさらに複雑です。日本語文字列の先頭と末尾にエスケープコード(1B 24 42, 1B 28 42)がつくためです。 エンコード文字列は ASCII 状態で終わる必要があるため、日本語で終わる場合は ASCII に戻すエスケープコード(1B 28 42)で終わらせないといけません(RFC2047 3)。

個人的には、ISO-2022-JP なんてもはや使う意味はないので、UTF-8 を使うのがよいと思ってます。「①」「㈱」「♡」「髙」「﨑」や半角カナも使えますし。

ASCII 文字はエンコードする必要がないためそのまま記述できますが、ABCあいうABC=?utf-8?b?44GC44GE44GG?= とエンコードするのは間違いです。 エンコード文字列は空白区切りの単語単位でないといけないためです。 この場合は ABCあいう をまとめて =?utf-8?b?QUJD44GC44GE44GG?= とする必要があります。 ABC =?utf-8?b?44GC44GE44GG?= だとデコードすると ABC あいう となり空白が入ってしまいます。

エンコードの際に単語単位に処理するとよいのではないかと思うかもしれませんが、たとえば かな 漢字 を単語単位でエンコードして、=?utf-8?b?44GL44Gq?= =?utf-8?b?5ryi5a2X?= としてしまうと、デコードすると かな漢字 となり空白が消えてしまいます。 エンコードする単語間にある空白は、それも含めてエンコードする必要があります。

あと、まずないと思いますが、MIMEヘッダエンコーディング文字列と同じ形式の単語をそのまま送りたい場合も、エンコードしておかないと、表示時にデコードされてしまいます。

このようにMIMEヘッダエンコーディングは複雑です。エンコードする処理も複雑ですが、間違った実装によってエンコードされた不正な文字列をそれなりに利用者に見せられるようにデコードするのも大変です。

以上、MIMEヘッダエンコーディングは複雑でつらい話でした。