mail 2.6.6 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.
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