mail 2.6.1 → 2.8.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 (188) hide show
  1. checksums.yaml +5 -5
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +150 -107
  4. data/lib/mail/attachments_list.rb +13 -10
  5. data/lib/mail/body.rb +104 -90
  6. data/lib/mail/check_delivery_params.rb +55 -10
  7. data/lib/mail/configuration.rb +3 -0
  8. data/lib/mail/constants.rb +79 -0
  9. data/lib/mail/elements/address.rb +96 -108
  10. data/lib/mail/elements/address_list.rb +13 -30
  11. data/lib/mail/elements/content_disposition_element.rb +10 -16
  12. data/lib/mail/elements/content_location_element.rb +9 -13
  13. data/lib/mail/elements/content_transfer_encoding_element.rb +7 -11
  14. data/lib/mail/elements/content_type_element.rb +17 -23
  15. data/lib/mail/elements/date_time_element.rb +8 -15
  16. data/lib/mail/elements/envelope_from_element.rb +23 -23
  17. data/lib/mail/elements/message_ids_element.rb +22 -15
  18. data/lib/mail/elements/mime_version_element.rb +8 -15
  19. data/lib/mail/elements/phrase_list.rb +13 -10
  20. data/lib/mail/elements/received_element.rb +28 -19
  21. data/lib/mail/elements.rb +1 -0
  22. data/lib/mail/encodings/7bit.rb +10 -14
  23. data/lib/mail/encodings/8bit.rb +5 -18
  24. data/lib/mail/encodings/base64.rb +15 -10
  25. data/lib/mail/encodings/binary.rb +4 -22
  26. data/lib/mail/encodings/identity.rb +24 -0
  27. data/lib/mail/encodings/quoted_printable.rb +13 -7
  28. data/lib/mail/encodings/transfer_encoding.rb +47 -28
  29. data/lib/mail/encodings/unix_to_unix.rb +20 -0
  30. data/lib/mail/encodings.rb +102 -92
  31. data/lib/mail/envelope.rb +12 -14
  32. data/lib/mail/field.rb +121 -85
  33. data/lib/mail/field_list.rb +62 -8
  34. data/lib/mail/fields/bcc_field.rb +42 -48
  35. data/lib/mail/fields/cc_field.rb +29 -50
  36. data/lib/mail/fields/comments_field.rb +28 -37
  37. data/lib/mail/fields/common_address_field.rb +170 -0
  38. data/lib/mail/fields/common_date_field.rb +58 -0
  39. data/lib/mail/fields/common_field.rb +77 -0
  40. data/lib/mail/fields/common_message_id_field.rb +42 -0
  41. data/lib/mail/fields/content_description_field.rb +8 -14
  42. data/lib/mail/fields/content_disposition_field.rb +20 -44
  43. data/lib/mail/fields/content_id_field.rb +25 -51
  44. data/lib/mail/fields/content_location_field.rb +12 -25
  45. data/lib/mail/fields/content_transfer_encoding_field.rb +32 -31
  46. data/lib/mail/fields/content_type_field.rb +51 -80
  47. data/lib/mail/fields/date_field.rb +24 -52
  48. data/lib/mail/fields/from_field.rb +29 -50
  49. data/lib/mail/fields/in_reply_to_field.rb +39 -49
  50. data/lib/mail/fields/keywords_field.rb +19 -32
  51. data/lib/mail/fields/message_id_field.rb +26 -71
  52. data/lib/mail/fields/mime_version_field.rb +20 -30
  53. data/lib/mail/fields/named_structured_field.rb +11 -0
  54. data/lib/mail/fields/named_unstructured_field.rb +11 -0
  55. data/lib/mail/fields/optional_field.rb +10 -7
  56. data/lib/mail/fields/{common/parameter_hash.rb → parameter_hash.rb} +16 -13
  57. data/lib/mail/fields/received_field.rb +44 -57
  58. data/lib/mail/fields/references_field.rb +36 -49
  59. data/lib/mail/fields/reply_to_field.rb +29 -50
  60. data/lib/mail/fields/resent_bcc_field.rb +29 -50
  61. data/lib/mail/fields/resent_cc_field.rb +29 -50
  62. data/lib/mail/fields/resent_date_field.rb +6 -30
  63. data/lib/mail/fields/resent_from_field.rb +29 -50
  64. data/lib/mail/fields/resent_message_id_field.rb +6 -29
  65. data/lib/mail/fields/resent_sender_field.rb +28 -57
  66. data/lib/mail/fields/resent_to_field.rb +29 -50
  67. data/lib/mail/fields/return_path_field.rb +51 -55
  68. data/lib/mail/fields/sender_field.rb +35 -56
  69. data/lib/mail/fields/structured_field.rb +4 -30
  70. data/lib/mail/fields/subject_field.rb +10 -11
  71. data/lib/mail/fields/to_field.rb +29 -50
  72. data/lib/mail/fields/unstructured_field.rb +36 -50
  73. data/lib/mail/fields.rb +1 -0
  74. data/lib/mail/header.rb +73 -110
  75. data/lib/mail/indifferent_hash.rb +1 -0
  76. data/lib/mail/mail.rb +6 -11
  77. data/lib/mail/matchers/attachment_matchers.rb +44 -0
  78. data/lib/mail/matchers/has_sent_mail.rb +53 -9
  79. data/lib/mail/message.rb +132 -136
  80. data/lib/mail/multibyte/chars.rb +24 -180
  81. data/lib/mail/multibyte/unicode.rb +31 -26
  82. data/lib/mail/multibyte/utils.rb +27 -43
  83. data/lib/mail/multibyte.rb +56 -16
  84. data/lib/mail/network/delivery_methods/exim.rb +9 -11
  85. data/lib/mail/network/delivery_methods/file_delivery.rb +14 -16
  86. data/lib/mail/network/delivery_methods/logger_delivery.rb +34 -0
  87. data/lib/mail/network/delivery_methods/sendmail.rb +68 -24
  88. data/lib/mail/network/delivery_methods/smtp.rb +77 -54
  89. data/lib/mail/network/delivery_methods/smtp_connection.rb +5 -9
  90. data/lib/mail/network/delivery_methods/test_mailer.rb +9 -9
  91. data/lib/mail/network/retriever_methods/base.rb +9 -8
  92. data/lib/mail/network/retriever_methods/imap.rb +21 -7
  93. data/lib/mail/network/retriever_methods/pop3.rb +6 -3
  94. data/lib/mail/network/retriever_methods/test_retriever.rb +4 -2
  95. data/lib/mail/network.rb +2 -0
  96. data/lib/mail/parser_tools.rb +15 -0
  97. data/lib/mail/parsers/address_lists_parser.rb +33226 -116
  98. data/lib/mail/parsers/address_lists_parser.rl +179 -0
  99. data/lib/mail/parsers/content_disposition_parser.rb +883 -49
  100. data/lib/mail/parsers/content_disposition_parser.rl +89 -0
  101. data/lib/mail/parsers/content_location_parser.rb +810 -23
  102. data/lib/mail/parsers/content_location_parser.rl +78 -0
  103. data/lib/mail/parsers/content_transfer_encoding_parser.rb +510 -21
  104. data/lib/mail/parsers/content_transfer_encoding_parser.rl +71 -0
  105. data/lib/mail/parsers/content_type_parser.rb +1031 -47
  106. data/lib/mail/parsers/content_type_parser.rl +90 -0
  107. data/lib/mail/parsers/date_time_parser.rb +879 -24
  108. data/lib/mail/parsers/date_time_parser.rl +69 -0
  109. data/lib/mail/parsers/envelope_from_parser.rb +3670 -40
  110. data/lib/mail/parsers/envelope_from_parser.rl +89 -0
  111. data/lib/mail/parsers/message_ids_parser.rb +5147 -25
  112. data/lib/mail/parsers/message_ids_parser.rl +93 -0
  113. data/lib/mail/parsers/mime_version_parser.rb +498 -26
  114. data/lib/mail/parsers/mime_version_parser.rl +68 -0
  115. data/lib/mail/parsers/phrase_lists_parser.rb +872 -21
  116. data/lib/mail/parsers/phrase_lists_parser.rl +90 -0
  117. data/lib/mail/parsers/received_parser.rb +8777 -42
  118. data/lib/mail/parsers/received_parser.rl +91 -0
  119. data/lib/mail/parsers/rfc2045_content_transfer_encoding.rl +13 -0
  120. data/lib/mail/parsers/rfc2045_content_type.rl +25 -0
  121. data/lib/mail/parsers/rfc2045_mime.rl +16 -0
  122. data/lib/mail/parsers/rfc2183_content_disposition.rl +15 -0
  123. data/lib/mail/parsers/rfc3629_utf8.rl +19 -0
  124. data/lib/mail/parsers/rfc5234_abnf_core_rules.rl +22 -0
  125. data/lib/mail/parsers/rfc5322.rl +74 -0
  126. data/lib/mail/parsers/rfc5322_address.rl +72 -0
  127. data/lib/mail/parsers/{ragel/date_time.rl → rfc5322_date_time.rl} +8 -1
  128. data/lib/mail/parsers/rfc5322_lexical_tokens.rl +60 -0
  129. data/lib/mail/parsers.rb +12 -25
  130. data/lib/mail/part.rb +11 -12
  131. data/lib/mail/parts_list.rb +88 -14
  132. data/lib/mail/smtp_envelope.rb +57 -0
  133. data/lib/mail/utilities.rb +377 -40
  134. data/lib/mail/values/unicode_tables.dat +0 -0
  135. data/lib/mail/version.rb +8 -15
  136. data/lib/mail/yaml.rb +30 -0
  137. data/lib/mail.rb +9 -32
  138. metadata +138 -94
  139. data/CHANGELOG.rdoc +0 -752
  140. data/CONTRIBUTING.md +0 -60
  141. data/Dependencies.txt +0 -2
  142. data/Gemfile +0 -15
  143. data/Rakefile +0 -29
  144. data/TODO.rdoc +0 -9
  145. data/VERSION +0 -4
  146. data/lib/mail/core_extensions/nil.rb +0 -19
  147. data/lib/mail/core_extensions/object.rb +0 -13
  148. data/lib/mail/core_extensions/smtp.rb +0 -24
  149. data/lib/mail/core_extensions/string/access.rb +0 -145
  150. data/lib/mail/core_extensions/string/multibyte.rb +0 -78
  151. data/lib/mail/core_extensions/string.rb +0 -43
  152. data/lib/mail/fields/common/address_container.rb +0 -16
  153. data/lib/mail/fields/common/common_address.rb +0 -135
  154. data/lib/mail/fields/common/common_date.rb +0 -35
  155. data/lib/mail/fields/common/common_field.rb +0 -57
  156. data/lib/mail/fields/common/common_message_id.rb +0 -48
  157. data/lib/mail/multibyte/exceptions.rb +0 -8
  158. data/lib/mail/parsers/ragel/common.rl +0 -184
  159. data/lib/mail/parsers/ragel/parser_info.rb +0 -61
  160. data/lib/mail/parsers/ragel/ruby/machines/address_lists_machine.rb +0 -14864
  161. data/lib/mail/parsers/ragel/ruby/machines/address_lists_machine.rb.rl +0 -37
  162. data/lib/mail/parsers/ragel/ruby/machines/content_disposition_machine.rb +0 -751
  163. data/lib/mail/parsers/ragel/ruby/machines/content_disposition_machine.rb.rl +0 -37
  164. data/lib/mail/parsers/ragel/ruby/machines/content_location_machine.rb +0 -614
  165. data/lib/mail/parsers/ragel/ruby/machines/content_location_machine.rb.rl +0 -37
  166. data/lib/mail/parsers/ragel/ruby/machines/content_transfer_encoding_machine.rb +0 -447
  167. data/lib/mail/parsers/ragel/ruby/machines/content_transfer_encoding_machine.rb.rl +0 -37
  168. data/lib/mail/parsers/ragel/ruby/machines/content_type_machine.rb +0 -825
  169. data/lib/mail/parsers/ragel/ruby/machines/content_type_machine.rb.rl +0 -37
  170. data/lib/mail/parsers/ragel/ruby/machines/date_time_machine.rb +0 -817
  171. data/lib/mail/parsers/ragel/ruby/machines/date_time_machine.rb.rl +0 -37
  172. data/lib/mail/parsers/ragel/ruby/machines/envelope_from_machine.rb +0 -2129
  173. data/lib/mail/parsers/ragel/ruby/machines/envelope_from_machine.rb.rl +0 -37
  174. data/lib/mail/parsers/ragel/ruby/machines/message_ids_machine.rb +0 -1570
  175. data/lib/mail/parsers/ragel/ruby/machines/message_ids_machine.rb.rl +0 -37
  176. data/lib/mail/parsers/ragel/ruby/machines/mime_version_machine.rb +0 -440
  177. data/lib/mail/parsers/ragel/ruby/machines/mime_version_machine.rb.rl +0 -37
  178. data/lib/mail/parsers/ragel/ruby/machines/phrase_lists_machine.rb +0 -564
  179. data/lib/mail/parsers/ragel/ruby/machines/phrase_lists_machine.rb.rl +0 -37
  180. data/lib/mail/parsers/ragel/ruby/machines/rb_actions.rl +0 -51
  181. data/lib/mail/parsers/ragel/ruby/machines/received_machine.rb +0 -5144
  182. data/lib/mail/parsers/ragel/ruby/machines/received_machine.rb.rl +0 -37
  183. data/lib/mail/parsers/ragel/ruby/parser.rb.rl.erb +0 -37
  184. data/lib/mail/parsers/ragel/ruby.rb +0 -39
  185. data/lib/mail/parsers/ragel.rb +0 -17
  186. data/lib/mail/patterns.rb +0 -37
  187. data/lib/mail/version_specific/ruby_1_8.rb +0 -119
  188. data/lib/mail/version_specific/ruby_1_9.rb +0 -159
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  module Mail
4
5
  # Raised when attempting to decode an unknown encoding type
