net-imap 0.4.18 → 0.4.20

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f71a5476fad0d1bbe6dd52a510fa083ef6b35cb98f27af536a5991ebe9f31244
4
- data.tar.gz: 10082ff774cfccf2b0b43c58cf62bdb99b3e2e75cf7ff7fcb9c508cde8dc33ed
3
+ metadata.gz: b965efa83b72d27d68fe55ce65f08ffeb696e4a4fb44219e6206a4b7dea4de91
4
+ data.tar.gz: 3554d7a749873b2eccec2470142315afddb4a6dd359a1d2cd6cfeacffa27d3f6
5
5
  SHA512:
6
- metadata.gz: c98013c4ad6493bb0e455cced96388e35214567eb0359085e5023beffc9d748472ff729dc70718a3e66ba86a78e5a1d5de803fc68e5012a6da852e57f951398c
7
- data.tar.gz: bd5b766443689dad66eb1caf4479763f8181d38e64b3db7fdf415b277d27721fda9d4ce634e4638ab4f0860e3a82f63a37581830e81a617fe08baaff07262694
6
+ metadata.gz: b97c154cfe1b5ce9020029908ec017f316ef36ec36a4948d637d54990ad422580c99bdb2090c4fac4533a503a9678474753d664a101896164ee729d3abaacf37
7
+ data.tar.gz: 518dc7c466a5200b5945f1b21b8e8bcb4523649a3ef5678811385613834f910f5be9a193fd80dee1a8a029882b62da61c265fd3c942a874bd5cdb43c59d5ccef
@@ -18,6 +18,8 @@ module Net
18
18
  super(attr)
19
19
  AttrTypeCoercion.attr_accessor(attr, type: type)
20
20
  end
21
+
22
+ module_function def Integer?; NilOrInteger end
21
23
  end
22
24
  private_constant :Macros
23
25
 
@@ -26,34 +28,36 @@ module Net
26
28
  end
27
29
  private_class_method :included
28
30
 
29
- def self.attr_accessor(attr, type: nil)
30
- return unless type
31
- if :boolean == type then boolean attr
32
- elsif Integer == type then integer attr
33
- elsif Array === type then enum attr, type
34
- else raise ArgumentError, "unknown type coercion %p" % [type]
35
- end
31
+ if defined?(Ractor.make_shareable)
32
+ def self.safe(...) Ractor.make_shareable nil.instance_eval(...).freeze end
33
+ else
34
+ def self.safe(...) nil.instance_eval(...).freeze end
36
35
  end
36
+ private_class_method :safe
37
37
 
38
- def self.boolean(attr)
39
- define_method :"#{attr}=" do |val| super !!val end
40
- define_method :"#{attr}?" do send attr end
38
+ Types = Hash.new do |h, type|
39
+ type.nil? || Proc === type or raise TypeError, "type not nil or Proc"
40
+ safe{type}
41
41
  end
42
+ Types[:boolean] = Boolean = safe{-> {!!_1}}
43
+ Types[Integer] = safe{->{Integer(_1)}}
42
44
 
43
- def self.integer(attr)
44
- define_method :"#{attr}=" do |val| super Integer val end
45
+ def self.attr_accessor(attr, type: nil)
46
+ type = Types[type] or return
47
+ define_method :"#{attr}=" do |val| super type[val] end
48
+ define_method :"#{attr}?" do send attr end if type == Boolean
45
49
  end
46
50
 
47
- def self.enum(attr, enum)
48
- enum = enum.dup.freeze
51
+ NilOrInteger = safe{->val { Integer val unless val.nil? }}
52
+
53
+ Enum = ->(*enum) {
54
+ enum = safe{enum}
49
55
  expected = -"one of #{enum.map(&:inspect).join(", ")}"
50
- define_method :"#{attr}=" do |val|
51
- unless enum.include?(val)
52
- raise ArgumentError, "expected %s, got %p" % [expected, val]
53
- end
54
- super val
55
- end
56
- end
56
+ safe{->val {
57
+ return val if enum.include?(val)
58
+ raise ArgumentError, "expected %s, got %p" % [expected, val]
59
+ }}
60
+ }
57
61
 
58
62
  end
59
63
  end
@@ -129,8 +129,25 @@ module Net
129
129
  def self.global; @global if defined?(@global) end
130
130
 
131
131
  # A hash of hard-coded configurations, indexed by version number or name.
