net-imap 0.5.5 → 0.5.7

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: c9b92943c4c4d17210f7374b4f5577f95471038a987540a8cdad2088e6bec25d
4
- data.tar.gz: 6a28ce5ca7778cbfa3de48c3451be4d17fe6298a01e2a946725caf022a34dd8c
3
+ metadata.gz: ee48ac78f129043fd8ccb4f6e0b42535cc0ef3a6acd8ea1067cbd9b512815e49
4
+ data.tar.gz: 5bfae3bf0ee2e63c5b61c19d30187fe722adfe8018746e64d583124531de87a8
5
5
  SHA512:
6
- metadata.gz: 5a575e282b7cd6828d56360003b1de29e8ca0e3d55ce733c8de250f781413dc8e9e6a90549c14b9ed21bb46e0cdc88f3e75d92aeaed769d594f93056359ee40d
7
- data.tar.gz: 87d57464c32eab235e241c74efea7953b9798b9c40a14f66170e751560943f728aabe852d38c746283cdcb4c03914ee375021c49fec67f653113d3330a6732d2
6
+ metadata.gz: f90a4500f3c218dd3a51cca84a76c620fa8f8488f2ca73486b538a43e67e21e0b897d23d957f490afe1590fd755c692f2e0dede562460ed2cfcd7f4d9bec3262
7
+ data.tar.gz: 22abb3cf5699bb715c33c69b596ead46ff400109925f58d633ecdc82797be9b80fbbd040c02aa7cd9286f3aa9c697e9fc426f1115b9019455b2ac4e6667cc236
data/Gemfile CHANGED
@@ -8,6 +8,7 @@ gem "digest"
8
8
  gem "strscan"
9
9
  gem "base64"
10
10
 
11
+ gem "irb"
11
12
  gem "rake"
12
13
  gem "rdoc"
13
14
  gem "test-unit"
@@ -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
21
23
  end
22
24
  private_constant :Macros
23
25
 
@@ -26,34 +28,29 @@ 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
36
- end
31
+ def self.safe(...) = Ractor.make_shareable nil.instance_eval(...).freeze
32
+ private_class_method :safe
37
33
 
38
- def self.boolean(attr)
39
- define_method :"#{attr}=" do |val| super !!val end
40
- define_method :"#{attr}?" do send attr end
41
- end
34
+ Types = Hash.new do |h, type| type => Proc | nil; safe{type} end
35
+ Types[:boolean] = Boolean = safe{-> {!!_1}}
36
+ Types[Integer] = safe{->{Integer(_1)}}
42
37
 
43
- def self.integer(attr)
44
- define_method :"#{attr}=" do |val| super Integer val end
38
+ def self.attr_accessor(attr, type: nil)
39
+ type = Types[type] or return
40
+ define_method :"#{attr}=" do |val| super type[val] end
41
+ define_method :"#{attr}?" do send attr end if type == Boolean
45
42
  end
46
43
 
47
- def self.enum(attr, enum)
48
- enum = enum.dup.freeze
44
+ NilOrInteger = safe{->val { Integer val unless val.nil? }}
45
+
46
+ Enum = ->(*enum) {
47
+ enum = safe{enum}
49
48
  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
49
+ safe{->val {
50
+ return val if enum.include?(val)
51
+ raise ArgumentError, "expected %s, got %p" % [expected, val]
52
+ }}
53
+ }
57
54
 
58
55
  end
59
56
  end
@@ -131,8 +131,25 @@ module Net
131
131
  def self.global; @global if defined?(@global) end
132
132
 
133
133
  # A hash of hard-coded configurations, indexed by version number or name.
134
+ # Values can be accessed with any object that responds to +to_sym+ or
135
+ # +to_r+/+to_f+ with a non-zero number.
136
+ #
137
+ # Config::[] gets named or numbered versions from this hash.
138
+ #
139
+ # For example:
140
+ # Net::IMAP::Config.version_defaults[0.5] == Net::IMAP::Config[0.5]
141
+ # Net::IMAP::Config[0.5] == Net::IMAP::Config[0.5r] # => true
142
+ # Net::IMAP::Config["current"] == Net::IMAP::Config[:current] # => true
143
+ # Net::IMAP::Config["0.5.6"] == Net::IMAP::Config[0.5r] # => true
134
144
  def self.version_defaults; @version_defaults end