@@ -6,8 +7,7 @@ module Mail
6
7
  end
7
8
 
8
9
  module Encodings
9
-
10
- include Mail::Patterns
10
+ include Mail::Constants
11
11
  extend Mail::Utilities
12
12
 
13
13
  @transfer_encodings = {}
@@ -18,7 +18,7 @@ module Mail
18
18
  #
19
19
  # Encodings.register "base64", Mail::Encodings::Base64
20
20
  def Encodings.register(name, cls)
21
- @transfer_encodings[get_name(name)] = cls
21
+ @transfer_encodings[get_name(name)] = cls
22
22
  end
23
23
 
24
24
  # Is the encoding we want defined?
@@ -26,8 +26,8 @@ module Mail
26
26
  # Example:
27
27
  #
28
28
  # Encodings.defined?(:base64) #=> true
29
- def Encodings.defined?( str )
30
- @transfer_encodings.include? get_name(str)
29
+ def Encodings.defined?(name)
30
+ @transfer_encodings.include? get_name(name)
31
31
  end
32
32
 
33
33
  # Gets a defined encoding type, QuotedPrintable or Base64 for now.
@@ -38,16 +38,24 @@ module Mail
38
38
  # Example:
39
39
  #
40
40
  # Encodings.get_encoding(:base64) #=> Mail::Encodings::Base64
