mail 2.6.6 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. checksums.yaml +5 -5
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +74 -90
  4. data/lib/mail/attachments_list.rb +8 -4
  5. data/lib/mail/body.rb +50 -38
  6. data/lib/mail/check_delivery_params.rb +8 -6
  7. data/lib/mail/configuration.rb +2 -0
  8. data/lib/mail/constants.rb +1 -1
  9. data/lib/mail/core_extensions/smtp.rb +19 -16
  10. data/lib/mail/core_extensions/string.rb +0 -4
  11. data/lib/mail/elements/address.rb +28 -22
  12. data/lib/mail/elements/address_list.rb +10 -18
  13. data/lib/mail/elements/content_disposition_element.rb +8 -15
  14. data/lib/mail/elements/content_location_element.rb +5 -10
  15. data/lib/mail/elements/content_transfer_encoding_element.rb +5 -10
  16. data/lib/mail/elements/content_type_element.rb +8 -19
  17. data/lib/mail/elements/date_time_element.rb +6 -14
  18. data/lib/mail/elements/envelope_from_element.rb +14 -21
  19. data/lib/mail/elements/message_ids_element.rb +8 -12
  20. data/lib/mail/elements/mime_version_element.rb +6 -14
  21. data/lib/mail/elements/phrase_list.rb +6 -9
  22. data/lib/mail/elements/received_element.rb +9 -15
  23. data/lib/mail/encodings/7bit.rb +5 -15
  24. data/lib/mail/encodings/8bit.rb +2 -21
  25. data/lib/mail/encodings/base64.rb +11 -12
  26. data/lib/mail/encodings/binary.rb +3 -22
  27. data/lib/mail/encodings/identity.rb +24 -0
  28. data/lib/mail/encodings/quoted_printable.rb +6 -6
  29. data/lib/mail/encodings/transfer_encoding.rb +38 -29
  30. data/lib/mail/encodings/unix_to_unix.rb +3 -1
  31. data/lib/mail/encodings.rb +99 -43
  32. data/lib/mail/envelope.rb +1 -1
  33. data/lib/mail/field.rb +96 -59
  34. data/lib/mail/fields/bcc_field.rb +2 -2
  35. data/lib/mail/fields/cc_field.rb +1 -1
  36. data/lib/mail/fields/comments_field.rb +1 -1
  37. data/lib/mail/fields/common/common_address.rb +32 -7
  38. data/lib/mail/fields/common/common_field.rb +1 -10
  39. data/lib/mail/fields/common/parameter_hash.rb +1 -1
  40. data/lib/mail/fields/content_description_field.rb +1 -1
  41. data/lib/mail/fields/content_disposition_field.rb +3 -3
  42. data/lib/mail/fields/content_id_field.rb +2 -2
  43. data/lib/mail/fields/content_location_field.rb +1 -1
  44. data/lib/mail/fields/content_transfer_encoding_field.rb +1 -1
  45. data/lib/mail/fields/content_type_field.rb +4 -9
  46. data/lib/mail/fields/date_field.rb +2 -3
  47. data/lib/mail/fields/from_field.rb +1 -1
  48. data/lib/mail/fields/in_reply_to_field.rb +1 -1
  49. data/lib/mail/fields/keywords_field.rb +1 -1
  50. data/lib/mail/fields/message_id_field.rb +1 -1
  51. data/lib/mail/fields/mime_version_field.rb +1 -1
  52. data/lib/mail/fields/optional_field.rb +4 -1
  53. data/lib/mail/fields/received_field.rb +1 -1
  54. data/lib/mail/fields/references_field.rb +1 -1
  55. data/lib/mail/fields/reply_to_field.rb +1 -1
  56. data/lib/mail/fields/resent_bcc_field.rb +1 -1
  57. data/lib/mail/fields/resent_cc_field.rb +1 -1
  58. data/lib/mail/fields/resent_date_field.rb +0 -1
  59. data/lib/mail/fields/resent_from_field.rb +1 -1
  60. data/lib/mail/fields/resent_message_id_field.rb +1 -1
  61. data/lib/mail/fields/resent_sender_field.rb +1 -1
  62. data/lib/mail/fields/resent_to_field.rb +1 -1
  63. data/lib/mail/fields/return_path_field.rb +1 -1
  64. data/lib/mail/fields/sender_field.rb +1 -1
  65. data/lib/mail/fields/subject_field.rb +1 -1
  66. data/lib/mail/fields/to_field.rb +1 -1
  67. data/lib/mail/fields/unstructured_field.rb +21 -4
  68. data/lib/mail/header.rb +10 -8
  69. data/lib/mail/mail.rb +2 -10
  70. data/lib/mail/matchers/has_sent_mail.rb +21 -1
  71. data/lib/mail/message.rb +78 -68
  72. data/lib/mail/multibyte/chars.rb +29 -28
  73. data/lib/mail/multibyte/unicode.rb +10 -10
  74. data/lib/mail/multibyte.rb +64 -15
  75. data/lib/mail/network/delivery_methods/logger_delivery.rb +37 -0
  76. data/lib/mail/network/delivery_methods/sendmail.rb +8 -5
  77. data/lib/mail/network/delivery_methods/smtp.rb +58 -49
  78. data/lib/mail/network/delivery_methods/smtp_connection.rb +9 -1
  79. data/lib/mail/network/retriever_methods/imap.rb +18 -5
  80. data/lib/mail/network/retriever_methods/pop3.rb +3 -1
  81. data/lib/mail/network.rb +1 -0
  82. data/lib/mail/parser_tools.rb +15 -0
  83. data/lib/mail/parsers/address_lists_parser.rb +33207 -104
  84. data/lib/mail/parsers/address_lists_parser.rl +172 -0
  85. data/lib/mail/parsers/content_disposition_parser.rb +876 -49
  86. data/lib/mail/parsers/content_disposition_parser.rl +82 -0
  87. data/lib/mail/parsers/content_location_parser.rb +803 -23
  88. data/lib/mail/parsers/content_location_parser.rl +71 -0
  89. data/lib/mail/parsers/content_transfer_encoding_parser.rb +501 -19
  90. data/lib/mail/parsers/content_transfer_encoding_parser.rl +64 -0
  91. data/lib/mail/parsers/content_type_parser.rb +1023 -48
  92. data/lib/mail/parsers/content_type_parser.rl +83 -0
  93. data/lib/mail/parsers/date_time_parser.rb +870 -24
  94. data/lib/mail/parsers/date_time_parser.rl +62 -0
  95. data/lib/mail/parsers/envelope_from_parser.rb +3569 -34
  96. data/lib/mail/parsers/envelope_from_parser.rl +82 -0
  97. data/lib/mail/parsers/message_ids_parser.rb +2839 -25
  98. data/lib/mail/parsers/message_ids_parser.rl +82 -0
  99. data/lib/mail/parsers/mime_version_parser.rb +491 -26
  100. data/lib/mail/parsers/mime_version_parser.rl +61 -0
  101. data/lib/mail/parsers/phrase_lists_parser.rb +860 -18
  102. data/lib/mail/parsers/phrase_lists_parser.rl +83 -0
  103. data/lib/mail/parsers/received_parser.rb +8764 -37
  104. data/lib/mail/parsers/received_parser.rl +84 -0
  105. data/lib/mail/parsers/rfc2045_content_transfer_encoding.rl +13 -0
  106. data/lib/mail/parsers/rfc2045_content_type.rl +25 -0
  107. data/lib/mail/parsers/rfc2045_mime.rl +16 -0
  108. data/lib/mail/parsers/rfc2183_content_disposition.rl +15 -0
  109. data/lib/mail/parsers/rfc3629_utf8.rl +19 -0
  110. data/lib/mail/parsers/rfc5234_abnf_core_rules.rl +22 -0
  111. data/lib/mail/parsers/rfc5322.rl +59 -0
  112. data/lib/mail/parsers/rfc5322_address.rl +72 -0
  113. data/lib/mail/parsers/{ragel/date_time.rl → rfc5322_date_time.rl} +8 -1
  114. data/lib/mail/parsers/rfc5322_lexical_tokens.rl +60 -0
  115. data/lib/mail/parsers.rb +16 -24
  116. data/lib/mail/part.rb +3 -3
  117. data/lib/mail/parts_list.rb +5 -6
  118. data/lib/mail/utilities.rb +59 -28
  119. data/lib/mail/version.rb +2 -2
  120. data/lib/mail/version_specific/ruby_1_8.rb +40 -3
  121. data/lib/mail/version_specific/ruby_1_9.rb +61 -9
  122. data/lib/mail.rb +3 -16
  123. metadata +44 -53
  124. data/CHANGELOG.rdoc +0 -803
  125. data/CONTRIBUTING.md +0 -60
  126. data/Dependencies.txt +0 -2
  127. data/Gemfile +0 -14
  128. data/Rakefile +0 -29
  129. data/TODO.rdoc +0 -9
  130. data/lib/mail/core_extensions/string/access.rb +0 -146
  131. data/lib/mail/core_extensions/string/multibyte.rb +0 -79
  132. data/lib/mail/multibyte/exceptions.rb +0 -9
  133. data/lib/mail/parsers/ragel/common.rl +0 -185
  134. data/lib/mail/parsers/ragel/parser_info.rb +0 -61
  135. data/lib/mail/parsers/ragel/ruby/machines/address_lists_machine.rb +0 -14864
  136. data/lib/mail/parsers/ragel/ruby/machines/address_lists_machine.rb.rl +0 -37
  137. data/lib/mail/parsers/ragel/ruby/machines/content_disposition_machine.rb +0 -751
  138. data/lib/mail/parsers/ragel/ruby/machines/content_disposition_machine.rb.rl +0 -37
  139. data/lib/mail/parsers/ragel/ruby/machines/content_location_machine.rb +0 -614
  140. data/lib/mail/parsers/ragel/ruby/machines/content_location_machine.rb.rl +0 -37
  141. data/lib/mail/parsers/ragel/ruby/machines/content_transfer_encoding_machine.rb +0 -447
  142. data/lib/mail/parsers/ragel/ruby/machines/content_transfer_encoding_machine.rb.rl +0 -37
  143. data/lib/mail/parsers/ragel/ruby/machines/content_type_machine.rb +0 -825
  144. data/lib/mail/parsers/ragel/ruby/machines/content_type_machine.rb.rl +0 -37
  145. data/lib/mail/parsers/ragel/ruby/machines/date_time_machine.rb +0 -817
  146. data/lib/mail/parsers/ragel/ruby/machines/date_time_machine.rb.rl +0 -37
  147. data/lib/mail/parsers/ragel/ruby/machines/envelope_from_machine.rb +0 -2149
  148. data/lib/mail/parsers/ragel/ruby/machines/envelope_from_machine.rb.rl +0 -37
  149. data/lib/mail/parsers/ragel/ruby/machines/message_ids_machine.rb +0 -1570
  150. data/lib/mail/parsers/ragel/ruby/machines/message_ids_machine.rb.rl +0 -37
  151. data/lib/mail/parsers/ragel/ruby/machines/mime_version_machine.rb +0 -440
  152. data/lib/mail/parsers/ragel/ruby/machines/mime_version_machine.rb.rl +0 -37
  153. data/lib/mail/parsers/ragel/ruby/machines/phrase_lists_machine.rb +0 -564
  154. data/lib/mail/parsers/ragel/ruby/machines/phrase_lists_machine.rb.rl +0 -37
  155. data/lib/mail/parsers/ragel/ruby/machines/rb_actions.rl +0 -51
  156. data/lib/mail/parsers/ragel/ruby/machines/received_machine.rb +0 -5144
  157. data/lib/mail/parsers/ragel/ruby/machines/received_machine.rb.rl +0 -37
  158. data/lib/mail/parsers/ragel/ruby/parser.rb.rl.erb +0 -37
  159. data/lib/mail/parsers/ragel/ruby.rb +0 -40
  160. data/lib/mail/parsers/ragel.rb +0 -18
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+ require 'mail/encodings/transfer_encoding'
4
+
5
+ module Mail
6
+ module Encodings
7
+ # Identity encodings do no encoding/decoding and have a fixed cost:
8
+ # 1 byte in -> 1 byte out.
9
+ class Identity < TransferEncoding #:nodoc:
10
+ def self.decode(str)
11
+ str
12
+ end
13
+
14
+ def self.encode(str)
15
+ str
16
+ end
17
+
18
+ # 1 output byte per input byte.
19
+ def self.cost(str)
20
+ 1.0
21
+ end
22
+ end
23
+ end
24
+ end
@@ -6,27 +6,27 @@ module Mail
6
6
  module Encodings
