net-imap 0.5.6 → 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: cdbdda0ed73da899ec338f66022a16104562d3701c568b0a6d4897270a608ac5
4
- data.tar.gz: b6a7ec70776b32f8eb57d01a0869503eb5d76f719ac091eb92e0206608e936e9
3
+ metadata.gz: ee48ac78f129043fd8ccb4f6e0b42535cc0ef3a6acd8ea1067cbd9b512815e49
4
+ data.tar.gz: 5bfae3bf0ee2e63c5b61c19d30187fe722adfe8018746e64d583124531de87a8
5
5
  SHA512:
6
- metadata.gz: 381bf2428719ed8decb5d241fda0e19f28031dd4a77980b3717bb29c37bed1c927f00e5b57862e209ecf24b2e9b38c01088d6e1a90fc4b4cc026cdd9e6611100
7
- data.tar.gz: 513c6a77d46b6d2cf67aea4511023acc76c69940e3b1a0d0eae7223b53ff63bc8e6e009f51fef826b09f76f6ad1d92e84243e8457f59d3912db7e74bf69d3d1b
6
+ metadata.gz: f90a4500f3c218dd3a51cca84a76c620fa8f8488f2ca73486b538a43e67e21e0b897d23d957f490afe1590fd755c692f2e0dede562460ed2cfcd7f4d9bec3262
7
+ data.tar.gz: 22abb3cf5699bb715c33c69b596ead46ff400109925f58d633ecdc82797be9b80fbbd040c02aa7cd9286f3aa9c697e9fc426f1115b9019455b2ac4e6667cc236
@@ -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
 
@@ -320,7 +373,7 @@ module Net
320
373
  #
321
374
  # [+false+ <em>(planned default for +v0.6+)</em>]
322
375
  # ResponseParser _only_ uses AppendUIDData and CopyUIDData.
