net-imap 0.5.14 → 0.6.0

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.
@@ -155,57 +155,106 @@ module Net
155
155
 
156
156
  # Common validators of number and nz_number types
157
157
  module NumValidator # :nodoc
158
+ NUMBER_RE = /\A(?:0|[1-9]\d*)\z/
158
159
  module_function
159
160
 
160
- # Check is passed argument valid 'number' in RFC 3501 terminology
161
+ # Check if argument is a valid 'number' according to RFC 3501
162
+ # number = 1*DIGIT
163
+ # ; Unsigned 32-bit integer
164
+ # ; (0 <= n < 4,294,967,296)
161
165
  def valid_number?(num)
162
- # [RFC 3501]
163
- # number = 1*DIGIT
164
- # ; Unsigned 32-bit integer
165
- # ; (0 <= n < 4,294,967,296)
166
- num >= 0 && num < 4294967296
166
+ 0 <= num && num <= 0xffff_ffff
167
167
  end
168
168
 
169
- # Check is passed argument valid 'nz_number' in RFC 3501 terminology
169
+ # Check if argument is a valid 'nz-number' according to RFC 3501
170
+ # nz-number = digit-nz *DIGIT
171
+ # ; Non-zero unsigned 32-bit integer
172
+ # ; (0 < n < 4,294,967,296)
170
173
  def valid_nz_number?(num)
171
- # [RFC 3501]
172
- # nz-number = digit-nz *DIGIT
173
- # ; Non-zero unsigned 32-bit integer
174
- # ; (0 < n < 4,294,967,296)
175
- num != 0 && valid_number?(num)
174
+ 0 < num && num <= 0xffff_ffff
176
175
  end
177
176
 
178
- # Check is passed argument valid 'mod_sequence_value' in RFC 4551 terminology
177
+ # Check if argument is a valid 'mod-sequence-value' according to RFC 4551
178
+ # mod-sequence-value = 1*DIGIT
179
+ # ; Positive unsigned 64-bit integer
180
+ # ; (mod-sequence)
181
+ # ; (1 <= n < 18,446,744,073,709,551,615)
179
182
  def valid_mod_sequence_value?(num)
180
- # mod-sequence-value = 1*DIGIT
181
- # ; Positive unsigned 64-bit integer
182
- # ; (mod-sequence)
183
- # ; (1 <= n < 18,446,744,073,709,551,615)
184
- num >= 1 && num < 18446744073709551615
183
+ 1 <= num && num < 0xffff_ffff_ffff_ffff
184
+ end
185
+
186
+ # Check if argument is a valid 'mod-sequence-valzer' according to RFC 4551
187
+ # mod-sequence-valzer = "0" / mod-sequence-value
188
+ def valid_mod_sequence_valzer?(num)
189
+ 0 <= num && num < 0xffff_ffff_ffff_ffff
185
190
  end
186
191
 
187
192
  # Ensure argument is 'number' or raise DataFormatError
188
193
  def ensure_number(num)
189
194
  return num if valid_number?(num)
190
-
191
- msg = "number must be unsigned 32-bit integer: #{num}"
192
- raise DataFormatError, msg
195
+ raise DataFormatError,
196
+ "number must be unsigned 32-bit integer: #{num}"
193
197
  end
194
198
 
195
- # Ensure argument is 'nz_number' or raise DataFormatError
199
+ # Ensure argument is 'nz-number' or raise DataFormatError
196
200
  def ensure_nz_number(num)
197
201
  return num if valid_nz_number?(num)
198
-
199
- msg = "nz_number must be non-zero unsigned 32-bit integer: #{num}"
200
- raise DataFormatError, msg
202
+ raise DataFormatError,
203
+ "nz-number must be non-zero unsigned 32-bit integer: #{num}"
201
204
  end
202
205
 
203
- # Ensure argument is 'mod_sequence_value' or raise DataFormatError
206
+ # Ensure argument is 'mod-sequence-value' or raise DataFormatError
204
207
  def ensure_mod_sequence_value(num)
205
208
  return num if valid_mod_sequence_value?(num)
209
+ raise DataFormatError,
210
+ "mod-sequence-value must be non-zero unsigned 64-bit integer: #{num}"
211
+ end
212
+
213
+ # Ensure argument is 'mod-sequence-valzer' or raise DataFormatError
214
+ def ensure_mod_sequence_valzer(num)
215
+ return num if valid_mod_sequence_valzer?(num)
216
+ raise DataFormatError,
217
+ "mod-sequence-valzer must be unsigned 64-bit integer: #{num}"
218
+ end
219
+
220
+ # Like #ensure_number, but usable with numeric String input.
221
+ def coerce_number(num)
222
+ case num
223
+ when Integer then ensure_number num
224
+ when NUMBER_RE then ensure_number Integer num
225
+ else
226
+ raise DataFormatError, "%p is not a valid number" % [num]
227
+ end
228
+ end
206
229
 
