4

我正在处理邮件,名称和主题有时是 q 编码的,如下所示:

=?UTF-8?Q?J=2E_Pablo_Fern=C3=A1ndez?=

有没有办法在 Ruby 中解码它们?似乎 TMail 应该处理它,但它没有这样做。

4

7 回答 7

7

我用它来解析电子邮件主题:

您可以尝试以下方法:

str = "=?UTF-8?Q?J=2E_Pablo_Fern=C3=A1ndez?="
if m = /=\?([A-Za-z0-9\-]+)\?(B|Q)\?([!->@-~]+)\?=/i.match(str)
        case m[2]
        when "B" # Base64 encoded
          decoded = Base64.decode64(m[3])
        when "Q" # Q encoded
          decoded = m[3].unpack("M").first.gsub('_',' ')
        else
          p "Could not find keyword!!!"
        end
        Iconv.conv('utf-8',m[1],decoded) # to convert to utf-8
end
于 2011-12-02T22:02:14.677 回答
3

Ruby 包含一种解码 Quoted-Printable 字符串的方法:

puts "Pablo_Fern=C3=A1ndez".unpack "M"
# => Pablo_Fernández

但这似乎不适用于您的整个字符串(包括=?UTF-8?Q?开头的部分。不过,也许您可​​以从那里解决。

于 2010-08-13T04:50:01.177 回答
2

这是一个相当古老的问题,但 TMail::Unquoter(或其新的化身 Mail::Encodings)也可以完成这项工作。

TMail::Unquoter.unquote_and_convert_to(str, 'utf-8' )

或者

Mail::Encodings.unquote_and_convert_to( str, 'utf-8' )
于 2012-05-07T18:56:15.110 回答
0

最有效和最新的解决方案似乎使用Mail gemvalue_decode的方法。

> Mail::Encodings.value_decode("=?UTF-8?Q?Greg_of_Google?=")
=> "Greg of Google"

https://www.rubydoc.info/github/mikel/mail/Mail/Encodings#value_decode-class_method

于 2020-05-19T11:27:11.157 回答
0

下面是你可以剪切和粘贴的 Ruby 代码,如果你愿意的话。如果直接使用 ruby​​ 执行,它将运行测试,ruby ./copy-pasted.rb. 正如在代码中所做的那样,我使用这个模块作为对 String 核心类的改进。

关于解决方案的几点说明:

  1. 其他解决方案.gsub('_', ' ')在解压后的字符串上执行。但是,我不认为这是正确的,并且可能会根据字符集导致不正确的解码。 RFC2047 第 4.2 (2) 节表明“_始终表示十六进制”,因此首先替换然后依赖解包结果20似乎是正确的。(这也使实现更加优雅。)这也在对相关问题的回答中进行了讨论=20_

  2. 为了更有指导意义,我以自由间距模式编写了正则表达式以允许注释(我发现这通常对复杂的正则表达式很有帮助)。如果您调整正则表达式,请注意自由间距模式会更改空格的匹配,然后必须转义或作为字符类(如代码中所示)。我还在regex101 上添加了正则表达式,因此您可以阅读命名捕获组、惰性量词等的说明并自己进行实验。

  3. 正则表达式将吸收单个字符串中多个 Q 编码短语之间的空格( ; 但不是换行符),如 string 所示。这是因为RFC2047 第 5 节 (1)表明多个 Q 编码的短语必须通过线性空格相互分隔。根据您的用例,可能不需要吸收空白。TABtest_4

  4. 名为 capture的正则表达式code允许意外引用的可打印代码(除非[bBqQ]发生匹配并且代码可能引发错误。这有助于我在处理文本时检测意外值。如果您不这样做code,请将名为 capture 的正则表达式更改为[bBqQ]想要这种行为。(将不匹配,将返回原始字符串。)

  5. 它利用全局Regexp.last_match作为gsub块中的便利。如果在多线程代码中使用它,您可能需要小心,我没有考虑到这一点。

其他参考和阅读:

require "minitest/autorun"

module QuotedPrintableDecode
  class UnhandledCodeError < StandardError
    def initialize(code)
      super("Unhandled quoted printable code: '#{code}'.")
    end
  end

  @@qp_text_regex = %r{
    =\?                # Opening literal: `=?`
    (?<charset>[^\?]+) # Character set, e.g. "Windows-1252" in `=?Windows-1252?`
    \?                 # Literal: `?`
    (?<code>[a-zA-Z])  # Encoding, e.g. "Q" in `?Q?` (`B`ase64); [BbQq] expected, others raise
    \?                 # Literal: `?`
    (?<text>[^\?]+?)   # Encoded text, lazy (non-greedy) matched, e.g. "Foo_bar" in `?Foo_bar?`
    \?=                # Closing literal: `?=`
    (?:[ ]+(?==\?))?   # Optional separating linear whitespace if another Q-encode follows
  }x                   # Free-spacing mode to allow above comments, also changes whitespace match

  refine String do
    def decode_q_p(to: "UTF-8")
      self.gsub(@@qp_text_regex) do
        code, from, text = Regexp.last_match.values_at(:code, :charset, :text)
        q_p_charset_to_charset(code, text, from, to)
      end
    end

    private

    def q_p_charset_to_charset(code, text, from, to)
      case code
        when "q", "Q"
          text.gsub("_", "=20").unpack("M")
        when "b", "B"
          text.unpack("m")
        else
          raise UnhandledCodeError.new(code)
      end.first.encode(to, from)
    end
  end
end

class TestQPDecode < Minitest::Test
  using QuotedPrintableDecode

  def test_decode_single_utf_8_phrase
    encoded = "=?UTF-8?Q?J=2E_Pablo_Fern=C3=A1ndez?="
    assert_equal encoded.decode_q_p, "J. Pablo Fernández"
  end

  def test_decoding_preserves_space_between_unencoded_phrase
    encoded = "=?utf-8?Q?Alfred_Sanford?= <me@example.com>"
    assert_equal encoded.decode_q_p, "Alfred Sanford <me@example.com>"
  end

  def test_decodinge_multiple_adjacent_phrases_absorbs_separating_whitespace
    encoded = "=?Windows-1252?Q?Foo_-_D?= =?Windows-1252?Q?ocument_World=9617=96520;_Recor?= =?Windows-1252?Q?d_People_to_C?= =?Windows-1252?Q?anada's_History?="
    assert_equal encoded.decode_q_p, "Foo - Document World–17–520; Record People to Canada's History"
  end

  def test_decoding_string_without_encoded_phrases_preserves_original
    encoded = "Contains no QP phrases"
    assert_equal encoded.decode_q_p, encoded
  end

  def test_unhandled_code_raises
    klass = QuotedPrintableDecode::UnhandledCodeError
    message = "Unhandled quoted printable code: 'Z'."
    encoded = "=?utf-8?Z?Unhandled code Z?="

    raised_error = assert_raises(klass) { encoded.decode_q_p }
    assert_equal message, raised_error.message
  end
end
于 2021-05-06T12:31:42.663 回答
0

这可能会帮助任何想要测试电子邮件的人。delivery.html_part 通常是编码的,但可以使用.decoded.

test "email test" do
  UserMailer.confirm_email(user).deliver_now
  assert_equal 1, ActionMailer::Base.deliveries.size
  delivery = ActionMailer::Base.deliveries.last
  assert_equal "Please confirm your email", delivery.subject
  assert delivery.html_part.decoded =~ /Click the link below to confirm your email/ # DECODING HERE
end
于 2018-07-24T15:28:59.340 回答
0

逐行解码:

line.unpack("M")

将 STDIN 或文件提供的编码字符串输入转换为解码输出:

if ARGV[0]
  lines = File.read(ARGV[0]).lines
else
  lines = STDIN.each_line.to_a
end

puts lines.map { |c| c.unpack("M") }.join
于 2017-02-22T20:04:55.537 回答