323
- attr_accessor :parser_use_deprecated_uidplus_data, type: [
376
+ attr_accessor :parser_use_deprecated_uidplus_data, type: Enum[
324
377
  true, :up_to_max_size, false
325
378
  ]
326
379
 
@@ -427,6 +480,7 @@ module Net
427
480
  idle_response_timeout: 5,
428
481
  sasl_ir: true,
429
482
  enforce_logindisabled: true,
483
+ max_response_size: 512 << 20, # 512 MiB
430
484
  responses_without_block: :warn,
431
485
  parser_use_deprecated_uidplus_data: :up_to_max_size,
432
486
  parser_max_deprecated_uidplus_data_size: 100,
@@ -435,36 +489,64 @@ module Net
435
489
  @global = default.new
436
490
 
437
491
  version_defaults[:default] = Config[default.send(:defaults_hash)]
438
- version_defaults[:current] = Config[:default]
439
492
 
440
- version_defaults[0] = Config[:current].dup.update(
493
+ version_defaults[0r] = Config[:default].dup.update(
441
494
  sasl_ir: false,
442
495
  responses_without_block: :silence_deprecation_warning,
443
496
  enforce_logindisabled: false,
497
+ max_response_size: nil,
444
498
  parser_use_deprecated_uidplus_data: true,
445
499
  parser_max_deprecated_uidplus_data_size: 10_000,
446
500
  ).freeze
447
- version_defaults[0.0] = Config[0]
448
- version_defaults[0.1] = Config[0]
449
- version_defaults[0.2] = Config[0]
450
- 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]
451
505
 
452
- version_defaults[0.4] = Config[0.3].dup.update(
506
+ version_defaults[0.4r] = Config[0.3r].dup.update(
453
507
  sasl_ir: true,
454
508
  parser_max_deprecated_uidplus_data_size: 1000,
455
509
  ).freeze
456
510
 
457
- 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
458
518
 
459
- version_defaults[0.6] = Config[0.5].dup.update(
519
+ version_defaults[0.6r] = Config[0.5r].dup.update(
460
520
  responses_without_block: :frozen_dup,
461
521
  parser_use_deprecated_uidplus_data: false,
462
522
  parser_max_deprecated_uidplus_data_size: 0,
463
523
  ).freeze
464
- version_defaults[:next] = Config[0.6]
465
- 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]
466
541
 
467
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
468
550
  end
469
551
  end
470
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
@@ -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
@@ -174,7 +174,7 @@ module Net
174
174
  #
175
175
  # <i>Set membership:</i>
176
176
  # - #include? (aliased as #member?):
177
- # Returns whether a given object (nz-number, range, or <tt>*</tt>) is
177
+ # Returns whether a given element (nz-number, range, or <tt>*</tt>) is
178
178
  # contained by the set.
179
179
  # - #include_star?: Returns whether the set contains <tt>*</tt>.
180
180
  #
@@ -239,13 +239,13 @@ module Net
239
239
  # These methods do not modify +self+.
240
240
  #
241
241
  # - #| (aliased as #union and #+): Returns a new set combining all members
242
- # from +self+ with all members from the other object.
242
+ # from +self+ with all members from the other set.
243
243
  # - #& (aliased as #intersection): Returns a new set containing all members
244
- # common to +self+ and the other object.
244
+ # common to +self+ and the other set.
245
245
  # - #- (aliased as #difference): Returns a copy of +self+ with all members
246
- # in the other object removed.
246
+ # in the other set removed.
247
247
  # - #^ (aliased as #xor): Returns a new set containing all members from
248
- # +self+ and the other object except those common to both.
248
+ # +self+ and the other set except those common to both.
249
249
  # - #~ (aliased as #complement): Returns a new set containing all members
250
250
  # that are not in +self+
251
251
  # - #limit: Returns a copy of +self+ which has replaced <tt>*</tt> with a
@@ -258,17 +258,17 @@ module Net
258
258
  #
259
259
  # These methods always update #string to be fully sorted and coalesced.
260
260
  #
261
- # - #add (aliased as #<<): Adds a given object to the set; returns +self+.
262
- # - #add?: If the given object is not an element in the set, adds it and
261
+ # - #add (aliased as #<<): Adds a given element to the set; returns +self+.
262
+ # - #add?: If the given element is not fully included the set, adds it and
263
263
  # returns +self+; otherwise, returns +nil+.
264
- # - #merge: Merges multiple elements into the set; returns +self+.
264
+ # - #merge: Adds all members of the given sets into this set; returns +self+.
265
265
  # - #complement!: Replaces the contents of the set with its own #complement.
266
266
  #
267
267
  # <i>Order preserving:</i>
268
268
  #
269
269
  # These methods _may_ cause #string to not be sorted or coalesced.
270
270
  #
271
- # - #append: Adds a given object to the set, appending it to the existing
271
+ # - #append: Adds the given entry to the set, appending it to the existing
272
272
  # string, and returns +self+.
273
273
  # - #string=: Assigns a new #string value and replaces #elements to match.
274
274
  # - #replace: Replaces the contents of the set with the contents
@@ -279,13 +279,14 @@ module Net
279
279
  # sorted and coalesced.
280
280
  #
281
281
  # - #clear: Removes all elements in the set; returns +self+.
282
- # - #delete: Removes a given object from the set; returns +self+.
283
- # - #delete?: If the given object is an element in the set, removes it and
282
+ # - #delete: Removes a given element from the set; returns +self+.
283
+ # - #delete?: If the given element is included in the set, removes it and
284
284
  # returns it; otherwise, returns +nil+.
285
285
  # - #delete_at: Removes the number at a given offset.
286
286
  # - #slice!: Removes the number or consecutive numbers at a given offset or
287
287
  # range of offsets.
288
- # - #subtract: Removes each given object from the set; returns +self+.
288
+ # - #subtract: Removes all members of the given sets from this set; returns
289
+ # +self+.
289
290
  # - #limit!: Replaces <tt>*</tt> with a given maximum value and removes all
290
291
  # members over that maximum; returns +self+.
291
292
  #
@@ -318,9 +319,12 @@ module Net
318
319
  class << self
319
320
 
320
321
  # :call-seq:
321
- # SequenceSet[*values] -> valid frozen sequence set
322
+ # SequenceSet[*inputs] -> valid frozen sequence set
322
323
  #
323
- # Returns a frozen SequenceSet, constructed from +values+.
324
+ # Returns a frozen SequenceSet, constructed from +inputs+.
325
+ #
326
+ # When only a single valid frozen SequenceSet is given, that same set is
327
+ # returned.
324
328
  #
325
329
  # An empty SequenceSet is invalid and will raise a DataFormatError.
326
330
  #
@@ -690,7 +694,7 @@ module Net
690
694
  alias complement :~
691
695
 
692
696
  # :call-seq:
693
- # add(object) -> self
697
+ # add(element) -> self
694
698
  # self << other -> self
695
699
  #
696
700
  # Adds a range or number to the set and returns +self+.
@@ -698,8 +702,8 @@ module Net
698
702
  # #string will be regenerated. Use #merge to add many elements at once.
699
703
  #
700
704
  # Related: #add?, #merge, #union
701
- def add(object)
702
- tuple_add input_to_tuple object
705
+ def add(element)
706
+ tuple_add input_to_tuple element
703
707
  normalize!
704
708
  end
705
709
  alias << add
@@ -708,9 +712,9 @@ module Net
708
712
  #
709
713
  # Unlike #add, #merge, or #union, the new value is appended to #string.
710
714
  # This may result in a #string which has duplicates or is out-of-order.
711
- def append(object)
715
+ def append(entry)
712
716
  modifying!
713
- tuple = input_to_tuple object
717
+ tuple = input_to_tuple entry
714
718
  entry = tuple_to_str tuple
715
719
  string unless empty? # write @string before tuple_add
716
720
  tuple_add tuple
@@ -718,19 +722,19 @@ module Net
718
722
  self
719
723
  end
720
724
 
721
- # :call-seq: add?(object) -> self or nil
725
+ # :call-seq: add?(element) -> self or nil
722
726
  #
723
727
  # Adds a range or number to the set and returns +self+. Returns +nil+
724
- # when the object is already included in the set.
728
+ # when the element is already included in the set.
725
729
  #
726
730
  # #string will be regenerated. Use #merge to add many elements at once.
727
731
  #
728
732
  # Related: #add, #merge, #union, #include?
729
- def add?(object)
730
- add object unless include? object
733
+ def add?(element)
734
+ add element unless include? element
731
735
  end
732
736
 
733
- # :call-seq: delete(object) -> self
737
+ # :call-seq: delete(element) -> self
734
738
  #
735
739
  # Deletes the given range or number from the set and returns +self+.
736
740
  #
@@ -738,8 +742,8 @@ module Net
738
742
  # many elements at once.
739
743
  #
740
744
  # Related: #delete?, #delete_at, #subtract, #difference
741
- def delete(object)
742
- tuple_subtract input_to_tuple object
745
+ def delete(element)
746
+ tuple_subtract input_to_tuple element
743
747
  normalize!
744
748
  end
745
749
 
@@ -775,8 +779,8 @@ module Net
775
779
  # #string will be regenerated after deletion.
776
780
  #
777
781
  # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
778
- def delete?(object)
779
- tuple = input_to_tuple object
782
+ def delete?(element)
783
+ tuple = input_to_tuple element
780
784
  if tuple.first == tuple.last
781
785
  return unless include_tuple? tuple
782
786
  tuple_subtract tuple
@@ -820,33 +824,31 @@ module Net
820
824
  deleted
821
825
  end
822
826
 
823
- # Merges all of the elements that appear in any of the +inputs+ into the
827
+ # Merges all of the elements that appear in any of the +sets+ into the
824
828
  # set, and returns +self+.
825
829
  #
826
- # The +inputs+ may be any objects that would be accepted by ::new:
827
- # non-zero 32 bit unsigned integers, ranges, <tt>sequence-set</tt>
828
- # formatted strings, other sequence sets, or enumerables containing any of
829
- # these.
830
+ # The +sets+ may be any objects that would be accepted by ::new: non-zero
831
+ # 32 bit unsigned integers, ranges, <tt>sequence-set</tt> formatted
832
+ # strings, other sequence sets, or enumerables containing any of these.
830
833
  #
831
- # #string will be regenerated after all inputs have been merged.
834
+ # #string will be regenerated after all sets have been merged.
832
835
  #
833
836
  # Related: #add, #add?, #union
834
- def merge(*inputs)
835
- tuples_add input_to_tuples inputs
837
+ def merge(*sets)
838
+ tuples_add input_to_tuples sets
836
839
  normalize!
837
840
  end
838
841
 
839
- # Removes all of the elements that appear in any of the given +objects+
840
- # from the set, and returns +self+.
842
+ # Removes all of the elements that appear in any of the given +sets+ from
843
+ # the set, and returns +self+.
841
844
  #
842
- # The +objects+ may be any objects that would be accepted by ::new:
843
- # non-zero 32 bit unsigned integers, ranges, <tt>sequence-set</tt>
844
- # formatted strings, other sequence sets, or enumerables containing any of
845
- # these.
845
+ # The +sets+ may be any objects that would be accepted by ::new: non-zero
846
+ # 32 bit unsigned integers, ranges, <tt>sequence-set</tt> formatted
847
+ # strings, other sequence sets, or enumerables containing any of these.
846
848
  #
847
849
  # Related: #difference
848
- def subtract(*objects)
849
- tuples_subtract input_to_tuples objects
850
+ def subtract(*sets)
851
+ tuples_subtract input_to_tuples sets
850
852
  normalize!
851
853
  end
852
854
 
@@ -1367,6 +1369,18 @@ module Net
1367
1369
  imap.__send__(:put_string, valid_string)
1368
1370
  end
1369
1371
 
1372
+ # For YAML serialization
1373
+ def encode_with(coder) # :nodoc:
1374
+ # we can perfectly reconstruct from the string
1375
+ coder['string'] = to_s
1376
+ end
1377
+
1378
+ # For YAML deserialization
1379
+ def init_with(coder) # :nodoc:
1380
+ @tuples = []
1381
+ self.string = coder['string']
1382
+ end
1383
+
1370
1384
  protected
1371
1385
 
1372
1386
  attr_reader :tuples # :nodoc:
@@ -1386,30 +1400,30 @@ module Net
1386
1400
  super
1387
1401
  end
1388
1402
 
1389
- def input_to_tuple(obj)
1390
- obj = input_try_convert obj
1391
- case obj
1392
- when *STARS, Integer then [int = to_tuple_int(obj), int]
1393
- when Range then range_to_tuple(obj)
1394
- when String then str_to_tuple(obj)
1403
+ def input_to_tuple(entry)
1404
+ entry = input_try_convert entry
1405
+ case entry
1406
+ when *STARS, Integer then [int = to_tuple_int(entry), int]
1407
+ when Range then range_to_tuple(entry)
1408
+ when String then str_to_tuple(entry)
1395
1409
  else
1396
- raise DataFormatError, "expected number or range, got %p" % [obj]
1410
+ raise DataFormatError, "expected number or range, got %p" % [entry]
1397
1411
  end
1398
1412
  end
1399
1413
 
1400
- def input_to_tuples(obj)
1401
- obj = input_try_convert obj
1402
- case obj
1403
- when *STARS, Integer, Range then [input_to_tuple(obj)]
1404
- when String then str_to_tuples obj
1405
- when SequenceSet then obj.tuples
1406
- when Set then obj.map { [to_tuple_int(_1)] * 2 }
1407
- when Array then obj.flat_map { input_to_tuples _1 }
1414
+ def input_to_tuples(set)
1415
+ set = input_try_convert set
1416
+ case set
1417
+ when *STARS, Integer, Range then [input_to_tuple(set)]
1418
+ when String then str_to_tuples set
1419
+ when SequenceSet then set.tuples
1420
+ when Set then set.map { [to_tuple_int(_1)] * 2 }
1421
+ when Array then set.flat_map { input_to_tuples _1 }
1408
1422
  when nil then []
1409
1423
  else
1410
1424
  raise DataFormatError,
1411
1425
  "expected nz-number, range, string, or enumerable; " \
1412
- "got %p" % [obj]
1426
+ "got %p" % [set]
1413
1427
  end
1414
1428
  end
1415
1429
 
data/lib/net/imap.rb CHANGED
@@ -43,10 +43,18 @@ module Net
43
43
  # To work on the messages within a mailbox, the client must
44
44
  # first select that mailbox, using either #select or #examine
45
45
  # (for read-only access). Once the client has successfully
46
- # selected a mailbox, they enter the "_selected_" state, and that
46
+ # selected a mailbox, they enter the +selected+ state, and that
47
47
  # mailbox becomes the _current_ mailbox, on which mail-item
48
48
  # related commands implicitly operate.
49
49
  #
50
+ # === Connection state
51
+ #
52
+ # Once an IMAP connection is established, the connection is in one of four
53
+ # states: <tt>not authenticated</tt>, +authenticated+, +selected+, and
54
+ # +logout+. Most commands are valid only in certain states.
55
+ #
56
+ # See #connection_state.
57
+ #
50
58
  # === Sequence numbers and UIDs
51
59
  #
52
60
  # Messages have two sorts of identifiers: message sequence
@@ -199,6 +207,42 @@ module Net
199
207
  #
200
208
  # This script invokes the FETCH command and the SEARCH command concurrently.
201
209
  #
210
+ # When running multiple commands, care must be taken to avoid ambiguity. For
211
+ # example, SEARCH responses are ambiguous about which command they are
212
+ # responding to, so search commands should not run simultaneously, unless the
213
+ # server supports +ESEARCH+ {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] or
214
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051]. See {RFC9051
215
+ # §5.5}[https://www.rfc-editor.org/rfc/rfc9051.html#section-5.5] for
216
+ # other examples of command sequences which should not be pipelined.
217
+ #
218
+ # == Unbounded memory use
219
+ #
220
+ # Net::IMAP reads server responses in a separate receiver thread per client.
221
+ # Unhandled response data is saved to #responses, and response_handlers run
222
+ # inside the receiver thread. See the list of methods for {handling server
223
+ # responses}[rdoc-ref:Net::IMAP@Handling+server+responses], below.
224
+ #
225
+ # Because the receiver thread continuously reads and saves new responses, some
226
+ # scenarios must be careful to avoid unbounded memory use:
227
+ #
228
+ # * Commands such as #list or #fetch can have an enormous number of responses.
229
+ # * Commands such as #fetch can result in an enormous size per response.
230
+ # * Long-lived connections will gradually accumulate unsolicited server
231
+ # responses, especially +EXISTS+, +FETCH+, and +EXPUNGE+ responses.
232
+ # * A buggy or untrusted server could send inappropriate responses, which
233
+ # could be very numerous, very large, and very rapid.
234
+ #
235
+ # Use paginated or limited versions of commands whenever possible.
236
+ #
237
+ # Use Config#max_response_size to impose a limit on incoming server responses
238
+ # as they are being read. <em>This is especially important for untrusted
239
+ # servers.</em>
240
+ #
241
+ # Use #add_response_handler to handle responses after each one is received.
242
+ # Use the +response_handlers+ argument to ::new to assign response handlers
243
+ # before the receiver thread is started. Use #extract_responses,
244
+ # #clear_responses, or #responses (with a block) to prune responses.
245
+ #
202
246
  # == Errors
203
247
  #
204
248
  # An \IMAP server can send three different types of responses to indicate
@@ -260,8 +304,9 @@ module Net
260
304
  #
261
305
  # - Net::IMAP.new: Creates a new \IMAP client which connects immediately and
262
306
  # waits for a successful server greeting before the method returns.
307
+ # - #connection_state: Returns the connection state.
263
308
  # - #starttls: Asks the server to upgrade a clear-text connection to use TLS.
264
- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
309
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
265
310
  # - #disconnect: Disconnects the connection (without sending #logout first).
266
311
  # - #disconnected?: True if the connection has been closed.
267
312
  #
@@ -317,37 +362,36 @@ module Net
317
362
  # <em>In general, #capable? should be used rather than explicitly sending a
318
363
  # +CAPABILITY+ command to the server.</em>
319
364
  # - #noop: Allows the server to send unsolicited untagged #responses.
320
- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
365
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
321
366
  #
322
367
  # ==== Not Authenticated state
323
368
  #
324
369
  # In addition to the commands for any state, the following commands are valid
325
- # in the "<em>not authenticated</em>" state:
370
+ # in the +not_authenticated+ state:
326
371
  #
327
372
  # - #starttls: Upgrades a clear-text connection to use TLS.
328
373
  #
329
374
  # <em>Requires the +STARTTLS+ capability.</em>
330
375
  # - #authenticate: Identifies the client to the server using the given
331
376
  # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
332
- # and credentials. Enters the "_authenticated_" state.
377
+ # and credentials. Enters the +authenticated+ state.
333
378
  #
334
379
  # <em>The server should list <tt>"AUTH=#{mechanism}"</tt> capabilities for
335
380
  # supported mechanisms.</em>
336
381
  # - #login: Identifies the client to the server using a plain text password.
337
- # Using #authenticate is generally preferred. Enters the "_authenticated_"
338
- # state.
382
+ # Using #authenticate is preferred. Enters the +authenticated+ state.
339
383
  #
340
384
  # <em>The +LOGINDISABLED+ capability</em> <b>must NOT</b> <em>be listed.</em>
341
385
  #
342
386
  # ==== Authenticated state
343
387
  #
344
388
  # In addition to the commands for any state, the following commands are valid
345
- # in the "_authenticated_" state:
389
+ # in the +authenticated+ state:
346
390
  #
347
391
  # - #enable: Enables backwards incompatible server extensions.
348
392
  # <em>Requires the +ENABLE+ or +IMAP4rev2+ capability.</em>
349
- # - #select: Open a mailbox and enter the "_selected_" state.
350
- # - #examine: Open a mailbox read-only, and enter the "_selected_" state.
393
+ # - #select: Open a mailbox and enter the +selected+ state.
394
+ # - #examine: Open a mailbox read-only, and enter the +selected+ state.
351
395
  # - #create: Creates a new mailbox.
352
396
  # - #delete: Permanently remove a mailbox.
353
397
  # - #rename: Change the name of a mailbox.
@@ -369,12 +413,12 @@ module Net
369
413
  #
370
414
  # ==== Selected state
371
415
  #
372
- # In addition to the commands for any state and the "_authenticated_"
373
- # commands, the following commands are valid in the "_selected_" state:
416
+ # In addition to the commands for any state and the +authenticated+
417
+ # commands, the following commands are valid in the +selected+ state:
374
418
  #
375
- # - #close: Closes the mailbox and returns to the "_authenticated_" state,
419
+ # - #close: Closes the mailbox and returns to the +authenticated+ state,
376
420
  # expunging deleted messages, unless the mailbox was opened as read-only.
377
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
421
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
378
422
  # without expunging any messages.
379
423
  # <em>Requires the +UNSELECT+ or +IMAP4rev2+ capability.</em>
380
424
  # - #expunge: Permanently removes messages which have the Deleted flag set.
@@ -395,7 +439,7 @@ module Net
395
439
  #
396
440
  # ==== Logout state
397
441
  #
398
- # No \IMAP commands are valid in the "_logout_" state. If the socket is still
442
+ # No \IMAP commands are valid in the +logout+ state. If the socket is still
399
443
  # open, Net::IMAP will close it after receiving server confirmation.
400
444
  # Exceptions will be raised by \IMAP commands that have already started and
401
445
  # are waiting for a response, as well as any that are called after logout.
@@ -449,7 +493,7 @@ module Net
449
493
  # ==== RFC3691: +UNSELECT+
450
494
  # Folded into IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] and also included
451
495
  # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands].
452
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
496
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
453
497
  # without expunging any messages.
454
498
  #
455
499
  # ==== RFC4314: +ACL+
@@ -744,7 +788,7 @@ module Net
744
788
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
745
789
  #
746
790
  class IMAP < Protocol
747
- VERSION = "0.5.6"
791
+ VERSION = "0.5.7"
748
792
 
749
793
  # Aliases for supported capabilities, to be used with the #enable command.
750
794
  ENABLE_ALIASES = {
@@ -752,9 +796,12 @@ module Net
752
796
  "UTF8=ONLY" => "UTF8=ACCEPT",
753
797
  }.freeze
754
798
 
755
- autoload :SASL, File.expand_path("imap/sasl", __dir__)
756
- autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__)
757
- autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)
799
+ dir = File.expand_path("imap", __dir__)
800
+ autoload :ConnectionState, "#{dir}/connection_state"
801
+ autoload :ResponseReader, "#{dir}/response_reader"
802
+ autoload :SASL, "#{dir}/sasl"
803
+ autoload :SASLAdapter, "#{dir}/sasl_adapter"
804
+ autoload :StringPrep, "#{dir}/stringprep"
758
805
 