132
+ # Values can be accessed with any object that responds to +to_sym+ or
133
+ # +to_r+/+to_f+ with a non-zero number.
134
+ #
135
+ # Config::[] gets named or numbered versions from this hash.
136
+ #
137
+ # For example:
138
+ # Net::IMAP::Config.version_defaults[0.5] == Net::IMAP::Config[0.5]
139
+ # Net::IMAP::Config[0.5] == Net::IMAP::Config[0.5r] # => true
140
+ # Net::IMAP::Config["current"] == Net::IMAP::Config[:current] # => true
141
+ # Net::IMAP::Config["0.5.6"] == Net::IMAP::Config[0.5r] # => true
132
142
  def self.version_defaults; @version_defaults end
133
- @version_defaults = {}
143
+ @version_defaults = Hash.new {|h, k|
144
+ # NOTE: String responds to both so the order is significant.
145
+ # And ignore non-numeric conversion to zero, because: "wat!?".to_r == 0
146
+ (h.fetch(k.to_r, nil) || h.fetch(k.to_f, nil) if k.is_a?(Numeric)) ||
147
+ (h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) ||
148
+ (h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) ||
149
+ (h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0)
150
+ }
134
151
 
135
152
  # :call-seq:
136
153
  # Net::IMAP::Config[number] -> versioned config
@@ -153,18 +170,17 @@ module Net
153
170
  elsif config.nil? && global.nil? then nil
154
171
  elsif config.respond_to?(:to_hash) then new(global, **config).freeze
155
172
  else
156
- version_defaults.fetch(config) do
173
+ version_defaults[config] or
157
174
  case config
158
175
  when Numeric
159
176
  raise RangeError, "unknown config version: %p" % [config]
160
- when Symbol
177
+ when String, Symbol
161
178
  raise KeyError, "unknown config name: %p" % [config]
162
179
  else
163
180
  raise TypeError, "no implicit conversion of %s to %s" % [
164
181
  config.class, Config
165
182
  ]
166
183
  end
167
- end
168
184
  end
169
185
  end
170
186
 
@@ -191,10 +207,13 @@ module Net
191
207
 
192
208
  # Seconds to wait until a connection is opened.
193
209
  #
210
+ # Applied separately for establishing TCP connection and starting a TLS
211
+ # connection.
212
+ #
194
213
  # If the IMAP object cannot open a connection within this time,
195
214
  # it raises a Net::OpenTimeout exception.
196
215
  #
197
- # See Net::IMAP.new.
216
+ # See Net::IMAP.new and Net::IMAP#starttls.
198
217
  #
199
218
  # The default value is +30+ seconds.
200
219
  attr_accessor :open_timeout, type: Integer
@@ -223,6 +242,40 @@ module Net
223
242
  # Use +SASL-IR+ when it is supported by the server and the mechanism.
224
243
  attr_accessor :sasl_ir, type: :boolean
225
244
 
245
+ # The maximum allowed server response size. When +nil+, there is no limit
246
+ # on response size.
247
+ #
248
+ # The default value (512 MiB, since +v0.5.7+) is <em>very high</em> and
249
+ # unlikely to be reached. To use a lower limit, fetch message bodies in
250
+ # chunks rather than all at once. A _much_ lower value should be used
251
+ # with untrusted servers (for example, when connecting to a user-provided
252
+ # hostname).
253
+ #
254
+ # <em>Please Note:</em> this only limits the size per response. It does
255
+ # not prevent a flood of individual responses and it does not limit how
256
+ # many unhandled responses may be stored on the responses hash. See
257
+ # Net::IMAP@Unbounded+memory+use.
258
+ #
259
+ # Socket reads are limited to the maximum remaining bytes for the current
260
+ # response: max_response_size minus the bytes that have already been read.
261
+ # When the limit is reached, or reading a +literal+ _would_ go over the
262
+ # limit, ResponseTooLargeError is raised and the connection is closed.
263
+ # See also #socket_read_limit.
264
+ #
265
+ # Note that changes will not take effect immediately, because the receiver
266
+ # thread may already be waiting for the next response using the previous
267
+ # value. Net::IMAP#noop can force a response and enforce the new setting
268
+ # immediately.
269
+ #
270
+ # ==== Versioned Defaults
271
+ #
272
+ # Net::IMAP#max_response_size <em>was added in +v0.2.5+ and +v0.3.9+ as an
273
+ # attr_accessor, and in +v0.4.20+ and +v0.5.7+ as a delegator to this
274
+ # config attribute.</em>
275
+ #
276
+ # * original: +nil+ <em>(no limit)</em>
277
+ # * +0.5+: 512 MiB
278
+ attr_accessor :max_response_size, type: Integer?
226
279
 