135
- @version_defaults = {}
145
+ @version_defaults = Hash.new {|h, k|
146
+ # NOTE: String responds to both so the order is significant.
147
+ # And ignore non-numeric conversion to zero, because: "wat!?".to_r == 0
148
+ (h.fetch(k.to_r, nil) || h.fetch(k.to_f, nil) if k.is_a?(Numeric)) ||
149
+ (h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) ||
150
+ (h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) ||
151
+ (h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0)
152
+ }
136
153
 
137
154
  # :call-seq:
138
155
  # Net::IMAP::Config[number] -> versioned config
@@ -155,18 +172,17 @@ module Net
155
172
  elsif config.nil? && global.nil? then nil
156
173
  elsif config.respond_to?(:to_hash) then new(global, **config).freeze
157
174
  else
158
- version_defaults.fetch(config) do
175
+ version_defaults[config] or
159
176
  case config
160
177
  when Numeric
161
178
  raise RangeError, "unknown config version: %p" % [config]
162
- when Symbol
179
+ when String, Symbol
163
180
  raise KeyError, "unknown config name: %p" % [config]
164
181
  else
165
182
  raise TypeError, "no implicit conversion of %s to %s" % [
166
183
  config.class, Config
167
184
  ]
168
185
  end
169
- end
170
186
  end
171
187
  end
172
188
 
@@ -193,10 +209,13 @@ module Net
193
209
 
194
210
  # Seconds to wait until a connection is opened.
195
211
  #
212
+ # Applied separately for establishing TCP connection and starting a TLS
213
+ # connection.
214
+ #
196
215
  # If the IMAP object cannot open a connection within this time,
197
216
  # it raises a Net::OpenTimeout exception.
198
217
  #
199
- # See Net::IMAP.new.
218
+ # See Net::IMAP.new and Net::IMAP#starttls.
200
219
  #
201
220
  # The default value is +30+ seconds.
202
221
  attr_accessor :open_timeout, type: Integer
@@ -245,10 +264,44 @@ module Net
245
264
  # present. When capabilities are unknown, Net::IMAP will automatically
246
265
  # send a +CAPABILITY+ command first before sending +LOGIN+.
247
266
  #