759
806
  include MonitorMixin
760
807
  if defined?(OpenSSL::SSL)
@@ -766,9 +813,11 @@ module Net
766
813
  def self.config; Config.global end
767
814
 
768
815
  # Returns the global debug mode.
816
+ # Delegates to {Net::IMAP.config.debug}[rdoc-ref:Config#debug].
769
817
  def self.debug; config.debug end
770
818
 
771
819
  # Sets the global debug mode.
820
+ # Delegates to {Net::IMAP.config.debug=}[rdoc-ref:Config#debug=].
772
821
  def self.debug=(val)
773
822
  config.debug = val
774
823
  end
@@ -789,7 +838,7 @@ module Net
789
838
  alias default_ssl_port default_tls_port
790
839
  end
791
840
 
792
- # Returns the initial greeting the server, an UntaggedResponse.
841
+ # Returns the initial greeting sent by the server, an UntaggedResponse.
793
842
  attr_reader :greeting
794
843
 
795
844
  # The client configuration. See Net::IMAP::Config.
@@ -798,13 +847,28 @@ module Net
798
847
  # Net::IMAP.config.
799
848
  attr_reader :config
800
849
 
801
- # Seconds to wait until a connection is opened.
802
- # If the IMAP object cannot open a connection within this time,
803
- # it raises a Net::OpenTimeout exception. The default value is 30 seconds.
804
- def open_timeout; config.open_timeout end
850
+ ##
851
+ # :attr_reader: open_timeout
852
+ # Seconds to wait until a connection is opened. Also used by #starttls.
853
+ # Delegates to {config.open_timeout}[rdoc-ref:Config#open_timeout].
805
854
 