227
280
  # Controls the behavior of Net::IMAP#responses when called without any
228
281
  # arguments (+type+ or +block+).
@@ -250,7 +303,7 @@ module Net
250
303
  # Raise an ArgumentError with the deprecation warning.
251
304
  #
252
305
  # Note: #responses_without_args is an alias for #responses_without_block.
253
- attr_accessor :responses_without_block, type: [
306
+ attr_accessor :responses_without_block, type: Enum[
254
307
  :silence_deprecation_warning, :warn, :frozen_dup, :raise,
255
308
  ]
256
309
 
@@ -262,6 +315,67 @@ module Net
262
315
  #
263
316
  # Alias for responses_without_block
264
317
 
318
+ # Whether ResponseParser should use the deprecated UIDPlusData or
319
+ # CopyUIDData for +COPYUID+ response codes, and UIDPlusData or
320
+ # AppendUIDData for +APPENDUID+ response codes.
321
+ #
322
+ # UIDPlusData stores its data in arrays of numbers, which is vulnerable to
323
+ # a memory exhaustion denial of service attack from an untrusted or
324
+ # compromised server. Set this option to +false+ to completely block this
325
+ # vulnerability. Otherwise, parser_max_deprecated_uidplus_data_size
326
+ # mitigates this vulnerability.
327
+ #
328
+ # AppendUIDData and CopyUIDData are _mostly_ backward-compatible with
329
+ # UIDPlusData. Most applications should be able to upgrade with little
330
+ # or no changes.
331
+ #
332
+ # <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
333
+ #
334
+ # <em>(Config option added in +v0.4.19+ and +v0.5.6+.)</em>
335
+ #
336
+ # <em>UIDPlusData will be removed in +v0.6+ and this config setting will
337
+ # be ignored.</em>
338
+ #
339
+ # ==== Valid options
340
+ #
341
+ # [+true+ <em>(original default)</em>]
342
+ # ResponseParser only uses UIDPlusData.
343
+ #
344
+ # [+:up_to_max_size+ <em>(default since +v0.5.6+)</em>]
345
+ # ResponseParser uses UIDPlusData when the +uid-set+ size is below
346
+ # parser_max_deprecated_uidplus_data_size. Above that size,
347
+ # ResponseParser uses AppendUIDData or CopyUIDData.
348
+ #
349
+ # [+false+ <em>(planned default for +v0.6+)</em>]
350
+ # ResponseParser _only_ uses AppendUIDData and CopyUIDData.
351
+ attr_accessor :parser_use_deprecated_uidplus_data, type: Enum[
352
+ true, :up_to_max_size, false
353
+ ]
354
+
355
+ # The maximum +uid-set+ size that ResponseParser will parse into
356
+ # deprecated UIDPlusData. This limit only applies when
357
+ # parser_use_deprecated_uidplus_data is not +false+.
358
+ #
359
+ # <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
360
+ #
361
+ # <em>Support for limiting UIDPlusData to a maximum size was added in
362
+ # +v0.3.8+, +v0.4.19+, and +v0.5.6+.</em>
363
+ #
364
+ # <em>UIDPlusData will be removed in +v0.6+.</em>
365
+ #
366
+ # ==== Versioned Defaults
367
+ #
368
+ # Because this limit guards against a remote server causing catastrophic
369
+ # memory exhaustion, the versioned default (used by #load_defaults) also
370
+ # applies to versions without the feature.
371
+ #
372
+ # * +0.3+ and prior: <tt>10,000</tt>
373
+ # * +0.4+: <tt>1,000</tt>
374
+ # * +0.5+: <tt>100</tt>
375
+ # * +0.6+: <tt>0</tt>
376
+ #
377
+ attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer
378
+
265
379
  # Creates a new config object and initialize its attribute with +attrs+.
266
380
  #
267
381
  # If +parent+ is not given, the global config is used by default.
@@ -340,35 +454,71 @@ module Net
340
454
  open_timeout: 30,
341
455
  idle_response_timeout: 5,
342
456
  sasl_ir: true,
457
+ max_response_size: nil,
343
458
  responses_without_block: :silence_deprecation_warning,
459
+ parser_use_deprecated_uidplus_data: true,
460
+ parser_max_deprecated_uidplus_data_size: 1000,
344
461
  ).freeze
345
462
 
346
463
  @global = default.new
347
464
 