207
- msg = "mod_sequence_value must be unsigned 64-bit integer: #{num}"
208
- raise DataFormatError, msg
230
+ # Like #ensure_nz_number, but usable with numeric String input.
231
+ def coerce_nz_number(num)
232
+ case num
233
+ when Integer then ensure_nz_number num
234
+ when NUMBER_RE then ensure_nz_number Integer num
235
+ else
236
+ raise DataFormatError, "%p is not a valid nz-number" % [num]
237
+ end
238
+ end
239
+
240
+ # Like #ensure_mod_sequence_value, but usable with numeric String input.
241
+ def coerce_mod_sequence_value(num)
242
+ case num
243
+ when Integer then ensure_mod_sequence_value num
244
+ when NUMBER_RE then ensure_mod_sequence_value Integer num
245
+ else
246
+ raise DataFormatError, "%p is not a valid mod-sequence-value" % [num]
247
+ end
248
+ end
249
+
250
+ # Like #ensure_mod_sequence_valzer, but usable with numeric String input.
251
+ def coerce_mod_sequence_valzer(num)
252
+ case num
253
+ when Integer then ensure_mod_sequence_valzer num
254
+ when NUMBER_RE then ensure_mod_sequence_valzer Integer num
255
+ else
256
+ raise DataFormatError, "%p is not a valid mod-sequence-valzer" % [num]
257
+ end
209
258
  end
210
259
 
211
260
  end
@@ -25,6 +25,12 @@ module Net
25
25
  # Some search extensions may result in the server sending ESearchResult
26
26
  # responses after the initiating command has completed. Use
27
27
  # IMAP#add_response_handler to handle these responses.
28
+ #
29
+ # ==== Compatibility with SearchResult
30
+ #
31
+ # Note that both SearchResult and ESearchResult implement +each+, +to_a+,
32
+ # and +to_sequence_set+. These methods can be used regardless of whether
33
+ # the server returns +SEARCH+ or +ESEARCH+ data (or no data).
28
34
  class ESearchResult < Data.define(:tag, :uid, :data)
29
35
  def initialize(tag: nil, uid: nil, data: nil)
30
36
  tag => String | nil; tag = -tag if tag
@@ -6,7 +6,6 @@ module Net
6
6
  autoload :FetchData, "#{__dir__}/fetch_data"
7
7
  autoload :UIDFetchData, "#{__dir__}/fetch_data"
8
8
  autoload :SearchResult, "#{__dir__}/search_result"
9
- autoload :UIDPlusData, "#{__dir__}/uidplus_data"
10
9
  autoload :AppendUIDData, "#{__dir__}/uidplus_data"
11
10
  autoload :CopyUIDData, "#{__dir__}/uidplus_data"
12
11
  autoload :VanishedData, "#{__dir__}/vanished_data"
@@ -260,8 +259,8 @@ module Net
260
259
  #
261
260
  # === +UIDPLUS+ extension
262
261
  # See {[RFC4315 §3]}[https://www.rfc-editor.org/rfc/rfc4315#section-3].
263
- # * +APPENDUID+, #data is UIDPlusData. See IMAP#append.
264
- # * +COPYUID+, #data is UIDPlusData. See IMAP#copy.
262
+ # * +APPENDUID+, #data is AppendUIDData. See IMAP#append.
263
+ # * +COPYUID+, #data is CopyUIDData. See IMAP#copy.
265
264
  # * +UIDNOTSTICKY+, #data is +nil+. See IMAP#select.
266
265
  #
267
266
  # === +SEARCHRES+ extension
@@ -307,14 +306,6 @@ module Net
307
306
  # because the server doesn't allow deletion of mailboxes with children.
308
307
  # #data is +nil+.
309
308
  #
310
- # === <tt>QUOTA=RES-*</tt> response codes
311
- # See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
312
- # * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
313
- # with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
314
- # the command would put the target mailbox over any quota, and with an
315
- # untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
316
- # be external events). #data is +nil+.
317
- #
318
309
  # === +CONDSTORE+ extension
319
310
  # See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
320
311
  # * +NOMODSEQ+, when selecting a mailbox that does not support
@@ -392,23 +383,14 @@ module Net
392
383
  # and MailboxQuota objects.
393
384
  #
394
385
  # == Required capability
395
- #
396
386
  # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
397
- # or <tt>QUOTA=RES-STORAGE</tt>
398
- # [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
387
+ # capability.
399
388
  class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
400
389
  ##
401
390
  # method: mailbox
402
391
  # :call-seq: mailbox -> string
403
392
  #
