Ruby で quoted-printable を読める文字列にする
メールヘッダに Content-Transfer-Encoding: quoted-printable
がある場合、メール本文には quoted-printable エンコーディングが施されています。これを ruby でデコードします。
ChatGPT に適当に質問すると mail
標準ライブラリを使いましょう、と言われるのですが、少なくとも ruby 3.3 時点で mail
は普通の gem です。 https://github.com/mikel/mail/
今回は gem を使わない実装をしたいのですが、しかし先人の実装を参考にさせていただき、シンプルな実装にしました。
TO_CRLF_REGEX = Regexp.new("(?<!\r)\n|\r(?!\n)")
def decode_quoted_printable(encoded_text)
encoded_text
.gsub(TO_CRLF_REGEX, "\r\n")
.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n")
.unpack1("M*")
.gsub(/\r\n|\r/, "\n")
.force_encoding("UTF-8")
end
実装というかほとんど mail
gem のコードを拾ってきただけです。
unpack1("M*")
unpack("M*").first
です。
"M" ディレクティブは quoted-printable 形式をデコードします。これで、ruby における quoted-printable 形式のデコードは完了です。
force_encoding("UTF-8")
unpack
だけでデコードが完了すると書いた直後ですが、このまま扱うと壊れる可能性があります。というのも、unpack1("M*")
後の文字列はエンコーディングが ASCII-BIN になっているため、それを UTF-8 で読もうとすると文字エンコーディングの変換が行われてしまいます。
unpack1("M*")
後はもともとの文字列が quoted-printable 形式にエンコードされる前の文字コード(=本来の文字コード)のバイナリ表現になっていますが、unpack1("M*") には本来の文字コードの情報は与えられていないため、とりあえず ASCII-BIN になっているっぽいです。
そのため、force_encoding("UTF-8")
で文字コードを指定します。
force_encoding
は「その文字列のバイナリをそのままで、文字コードだけを再設定する」メソッドです。Ruby の文字列はエンコードの情報を含んでいて、そのエンコード情報だけを置き換えてあげるわけです。もちろん、quoted-printable 形式にエンコードされる前の文字列が UTF-8 でない別の文字コードで表現されているなら、その文字コードを指定する必要があります。
irb(main):002> encoded_text.unpack1("M*").encoding
=> #<Encoding:ASCII-8BIT>
irb(main):003> puts encoded_text.unpack1("M*").force_encoding("UTF-8")
笑い男
=> nil
irb(main):004> puts encoded_text.unpack1("M*")
(irb):15:in `write': "\xE7" from ASCII-8BIT to UTF-8 (Encoding::UndefinedConversionError)
メールの場合、例えば Content-Type
などに text/plain; charset=UTF-8
とか書かれていたりします。
Discussion