Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix folding non-ASCII header #1566

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 8 additions & 8 deletions lib/mail/fields/unstructured_field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ def fold(prepend = 0) # :nodoc:
encoding = normalized_encoding
decoded_string = decoded.to_s
should_encode = !decoded_string.ascii_only?
# If a header with only ASCII characters exceeds 998 characters without whitespace,
# it can be safely folded by encoding it.
if !should_encode && decoded_string.split(/[ \t]/).any? { |word| word.length > 900 }
should_encode = true
end
if should_encode
first = true
words = decoded_string.split(/[ \t]/).map do |word|
Expand All @@ -110,11 +115,9 @@ def fold(prepend = 0) # :nodoc:
else
word = " #{word}"
end
if !word.ascii_only?
word
else
word.scan(/.{7}|.+$/)
end
# A UTF-8 character can take up to 4 bytes, which becomes 12 bytes when encoded with QP.
# 70 characters is safe for the 998-byte limit.
word.scan(/.{1,70}/)
end.flatten
else
words = decoded_string.split(/[ \t]/)
Expand All @@ -140,9 +143,6 @@ def fold(prepend = 0) # :nodoc:
word = encode_crlf(word)
# Skip to next line if we're going to go past the limit
# Unless this is the first word, in which case we're going to add it anyway
# Note: This means that a word that's longer than 998 characters is going to break the spec. Please fix if this is a problem for you.
# (The fix, it seems, would be to use encoded-word encoding on it, because that way you can break it across multiple lines and
# the linebreak will be ignored)
break if !line.empty? && (line.length + word.length + 1 > limit)
# Remove the word from the queue ...
words.shift
Expand Down
9 changes: 9 additions & 0 deletions spec/mail/encodings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,15 @@
expect(unwrapped.gsub("Subject: ", "")).to eq original
end

it "should round trip ASCII-only word with more than 998 characters and no white space" do
original = "ThisIsASubjectHeaderMessageThatIsGoingToBeMoreThan998CharactersLong." * 20
mail = Mail.new
mail.subject = original
wrapped = mail[:subject].wrapped_value
unwrapped = Mail::Encodings.value_decode(wrapped)
expect(unwrapped.gsub("Subject: ", "")).to eq original
end