404
- # The quota root with the associated quota.
405
- #
406
- # NOTE: this was mistakenly named "mailbox". But the quota root's name may
407
- # differ from the mailbox. A single quota root may cover multiple
408
- # mailboxes, and a single mailbox may be governed by multiple quota roots.
409
-
410
- # The quota root with the associated quota.
411
- alias quota_root mailbox
393
+ # The mailbox with the associated quota.
412
394
 
413
395
  ##
414
396
  # method: usage
@@ -420,7 +402,7 @@ module Net
420
402
  # method: quota
421
403
  # :call-seq: quota -> Integer
422
404
  #
423
- # Storage limit imposed on the mailbox.
405
+ # Quota limit imposed on the mailbox.
424
406
  #
425
407
  end
426
408
 
@@ -2017,24 +2017,19 @@ module Net
2017
2017
  CopyUID(validity, src_uids, dst_uids)
2018
2018
  end
2019
2019
 
2020
- def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
2021
- def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
2022
-
2023
2020
  # TODO: remove this code in the v0.6.0 release
2024
2021
  def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
2025
2022
  return unless config.parser_use_deprecated_uidplus_data
2026
- compact_uid_sets = [src_uids, dst_uids].compact
2027
- count = compact_uid_sets.map { _1.count_with_duplicates }.max
2028
- max = config.parser_max_deprecated_uidplus_data_size
2029
- if count <= max
2030
- src_uids &&= src_uids.each_ordered_number.to_a
2031
- dst_uids = dst_uids.each_ordered_number.to_a
2032
- UIDPlusData.new(validity, src_uids, dst_uids)
2033
- elsif config.parser_use_deprecated_uidplus_data != :up_to_max_size
2034
- parse_error("uid-set is too large: %d > %d", count, max)
2035
- end
2023
+ warn("#{Config}#parser_use_deprecated_uidplus_data is ignored " \
2024
+ "since v0.6.0. Disable this warning by setting " \
2025
+ "config.parser_use_deprecated_uidplus_data = false.",
2026
+ category: :deprecated, uplevel: 9)
2027
+ nil
2036
2028
  end
2037
2029
 
2030
+ def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
2031
+ def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
2032
+
2038
2033
  ADDRESS_REGEXP = /\G