248
- attr_accessor :enforce_logindisabled, type: [
267
+ attr_accessor :enforce_logindisabled, type: Enum[
249
268
  false, :when_capabilities_cached, true
250
269
  ]
251
270
 
271
+ # The maximum allowed server response size. When +nil+, there is no limit
272
+ # on response size.
273
+ #
274
+ # The default value (512 MiB, since +v0.5.7+) is <em>very high</em> and
275
+ # unlikely to be reached. A _much_ lower value should be used with
276
+ # untrusted servers (for example, when connecting to a user-provided
277
+ # hostname). When using a lower limit, message bodies should be fetched
278
+ # in chunks rather than all at once.
279
+ #
280
+ # <em>Please Note:</em> this only limits the size per response. It does
281
+ # not prevent a flood of individual responses and it does not limit how
282
+ # many unhandled responses may be stored on the responses hash. See
283
+ # Net::IMAP@Unbounded+memory+use.
284
+ #
285
+ # Socket reads are limited to the maximum remaining bytes for the current
286
+ # response: max_response_size minus the bytes that have already been read.
287
+ # When the limit is reached, or reading a +literal+ _would_ go over the
288
+ # limit, ResponseTooLargeError is raised and the connection is closed.
289
+ #
290
+ # Note that changes will not take effect immediately, because the receiver
291
+ # thread may already be waiting for the next response using the previous
292
+ # value. Net::IMAP#noop can force a response and enforce the new setting
293
+ # immediately.
294
+ #
295
+ # ==== Versioned Defaults
296
+ #
297
+ # Net::IMAP#max_response_size <em>was added in +v0.2.5+ and +v0.3.9+ as an
298
+ # attr_accessor, and in +v0.4.20+ and +v0.5.7+ as a delegator to this
299
+ # config attribute.</em>
300
+ #
301
+ # * original: +nil+ <em>(no limit)</em>
302
+ # * +0.5+: 512 MiB
303
+ attr_accessor :max_response_size, type: Integer?
304
+
252
305
  # Controls the behavior of Net::IMAP#responses when called without any
253
306
  # arguments (+type+ or +block+).
254
307
  #
@@ -275,7 +328,7 @@ module Net
275
328
  # Raise an ArgumentError with the deprecation warning.
276
329
  #
277
330
  # Note: #responses_without_args is an alias for #responses_without_block.
278
- attr_accessor :responses_without_block, type: [
331
+ attr_accessor :responses_without_block, type: Enum[
279
332
  :silence_deprecation_warning, :warn, :frozen_dup, :raise,
280
333
  ]
281
334
 
@@ -287,6 +340,67 @@ module Net
287
340
  #
288
341
  # Alias for responses_without_block
289
342
 
343
+ # Whether ResponseParser should use the deprecated UIDPlusData or
344
+ # CopyUIDData for +COPYUID+ response codes, and UIDPlusData or
345
+ # AppendUIDData for +APPENDUID+ response codes.
346
+ #
347
+ # UIDPlusData stores its data in arrays of numbers, which is vulnerable to
348
+ # a memory exhaustion denial of service attack from an untrusted or
349
+ # compromised server. Set this option to +false+ to completely block this
350
+ # vulnerability. Otherwise, parser_max_deprecated_uidplus_data_size
351
+ # mitigates this vulnerability.
352
+ #
353
+ # AppendUIDData and CopyUIDData are _mostly_ backward-compatible with
354
+ # UIDPlusData. Most applications should be able to upgrade with little
355
+ # or no changes.
356
+ #
357
+ # <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
358
+ #
359
+ # <em>(Config option added in +v0.4.19+ and +v0.5.6+.)</em>
360
+ #
361
+ # <em>UIDPlusData will be removed in +v0.6+ and this config setting will
362
+ # be ignored.</em>
363
+ #
364
+ # ==== Valid options
365
+ #
366
+ # [+true+ <em>(original default)</em>]
367
+ # ResponseParser only uses UIDPlusData.
368
+ #
369
+ # [+:up_to_max_size+ <em>(default since +v0.5.6+)</em>]
370
+ # ResponseParser uses UIDPlusData when the +uid-set+ size is below
371
+ # parser_max_deprecated_uidplus_data_size. Above that size,
372
+ # ResponseParser uses AppendUIDData or CopyUIDData.
373
+ #
374
+ # [+false+ <em>(planned default for +v0.6+)</em>]
375
+ # ResponseParser _only_ uses AppendUIDData and CopyUIDData.
376
+ attr_accessor :parser_use_deprecated_uidplus_data, type: Enum[
377
+ true, :up_to_max_size, false
378
+ ]
379
+
380
+ # The maximum +uid-set+ size that ResponseParser will parse into
381
+ # deprecated UIDPlusData. This limit only applies when
382
+ # parser_use_deprecated_uidplus_data is not +false+.
383
+ #
384
+ # <em>(Parser support for +UIDPLUS+ added in +v0.3.2+.)</em>
385
+ #
386
+ # <em>Support for limiting UIDPlusData to a maximum size was added in
387
+ # +v0.3.8+, +v0.4.19+, and +v0.5.6+.</em>
388
+ #
389
+ # <em>UIDPlusData will be removed in +v0.6+.</em>
390
+ #
391
+ # ==== Versioned Defaults
392
+ #
393
+ # Because this limit guards against a remote server causing catastrophic
394
+ # memory exhaustion, the versioned default (used by #load_defaults) also
395
+ # applies to versions without the feature.
396
+ #
397
+ # * +0.3+ and prior: <tt>10,000</tt>
398
+ # * +0.4+: <tt>1,000</tt>
399
+ # * +0.5+: <tt>100</tt>
400
+ # * +0.6+: <tt>0</tt>
401
+ #
402
+ attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer
403
+
290
404
  # Creates a new config object and initialize its attribute with +attrs+.
291
405
  #
292
406
  # If +parent+ is not given, the global config is used by default.
@@ -366,37 +480,73 @@ module Net
366
480
  idle_response_timeout: 5,
367
481
  sasl_ir: true,
368
482
  enforce_logindisabled: true,
483
+ max_response_size: 512 << 20, # 512 MiB
369
484
  responses_without_block: :warn,
485
+ parser_use_deprecated_uidplus_data: :up_to_max_size,
486
+ parser_max_deprecated_uidplus_data_size: 100,
370
487
  ).freeze
371
488
 
372
489
  @global = default.new
373
490
 
374
491
  version_defaults[:default] = Config[default.send(:defaults_hash)]
375
- version_defaults[:current] = Config[:default]
376
492
 
377
- version_defaults[0] = Config[:current].dup.update(
493
+ version_defaults[0r] = Config[:default].dup.update(
378
494
  sasl_ir: false,
379
495
  responses_without_block: :silence_deprecation_warning,
380
496
  enforce_logindisabled: false,
497
+ max_response_size: nil,
498
+ parser_use_deprecated_uidplus_data: true,
499
+ parser_max_deprecated_uidplus_data_size: 10_000,
381
500
  ).freeze
382
- version_defaults[0.0] = Config[0]
383
- version_defaults[0.1] = Config[0]
384
- version_defaults[0.2] = Config[0]
385
- version_defaults[0.3] = Config[0]
501
+ version_defaults[0.0r] = Config[0r]
502
+ version_defaults[0.1r] = Config[0r]
503
+ version_defaults[0.2r] = Config[0r]
504
+ version_defaults[0.3r] = Config[0r]
386
505
 
387
- version_defaults[0.4] = Config[0.3].dup.update(
506
+ version_defaults[0.4r] = Config[0.3r].dup.update(
388
507
  sasl_ir: true,
508
+ parser_max_deprecated_uidplus_data_size: 1000,
389
509
  ).freeze
390
510
 
391
- version_defaults[0.5] = Config[:current]
511
+ version_defaults[0.5r] = Config[0.4r].dup.update(
512
+ enforce_logindisabled: true,
513
+ max_response_size: 512 << 20, # 512 MiB
514
+ responses_without_block: :warn,
515
+ parser_use_deprecated_uidplus_data: :up_to_max_size,
516
+ parser_max_deprecated_uidplus_data_size: 100,
517
+ ).freeze
392
518
 
393
- version_defaults[0.6] = Config[0.5].dup.update(
519
+ version_defaults[0.6r] = Config[0.5r].dup.update(
394
520
  responses_without_block: :frozen_dup,
521
+ parser_use_deprecated_uidplus_data: false,
522
+ parser_max_deprecated_uidplus_data_size: 0,
395
523
  ).freeze
396
- version_defaults[:next] = Config[0.6]
397
- version_defaults[:future] = Config[:next]
524
+
525
+ version_defaults[0.7r] = Config[0.6r].dup.update(
526
+ ).freeze
527
+
528
+ # Safe conversions one way only:
529
+ # 0.6r.to_f == 0.6 # => true
530
+ # 0.6 .to_r == 0.6r # => false
531
+ version_defaults.to_a.each do |k, v|
532
+ next unless k in Rational
533
+ version_defaults[k.to_f] = v
534
+ end
535
+
536
+ current = VERSION.to_r
537
+ version_defaults[:original] = Config[0]
538
+ version_defaults[:current] = Config[current]
539
+ version_defaults[:next] = Config[current + 0.1r]
540
+ version_defaults[:future] = Config[current + 0.2r]
398
541
 
399
542
  version_defaults.freeze
543
+
544
+ if ($VERBOSE || $DEBUG) && self[:current].to_h != self[:default].to_h
545
+ warn "Misconfigured Net::IMAP::Config[:current] => %p,\n" \
546
+ " not equal to Net::IMAP::Config[:default] => %p" % [
547
+ self[:current].to_h, self[:default].to_h
548
+ ]
549
+ end
400
550
  end
401
551
  end
402
552
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ class ConnectionState < Net::IMAP::Data # :nodoc:
6
+ def self.define(symbol, *attrs)
7
+ symbol => Symbol
8
+ state = super(*attrs)
9
+ state.const_set :NAME, symbol
10
+ state
11
+ end
12
+
13
+ def symbol; self.class::NAME end
14
+ def name; self.class::NAME.name end
15
+ alias to_sym symbol
16
+
17
+ def deconstruct; [symbol, *super] end
18
+
19
+ def deconstruct_keys(names)
20
+ hash = super
21
+ hash[:symbol] = symbol if names.nil? || names.include?(:symbol)
22
+ hash[:name] = name if names.nil? || names.include?(:name)
23
+ hash
24
+ end
25
+
26
+ def to_h(&block)
27
+ hash = deconstruct_keys(nil)
28
+ block ? hash.to_h(&block) : hash
29
+ end
30
+
31
+ def not_authenticated?; to_sym == :not_authenticated end
32
+ def authenticated?; to_sym == :authenticated end
33
+ def selected?; to_sym == :selected end
34
+ def logout?; to_sym == :logout end
35
+
36
+ NotAuthenticated = define(:not_authenticated)
37
+ Authenticated = define(:authenticated)
38
+ Selected = define(:selected)
39
+ Logout = define(:logout)
40
+
41
+ class << self
42
+ undef :define
43
+ end
44
+ freeze
45
+ end
46
+
47
+ end
48
+ end
@@ -17,6 +17,39 @@ module Net
17
17
  class DataFormatError < Error
18
18
  end
19
19
 
20
+ # Error raised when the socket cannot be read, due to a Config limit.
21
+ class ResponseReadError < Error
22
+ end
23
+
24
+ # Error raised when a response is larger than IMAP#max_response_size.
25
+ class ResponseTooLargeError < ResponseReadError
26
+ attr_reader :bytes_read, :literal_size
27
+ attr_reader :max_response_size
28
+
29
+ def initialize(msg = nil, *args,
30
+ bytes_read: nil,
31
+ literal_size: nil,
32
+ max_response_size: nil,
33
+ **kwargs)
34
+ @bytes_read = bytes_read
35
+ @literal_size = literal_size
36
+ @max_response_size = max_response_size
37
+ msg ||= [
38
+ "Response size", response_size_msg, "exceeds max_response_size",
39
+ max_response_size && "(#{max_response_size}B)",
40
+ ].compact.join(" ")
41
+ super(msg, *args, **kwargs)
42
+ end
43
+
44
+ private
45
+
46
+ def response_size_msg
47
+ if bytes_read && literal_size
48
+ "(#{bytes_read}B read + #{literal_size}B literal)"
49
+ end
50
+ end
51
+ end
52
+
20
53
  # Error raised when a response from the server is non-parsable.
21
54
  class ResponseParseError < Error
22
55
  end
@@ -7,6 +7,9 @@ module Net
7
7
  autoload :UIDFetchData, "#{__dir__}/fetch_data"
8
8
  autoload :SearchResult, "#{__dir__}/search_result"
9
9
  autoload :SequenceSet, "#{__dir__}/sequence_set"
10
+ autoload :UIDPlusData, "#{__dir__}/uidplus_data"
11
+ autoload :AppendUIDData, "#{__dir__}/uidplus_data"
12
+ autoload :CopyUIDData, "#{__dir__}/uidplus_data"
10
13
  autoload :VanishedData, "#{__dir__}/vanished_data"
11
14
 
12
15
  # Net::IMAP::ContinuationRequest represents command continuation requests.
@@ -344,55 +347,6 @@ module Net
344
347
  # code data can take.
345
348
  end
346
349
 
347
- # UIDPlusData represents the ResponseCode#data that accompanies the
348
- # +APPENDUID+ and +COPYUID+ {response codes}[rdoc-ref:ResponseCode].
349
- #
350
- # A server that supports +UIDPLUS+ should send a UIDPlusData object inside
351
- # every TaggedResponse returned by the append[rdoc-ref:Net::IMAP#append],
352
- # copy[rdoc-ref:Net::IMAP#copy], move[rdoc-ref:Net::IMAP#move], {uid
353
- # copy}[rdoc-ref:Net::IMAP#uid_copy], and {uid
354
- # move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the destination
355
- # mailbox reports +UIDNOTSTICKY+.
356
- #
357
- # == Required capability
358
- # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
359
- # or +IMAP4rev2+ capability.
360
- #
361
- class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids)
362
- ##
363
- # method: uidvalidity
364
- # :call-seq: uidvalidity -> nonzero uint32
365
- #
366
- # The UIDVALIDITY of the destination mailbox.
367
-
368
- ##
369
- # method: source_uids
370
- # :call-seq: source_uids -> nil or an array of nonzero uint32
371
- #
372
- # The UIDs of the copied or moved messages.
373
- #
374
- # Note:: Returns +nil+ for Net::IMAP#append.
375
-
376
- ##
377
- # method: assigned_uids
378
- # :call-seq: assigned_uids -> an array of nonzero uint32
379
- #
380
- # The newly assigned UIDs of the copied, moved, or appended messages.
381
- #
382
- # Note:: This always returns an array, even when it contains only one UID.
383
-
384
- ##
385
- # :call-seq: uid_mapping -> nil or a hash
386
- #
387
- # Returns a hash mapping each source UID to the newly assigned destination
388
- # UID.
389
- #
390
- # Note:: Returns +nil+ for Net::IMAP#append.
391
- def uid_mapping
392
- source_uids&.zip(assigned_uids)&.to_h
393
- end
394
- end
395
-
396
350
  # MailboxList represents the data of an untagged +LIST+ response, for a
397
351
  # _single_ mailbox path. IMAP#list returns an array of MailboxList objects.
398
352
  #
@@ -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:
@@ -1997,11 +2001,10 @@ module Net
1997
2001
  #
1998
2002
  # n.b, uniqueid ⊂ uid-set. To avoid inconsistent return types, we always
1999
2003
  # match uid_set even if that returns a single-member array.
2000
- #
2001
2004
  def resp_code_apnd__data
2002
2005
  validity = number; SP!
2003
2006
  dst_uids = uid_set # uniqueid ⊂ uid-set
2004
- UIDPlusData.new(validity, nil, dst_uids)
2007
+ AppendUID(validity, dst_uids)
2005
2008
  end
2006
2009
 
2007
2010
  # already matched: "COPYUID"
@@ -2011,7 +2014,25 @@ module Net
2011
2014
  validity = number; SP!
2012
2015
  src_uids = uid_set; SP!
2013
2016
  dst_uids = uid_set
2014
- UIDPlusData.new(validity, src_uids, dst_uids)
2017
+ CopyUID(validity, src_uids, dst_uids)
2018
+ end
2019
+
2020
+ def AppendUID(...) DeprecatedUIDPlus(...) || AppendUIDData.new(...) end
2021
+ def CopyUID(...) DeprecatedUIDPlus(...) || CopyUIDData.new(...) end
2022
+
2023
+ # TODO: remove this code in the v0.6.0 release
2024
+ def DeprecatedUIDPlus(validity, src_uids = nil, dst_uids)
2025
+ 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
2015
2036
  end
2016
2037
 
2017
2038
  ADDRESS_REGEXP = /\G
@@ -2137,15 +2158,9 @@ module Net
2137
2158
  # uniqueid = nz-number
2138
2159
  # ; Strictly ascending
2139
2160
  def uid_set
2140
- token = match(T_NUMBER, T_ATOM)
2141
- case token.symbol
2142
- when T_NUMBER then [Integer(token.value)]
2143
- when T_ATOM
2144
- token.value.split(",").flat_map {|range|
2145
- range = range.split(":").map {|uniqueid| Integer(uniqueid) }
2146
- range.size == 1 ? range : Range.new(range.min, range.max).to_a
2147
- }
2148
- end
2161
+ set = sequence_set
2162
+ parse_error("uid-set cannot contain '*'") if set.include_star?
2163
+ set
2149
2164
  end
2150
2165
 
2151
2166
  def nil_atom
@@ -0,0 +1,73 @@
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
32
+ def empty? = buff.empty?
33
+ def done? = line_done? && !get_literal_size
34
+ def line_done? = buff.end_with?(CRLF)
35
+ def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
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
56
+ def max_response_remaining = max_response_size &.- bytes_read
57
+ def response_too_large? = max_response_size &.< min_response_size
58
+ def min_response_size = bytes_read + min_response_remaining
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:, bytes_read:, literal_size:,
68
+ )
69
+ end
70
+
71
+ end
72
+ end
73
+ end