348
- version_defaults[0.4] = Config[default.send(:defaults_hash)]
465
+ version_defaults[:default] = Config[default.send(:defaults_hash)]
349
466
 
350
- version_defaults[0] = Config[0.4].dup.update(
467
+ version_defaults[0r] = Config[:default].dup.update(
351
468
  sasl_ir: false,
469
+ max_response_size: nil,
470
+ parser_use_deprecated_uidplus_data: true,
471
+ parser_max_deprecated_uidplus_data_size: 10_000,
352
472
  ).freeze
353
- version_defaults[0.0] = Config[0]
354
- version_defaults[0.1] = Config[0]
355
- version_defaults[0.2] = Config[0]
356
- version_defaults[0.3] = Config[0]
473
+ version_defaults[0.0r] = Config[0r]
474
+ version_defaults[0.1r] = Config[0r]
475
+ version_defaults[0.2r] = Config[0r]
476
+ version_defaults[0.3r] = Config[0r]
357
477
 
358
- version_defaults[0.5] = Config[0.4].dup.update(
359
- responses_without_block: :warn,
478
+ version_defaults[0.4r] = Config[0.3r].dup.update(
479
+ sasl_ir: true,
480
+ parser_max_deprecated_uidplus_data_size: 1000,
360
481
  ).freeze
361
482
 
362
- version_defaults[:default] = Config[0.4]
363
- version_defaults[:current] = Config[0.4]
364
- version_defaults[:next] = Config[0.5]
483
+ version_defaults[0.5r] = Config[0.4r].dup.update(
484
+ max_response_size: 512 << 20, # 512 MiB
485
+ responses_without_block: :warn,
486
+ parser_use_deprecated_uidplus_data: :up_to_max_size,
487
+ parser_max_deprecated_uidplus_data_size: 100,
488
+ ).freeze
365
489
 
366
- version_defaults[0.6] = Config[0.5].dup.update(
490
+ version_defaults[0.6r] = Config[0.5r].dup.update(
367
491
  responses_without_block: :frozen_dup,
492
+ parser_use_deprecated_uidplus_data: false,
493
+ parser_max_deprecated_uidplus_data_size: 0,
494
+ ).freeze
495
+
496
+ version_defaults[0.7r] = Config[0.6r].dup.update(
368
497
  ).freeze
369
- version_defaults[:future] = Config[0.6]
498
+
499
+ # Safe conversions one way only:
500
+ # 0.6r.to_f == 0.6 # => true
501
+ # 0.6 .to_r == 0.6r # => false
502
+ version_defaults.to_a.each do |k, v|
503
+ next unless k.is_a? Rational
504
+ version_defaults[k.to_f] = v
505
+ end
506
+
507
+ current = VERSION.to_r
508
+ version_defaults[:original] = Config[0]
509
+ version_defaults[:current] = Config[current]
510
+ version_defaults[:next] = Config[current + 0.1r]
511
+
512
+ version_defaults[:future] = Config[0.7r]
370
513
 
371
514
  version_defaults.freeze
515
+
516
+ if ($VERBOSE || $DEBUG) && self[:current].to_h != self[:default].to_h
517
+ warn "Misconfigured Net::IMAP::Config[:current] => %p,\n" \
518
+ " not equal to Net::IMAP::Config[:default] => %p" % [
519
+ self[:current].to_h, self[:default].to_h
520
+ ]
521
+ end
372
522
  end
373
523
  end
374
524
  end
@@ -11,6 +11,39 @@ module Net
11
11
  class DataFormatError < Error
12
12
  end
13
13
 
14
+ # Error raised when the socket cannot be read, due to a Config limit.
15
+ class ResponseReadError < Error
16
+ end
17
+
18
+ # Error raised when a response is larger than IMAP#max_response_size.
19
+ class ResponseTooLargeError < ResponseReadError
20
+ attr_reader :bytes_read, :literal_size
21
+ attr_reader :max_response_size
22
+
23
+ def initialize(msg = nil, *args,
24
+ bytes_read: nil,
25
+ literal_size: nil,
26
+ max_response_size: nil,
27
+ **kwargs)
28
+ @bytes_read = bytes_read
29
+ @literal_size = literal_size
30
+ @max_response_size = max_response_size
31
+ msg ||= [
32
+ "Response size", response_size_msg, "exceeds max_response_size",
33
+ max_response_size && "(#{max_response_size}B)",
34
+ ].compact.join(" ")
35
+ super(msg, *args, **kwargs)
36
+ end
37
+
38
+ private
39
+
40
+ def response_size_msg
41
+ if bytes_read && literal_size
42
+ "(#{bytes_read}B read + #{literal_size}B literal)"
43
+ end
44
+ end
45
+ end
46
+
14
47
  # Error raised when a response from the server is non-parsable.
15
48
  class ResponseParseError < Error
16
49
  end
@@ -5,6 +5,9 @@ module Net
5
5
  autoload :FetchData, "#{__dir__}/fetch_data"
6
6
  autoload :SearchResult, "#{__dir__}/search_result"
7
7
  autoload :SequenceSet, "#{__dir__}/sequence_set"
8
+ autoload :UIDPlusData, "#{__dir__}/uidplus_data"
9
+ autoload :AppendUIDData, "#{__dir__}/uidplus_data"
10
+ autoload :CopyUIDData, "#{__dir__}/uidplus_data"
8
11
 
9
12
  # Net::IMAP::ContinuationRequest represents command continuation requests.
10
13
  #
@@ -324,60 +327,6 @@ module Net
324
327
  # code data can take.
325
328
  end
326
329
 
327
- # Net::IMAP::UIDPlusData represents the ResponseCode#data that accompanies
328
- # the +APPENDUID+ and +COPYUID+ response codes.
329
- #
330
- # See [[UIDPLUS[https://www.rfc-editor.org/rfc/rfc4315.html]].
331
- #
332
- # ==== Capability requirement
333
- #
334
- # The +UIDPLUS+ capability[rdoc-ref:Net::IMAP#capability] must be supported.
335
- # A server that supports +UIDPLUS+ should send a UIDPlusData object inside
336
- # every TaggedResponse returned by the append[rdoc-ref:Net::IMAP#append],
337
- # copy[rdoc-ref:Net::IMAP#copy], move[rdoc-ref:Net::IMAP#move], {uid
338
- # copy}[rdoc-ref:Net::IMAP#uid_copy], and {uid
339
- # move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the destination
340
- # mailbox reports +UIDNOTSTICKY+.
341
- #
342
- #--
343
- # TODO: support MULTIAPPEND
344
- #++
345
- #
346
- class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids)
347
- ##
348
- # method: uidvalidity
349
- # :call-seq: uidvalidity -> nonzero uint32
350
- #
351
- # The UIDVALIDITY of the destination mailbox.
352
-
353
- ##
354
- # method: source_uids
355
- # :call-seq: source_uids -> nil or an array of nonzero uint32
356
- #
357
- # The UIDs of the copied or moved messages.
358
- #
359
- # Note:: Returns +nil+ for Net::IMAP#append.
360
-
361
- ##
362
- # method: assigned_uids
363
- # :call-seq: assigned_uids -> an array of nonzero uint32
364
- #
365
- # The newly assigned UIDs of the copied, moved, or appended messages.
366
- #
367
- # Note:: This always returns an array, even when it contains only one UID.
368
-
369
- ##
370
- # :call-seq: uid_mapping -> nil or a hash
371
- #
372
- # Returns a hash mapping each source UID to the newly assigned destination
373
- # UID.
374
- #
375
- # Note:: Returns +nil+ for Net::IMAP#append.
376
- def uid_mapping
377
- source_uids&.zip(assigned_uids)&.to_h
378
- end
379
- end
380
-
381
330
  # Net::IMAP::MailboxList represents contents of the LIST response,
382
331
  # representing a single mailbox path.
383
332
  #
@@ -13,13 +13,17 @@ module Net
13
13
 
14
14
  attr_reader :config
15
15
 
16
- # :call-seq: Net::IMAP::ResponseParser.new -> Net::IMAP::ResponseParser
16
+ # Creates a new ResponseParser.
17
+ #
18
+ # When +config+ is frozen or global, the parser #config inherits from it.
19
+ # Otherwise, +config+ will be used directly.
17
20
  def initialize(config: Config.global)
18
21
  @str = nil
19
22
  @pos = nil
20
23
  @lex_state = nil
21
24
  @token = nil
22
25
  @config = Config[config]
26
+ @config = @config.new if @config == Config.global || @config.frozen?
23
27
  end
24
28
 
25
29
  # :call-seq:
@@ -1863,11 +1867,10 @@ module Net
1863
1867
  #
1864
1868
  # n.b, uniqueid ⊂ uid-set. To avoid inconsistent return types, we always
1865
1869
  # match uid_set even if that returns a single-member array.
1866
- #
1867
1870
  def resp_code_apnd__data
1868
1871
  validity = number; SP!
1869
1872
  dst_uids = uid_set # uniqueid ⊂ uid-set
1870
- UIDPlusData.new(validity, nil, dst_uids)
1873
+ AppendUID(validity, dst_uids)
1871
1874
  end
1872
1875
 
1873
1876
  # already matched: "COPYUID"
@@ -1877,7 +1880,25 @@ module Net
1877
1880
  validity = number; SP!
1878
1881
  src_uids = uid_set; SP!
1879
1882
  dst_uids = uid_set
1880
- UIDPlusData.new(validity, src_uids, dst_uids)
1883
+ CopyUID(validity, src_uids, dst_uids)
1884
+ end
1885
+
1886
+ def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
1887
+ def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
1888
+
1889
+ # TODO: remove this code in the v0.6.0 release
1890
+ def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
1891
+ return unless config.parser_use_deprecated_uidplus_data
1892
+ compact_uid_sets = [src_uids, dst_uids].compact
1893
+ count = compact_uid_sets.map { _1.count_with_duplicates }.max
1894
+ max = config.parser_max_deprecated_uidplus_data_size
1895
+ if count <= max
1896
+ src_uids &&= src_uids.each_ordered_number.to_a
1897
+ dst_uids = dst_uids.each_ordered_number.to_a
1898
+ UIDPlusData.new(validity, src_uids, dst_uids)
1899
+ elsif config.parser_use_deprecated_uidplus_data != :up_to_max_size
1900
+ parse_error("uid-set is too large: %d > %d", count, max)
1901
+ end
1881
1902
  end
1882
1903
 
1883
1904
  ADDRESS_REGEXP = /\G
@@ -2003,15 +2024,9 @@ module Net
2003
2024
  # uniqueid = nz-number
2004
2025
  # ; Strictly ascending
2005
2026
  def uid_set
2006
- token = match(T_NUMBER, T_ATOM)
2007
- case token.symbol
2008
- when T_NUMBER then [Integer(token.value)]
2009
- when T_ATOM
2010
- token.value.split(",").flat_map {|range|
2011
- range = range.split(":").map {|uniqueid| Integer(uniqueid) }
2012
- range.size == 1 ? range : Range.new(range.min, range.max).to_a
2013
- }
2014
- end
2027
+ set = sequence_set
2028
+ parse_error("uid-set cannot contain '*'") if set.include_star?
2029
+ set
2015
2030
  end
2016
2031
 
2017
2032
  def nil_atom
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ # See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2
6
+ class ResponseReader # :nodoc:
7
+ attr_reader :client
8
+
9
+ def initialize(client, sock)
10
+ @client, @sock = client, sock
11
+ end
12
+
13
+ def read_response_buffer
14
+ @buff = String.new
15
+ catch :eof do
16
+ while true
17
+ read_line
18
+ break unless (@literal_size = get_literal_size)
19
+ read_literal
20
+ end
21
+ end
22
+ buff
23
+ ensure
24
+ @buff = nil
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :buff, :literal_size
30
+
31
+ def bytes_read; buff.bytesize end
32
+ def empty?; buff.empty? end
33
+ def done?; line_done? && !get_literal_size end
34
+ def line_done?; buff.end_with?(CRLF) end
35
+ def get_literal_size; /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i end
36
+
37
+ def read_line
38
+ buff << (@sock.gets(CRLF, read_limit) or throw :eof)
39
+ max_response_remaining! unless line_done?
40
+ end
41
+
42
+ def read_literal
43
+ # check before allocating memory for literal
44
+ max_response_remaining!
45
+ literal = String.new(capacity: literal_size)
46
+ buff << (@sock.read(read_limit(literal_size), literal) or throw :eof)
47
+ ensure
48
+ @literal_size = nil
49
+ end
50
+
51
+ def read_limit(limit = nil)
52
+ [limit, max_response_remaining!].compact.min
53
+ end
54
+
55
+ def max_response_size; client.max_response_size end
56
+ def max_response_remaining; max_response_size &.- bytes_read end
57
+ def response_too_large?; max_response_size &.< min_response_size end
58
+ def min_response_size; bytes_read + min_response_remaining end
59
+
60
+ def min_response_remaining
61
+ empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
62
+ end
63
+
64
+ def max_response_remaining!
65
+ return max_response_remaining unless response_too_large?
66
+ raise ResponseTooLargeError.new(
67
+ max_response_size: max_response_size,
68
+ bytes_read: bytes_read,
69
+ literal_size: literal_size,
70
+ )
71
+ end
72
+
73
+ end
74
+ end
75
+ end