雑記

2000|01|
2003|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|
2007|01|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|09|11|
2009|02|03|05|06|07|08|10|11|12|
2010|01|03|04|05|06|07|08|09|10|
2011|05|06|09|10|
2012|03|07|09|12|
2013|01|02|04|05|07|08|10|11|
2014|04|05|08|10|12|
2015|01|05|
2016|09|

2009-07-10 [長年日記]

[Ruby][Rails] TMailでSubjectにJIS補助漢字を使用すると文字化け

前回の続き。

前回の対処後、他に文字化けしないか検証していたところ、SubjectにJIS補助漢字が入ると文字化けすることを確認。

単純に、前回のソースの

 @mail.subject = base64(@mail.subject)

の部分を

 if NKF.guess(@mail.subject) == NKF::UTF8
    @mail.subject = @mail.subject.gsub("\xef\xbd\x9e", "\xe3\x80\x9c")
 end
 @mail.subject = base64(@mail.subject)

のように書き換えると、「〜」に関してはうまくいくようになるが、コピーライトマークのような他のJIS補助漢字を試すと、Thunderbirdなどの、本文の時には文字化けが発生していなかったクライアントでも化けてしまう。

具体例を挙げると、コピーライトマーク一文字のSubjectは、「=?iso-2022-jp?B?GyRCRCJtGyhC?=」となり、これをデコードすると 「ESC $ B D " ( B ESC ( B \n」という完全に壊れた文字列になる。

で、ソース探索の旅に出て、ようやくたどり着いたのがTMailのEncoderクラスの中のscanaddとextract_Jの両メソッド。これらがJIS補助漢字のことを考慮に入れていないため、文字列を完全に壊してしまうらしい。

ということで、サブジェクトの文字化けにも対応した版はこちら。前回のgettext_ext.rbを置き換えて下さい。

if defined? ActionMailer
  module ActionMailer #:nodoc:
    class Base #:nodoc:
      def create!(*arg) #:nodoc:
        create_without_gettext!(*arg)
        if Locale.get.language == "ja"
          require 'nkf'
          if NKF.guess(@mail.subject) == NKF::UTF8
            @mail.subject = @mail.subject.gsub("\xef\xbd\x9e", "\xe3\x80\x9c")
          end
          @mail.subject = NKF.nkf('-j -m0', @mail.subject)
          part = @mail.parts.empty? ? @mail : @mail.parts.first
          if part.content_type == 'text/plain'
            part.charset = 'iso-2022-jp'
            if NKF.guess(part.body) == NKF::UTF8
               part.body = part.body.gsub("\xef\xbd\x9e", "\xe3\x80\x9c")
            end
            part.body = NKF.nkf('-j', part.body)
          end
        end
        @mail
      end
    end
  end

  module TMail
    class Encoder
      def scanadd( str, force = false )
        types = ''
        strs = []
        ki = false
        if str.respond_to?(:encoding)
          enc = str.encoding
          str.force_encoding(Encoding::ASCII_8BIT)
        end
        until str.empty?
          if m = /\A[^\e\t\r\n ]+/.match(str)
            types << (force ? 'j' : 'a')
            if str.respond_to?(:encoding)
              strs.push m[0].force_encoding(enc)
            else
              strs.push m[0]
            end
          elsif m = /\A[\t\r\n ]+/.match(str)
            types << 's'
            if str.respond_to?(:encoding)
              strs.push m[0].force_encoding(enc)
            else
              strs.push m[0]
            end
          elsif m = /\A\e(\x24(?:[B@]|\([BD@])[^\e]+)/.match(str)
            types << 'j'
            ki = true
            str = m.post_match
            if str.respond_to?(:encoding)
              strs.push m[1].force_encoding(enc)
            else
              strs.push m[1]
            end
          elsif m = /\A\e\(B/.match(str)
            str = m.post_match
            if ki
              ki = false
            else
              types << 'a'
              if str.respond_to?(:encoding)
                strs.push m[0].force_encoding(enc)
              else
                strs.push m[0]
              end
            end
          else
            raise 'TMail FATAL: encoder scan fail'
          end
          (str = m.post_match) unless m.nil?
        end

        do_encode types, strs
      end

      def extract_J( chunksize, str )
        size = max_bytes(chunksize, str.size) - 6
        size = (size % 2 == 0) ? (size) : (size - 1)
        return nil if size <= 0
        "\e#{str.slice!(0, size)}\e(B"
      end
    end
  end

end

ちなみに、前回

 @mail.subject = base64(@mail.subject)

だったところが、

 @mail.subject = NKF.nkf('-j -m0', @mail.subject)

に書き換わってますが、これは追跡の途中、ここでbase64エンコードしておいてもまったく意味が無いことが分かったので、JISに変換するだけにしました。

余談ですが、今回、grep頼りのソース探査の限界をひしひしと感じました。IDEとか使うとこういう作業は楽になるんですかね?