7
7
  class QuotedPrintable < SevenBit
8
8
  NAME='quoted-printable'
9
-
9
+
10
10
  PRIORITY = 2
11
11
 
12
- def self.can_encode?(str)
13
- EightBit.can_encode? str
12
+ def self.can_encode?(enc)
13
+ EightBit.can_encode? enc
14
14
  end
15
15
 
16
16
  # Decode the string from Quoted-Printable. Cope with hard line breaks
17
17
  # that were incorrectly encoded as hex instead of literal CRLF.
18
18
  def self.decode(str)
19
- ::Mail::Utilities.to_lf str.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack("M*").first
19
+ str.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack("M*").first
20
20
  end
21
21
 
22
22
  def self.encode(str)
23
- ::Mail::Utilities.to_crlf([::Mail::Utilities.to_lf(str)].pack("M"))
23
+ [str].pack("M")
24
24
  end
25
25
 
26
26
  def self.cost(str)
27
27
  # These bytes probably do not need encoding
28
28
  c = str.count("\x9\xA\xD\x20-\x3C\x3E-\x7E")
29
- # Everything else turns into =XX where XX is a
29
+ # Everything else turns into =XX where XX is a
30
30
  # two digit hex number (taking 3 bytes)
31
31
  total = (str.bytesize - c)*3 + c
