📚

Ruby で quoted-printable を読める文字列にする

2024/11/18に公開

メールヘッダに 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