41
- def Encodings.get_encoding( str )
42
- @transfer_encodings[get_name(str)]
41
+ def Encodings.get_encoding(name)
42
+ @transfer_encodings[get_name(name)]
43
43
  end
44
44
 
45
45
  def Encodings.get_all
46
46
  @transfer_encodings.values
47
47
  end
48
48
 
49
- def Encodings.get_name(enc)
50
- enc = enc.to_s.gsub("-", "_").downcase
49
+ def Encodings.get_name(name)
50
+ underscoreize(name).downcase
51
+ end
52
+
53
+ def Encodings.transcode_charset(str, from_charset, to_charset = 'UTF-8')
54
+ if from_charset
55
+ Utilities.transcode_charset str, from_charset, to_charset
56
+ else
57
+ str
58
+ end
51
59
  end
52
60
 
53
61
  # Encodes a parameter value using URI Escaping, note the language field 'en' can
@@ -57,8 +65,7 @@ module Mail
57
65
  # param_encode_language 'jp'
58
66
  # end
59
67
  #
60
- # The character set used for encoding will either be the value of $KCODE for
61
- # Ruby < 1.9 or the encoding on the string passed in.
68
+ # The character set used for encoding will be the encoding on the string passed in.
62
69
  #
63
70
  # Example:
64
71
  #