32
32
  total.to_f/str.bytesize
@@ -7,17 +7,17 @@ module Mail
7
7
 
8
8
  PRIORITY = -1
9
9
 
10
+ # And encoding's superclass can always transport it since the
11
+ # class hierarchy is arranged e.g. Base64 < 7bit < 8bit < Binary.
10
12
  def self.can_transport?(enc)
11
- enc = Encodings.get_name(enc)
12
- if Encodings.defined? enc
13
- Encodings.get_encoding(enc).new.is_a? self
14
- else
15
- false
16
- end
13
+ enc && enc <= self
17
14
  end
18
15
 
16
+ # Override in subclasses to indicate that they can encode text
17
+ # that couldn't be directly transported, e.g. Base64 has 7bit output,
18
+ # but it can encode binary.
19
19
  def self.can_encode?(enc)
20
- can_transport? enc
20
+ can_transport? enc
21
21
  end
22
22
 
23
23
  def self.cost(str)
@@ -32,36 +32,45 @@ module Mail
32
32
  self::NAME
33
33
  end
34
34
 
35
- def self.get_best_compatible(source_encoding, str)
36
- if self.can_transport?(source_encoding) && self.compatible_input?(str)
35
+ def self.negotiate(message_encoding, source_encoding, str, allowed_encodings = nil)
36
+ message_encoding = Encodings.get_encoding(message_encoding) || Encodings.get_encoding('8bit')
37
+ source_encoding = Encodings.get_encoding(source_encoding)
38
+
39
+ if message_encoding && source_encoding && message_encoding.can_transport?(source_encoding) && source_encoding.compatible_input?(str)
37
40
  source_encoding