it "should decode a blank string" do
expect(Mail::Encodings.value_decode("=?utf-8?Q??=")).to eq ""
end
Expand Down
23 changes: 22 additions & 1 deletion spec/mail/fields/unstructured_field_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,32 @@
string = %|{"unique_args": {"mailing_id":147,"account_id":2}, "to": ["larspind@gmail.com"], "category": "mailing", "filters": {"domainkeys": {"settings": {"domain":1,"enable":1}}}, "sub": {"{{open_image_url}}": ["http://betaling.larspind.local/O/token/147/Mailing::FakeRecipient"], "{{name}}": ["[FIRST NAME]"], "{{signup_reminder}}": ["(her kommer til at stå hvornår folk har skrevet sig op ...)"], "{{unsubscribe_url}}": ["http://betaling.larspind.local/U/token/147/Mailing::FakeRecipient"], "{{email}}": ["larspind@gmail.com"], "{{link:308}}": ["http://betaling.larspind.local/L/308/0/Mailing::FakeRecipient"], "{{confirm_url}}": [""], "{{ref}}": ["[REF]"]}}|
@field = Mail::UnstructuredField.new("X-SMTPAPI", string)
string = string.dup.force_encoding('UTF-8')
result = "X-SMTPAPI: =?UTF-8?Q?{=22unique=5Fargs=22:_{=22mailing=5Fid=22:147,=22a?=\r\n =?UTF-8?Q?ccount=5Fid=22:2},_=22to=22:_[=22larspind@gmail.com=22],_=22categ?=\r\n =?UTF-8?Q?ory=22:_=22mailing=22,_=22filters=22:_{=22domainkeys=22:_{=22sett?=\r\n =?UTF-8?Q?ings=22:_{=22domain=22:1,=22enable=22:1}}},_=22sub=22:_{=22{{op?=\r\n =?UTF-8?Q?en=5Fimage=5Furl}}=22:_[=22http://betaling.larspind.local/O?=\r\n =?UTF-8?Q?/token/147/Mailing::FakeRecipient=22],_=22{{name}}=22:_[=22[FIRST?=\r\n =?UTF-8?Q?_NAME]=22],_=22{{signup=5Freminder}}=22:_[=22=28her_kommer_til_at?=\r\n =?UTF-8?Q?_st=C3=A5_hvorn=C3=A5r_folk_har_skrevet_sig_op_...=29=22],?=\r\n =?UTF-8?Q?_=22{{unsubscribe=5Furl}}=22:_[=22http://betaling.larspind.?=\r\n =?UTF-8?Q?local/U/token/147/Mailing::FakeRecipient=22],_=22{{email}}=22:?=\r\n =?UTF-8?Q?_[=22larspind@gmail.com=22],_=22{{link:308}}=22:_[=22http://beta?=\r\n =?UTF-8?Q?ling.larspind.local/L/308/0/Mailing::FakeRecipient=22],_=22{{con?=\r\n =?UTF-8?Q?firm=5Furl}}=22:_[=22=22],_=22{{ref}}=22:_[=22[REF]=22]}}?=\r\n"
result = "X-SMTPAPI: =?UTF-8?Q?{=22unique=5Fargs=22:?=\r\n =?UTF-8?Q?_{=22mailing=5Fid=22:147,=22account=5Fid=22:2},_=22to=22:?=\r\n =?UTF-8?Q?_[=22larspind@gmail.com=22],_=22category=22:_=22mailing=22,?=\r\n =?UTF-8?Q?_=22filters=22:_{=22domainkeys=22:_{=22settings=22:?=\r\n =?UTF-8?Q?_{=22domain=22:1,=22enable=22:1}}},_=22sub=22:?=\r\n =?UTF-8?Q?_{=22{{open=5Fimage=5Furl}}=22:?=\r\n =?UTF-8?Q?_[=22http://betaling.larspind.local/O/token/147/Mailing::FakeRecipient=22]?=\r\n =?UTF-8?Q?,_=22{{name}}=22:_[=22[FIRST_NAME]=22],?=\r\n =?UTF-8?Q?_=22{{signup=5Freminder}}=22:_[=22=28her_kommer_til_at_st=C3=A5?=\r\n =?UTF-8?Q?_hvorn=C3=A5r_folk_har_skrevet_sig_op_...=29=22],?=\r\n =?UTF-8?Q?_=22{{unsubscribe=5Furl}}=22:?=\r\n =?UTF-8?Q?_[=22http://betaling.larspind.local/U/token/147/Mailing::FakeRecipient=22]?=\r\n =?UTF-8?Q?,_=22{{email}}=22:_[=22larspind@gmail.com=22],?=\r\n =?UTF-8?Q?_=22{{link:308}}=22:?=\r\n =?UTF-8?Q?_[=22http://betaling.larspind.local/L/308/0/Mailing::FakeRecipient=22],?=\r\n =?UTF-8?Q?_=22{{confirm=5Furl}}=22:_[=22=22],_=22{{ref}}=22:?=\r\n =?UTF-8?Q?_[=22[REF]=22]}}?=\r\n"
expect(@field.encoded).to eq result
expect(@field.decoded).to eq string
end

it "should fold an ASCII-only subject with more than 998 characters and no white space" do
value = "ThisIsASubjectHeaderMessageThatIsGoingToBeMoreThan998CharactersLong." * 20
@field = Mail::UnstructuredField.new("Subject", value)
lines = @field.encoded.split("\r\n\s")
lines.each { |line| expect(line.length).to be < 998 }
end

it "should fold a Japanese subject with more than 998 characters long and no white space" do
value = "これは非常に長い日本語のSubjectです。空白はありません。" * 5
@field = Mail::UnstructuredField.new("Subject", value)
lines = @field.encoded.split("\r\n\s")
lines.each { |line| expect(line.length).to be < 998 }
end

it "should fold full of emoji subject that is going to be more than 998 bytes unfolded" do
value = "😄" * 90
@field = Mail::UnstructuredField.new("Subject", value)
lines = @field.encoded.split("\r\n\s")
lines.each { |line| expect(line.length).to be < 998 }
end

it "should fold properly with continuous spaces around the linebreak" do
@field = Mail::UnstructuredField.new("Subject", "This is a header that has continuous spaces around line break point, which should be folded properly")
result = "Subject: This is a header that has continuous spaces around line break point,\s\r\n\s\s\s\swhich should be folded properly\r\n"
Expand Down