@@ -70,7 +77,7 @@ module Mail
70
77
  when str.ascii_only?
71
78
  str
72
79
  else
73
- RubyVer.param_encode(str)
80
+ Utilities.param_encode(str)
74
81
  end
75
82
  end
76
83
 
@@ -84,15 +91,15 @@ module Mail
84
91
  # str.encoding #=> 'ISO-8859-1' ## Only on Ruby 1.9
85
92
  # str #=> "This is fun"
86
93
  def Encodings.param_decode(str, encoding)
87
- RubyVer.param_decode(str, encoding)
94
+ Utilities.param_decode(str, encoding)
88
95
  end
89
96
 
90
97
  # Decodes or encodes a string as needed for either Base64 or QP encoding types in
91
98
  # the =?<encoding>?[QB]?<string>?=" format.
92
99
  #
93
100
  # The output type needs to be :decode to decode the input string or :encode to
94
- # encode the input string. The character set used for encoding will either be
95
- # the value of $KCODE for Ruby < 1.9 or the encoding on the string passed in.
101
+ # encode the input string. The character set used for encoding will be the
102
+ # encoding on the string passed in.
96
103
  #
97
104
  # On encoding, will only send out Base64 encoded strings.
98
105
  def Encodings.decode_encode(str, output_type)
@@ -103,7 +110,7 @@ module Mail
103
110
  if str.ascii_only?
104
111
  str
105
112
  else
106
- Encodings.b_value_encode(str, find_encoding(str))
113
+ Encodings.b_value_encode(str, str.encoding)
107
114
  end
108
115
  end
109
116
  end
@@ -114,34 +121,19 @@ module Mail
114
121
  # String has to be of the format =?<encoding>?[QB]?<string>?=
115
122
  def Encodings.value_decode(str)
116
123
  # Optimization: If there's no encoded-words in the string, just return it
117
- return str unless str =~ /\=\?[^?]+\?[QB]\?[^?]+?\?\=/xmi
124
+ return str unless str =~ ENCODED_VALUE
118
125
 
119
126
  lines = collapse_adjacent_encodings(str)
120
127
 
121
128
  # Split on white-space boundaries with capture, so we capture the white-space as well
122
- lines.map do |line|
123
- line.split(/([ \t])/).map do |text|
124
- if text.index('=?').nil?
125
- text
126
- else
127
- # Search for occurences of quoted strings or plain strings
128
- text.scan(/( # Group around entire regex to include it in matches
129
- \=\?[^?]+\?([QB])\?[^?]+?\?\= # Quoted String with subgroup for encoding method
130
- | # or
131
- .+?(?=\=\?|$) # Plain String
132
- )/xmi).map do |matches|
133
- string, method = *matches
134
- if method == 'b' || method == 'B'
135
- b_value_decode(string)
136
- elsif method == 'q' || method == 'Q'
137
- q_value_decode(string)
138
- else
139
- string
140
- end
141
- end
129
+ lines.each do |line|
130
+ line.gsub!(ENCODED_VALUE) do |string|
131
+ case $2
132
+ when *B_VALUES then b_value_decode(string)
133
+ when *Q_VALUES then q_value_decode(string)
142
134
  end
143
135
  end
144
- end.flatten.join("")
136
+ end.join("")
145
137
  end
146
138
 
147
139
  # Takes an encoded string of the format =?<encoding>?[QB]?<string>?=
@@ -152,13 +144,8 @@ module Mail
152
144
  output
153
145
  elsif to_encoding
154
146
  begin
