From a2442d4f54c217b6b58d464af52c0b966d2b396b Mon Sep 17 00:00:00 2001 From: Takeshi Sone Date: Wed, 22 Feb 2023 15:13:42 +0900 Subject: [PATCH 1/2] Add folding tests --- spec/mail/encodings_spec.rb | 9 ++++++++ spec/mail/fields/unstructured_field_spec.rb | 23 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/spec/mail/encodings_spec.rb b/spec/mail/encodings_spec.rb index 7428b9383..355e166bb 100644 --- a/spec/mail/encodings_spec.rb +++ b/spec/mail/encodings_spec.rb @@ -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 diff --git a/spec/mail/fields/unstructured_field_spec.rb b/spec/mail/fields/unstructured_field_spec.rb index af3407dad..4c57308e7 100644 --- a/spec/mail/fields/unstructured_field_spec.rb +++ b/spec/mail/fields/unstructured_field_spec.rb @@ -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" From 515c535d3a1adafc18e09192514a401876da092f Mon Sep 17 00:00:00 2001 From: Takeshi Sone Date: Wed, 22 Feb 2023 15:14:00 +0900 Subject: [PATCH 2/2] Fix folding long word --- lib/mail/fields/unstructured_field.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/mail/fields/unstructured_field.rb b/lib/mail/fields/unstructured_field.rb index 53e27c2c9..9d4a58bc3 100644 --- a/lib/mail/fields/unstructured_field.rb +++ b/lib/mail/fields/unstructured_field.rb @@ -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| @@ -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]/) @@ -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