38
41
  else
39
- choices = Encodings.get_all.select do |enc|
40
- self.can_transport?(enc) && enc.can_encode?(source_encoding)
41
- end
42
+ renegotiate(message_encoding, source_encoding, str, allowed_encodings)
43
+ end
44
+ end
42
45
 
43
- best = nil
44
- best_cost = nil
46
+ def self.renegotiate(message_encoding, source_encoding, str, allowed_encodings = nil)
47
+ encodings = Encodings.get_all.select do |enc|
48
+ (allowed_encodings.nil? || allowed_encodings.include?(enc)) &&
49
+ message_encoding.can_transport?(enc) &&
50
+ enc.can_encode?(source_encoding)
51
+ end
45
52
 
46
- choices.each do |enc|
47
- # If the current choice cannot be transported safely,
48
- # give priority to other choices but allow it to be used as a fallback.
49
- this_cost = enc.cost(str) if enc.compatible_input?(str)
53
+ lowest_cost(str, encodings)
54
+ end
50
55
 
51
- if !best_cost || (this_cost && this_cost < best_cost)
52
- best_cost = this_cost
53
- best = enc
54
- elsif this_cost == best_cost
55
- best = enc if enc::PRIORITY < best::PRIORITY
56
- end
57
- end
56
+ def self.lowest_cost(str, encodings)
57
+ best = nil
58
+ best_cost = nil
58
59
 
59
- best
60
+ encodings.each do |enc|
61
+ # If the current choice cannot be transported safely, give priority
62
+ # to other choices but allow it to be used as a fallback.
63
+ this_cost = enc.cost(str) if enc.compatible_input?(str)
64
+
65
+ if !best_cost || (this_cost && this_cost < best_cost)
66
+ best_cost = this_cost
67
+ best = enc
68
+ elsif this_cost == best_cost
69
+ best = enc if enc::PRIORITY < best::PRIORITY
70
+ end
60
71
  end
61
- end
62
72
 
63
- def to_s
64
- self.class.to_s
73
+ best
65
74
  end
66
75
  end
67
76
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Mail
3
3
  module Encodings
4
- module UnixToUnix
4
+ class UnixToUnix < TransferEncoding
5
5
  NAME = "x-uuencode"
6
6
 
7
7
  def self.decode(str)