155
- if RUBY_VERSION >= '1.9'
156
- output.encode(to_encoding)
157
- else
158
- require 'iconv'
159
- Iconv.iconv(to_encoding, 'UTF-8', output).first
160
- end
161
- rescue Iconv::IllegalSequence, Iconv::InvalidEncoding, Errno::EINVAL
147
+ output.encode(to_encoding)
148
+ rescue Errno::EINVAL
162
149
  # the 'from' parameter specifies a charset other than what the text
163
150
  # actually is...not much we can do in this case but just return the
164
151
  # unconverted text.
@@ -174,21 +161,21 @@ module Mail
174
161
 
175
162
  def Encodings.address_encode(address, charset = 'utf-8')
176
163
  if address.is_a?(Array)
177
- # loop back through for each element
178
164
  address.compact.map { |a| Encodings.address_encode(a, charset) }.join(", ")
179
- else
180
- # find any word boundary that is not ascii and encode it
181
- encode_non_usascii(address, charset) if address
165
+ elsif address
166
+ encode_non_usascii(address, charset)
182
167
  end
183
168
  end
184
169
 
185
170
  def Encodings.encode_non_usascii(address, charset)
186
171
  return address if address.ascii_only? or charset.nil?
187
- us_ascii = %Q{\x00-\x7f}
188
- # Encode any non usascii strings embedded inside of quotes
189
- address = address.gsub(/(".*?[^#{us_ascii}].*?")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
172
+
173
+ # Encode all strings embedded inside of quotes
174
+ address = address.gsub(/("[^"]*[^\/]")/) { |s| Encodings.b_value_encode(unquote(s), charset) }
175
+
190
176
  # Then loop through all remaining items and encode as needed
191
177
  tokens = address.split(/\s/)
178
+
192
179
  map_with_index(tokens) do |word, i|
193
180
  if word.ascii_only?
194
181
  word
@@ -209,12 +196,15 @@ module Mail
209
196
  #
210
197
  # Encodings.b_value_encode('This is あ string', 'UTF-8')
211
198
  # #=> "=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?="
212
- def Encodings.b_value_encode(encoded_str, encoding = nil)
213
- return encoded_str if encoded_str.to_s.ascii_only?
214
- string, encoding = RubyVer.b_value_encode(encoded_str, encoding)
215
- map_lines(string) do |str|
216
- "=?#{encoding}?B?#{str.chomp}?="
217
- end.join(" ")
199
+ def Encodings.b_value_encode(string, encoding = nil)
200
+ if string.to_s.ascii_only?
201
+ string
202
+ else
203
+ Encodings.each_base64_chunk_byterange(string, 60).map do |chunk|
204
+ str, encoding = Utilities.b_value_encode(chunk, encoding)
205
+ "=?#{encoding}?B?#{str.chomp}?="
206
+ end.join(" ")
207
+ end
218
208
  end
219
209
 
220
210
  # Encode a string with Quoted-Printable Encoding and returns it ready to be inserted
@@ -226,7 +216,7 @@ module Mail
226
216
  # #=> "=?UTF-8?Q?This_is_=E3=81=82_string?="
227
217
  def Encodings.q_value_encode(encoded_str, encoding = nil)
228
218
  return encoded_str if encoded_str.to_s.ascii_only?
229
- string, encoding = RubyVer.q_value_encode(encoded_str, encoding)
219
+ string, encoding = Utilities.q_value_encode(encoded_str, encoding)
230
220
  string.gsub!("=\r\n", '') # We already have limited the string to the length we want
231
221
  map_lines(string) do |str|
232
222
  "=?#{encoding}?Q?#{str.chomp.gsub(/ /, '_')}?="
@@ -242,7 +232,7 @@ module Mail
242
232
  # Encodings.b_value_decode("=?UTF-8?B?VGhpcyBpcyDjgYIgc3RyaW5n?=")
243
233
  # #=> 'This is あ string'
244
234
  def Encodings.b_value_decode(str)
245
- RubyVer.b_value_decode(str)
235
+ Utilities.b_value_decode(str)
246
236
  end
247
237
 
248
238
  # Decodes a Quoted-Printable string from the "=?UTF-8?Q?This_is_=E3=81=82_string?=" format
@@ -252,53 +242,73 @@ module Mail
252
242
  # Encodings.q_value_decode("=?UTF-8?Q?This_is_=E3=81=82_string?=")
253
243
  # #=> 'This is あ string'
254
244
  def Encodings.q_value_decode(str)
255
- RubyVer.q_value_decode(str)
256
- end
257
-
258
- def Encodings.split_encoding_from_string( str )
259
- match = str.match(/\=\?([^?]+)?\?[QB]\?(.+)?\?\=/mi)
260
- if match
261
- match[1]
262
- else
263
- nil
264
- end
265
- end
266
-
267
- def Encodings.find_encoding(str)
268
- RUBY_VERSION >= '1.9' ? str.encoding : $KCODE
245
+ Utilities.q_value_decode(str)
269
246
  end
270
247
 
271
248
  # Gets the encoding type (Q or B) from the string.
272
- def Encodings.split_value_encoding_from_string(str)
273
- match = str.match(/\=\?[^?]+?\?([QB])\?(.+)?\?\=/mi)
274
- if match
275
- match[1]
276
- else
277
- nil
278
- end
249
+ def Encodings.value_encoding_from_string(str)
250
+ str[ENCODED_VALUE, 1]
279
251
  end
280
252
 
281
- # When the encoded string consists of multiple lines, lines with the same
282
- # encoding (Q or B) can be joined together.
253
+ # Split header line into proper encoded and unencoded parts.
283
254
  #
284
255
  # String has to be of the format =?<encoding>?[QB]?<string>?=
256
+ #
257
+ # Omit unencoded space after an encoded-word.
285
258
  def Encodings.collapse_adjacent_encodings(str)
286
- lines = str.split(/(\?=)\s*(=\?)/).each_slice(2).map(&:join)
287
259
  results = []
288
- previous_encoding = nil
260
+ last_encoded = nil # Track whether to preserve or drop whitespace
289
261
 
290
- lines.each do |line|
291
- encoding = split_value_encoding_from_string(line)
262
+ lines = str.split(FULL_ENCODED_VALUE)
263
+ lines.each_slice(2) do |unencoded, encoded|
264
+ if last_encoded = encoded
265
+ if !Utilities.blank?(unencoded) || (!last_encoded && unencoded != EMPTY)
266
+ results << unencoded
267
+ end
292
268
 
293
- if encoding == previous_encoding
294
- line = results.pop + line
269
+ results << encoded
270
+ else
271
+ results << unencoded
295
272
  end
296
-
297
- previous_encoding = encoding
298
- results << line
299
273
  end
300
274
 
301
275
  results
302
276
  end
277
+
278
+ # Partition the string into bounded-size chunks without splitting
279
+ # multibyte characters.
280
+ def Encodings.each_base64_chunk_byterange(str, max_bytesize_per_base64_chunk, &block)
281
+ raise "size per chunk must be multiple of 4" if (max_bytesize_per_base64_chunk % 4).nonzero?
282
+
283
+ if block_given?
284
+ max_bytesize = ((3 * max_bytesize_per_base64_chunk) / 4.0).floor
285
+ each_chunk_byterange(str, max_bytesize, &block)
286
+ else
287
+ enum_for :each_base64_chunk_byterange, str, max_bytesize_per_base64_chunk
288
+ end
289
+ end
290
+
291
+ # Partition the string into bounded-size chunks without splitting
292
+ # multibyte characters.
293
+ def Encodings.each_chunk_byterange(str, max_bytesize_per_chunk)
294
+ return enum_for(:each_chunk_byterange, str, max_bytesize_per_chunk) unless block_given?
295
+
296
+ offset = 0
297
+ chunksize = 0
298
+
299
+ str.each_char do |chr|
300
+ charsize = chr.bytesize
301
+
302
+ if chunksize + charsize > max_bytesize_per_chunk
303
+ yield Utilities.string_byteslice(str, offset, chunksize)
304
+ offset += chunksize
305
+ chunksize = charsize
306
+ else
307
+ chunksize += charsize
308
+ end
309
+ end
310
+
311
+ yield Utilities.string_byteslice(str, offset, chunksize)
312
+ end
303
313
  end
304
314
  end
data/lib/mail/envelope.rb CHANGED
@@ -1,30 +1,28 @@
1
1
  # encoding: utf-8
2
- #
2
+ # frozen_string_literal: true
3
+ #
3
4
  # = Mail Envelope
4
- #
5
+ #
5
6
  # The Envelope class provides a field for the first line in an
6
7
  # mbox file, that looks like "From mikel@test.lindsaar.net DATETIME"
7
- #
8
+ #
8
9
  # This envelope class reads that line, and turns it into an
9
10
  # Envelope.from and Envelope.date for your use.
11
+
10
12
  module Mail
11
- class Envelope < StructuredField
12
-
13
- def initialize(*args)
14
- super(FIELD_NAME, strip_field(FIELD_NAME, args.last))
15
- end
16
-
13
+ class Envelope < NamedStructuredField
14
+ NAME = 'Envelope-From'
15
+
17
16
  def element
18
17
  @element ||= Mail::EnvelopeFromElement.new(value)
19
18
  end
20
-
21
- def date
22
- ::DateTime.parse("#{element.date_time}")
23
- end
24
19
 
25
20
  def from
26
21
  element.address
27
22
  end
28
-
23
+
24
+ def date
25
+ element.date_time
26
+ end
29
27
  end
30
28
  end
data/lib/mail/field.rb CHANGED
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'mail/fields'
3
+ require 'mail/constants'
2
4
 
3
5
  # encoding: utf-8
4
6
  module Mail
@@ -21,8 +23,6 @@ module Mail
21
23
  # sections 3 and 4 of this standard.
22
24
  #
23
25
  class Field
24
-
25
- include Utilities
26
26
  include Comparable
27
27
 
28
28
  STRUCTURED_FIELDS = %w[ bcc cc content-description content-disposition
@@ -68,7 +68,7 @@ module Mail
68
68
  }
69
69
 
70
70
  FIELD_NAME_MAP = FIELDS_MAP.inject({}) do |map, (field, field_klass)|
71
- map.update(field => field_klass::CAPITALIZED_FIELD)
71
+ map.update(field => field_klass::NAME)
72
72
  end
73
73
 
74
74
  # Generic Field Exception
@@ -82,9 +82,31 @@ module Mail
82
82
 
83
83
  def initialize(element, value, reason)
84
84
  @element = element
85
- @value = value
86
- @reason = reason
87
- super("#{element} can not parse |#{value}|\nReason was: #{reason}")
85
+ @value = to_utf8(value)
86
+ @reason = to_utf8(reason)
87
+ super("#{@element} can not parse |#{@value}|: #{@reason}")
88
+ end
89
+
90
+ private
91
+ def to_utf8(text)
92
+ if text.respond_to?(:force_encoding)
93
+ text.dup.force_encoding(Encoding::UTF_8)
94
+ else
95
+ text
96
+ end
97
+ end
98
+ end
99
+
100
+ class NilParseError < ParseError #:nodoc:
101
+ def initialize(element)
102
+ super element, nil, 'nil is invalid'
103
+ end
104
+ end
105
+
106
+ class IncompleteParseError < ParseError #:nodoc:
107
+ def initialize(element, original_text, unparsed_index)
108
+ parsed_text = to_utf8(original_text[0...unparsed_index])
109
+ super element, original_text, "Only able to parse up to #{parsed_text.inspect}"
88
110
  end
89
111
  end
90
112
 
@@ -92,53 +114,77 @@ module Mail
92
114
  class SyntaxError < FieldError #:nodoc:
93
115
  end
94
116
 
95
- # Accepts a string:
96
- #
97
- # Field.new("field-name: field data")
98
- #
99
- # Or name, value pair:
100
- #
101
- # Field.new("field-name", "value")
117
+ class << self
118
+ # Parse a field from a raw header line:
119
+ #
120
+ # Mail::Field.parse("field-name: field data")
121
+ # # => #<Mail::Field …>
122
+ def parse(field, charset = 'utf-8')
123
+ name, value = split(field)
124
+ if name && value
125
+ new name, value, charset
126
+ end
127
+ end
128
+
129
+ def split(raw_field) #:nodoc:
130
+ if raw_field.index(Constants::COLON)
131
+ name, value = raw_field.split(Constants::COLON, 2)
132
+ name.rstrip!
133
+ if name =~ /\A#{Constants::FIELD_NAME}\z/
134
+ [ name.rstrip, value.strip ]
135
+ else
136
+ Kernel.warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: invalid header name syntax: #{name.inspect}"
137
+ nil
138
+ end
139
+ else
140
+ raw_field.strip
141
+ end
142
+ rescue => error
143
+ warn "WARNING: Ignoring unparsable header #{raw_field.inspect}: #{error.class}: #{error.message}"
144
+ nil
145
+ end
146
+
147
+ def field_class_for(name) #:nodoc:
148
+ FIELDS_MAP[name.to_s.downcase]
149
+ end
150
+ end
151
+
152
+ attr_reader :unparsed_value
153
+
154
+ # Create a field by name and optional value:
102
155
  #
103
- # Or a name by itself:
156
+ # Mail::Field.new("field-name", "value")
157
+ # # => #<Mail::Field …>
104
158
  #
105
- # Field.new("field-name")
159
+ # Values that aren't strings or arrays are coerced to Strings with `#to_s`.
106
160
  #
107
- # Note, does not want a terminating carriage return. Returns
108
- # self appropriately parsed. If value is not a string, then
109
- # it will be passed through as is, for example, content-type
110
- # field can accept an array with the type and a hash of
111
- # parameters:
161
+ # Mail::Field.new("field-name", 1234)
162
+ # # => #<Mail::Field …>
112
163
  #
113
- # Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
164
+ # Mail::Field.new('content-type', ['text', 'plain', {:charset => 'UTF-8'}])
165
+ # # => #<Mail::Field …>
114
166
  def initialize(name, value = nil, charset = 'utf-8')
115
167
  case
116
- when name =~ /:/ # Field.new("field-name: field data")
117
- @charset = value.blank? ? charset : value
118
- @name = name[FIELD_PREFIX]
119
- @raw_value = name
120
- @value = nil
121
- when name !~ /:/ && value.blank? # Field.new("field-name")
168
+ when name.index(Constants::COLON)
169
+ raise ArgumentError, 'Passing an unparsed header field to Mail::Field.new is not supported in Mail 2.8.0+. Use Mail::Field.parse instead.'
170
+ when Utilities.blank?(value)
122
171
  @name = name
123
- @value = nil
124
- @raw_value = nil
172
+ @unparsed_value = nil
125
173
  @charset = charset
126
- else # Field.new("field-name", "value")
174
+ else
127
175
  @name = name
128
- @value = value
129
- @raw_value = nil
176
+ @unparsed_value = value
130
177
  @charset = charset
131
178
  end
132
179
  @name = FIELD_NAME_MAP[@name.to_s.downcase] || @name
133
180
  end
134
181
 
135
- def field=(value)
136
- @field = value
182
+ def field=(field)
183
+ @field = field
137
184
  end
138
185
 
139
186
  def field
140
- _, @value = split(@raw_value) if @raw_value && !@value
141
- @field ||= create_field(@name, @value, @charset)
187
+ @field ||= create_field(@name, @unparsed_value, @charset)
142
188
  end
143
189
 
144
190
  def name
@@ -163,50 +209,63 @@ module Mail
163
209
  end.join(" ")}>"
164
210
  end
165
211
 
166
- def update(name, value)
167
- @field = create_field(name, value, @charset)
212
+ def same(other)
213
+ other.kind_of?(self.class) && Utilities.match_to_s(other.name, name)
168
214
  end
169
215
 
170
- def same( other )
171
- match_to_s(other.name, self.name)
216
+ def ==(other)
217
+ same(other) && Utilities.match_to_s(other.value, value)
172
218
  end
173
219
 
174
- def responsible_for?( val )
175
- name.to_s.casecmp(val.to_s) == 0
220
+ def responsible_for?(field_name)
221
+ name.to_s.casecmp(field_name.to_s) == 0
176
222
  end
177
223
 
178
- alias_method :==, :same
179
-
180
- def <=>( other )
181
- self.field_order_id <=> other.field_order_id
224
+ def <=>(other)
225
+ field_order_id <=> other.field_order_id
182
226
  end
183
227
 
184
228
  def field_order_id
185
- @field_order_id ||= (FIELD_ORDER_LOOKUP[self.name.to_s.downcase] || 100)
229
+ @field_order_id ||= FIELD_ORDER_LOOKUP.fetch(self.name.to_s.downcase, 100)
186
230
  end
187
231
 
188
232
  def method_missing(name, *args, &block)
189
233
  field.send(name, *args, &block)
190
234
  end
191
235
 
192
- FIELD_ORDER = %w[ return-path received
193
- resent-date resent-from resent-sender resent-to
194
- resent-cc resent-bcc resent-message-id
195
- date from sender reply-to to cc bcc
196
- message-id in-reply-to references
197
- subject comments keywords
198
- mime-version content-type content-transfer-encoding
199
- content-location content-disposition content-description ]
236
+ def respond_to_missing?(method_name, include_private)
237
+ field.respond_to?(method_name, include_private) || super
238
+ end
200
239
 
201
- FIELD_ORDER_LOOKUP = Hash[FIELD_ORDER.each_with_index.to_a]
240
+ FIELD_ORDER_LOOKUP = Hash[%w[
241
+ return-path received
242
+ resent-date resent-from resent-sender resent-to
243
+ resent-cc resent-bcc resent-message-id
244
+ date from sender reply-to to cc bcc
245
+ message-id in-reply-to references
246
+ subject comments keywords
247
+ mime-version content-type content-transfer-encoding
248
+ content-location content-disposition content-description
249
+ ].each_with_index.to_a]
202
250
 
203
251
  private
204
252
 
205
- def split(raw_field)
206
- match_data = raw_field.mb_chars.match(FIELD_SPLIT)
207
- [match_data[1].to_s.mb_chars.strip, match_data[2].to_s.mb_chars.strip.to_s]
208
- rescue
209
- STDERR.puts "WARNING: Could not parse (and so ignoring) '#{raw_field}'"
253
+ def create_field(name, value, charset)
254
+ parse_field(name, value, charset)
255
+ rescue Mail::Field::ParseError => e
256
+ field = Mail::UnstructuredField.new(name, value)
257
+ field.errors << [name, value, e]
258
+ field
259
+ end
260
+
261
+ def parse_field(name, value, charset)
262
+ value = unfold(value) if value.is_a?(String)
263
+
264
+ if klass = self.class.field_class_for(name)
265
+ klass.parse(value, charset)
266
+ else
267
+ OptionalField.parse(name, value, charset)
268
+ end
210
269
  end
211
270
 
212
271
  # 2.2.3. Long Header Fields
@@ -218,30 +277,7 @@ module Mail
218
277
  # treated in its unfolded form for further syntactic and semantic
219
278
  # evaluation.
220
279
  def unfold(string)
221
- string.gsub(/[\r\n \t]+/m, ' ')
222
- end
223
-
224
- def create_field(name, value, charset)
225
- value = unfold(value) if value.is_a?(String)
226
-
227
- begin
228
- new_field(name, value, charset)
229
- rescue Mail::Field::ParseError => e
230
- field = Mail::UnstructuredField.new(name, value)
231
- field.errors << [name, value, e]
232
- field
233
- end
234
- end
235
-
236
- def new_field(name, value, charset)
237
- lower_case_name = name.to_s.downcase
238
- if field_klass = FIELDS_MAP[lower_case_name]
239
- field_klass.new(value, charset)
240
- else
241
- OptionalField.new(name, value, charset)
242
- end
280
+ string.gsub(Constants::UNFOLD_WS, '\1')
243
281
  end
244
-
245
282
  end
246
-
247
283
  end