我正在处理邮件,名称和主题有时是 q 编码的,如下所示:
=?UTF-8?Q?J=2E_Pablo_Fern=C3=A1ndez?=
有没有办法在 Ruby 中解码它们?似乎 TMail 应该处理它,但它没有这样做。
我用它来解析电子邮件主题:
您可以尝试以下方法:
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
Ruby 包含一种解码 Quoted-Printable 字符串的方法:
puts "Pablo_Fern=C3=A1ndez".unpack "M"
# => Pablo_Fernández
但这似乎不适用于您的整个字符串(包括=?UTF-8?Q?
开头的部分。不过,也许您可以从那里解决。
这是一个相当古老的问题,但 TMail::Unquoter(或其新的化身 Mail::Encodings)也可以完成这项工作。
TMail::Unquoter.unquote_and_convert_to(str, 'utf-8' )
或者
Mail::Encodings.unquote_and_convert_to( str, 'utf-8' )
最有效和最新的解决方案似乎使用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
下面是你可以剪切和粘贴的 Ruby 代码,如果你愿意的话。如果直接使用 ruby 执行,它将运行测试,ruby ./copy-pasted.rb
. 正如在代码中所做的那样,我使用这个模块作为对 String 核心类的改进。
关于解决方案的几点说明:
其他解决方案.gsub('_', ' ')
在解压后的字符串上执行。但是,我不认为这是正确的,并且可能会根据字符集导致不正确的解码。 RFC2047 第 4.2 (2) 节表明“_
始终表示十六进制”,因此首先替换然后依赖解包结果20
似乎是正确的。(这也使实现更加优雅。)这也在对相关问题的回答中进行了讨论。=20
_
为了更有指导意义,我以自由间距模式编写了正则表达式以允许注释(我发现这通常对复杂的正则表达式很有帮助)。如果您调整正则表达式,请注意自由间距模式会更改空格的匹配,然后必须转义或作为字符类(如代码中所示)。我还在regex101 上添加了正则表达式,因此您可以阅读命名捕获组、惰性量词等的说明并自己进行实验。
正则表达式将吸收单个字符串中多个 Q 编码短语之间的空格(
; 但不是换行符),如 string 所示。这是因为RFC2047 第 5 节 (1)表明多个 Q 编码的短语必须通过线性空格相互分隔。根据您的用例,可能不需要吸收空白。TAB
test_4
名为 capture的正则表达式code
允许意外引用的可打印代码(除非[bBqQ]
发生匹配并且代码可能引发错误。这有助于我在处理文本时检测意外值。如果您不这样做code
,请将名为 capture 的正则表达式更改为[bBqQ]
想要这种行为。(将不匹配,将返回原始字符串。)
它利用全局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
这可能会帮助任何想要测试电子邮件的人。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
逐行解码:
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