855
+ ##
856
+ # :attr_reader: idle_response_timeout
806
857
  # Seconds to wait until an IDLE response is received.
807
- def idle_response_timeout; config.idle_response_timeout end
858
+ # Delegates to {config.idle_response_timeout}[rdoc-ref:Config#idle_response_timeout].
859
+
860
+ ##
861
+ # :attr_accessor: max_response_size
862
+ #
863
+ # The maximum allowed server response size, in bytes.
864
+ # Delegates to {config.max_response_size}[rdoc-ref:Config#max_response_size].
865
+
866
+ # :stopdoc:
867
+ def open_timeout; config.open_timeout end
868
+ def idle_response_timeout; config.idle_response_timeout end
869
+ def max_response_size; config.max_response_size end
870
+ def max_response_size=(val) config.max_response_size = val end
871
+ # :startdoc:
808
872
 
809
873
  # The hostname this client connected to
810
874
  attr_reader :host
@@ -827,6 +891,67 @@ module Net
827
891
  # Returns +false+ for a plaintext connection.
828
892
  attr_reader :ssl_ctx_params
829
893
 
894
+ # Returns the current connection state.
895
+ #
896
+ # Once an IMAP connection is established, the connection is in one of four
897
+ # states: +not_authenticated+, +authenticated+, +selected+, and +logout+.
898
+ # Most commands are valid only in certain states.
899
+ #
900
+ # The connection state object responds to +to_sym+ and +name+ with the name
901
+ # of the current connection state, as a Symbol or String. Future versions
902
+ # of +net-imap+ may store additional information on the state object.
903
+ #
904
+ # From {RFC9051}[https://www.rfc-editor.org/rfc/rfc9051#section-3]:
905
+ # +----------------------+
906
+ # |connection established|
907
+ # +----------------------+
908
+ # ||
909
+ # \/
910
+ # +--------------------------------------+
911
+ # | server greeting |
912
+ # +--------------------------------------+
913
+ # || (1) || (2) || (3)
914
+ # \/ || ||
915
+ # +-----------------+ || ||
916
+ # |Not Authenticated| || ||
917
+ # +-----------------+ || ||
918
+ # || (7) || (4) || ||
919
+ # || \/ \/ ||
920
+ # || +----------------+ ||
921
+ # || | Authenticated |<=++ ||
922
+ # || +----------------+ || ||
923
+ # || || (7) || (5) || (6) ||
924
+ # || || \/ || ||
925
+ # || || +--------+ || ||
926
+ # || || |Selected|==++ ||
927
+ # || || +--------+ ||
928
+ # || || || (7) ||
929
+ # \/ \/ \/ \/
930
+ # +--------------------------------------+
931
+ # | Logout |
932
+ # +--------------------------------------+
933
+ # ||
934
+ # \/
935
+ # +-------------------------------+
936
+ # |both sides close the connection|
937
+ # +-------------------------------+
938
+ #
939
+ # >>>
940
+ # Legend for the above diagram:
941
+ #
942
+ # 1. connection without pre-authentication (+OK+ #greeting)
943
+ # 2. pre-authenticated connection (+PREAUTH+ #greeting)
944
+ # 3. rejected connection (+BYE+ #greeting)
945
+ # 4. successful #login or #authenticate command
946
+ # 5. successful #select or #examine command
947
+ # 6. #close or #unselect command, unsolicited +CLOSED+ response code, or
948
+ # failed #select or #examine command
949
+ # 7. #logout command, server shutdown, or connection closed
950
+ #
951
+ # Before the server greeting, the state is +not_authenticated+.
952
+ # After the connection closes, the state remains +logout+.
953
+ attr_reader :connection_state
954
+
830
955
  # Creates a new Net::IMAP object and connects it to the specified