@@ -13,6 +13,8 @@ module Mail
13
13
  end
14
14
 
15
15
  Encodings.register(NAME, self)
16
+ Encodings.register("uuencode", self)
17
+ Encodings.register("x-uue", self)
16
18
  end
17
19
  end
18
20
  end
@@ -7,7 +7,6 @@ module Mail
7
7
  end
8
8
 
9
9
  module Encodings
10
-
11
10
  include Mail::Constants
12
11
  extend Mail::Utilities
13
12
 
@@ -19,7 +18,7 @@ module Mail
19
18
  #
20
19
  # Encodings.register "base64", Mail::Encodings::Base64
21
20
  def Encodings.register(name, cls)
22
- @transfer_encodings[get_name(name)] = cls
21
+ @transfer_encodings[get_name(name)] = cls
23
22
  end
24
23
 
25
24
  # Is the encoding we want defined?
@@ -27,8 +26,8 @@ module Mail
27
26
  # Example:
28
27
  #
29
28
  # Encodings.defined?(:base64) #=> true
30
- def Encodings.defined?( str )
31
- @transfer_encodings.include? get_name(str)
29
+ def Encodings.defined?(name)
30
+ @transfer_encodings.include? get_name(name)
32
31
  end
33
32
 
34
33
  # Gets a defined encoding type, QuotedPrintable or Base64 for now.
@@ -39,16 +38,16 @@ module Mail
39
38
  # Example:
40
39
  #
41
40
  # Encodings.get_encoding(:base64) #=> Mail::Encodings::Base64
42
- def Encodings.get_encoding( str )
43
- @transfer_encodings[get_name(str)]
41
+ def Encodings.get_encoding(name)
42
+ @transfer_encodings[get_name(name)]
44
43
  end
45
44
 
46
45
  def Encodings.get_all
47
46
  @transfer_encodings.values
48
47
  end
49
48
 
50
- def Encodings.get_name(enc)
51
- underscoreize(enc).downcase
49
+ def Encodings.get_name(name)
50
+ underscoreize(name).downcase
52
51
  end
53
52
 
54
53
  def Encodings.transcode_charset(str, from_charset, to_charset = 'UTF-8')
@@ -168,32 +167,51 @@ module Mail
168
167
 
169
168
  def Encodings.address_encode(address, charset = 'utf-8')
170
169
  if address.is_a?(Array)
171
- # loop back through for each element
172
170
  address.compact.map { |a| Encodings.address_encode(a, charset) }.join(", ")
173
- else
174
- # find any word boundary that is not ascii and encode it
175
- encode_non_usascii(address, charset) if address
171
+ elsif address
172
+ encode_non_usascii(address, charset)
176
173
  end
177
174
  end
178
175
 
179
176
  def Encodings.encode_non_usascii(address, charset)
180
177
  return address if address.ascii_only? or charset.nil?