2039
2034
  \( (?: NIL | #{Patterns::QUOTED_rev2} ) # 1: NAME
2040
2035
  \s (?: NIL | #{Patterns::QUOTED_rev2} ) # 2: ROUTE
@@ -8,7 +8,6 @@ module Net
8
8
 
9
9
  def initialize(client, sock)
10
10
  @client, @sock = client, sock
11
- @buff = @literal_size = nil
12
11
  end
13
12
 
14
13
  def read_response_buffer
@@ -16,13 +15,13 @@ module Net
16
15
  catch :eof do
17
16
  while true
18
17
  read_line
19
- break unless literal_size
18
+ break unless (@literal_size = get_literal_size)
20
19
  read_literal
21
20
  end
22
21
  end
23
22
  buff
24
23
  ensure
25
- @buff = @literal_size = nil
24
+ @buff = nil
26
25
  end
27
26
 
28
27
  private
@@ -31,18 +30,13 @@ module Net
31
30
 
32
31
  def bytes_read = buff.bytesize
33
32
  def empty? = buff.empty?
34
- def done? = line_done? && !literal_size
33
+ def done? = line_done? && !get_literal_size
35
34
  def line_done? = buff.end_with?(CRLF)
36
-
37
- def get_literal_size(buff)
38
- buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
39
- end
35
+ def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
40
36
 
41
37
  def read_line
42
- line = (@sock.gets(CRLF, read_limit) or throw :eof)
43
- buff << line
38
+ buff << (@sock.gets(CRLF, read_limit) or throw :eof)
44
39
  max_response_remaining! unless line_done?
45
- @literal_size = get_literal_size(line)
46
40
  end
47
41
 
48
42
  def read_literal
@@ -75,19 +75,13 @@ module Net
75
75
  # * #password ― Password or passphrase associated with this #username.
76
76
  # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
77
77
  # * _optional_ #min_iterations - Overrides the default value (4096).
78
- # * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
79
78
  #
80
79
  # Any other keyword parameters are quietly ignored.
81
- #
82
- # *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
83
- # and maximum iteration counts that are appropriate for their security
84
- # context.
85
80
  def initialize(username_arg = nil, password_arg = nil,
86
81
  authcid: nil, username: nil,
87
82
  authzid: nil,
88
83
  password: nil, secret: nil,
89
84
  min_iterations: 4096, # see both RFC5802 and RFC7677
90
- max_iterations: 2**31 - 1, # max int32
91
85
  cnonce: nil, # must only be set in tests
92
86
  **options)
93
87
  @username = username || username_arg || authcid or
@@ -100,22 +94,7 @@ module Net
100
94
  @min_iterations.positive? or
101
95
  raise ArgumentError, "min_iterations must be positive"
102
96
 
103
- @max_iterations = Integer max_iterations.to_int
104
- @min_iterations <= @max_iterations or
105
- raise ArgumentError, "max_iterations must be more than min_iterations"
106
-
107
97
  @cnonce = cnonce || SecureRandom.base64(32)
108
-
109
- # These attrs are set from the server challenges
110
- @server_first_message = @snonce = @salt = @iterations = nil
111
- @server_error = nil
112
-
113
- # Memoized after @salt and @iterations have been sent.
114
- @salted_password = @client_key = @server_key = nil
115
-
116
- # These values are created and cached in response to server challenges
117
- @client_first_message_bare = nil
118
- @client_final_message_without_proof = nil
119
98
  end
120
99
 
121
100
  # Authentication identity: the identity that matches the #password.
@@ -148,43 +127,8 @@ module Net
148
127
 
149
128
  # The minimal allowed iteration count. Lower #iterations will raise an
150
129
  # Error.
151
- #
152
- # *WARNING:* The default value (4096) is set to match guidance from
153
- # both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
154
- # and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
155
- # {modern recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
156
- # are significantly higher.
157
- #
158
- # It is ultimately the server's responsibility to securely store
159
- # password hashes. While this parameter can alert the user to
160
- # insecure password storage and prevent insecure authentication
161
- # exchange, updating the iteration count generally requires resetting
162
- # the password on the server.
163
130
  attr_reader :min_iterations
164
131
 
165
- # The maximal allowed iteration count. Higher #iterations will raise an
166
- # Error.
167
- #
168
- # As noted in {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
169
- # >>>
170
- # A hostile server can perform a computational denial-of-service
171
- # attack on clients by sending a big iteration count value.
172
- #
173
- # *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
174
- # 32-bit integer. This is large enough for the computation to take
175
- # several minutes, and insufficient protection against hostile servers.
176
- #
177
- # Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
178
- # blocking C function, and cannot be interrupted by +Timeout+ or
179
- # <tt>Thread.raise</tt>. And it keeps the Global VM lock, as of v4.0 of
180
- # the +openssl+ gem, so other ruby threads will not be able to run.
181
- #
182
- # <em>To prevent a denial of service attack,</em> this must be set to a
183
- # safe value, depending on hardware and version of OpenSSL. <em>It is
184
- # the user's responsibility</em> to enforce minimum and maximum
185
- # iteration counts that are appropriate for their security context.
186
- attr_reader :max_iterations
187
-
188
132
  # The client nonce, generated by SecureRandom
189
133
  attr_reader :cnonce
190
134
 
@@ -203,15 +147,6 @@ module Net
203
147
  # Net::IMAP::NoResponseError.
204
148
  attr_reader :server_error
205
149
 
206
- # Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
207
- def salted_password; @salted_password ||= compute_salted { super } end
208
-
209
- # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
210
- def client_key; @client_key ||= compute_salted { super } end
211
-
212
- # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
213
- def server_key; @server_key ||= compute_salted { super } end
214
-
215
150
  # Returns a new OpenSSL::Digest object, set to the appropriate hash
216
151
  # function for the chosen mechanism.
217
152
  #
@@ -251,13 +186,6 @@ module Net
251
186
 
252
187
  private
253
188
 
254
- # Checks for +salt+ and +iterations+ before yielding
255
- def compute_salted
256
- salt in String or raise Error, "unknown salt"
257
- iterations in Integer or raise Error, "unknown iterations"
258
- yield
259
- end
260
-
261
189
  # Need to store this for auth_message
262
190
  attr_reader :server_first_message
263
191
 
@@ -274,8 +202,6 @@ module Net
274
202
  raise Error, "server did not send iteration count"
275
203
  min_iterations <= iterations or
276
204
  raise Error, "too few iterations: %d" % [iterations]
277
- max_iterations.nil? || iterations <= max_iterations or
278
- raise Error, "too many iterations: %d" % [iterations]
279
205
  mext = sparams["m"] and
280
206
  raise Error, "mandatory extension: %p" % [mext]
281
207
  snonce.start_with? cnonce or
@@ -7,6 +7,12 @@ module Net
7
7
  # identifiers returned by Net::IMAP#uid_search.
8
8
  #
9
9
  # For backward compatibility, SearchResult inherits from Array.
10
+ #
11
+ # ==== Compatibility with ESearchResult
12
+ #
13
+ # Note that both SearchResult and ESearchResult implement +each+, +to_a+,
14
+ # and +to_sequence_set+. These methods can be used regardless of whether
15
+ # the server returns +SEARCH+ or +ESEARCH+ data (or no data).
10
16
  class SearchResult < Array
11
17
 
12
18
  # Returns a SearchResult populated with the given +seq_nums+.