831
956
  # +host+.
832
957
  #
@@ -860,6 +985,12 @@ module Net
860
985
  #
861
986
  # See DeprecatedClientOptions.new for deprecated SSL arguments.
862
987
  #
988
+ # [response_handlers]
989
+ # A list of response handlers to be added before the receiver thread is
990
+ # started. This ensures every server response is handled, including the
991
+ # #greeting. Note that the greeting is handled in the current thread, but
992
+ # all other responses are handled in the receiver thread.
993
+ #
863
994
  # [config]
864
995
  # A Net::IMAP::Config object to use as the basis for #config. By default,
865
996
  # the global Net::IMAP.config is used.
@@ -931,7 +1062,7 @@ module Net
931
1062
  # [Net::IMAP::ByeResponseError]
932
1063
  # Connected to the host successfully, but it immediately said goodbye.
933
1064
  #
934
- def initialize(host, port: nil, ssl: nil,
1065
+ def initialize(host, port: nil, ssl: nil, response_handlers: nil,
935
1066
  config: Config.global, **config_options)
936
1067
  super()
937
1068
  # Config options
@@ -946,6 +1077,8 @@ module Net
946
1077
  @exception = nil
947
1078
  @greeting = nil
948
1079
  @capabilities = nil
1080
+ @tls_verified = false
1081
+ @connection_state = ConnectionState::NotAuthenticated.new
949
1082
 
950
1083
  # Client Protocol Receiver
951
1084
  @parser = ResponseParser.new(config: @config)
@@ -954,6 +1087,7 @@ module Net
954
1087
  @receiver_thread = nil
955
1088
  @receiver_thread_exception = nil
956
1089
  @receiver_thread_terminating = false
1090
+ response_handlers&.each do add_response_handler(_1) end
957
1091
 
958
1092
  # Client Protocol Sender (including state for currently running commands)
959
1093
  @tag_prefix = "RUBY"
@@ -967,8 +1101,8 @@ module Net
967
1101
  @logout_command_tag = nil
968
1102
 
969
1103
  # Connection
970
- @tls_verified = false
971
1104
  @sock = tcp_socket(@host, @port)
1105
+ @reader = ResponseReader.new(self, @sock)
972
1106
  start_tls_session if ssl_ctx
973
1107
  start_imap_connection
974
1108
  end
@@ -983,6 +1117,7 @@ module Net
983
1117
  # Related: #logout, #logout!
984
1118
  def disconnect
985
1119
  return if disconnected?
1120
+ state_logout!
986
1121
  begin
987
1122
  begin
988
1123
  # try to call SSL::SSLSocket#io.
@@ -1221,6 +1356,10 @@ module Net
1221
1356
  # both successful. Any error indicates that the connection has not been
1222
1357
  # secured.
1223
1358
  #
1359
+ # After the server agrees to start a TLS connection, this method waits up to
1360
+ # {config.open_timeout}[rdoc-ref:Config#open_timeout] before raising
1361
+ # +Net::OpenTimeout+.
1362
+ #
1224
1363
  # *Note:*
1225
1364
  # >>>
1226
1365
  # Any #response_handlers added before STARTTLS should be aware that the
@@ -1368,7 +1507,7 @@ module Net
1368
1507
  # capabilities, they will be cached.
1369
1508
  def authenticate(*args, sasl_ir: config.sasl_ir, **props, &callback)
1370
1509
  sasl_adapter.authenticate(*args, sasl_ir: sasl_ir, **props, &callback)
1371
- .tap { @capabilities = capabilities_from_resp_code _1 }
1510
+ .tap do state_authenticated! _1 end
1372
1511
  end
1373
1512
 
1374
1513
  # Sends a {LOGIN command [IMAP4rev1 §6.2.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.3]
@@ -1402,7 +1541,7 @@ module Net
1402
1541
  raise LoginDisabledError
1403
1542
  end
1404
1543
  send_command("LOGIN", user, password)
1405
- .tap { @capabilities = capabilities_from_resp_code _1 }
1544
+ .tap do state_authenticated! _1 end
1406
1545
  end
1407
1546
 
1408
1547
  # Sends a {SELECT command [IMAP4rev1 §6.3.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.1]
@@ -1442,8 +1581,10 @@ module Net
1442
1581
  args = ["SELECT", mailbox]
1443
1582
  args << ["CONDSTORE"] if condstore
1444
1583
  synchronize do
1584
+ state_unselected! # implicitly closes current mailbox
1445
1585
  @responses.clear
1446
1586
  send_command(*args)
1587
+ .tap do state_selected! end
1447
1588
  end
1448
1589
  end
1449
1590
 
@@ -1460,8 +1601,10 @@ module Net
1460
1601
  args = ["EXAMINE", mailbox]
1461
1602
  args << ["CONDSTORE"] if condstore
1462
1603
  synchronize do
1604
+ state_unselected! # implicitly closes current mailbox
1463
1605
  @responses.clear
1464
1606
  send_command(*args)
1607
+ .tap do state_selected! end
1465
1608
  end
1466
1609
  end
1467
1610
 
@@ -1900,6 +2043,7 @@ module Net
1900
2043
  # Related: #unselect
1901
2044
  def close
1902
2045
  send_command("CLOSE")
2046
+ .tap do state_authenticated! end
1903
2047
  end
1904
2048
 
1905
2049
  # Sends an {UNSELECT command [RFC3691 §2]}[https://www.rfc-editor.org/rfc/rfc3691#section-3]
@@ -1916,6 +2060,7 @@ module Net
1916
2060
  # [RFC3691[https://www.rfc-editor.org/rfc/rfc3691]].
1917
2061
  def unselect
1918
2062
  send_command("UNSELECT")
2063
+ .tap do state_authenticated! end
1919
2064
  end
1920
2065
 
1921
2066
  # call-seq:
@@ -3146,6 +3291,10 @@ module Net
3146
3291
  # end
3147
3292
  # }
3148
3293
  #
3294
+ # Response handlers can also be added when the client is created before the
3295
+ # receiver thread is started, by the +response_handlers+ argument to ::new.
3296
+ # This ensures every server response is handled, including the #greeting.
3297
+ #
3149
3298
  # Related: #remove_response_handler, #response_handlers
3150
3299
  def add_response_handler(handler = nil, &block)
3151
3300
  raise ArgumentError, "two Procs are passed" if handler && block
@@ -3172,8 +3321,10 @@ module Net
3172
3321
  def start_imap_connection
3173
3322
  @greeting = get_server_greeting
3174
3323
  @capabilities = capabilities_from_resp_code @greeting
3324
+ @response_handlers.each do |handler| handler.call(@greeting) end
3175
3325
  @receiver_thread = start_receiver_thread
3176
3326
  rescue Exception
3327
+ state_logout!
3177
3328
  @sock.close
3178
3329
  raise
3179
3330
  end
@@ -3182,7 +3333,10 @@ module Net
3182
3333
  greeting = get_response
3183
3334
  raise Error, "No server greeting - connection closed" unless greeting
3184
3335
  record_untagged_response_code greeting
3185
- raise ByeResponseError, greeting if greeting.name == "BYE"
3336
+ case greeting.name
3337
+ when "PREAUTH" then state_authenticated!
3338
+ when "BYE" then state_logout!; raise ByeResponseError, greeting
3339
+ end
3186
3340
  greeting
3187
3341
  end
3188
3342
 
@@ -3192,6 +3346,8 @@ module Net
3192
3346
  rescue Exception => ex
3193
3347
  @receiver_thread_exception = ex
3194
3348
  # don't exit the thread with an exception
3349
+ ensure
3350
+ state_logout!
3195
3351
  end
3196
3352
  end
3197
3353
 
@@ -3214,6 +3370,7 @@ module Net
3214
3370
  resp = get_response
3215
3371
  rescue Exception => e
3216
3372
  synchronize do
3373
+ state_logout!
3217
3374
  @sock.close
3218
3375
  @exception = e
3219
3376
  end
@@ -3233,6 +3390,7 @@ module Net
3233
3390
  @tagged_response_arrival.broadcast
3234
3391
  case resp.tag
3235
3392
  when @logout_command_tag
3393
+ state_logout!
3236
3394
  return
3237
3395
  when @continued_command_tag
3238
3396
  @continuation_request_exception =
@@ -3242,6 +3400,7 @@ module Net
3242
3400
  when UntaggedResponse
3243
3401
  record_untagged_response(resp)
3244
3402
  if resp.name == "BYE" && @logout_command_tag.nil?
3403
+ state_logout!
3245
3404
  @sock.close
3246
3405
  @exception = ByeResponseError.new(resp)
3247
3406
  connection_closed = true
@@ -3249,6 +3408,7 @@ module Net
3249
3408
  when ContinuationRequest
3250
3409
  @continuation_request_arrival.signal
3251
3410
  end
3411
+ state_unselected! if resp in {data: {code: {name: "CLOSED"}}}
3252
3412
  @response_handlers.each do |handler|
3253
3413
  handler.call(resp)
3254
3414
  end
@@ -3300,23 +3460,10 @@ module Net
3300
3460
  end
3301
3461
 
3302
3462
  def get_response
3303
- buff = String.new
3304
- while true
3305
- s = @sock.gets(CRLF)
3306
- break unless s
3307
- buff.concat(s)
3308
- if /\{(\d+)\}\r\n/n =~ s
3309
- s = @sock.read($1.to_i)
3310
- buff.concat(s)
3311
- else
3312
- break
3313
- end
3314
- end
3463
+ buff = @reader.read_response_buffer
3315
3464
  return nil if buff.length == 0
3316
- if config.debug?
3317
- $stderr.print(buff.gsub(/^/n, "S: "))
3318
- end
3319
- return @parser.parse(buff)
3465
+ $stderr.print(buff.gsub(/^/n, "S: ")) if config.debug?
3466
+ @parser.parse(buff)
3320
3467
  end
3321
3468
 
3322
3469
  #############################
@@ -3620,6 +3767,7 @@ module Net
3620
3767
  raise "already using SSL" if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
3621
3768
  raise "cannot start TLS without SSLContext" unless ssl_ctx
3622
3769
  @sock = SSLSocket.new(@sock, ssl_ctx)
3770
+ @reader = ResponseReader.new(self, @sock)
3623
3771
  @sock.sync_close = true
3624
3772
  @sock.hostname = @host if @sock.respond_to? :hostname=
3625
3773
  ssl_socket_connect(@sock, open_timeout)
@@ -3629,6 +3777,29 @@ module Net
3629
3777
  end
3630
3778
  end
3631
3779
 
3780
+ def state_authenticated!(resp = nil)
3781
+ synchronize do
3782
+ @capabilities = capabilities_from_resp_code resp if resp
3783
+ @connection_state = ConnectionState::Authenticated.new
3784
+ end
3785
+ end
3786
+
3787
+ def state_selected!
3788
+ synchronize do
3789
+ @connection_state = ConnectionState::Selected.new
3790
+ end
3791
+ end
3792
+
3793
+ def state_unselected!
3794
+ state_authenticated! if connection_state.to_sym == :selected
3795
+ end
3796
+
3797
+ def state_logout!
3798
+ synchronize do
3799
+ @connection_state = ConnectionState::Logout.new
3800
+ end
3801
+ end
3802
+
3632
3803
  def sasl_adapter
3633
3804
  SASLAdapter.new(self, &method(:send_command_with_continuations))
3634
3805
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.6
4
+ version: 0.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
8
8
  - nicholas a. evans
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-07 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-protocol
@@ -60,6 +60,7 @@ files:
60
60
  - lib/net/imap/config/attr_accessors.rb
61
61
  - lib/net/imap/config/attr_inheritance.rb
62
62
  - lib/net/imap/config/attr_type_coercion.rb
63
+ - lib/net/imap/connection_state.rb
63
64
  - lib/net/imap/data_encoding.rb
64
65
  - lib/net/imap/data_lite.rb
65
66
  - lib/net/imap/deprecated_client_options.rb
@@ -70,6 +71,7 @@ files:
70
71
  - lib/net/imap/response_data.rb
71
72
  - lib/net/imap/response_parser.rb
72
73
  - lib/net/imap/response_parser/parser_utils.rb
74
+ - lib/net/imap/response_reader.rb
73
75
  - lib/net/imap/sasl.rb
74
76
  - lib/net/imap/sasl/anonymous_authenticator.rb
75
77
  - lib/net/imap/sasl/authentication_exchange.rb
@@ -127,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
129
  - !ruby/object:Gem::Version
128
130
  version: '0'
129
131
  requirements: []
130
- rubygems_version: 3.6.2
132
+ rubygems_version: 3.6.7
131
133
  specification_version: 4
132
134
  summary: Ruby client api for Internet Message Access Protocol
133
135
  test_files: []