181
- us_ascii = %Q{\x00-\x7f}
182
- # Encode any non usascii strings embedded inside of quotes
183
- address = address.gsub(/(".*?[^#{us_ascii}].*?")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
184
- # Then loop through all remaining items and encode as needed
185
- tokens = address.split(/\s/)
186
- map_with_index(tokens) do |word, i|
187
- if word.ascii_only?
188
- word
189
- else
190
- previous_non_ascii = i>0 && tokens[i-1] && !tokens[i-1].ascii_only?
191
- if previous_non_ascii #why are we adding an extra space here?
192
- word = " #{word}"
178
+
179
+ # With KCODE=u we can't use regexps on other encodings. Go ASCII.
180
+ with_ascii_kcode do
181
+ # Encode all strings embedded inside of quotes
182
+ address = address.gsub(/("[^"]*[^\/]")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
183
+
184
+ # Then loop through all remaining items and encode as needed
185
+ tokens = address.split(/\s/)
186
+
187
+ map_with_index(tokens) do |word, i|
188
+ if word.ascii_only?
189
+ word
190
+ else
191
+ previous_non_ascii = i>0 && tokens[i-1] && !tokens[i-1].ascii_only?
192
+ if previous_non_ascii #why are we adding an extra space here?
193
+ word = " #{word}"
194
+ end
195
+ Encodings.b_value_encode(word, charset)
193
196
  end
194
- Encodings.b_value_encode(word, charset)
197
+ end.join(' ')
198
+ end
199
+ end
200
+
201
+ if RUBY_VERSION < '1.9'
202
+ # With KCODE=u we can't use regexps on other encodings. Go ASCII.
203
+ def Encodings.with_ascii_kcode #:nodoc:
204
+ if $KCODE
205
+ $KCODE, original_kcode = '', $KCODE
195
206
  end
196
- end.join(' ')
207
+ yield
208
+ ensure
209
+ $KCODE = original_kcode if original_kcode
210
+ end
211
+ else
212
+ def Encodings.with_ascii_kcode #:nodoc:
213
+ yield
214
+ end
197
215
  end
198
216
 
199
217
  # Encode a string with Base64 Encoding and returns it ready to be inserted
@@ -203,12 +221,15 @@ module Mail
203
221
  #
204
222
  # Encodings.b_value_encode('This is あ string', 'UTF-8')
205
223
  # #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
206
- def Encodings.b_value_encode(encoded_str, encoding = nil)
207
- return encoded_str if encoded_str.to_s.ascii_only?
208
- string, encoding = RubyVer.b_value_encode(encoded_str, encoding)
209
- map_lines(string) do |str|
210
- "=?#{encoding}?B?#{str.chomp}?="
211
- end.join(" ")
224
+ def Encodings.b_value_encode(string, encoding = nil)
225
+ if string.to_s.ascii_only?
226
+ string
227
+ else
228
+ Encodings.each_base64_chunk_byterange(string, 60).map do |chunk|
229
+ str, encoding = RubyVer.b_value_encode(chunk, encoding)
230
+ "=?#{encoding}?B?#{str.chomp}?="
231
+ end.join(" ")
232
+ end
212
233
  end
213
234
 
214
235
  # Encode a string with Quoted-Printable Encoding and returns it ready to be inserted
@@ -258,24 +279,23 @@ module Mail
258
279
  str[ENCODED_VALUE, 1]
259
280
  end
260
281
 
261
- # When the encoded string consists of multiple lines, lines with the same
262
- # encoding (Q or B) can be joined together.
282
+ # Split header line into proper encoded and unencoded parts.
263
283
  #
264
284
  # String has to be of the format =?<encoding>?[QB]?<string>?=
285
+ #
286
+ # Omit unencoded space after an encoded-word.
265
287
  def Encodings.collapse_adjacent_encodings(str)
266
288
  results = []
267
- previous_encoding = nil
289
+ last_encoded = nil # Track whether to preserve or drop whitespace
290
+
268
291
  lines = str.split(FULL_ENCODED_VALUE)
269
292
  lines.each_slice(2) do |unencoded, encoded|
270
- if encoded
271
- encoding = value_encoding_from_string(encoded)
272
- if encoding == previous_encoding && Utilities.blank?(unencoded)
273
- results.last << encoded
274
- else
275
- results << unencoded unless unencoded == EMPTY
276
- results << encoded
293
+ if last_encoded = encoded
294
+ if !Utilities.blank?(unencoded) || (!last_encoded && unencoded != EMPTY)
295
+ results << unencoded
277
296
  end
278
- previous_encoding = encoding
297
+
298
+ results << encoded
279
299
  else
280
300
  results << unencoded
281
301
  end
@@ -283,5 +303,41 @@ module Mail
283
303
 
284
304
  results
285
305
  end
306
+
307
+ # Partition the string into bounded-size chunks without splitting
308
+ # multibyte characters.
309
+ def Encodings.each_base64_chunk_byterange(str, max_bytesize_per_base64_chunk, &block)
310
+ raise "size per chunk must be multiple of 4" if (max_bytesize_per_base64_chunk % 4).nonzero?
311
+
312
+ if block_given?
313
+ max_bytesize = ((3 * max_bytesize_per_base64_chunk) / 4.0).floor
314
+ each_chunk_byterange(str, max_bytesize, &block)
315
+ else
316
+ enum_for :each_base64_chunk_byterange, str, max_bytesize_per_base64_chunk
317
+ end
318
+ end
319
+
320
+ # Partition the string into bounded-size chunks without splitting
321
+ # multibyte characters.
322
+ def Encodings.each_chunk_byterange(str, max_bytesize_per_chunk)
323
+ return enum_for(:each_chunk_byterange, str, max_bytesize_per_chunk) unless block_given?
324
+
325
+ offset = 0
326
+ chunksize = 0
327
+
328
+ str.each_char do |chr|
329
+ charsize = chr.bytesize
330
+
331
+ if chunksize + charsize > max_bytesize_per_chunk
332
+ yield RubyVer.string_byteslice(str, offset, chunksize)
333
+ offset += chunksize
334
+ chunksize = charsize
335
+ else
336
+ chunksize += charsize
337
+ end
338
+ end
339
+
340
+ yield RubyVer.string_byteslice(str, offset, chunksize)
341
+ end
286
342
  end
287
343
  end
data/lib/mail/envelope.rb CHANGED
@@ -12,7 +12,7 @@ module Mail
12
12
  class Envelope < StructuredField
13
13
 
14
14
  def initialize(*args)
15
- super(FIELD_NAME, strip_field(FIELD_NAME, args.last))
15
+ super(FIELD_NAME, args.last.to_s)
16
16
  end
17
17
 
18
18
  def element
data/lib/mail/field.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'mail/fields'
3
+ require 'mail/constants'
3
4
 
4
5
  # encoding: utf-8
5
6
  module Mail
@@ -83,9 +84,31 @@ module Mail
83
84
 
84
85
  def initialize(element, value, reason)
85
86
  @element = element
86
- @value = value
87
- @reason = reason
88
- super("#{element} can not parse |#{value}|\nReason was: #{reason}")
87
+ @value = to_utf8(value)
88
+ @reason = to_utf8(reason)
89
+ super("#{@element} can not parse |#{@value}|: #{@reason}")
90
+ end
91
+
92
+ private
93
+ def to_utf8(text)
94
+ if text.respond_to?(:force_encoding)
95
+ text.dup.force_encoding(Encoding::UTF_8)
96
+ else
97
+ text
98
+ end
99
+ end
100
+ end
101
+
102
+ class NilParseError < ParseError #:nodoc:
103
+ def initialize(element)
104
+ super element, nil, 'nil is invalid'
105
+ end
106
+ end
107
+
108
+ class IncompleteParseError < ParseError #:nodoc:
109
+ def initialize(element, original_text, unparsed_index)
110
+ parsed_text = to_utf8(original_text[0...unparsed_index])
111
+ super element, original_text, "Only able to parse up to #{parsed_text.inspect}"
89
112
  end
90
113
  end
91
114
 
@@ -93,41 +116,64 @@ module Mail
93
116
  class SyntaxError < FieldError #:nodoc:
94
117
  end
95
118
 
96
- # Accepts a string:
97
- #
98
- # Field.new("field-name: field data")
99
- #
100
- # Or name, value pair:
101
- #
102
- # Field.new("field-name", "value")
119
+ class << self
120
+ # Parse a field from a raw header line:
121
+ #
122
+ # Mail::Field.parse("field-name: field data")
123
+ # # => #<Mail::Field …>
124
+ def parse(field, charset = nil)
125
+ name, value = split(field)
126
+ if name && value
127
+ new name, value, charset
128
+ end
129
+ end
130
+
131
+ def split(raw_field) #:nodoc:
132
+ if raw_field.index(Constants::COLON)
133
+ name, value = raw_field.split(Constants::COLON, 2)
134
+ name.rstrip!
135
+ if name =~ /\A#{Constants::FIELD_NAME}\z/
136
+ [ name.rstrip, value.strip ]
137
+ else
138
+ Kernel.warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: invalid header name syntax: #{name.inspect}"
139
+ nil
140
+ end
141
+ else
142
+ raw_field.strip
143
+ end
144
+ rescue => error
145
+ warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: #{error.class}: #{error.message}"
146
+ nil
147
+ end
148
+ end
149
+
150
+ attr_reader :unparsed_value
151
+
152
+ # Create a field by name and optional value:
103
153
  #
104
- # Or a name by itself:
154
+ # Mail::Field.new("field-name", "value")
155
+ # # => #<Mail::Field …>
105
156
  #
106
- # Field.new("field-name")
157
+ # Values that aren't strings or arrays are coerced to Strings with `#to_s`.
107
158
  #
108
- # Note, does not want a terminating carriage return. Returns
109
- # self appropriately parsed. If value is not a string, then
110
- # it will be passed through as is, for example, content-type
111
- # field can accept an array with the type and a hash of
112
- # parameters:
159
+ # Mail::Field.new("field-name", 1234)
160
+ # # => #<Mail::Field …>
113
161
  #
114
- # Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
162
+ # Mail::Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
163
+ # # => #<Mail::Field …>
115
164
  def initialize(name, value = nil, charset = 'utf-8')
116
165
  case
117
- when name.index(COLON) # Field.new("field-name: field data")
166
+ when name.index(COLON)
167
+ Kernel.warn 'Passing an unparsed header field to Mail::Field.new is deprecated and will be removed in Mail 2.8.0. Use Mail::Field.parse instead.'
168
+ @name, @unparsed_value = self.class.split(name)
118
169
  @charset = Utilities.blank?(value) ? charset : value
119
- @name = name[FIELD_PREFIX]
120
- @raw_value = name
121
- @value = nil
122
- when Utilities.blank?(value) # Field.new("field-name")
170
+ when Utilities.blank?(value)
123
171
  @name = name
124
- @value = nil
125
- @raw_value = nil
172
+ @unparsed_value = nil
126
173
  @charset = charset
127
- else # Field.new("field-name", "value")
174
+ else
128
175
  @name = name
129
- @value = value
130
- @raw_value = nil
176
+ @unparsed_value = value
131
177
  @charset = charset
132
178
  end
133
179
  @name = FIELD_NAME_MAP[@name.to_s.downcase] || @name
@@ -138,8 +184,7 @@ module Mail
138
184
  end
139
185
 
140
186
  def field
141
- _, @value = split(@raw_value) if @raw_value && !@value
142
- @field ||= create_field(@name, @value, @charset)
187
+ @field ||= create_field(@name, @unparsed_value, @charset)
143
188
  end
144
189
 
145
190
  def name
@@ -217,11 +262,26 @@ module Mail
217
262
 
218
263
  private
219
264
 
220
- def split(raw_field)
221
- match_data = raw_field.mb_chars.match(FIELD_SPLIT)
222
- [match_data[1].to_s.mb_chars.strip, match_data[2].to_s.mb_chars.strip.to_s]
223
- rescue
224
- STDERR.puts "WARNING: Could not parse (and so ignoring) '#{raw_field}'"
265
+ def create_field(name, value, charset)
266
+ new_field(name, value, charset)
267
+ rescue Mail::Field::ParseError => e
268
+ field = Mail::UnstructuredField.new(name, value)
269
+ field.errors << [name, value, e]
270
+ field
271
+ end
272
+
273
+ def new_field(name, value, charset)
274
+ value = unfold(value) if value.is_a?(String)
275
+
276
+ if klass = field_class_for(name)
277
+ klass.new(value, charset)
278
+ else
279
+ OptionalField.new(name, value, charset)
280
+ end
281
+ end
282
+
283
+ def field_class_for(name)
284
+ FIELDS_MAP[name.to_s.downcase]
225
285
  end
226
286
 
227
287
  # 2.2.3. Long Header Fields
@@ -233,30 +293,7 @@ module Mail
233
293
  # treated in its unfolded form for further syntactic and semantic
234
294
  # evaluation.
235
295
  def unfold(string)
236
- string.gsub(/[\r\n \t]+/m, ' ')
296
+ string.gsub(/#{Constants::CRLF}(#{Constants::WSP})/m, '\1')
237
297
  end
238
-
239
- def create_field(name, value, charset)
240
- value = unfold(value) if value.is_a?(String)
241
-
242
- begin
243
- new_field(name, value, charset)
244
- rescue Mail::Field::ParseError => e
245
- field = Mail::UnstructuredField.new(name, value)
246
- field.errors << [name, value, e]
247
- field
248
- end
249
- end
250
-
251
- def new_field(name, value, charset)
252
- lower_case_name = name.to_s.downcase
253
- if field_klass = FIELDS_MAP[lower_case_name]
254
- field_klass.new(value, charset)
255
- else
256
- OptionalField.new(name, value, charset)
257
- end
258
- end
259
-
260
298
  end
261
-
262
299
  end
@@ -37,9 +37,9 @@ module Mail
37
37
  FIELD_NAME = 'bcc'
38
38
  CAPITALIZED_FIELD = 'Bcc'
39
39
 
40
- def initialize(value = '', charset = 'utf-8')
40
+ def initialize(value = nil, charset = 'utf-8')
41
41
  @charset = charset
42
- super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
42
+ super(CAPITALIZED_FIELD, value, charset)
43
43
  self
44
44
  end
45
45
 
@@ -39,7 +39,7 @@ module Mail
39
39
 
40
40
  def initialize(value = nil, charset = 'utf-8')
41
41
  self.charset = charset
42
- super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value), charset)
42
+ super(CAPITALIZED_FIELD, value, charset)
43
43
  self
44
44
  end
45
45
 
@@ -33,7 +33,7 @@ module Mail
33
33
 
34
34
  def initialize(value = nil, charset = 'utf-8')
35
35
  @charset = charset
36
- super(CAPITALIZED_FIELD, strip_field(FIELD_NAME, value))
36
+ super(CAPITALIZED_FIELD, value)
37
37
  self.parse
38
38
  self
39
39
  end