net-imap 0.5.12 → 0.5.14

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: 57bfc92d10c2b8f139254627f6998a5c6e1c83111c41dac22a1cae22fdc11a75
4
- data.tar.gz: 7ca4ce789ff544967728d1b4b9106617f5c7a1b0b79beae999d0e22505e2b49c
3
+ metadata.gz: 33bccbb75eba778cb42fc5340afc2f10a899ca671123e8b538a39acbdf16bd1b
4
+ data.tar.gz: c4252164f38a0f36b827fb32247500e95293880e2f8026f4b7ed04926614df41
5
5
  SHA512:
6
- metadata.gz: 8ec55fe5a02b72fa061cb77f23c592806fd8361d4b371f8f124fed106c93ac01359af7bb4f194341ad66035ba24e1ddee3a100ab2de08d60367e0af2a9c3c94b
7
- data.tar.gz: b97339aae765227112a0e7308a68e969028495b243438e0dc37f049cfa310c385f4735bf5bd2460711d907c99a43abb1f8411aa72d742b5e4a3c337916500167
6
+ metadata.gz: e682041f5c1f0e071578c0910f3eace9438064741cce16b148870c73137fd38926154269907f4948dec365e83b9115facff5ec21ac23072270ef39bab64cea10
7
+ data.tar.gz: 8a04cac2ad54cd0bc4b2e2f5855bac16f571c3ab6aa0183caa1427ad7c420f8c3920e74b85871c22454dbd827c7772b24cfe96bcaf262222664c05f7483f5dbd
data/Gemfile CHANGED
@@ -4,7 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
- gem "digest"
7
+ # gem "digest" # not included as a workaround for #576
8
8
  gem "strscan"
9
9
  gem "base64"
10
10
 
@@ -5,6 +5,8 @@ require "date"
5
5
  require_relative "errors"
6
6
  require_relative "data_lite"
7
7
 
8
+ # :enddoc:
9
+
8
10
  module Net
9
11
  class IMAP < Protocol
10
12
 
@@ -26,6 +28,7 @@ module Net
26
28
  end
27
29
  when Time, Date, DateTime
28
30
  when Symbol
31
+ Flag.validate(data)
29
32
  else
30
33
  data.validate
31
34
  end
@@ -46,7 +49,7 @@ module Net
46
49
  when Date
47
50
  send_date_data(data)
48
51
  when Symbol
49
- send_symbol_data(data)
52
+ Flag[data].send_data(self, tag)
50
53
  else
51
54
  data.send_data(self, tag)
52
55
  end
@@ -78,9 +81,23 @@ module Net
78
81
  put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
79
82
  end
80
83
 
81
- def send_literal(str, tag = nil)
84
+ def send_binary_literal(*a, **kw); send_literal(*a, **kw, binary: true) end
85
+
86
+ # `non_sync` is an optional tri-state flag:
87
+ # * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
88
+ # TODO: raise or warn when capabilities don't allow non_sync.
89
+ # * `false` -> Force normal synchronizing literal behavior.
90
+ # * `nil` -> (default) Currently behaves like `false` (will be dynamic).
91
+ # TODO: Dynamic, based on capabilities and bytesize.
92
+ def send_literal(str, tag = nil, binary: false, non_sync: nil)
82
93
  synchronize do
83
- put_string("{" + str.bytesize.to_s + "}" + CRLF)
94
+ prefix = "~" if binary
95
+ plus = "+" if non_sync
96
+ put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
97
+ if non_sync
98
+ put_string(str)
99
+ return
100
+ end
84
101
  @continued_command_tag = tag
85
102
  @continuation_request_exception = nil
86
103
  begin
@@ -116,11 +133,13 @@ module Net
116
133
  def send_date_data(date) put_string Net::IMAP.encode_date(date) end
117
134
  def send_time_data(time) put_string Net::IMAP.encode_time(time) end
118
135
 
119
- def send_symbol_data(symbol)
120
- put_string("\\" + symbol.to_s)
121
- end
122
-
123
136
  CommandData = Data.define(:data) do # :nodoc:
137
+ def self.validate(...)
138
+ data = new(...)
139
+ data.validate
140
+ data
141
+ end
142
+
124
143
  def send_data(imap, tag)
125
144
  raise NoMethodError, "#{self.class} must implement #{__method__}"
126
145
  end
@@ -129,15 +148,109 @@ module Net
129
148
  end
130
149
  end
131
150
 
151
+ # Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
152
+ # except for +NULL+, +CR+, or +LF+. +text+ is extended to allow any
153
+ # multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
154
+ # been enabled, or when the server supports only +IMAP4rev2+ and not earlier
155
+ # IMAP revisions, or when the server advertises +UTF8=ONLY+.
156
+ #
157
+ # NOTE: The current implementation does not validate whether the connection
158
+ # currently supports UTF-8. Future versions may change.
159
+ #
160
+ # The string's bytes must be valid ASCII or valid UTF-8. The string's
161
+ # reported encoding is ignored, but the string is _not_ transcoded.
162
+ class RawText < CommandData # :nodoc:
163
+ def initialize(data:)
164
+ data = String(data.to_str)
165
+ data = if data.encoding in Encoding::ASCII | Encoding::UTF_8
166
+ -data
167
+ elsif data.ascii_only?
168
+ -(data.dup.force_encoding("ASCII"))
169
+ else
170
+ -(data.dup.force_encoding("UTF-8"))
171
+ end
172
+ super
173
+ validate
174
+ end
175
+
176
+ def validate
177
+ if data.include?("\0")
178
+ raise DataFormatError, "NULL byte must be binary literal encoded"
179
+ elsif !data.valid_encoding?
180
+ raise DataFormatError, "invalid UTF-8 must be literal encoded"
181
+ elsif /[\r\n]/.match?(data)
182
+ raise DataFormatError, "CR and LF bytes must be literal encoded"
183
+ end
184
+ end
185
+
186
+ def ascii_only? = data.ascii_only?
187
+
188
+ def send_data(imap, tag) = imap.__send__(:put_string, data)
189
+ end
190
+
132
191
  class RawData < CommandData # :nodoc:
133
- def send_data(imap, tag)
134
- imap.__send__(:put_string, data)
192
+ def initialize(data:)
193
+ data = split_parts(data)
194
+ super
195
+ validate
196
+ end
197
+
198
+ def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
199
+
200
+ def validate
201
+ return unless data.last in RawText(data: text)
202
+ if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
203
+ raise DataFormatError, "RawData cannot end with literal continuation"
204
+ end
205
+ end
206
+
207
+ private
208
+
209
+ def split_parts(data)
210
+ data = data.b # dups and ensures BINARY encoding
211
+ parts = []
212
+ while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
213
+ text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
214
+ bytesize = Integer bytesize, 10
215
+ parts << RawText[text] unless text.empty?
216
+ parts << extract_literal(data, binary:, bytesize:, non_sync:)
217
+ data[0, bytesize] = ""
218
+ end
219
+ parts << RawText[data] unless data.empty?
220
+ parts
221
+ end
222
+
223
+ def extract_literal(data, binary:, bytesize:, non_sync:)
224
+ if data.bytesize < bytesize
225
+ raise DataFormatError, "Too few bytes in string for literal, " \
226
+ "expected: %s, remaining: %s" % [bytesize, data.bytesize]
227
+ end
228
+ literal = data.byteslice(0, bytesize)
229
+ (binary ? Literal8 : Literal).new(data: literal, non_sync:)
135
230
  end
136
231
  end
137
232
 
138
233
  class Atom < CommandData # :nodoc:
234
+ def initialize(**)
235
+ super
236
+ validate
237
+ end
238
+
239
+ def validate
240
+ data.to_s.ascii_only? \
241
+ or raise DataFormatError, "#{self.class} must be ASCII only"
242
+ data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
243
+ and raise DataFormatError, "#{self.class} must not contain atom-specials"
244
+ end
245
+
246
+ def send_data(imap, tag)
247
+ imap.__send__(:put_string, data.to_s)
248
+ end
249
+ end
250
+
251
+ class Flag < Atom # :nodoc:
139
252
  def send_data(imap, tag)
140
- imap.__send__(:put_string, data)
253
+ imap.__send__(:put_string, "\\#{data}")
141
254
  end
142
255
  end
143
256
 
@@ -147,9 +260,39 @@ module Net
147
260
  end
148
261
  end
149
262
 
150
- class Literal < CommandData # :nodoc:
263
+ class Literal < Data.define(:data, :non_sync) # :nodoc:
264
+ def self.validate(...)
265
+ data = new(...)
266
+ data.validate
267
+ data
268
+ end
269
+
270
+ def initialize(data:, non_sync: nil)
271
+ data = -String(data.to_str).b or
272
+ raise DataFormatError, "#{self.class} expects string input"
273
+ super
274
+ validate
275
+ end
276
+
277
+ def bytesize; data.bytesize end
278
+
279
+ def validate
280
+ if data.include?("\0")
281
+ raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
282
+ "Use #{Literal8} or a null-safe encoding."
283
+ end
284
+ end
285
+
286
+ def send_data(imap, tag)
287
+ imap.__send__(:send_literal, data, tag, non_sync:)
288
+ end
289
+ end
290
+
291
+ class Literal8 < Literal # :nodoc:
292
+ def validate; nil end # all bytes are okay
293
+
151
294
  def send_data(imap, tag)
152
- imap.__send__(:send_literal, data, tag)
295
+ imap.__send__(:send_binary_literal, data, tag, non_sync:)
153
296
  end
154
297
  end
155
298
 
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Net
6
+ class IMAP
7
+ class Config
8
+ # >>>
9
+ # *NOTE:* This module is an internal implementation detail, with no
10
+ # guarantee of backward compatibility.
11
+ #
12
+ # Adds a +defaults+ parameter to +attr_accessor+, which is used to compile
13
+ # Config.version_defaults.
14
+ module AttrVersionDefaults
15
+ # The <tt>x.y</tt> part of Net::IMAP::VERSION, as a Rational number.
16
+ CURRENT_VERSION = VERSION.to_r
17
+
18
+ # The config version used for <tt>Config[:next]</tt>.
19
+ NEXT_VERSION = CURRENT_VERSION + 0.1r
20
+
21
+ # The config version used for <tt>Config[:future]</tt>.
22
+ FUTURE_VERSION = 1.0r
23
+
24
+ VERSIONS = ((0.0r..FUTURE_VERSION) % 0.1r).to_a.freeze
25
+
26
+ # See Config.version_defaults.
27
+ singleton_class.attr_reader :version_defaults
28
+
29
+ @version_defaults = Hash.new {|h, k|
30
+ # NOTE: String responds to both so the order is significant.
31
+ # And ignore non-numeric conversion to zero, because: "wat!?".to_r == 0
32
+ (h.fetch(k.to_r, nil) || h.fetch(k.to_f, nil) if k.is_a?(Numeric)) ||
33
+ (h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) ||
34
+ (h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) ||
35
+ (h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0)
36
+ }
37
+
38
+ # :stopdoc: internal APIs only
39
+
40
+ def attr_accessor(name, defaults: nil, default: (unset = true), **kw)
41
+ unless unset
42
+ version = DEFAULT_TO_INHERIT.include?(name) ? nil : 0.0r
43
+ defaults = { version => default }
44
+ end
45
+ defaults&.each_pair do |version, default|
46
+ AttrVersionDefaults.version_defaults[version] ||= {}
47
+ AttrVersionDefaults.version_defaults[version][name] = default
48
+ end
49
+ super(name, **kw)
50
+ end
51
+
52
+ def self.compile_default!
53
+ raise "Config.default already compiled" if Config.default
54
+ default = VERSIONS.select { _1 <= CURRENT_VERSION }
55
+ .filter_map { version_defaults[_1] }
56
+ .prepend(version_defaults.delete(nil))
57
+ .inject(&:merge)
58
+ Config.new(**default).freeze
59
+ end
60
+
61
+ def self.compile_version_defaults!
62
+ # Temporarily assign Config.default, enabling #load_defaults(:default)
63
+ version_defaults[:default] = Config.default
64
+ # Use #load_defaults so some attributes are inherited from global.
65
+ version_defaults[:default] = Config.new.load_defaults(:default).freeze
66
+ version_defaults[0.0r] = Config[version_defaults.fetch(0.0r)]
67
+
68
+ VERSIONS.each_cons(2) do |prior, version|
69
+ updates = version_defaults[version]
70
+ version_defaults[version] = version_defaults[prior]
71
+ .then { updates ? _1.dup.update(**updates).freeze : _1 }
72
+ end
73
+
74
+ # Safe conversions one way only:
75
+ # 0.6r.to_f == 0.6 # => true
76
+ # 0.6 .to_r == 0.6r # => false
77
+ version_defaults.to_a.each do |k, v|
78
+ next unless k in Rational
79
+ version_defaults[k.to_f] = v
80
+ end
81
+
82
+ version_defaults[:original] = Config[0.0r]
83
+ version_defaults[:current] = Config[CURRENT_VERSION]
84
+ version_defaults[:next] = Config[NEXT_VERSION]
85
+ version_defaults[:future] = Config[FUTURE_VERSION]
86
+
87
+ version_defaults.freeze
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "config/attr_accessors"
4
4
  require_relative "config/attr_inheritance"
5
5
  require_relative "config/attr_type_coercion"
6
+ require_relative "config/attr_version_defaults"
6
7
 
7
8
  module Net
8
9
  class IMAP
@@ -141,15 +142,7 @@ module Net
141
142
  # Net::IMAP::Config[0.5] == Net::IMAP::Config[0.5r] # => true
142
143
  # Net::IMAP::Config["current"] == Net::IMAP::Config[:current] # => true
143
144
  # Net::IMAP::Config["0.5.6"] == Net::IMAP::Config[0.5r] # => true
144
- def self.version_defaults; @version_defaults end
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
- }
145
+ def self.version_defaults; AttrVersionDefaults.version_defaults end
153
146
 
154
147
  # :call-seq:
155
148
  # Net::IMAP::Config[number] -> versioned config
@@ -189,6 +182,7 @@ module Net
189
182
  include AttrAccessors
190
183
  include AttrInheritance
191
184
  include AttrTypeCoercion
185
+ extend AttrVersionDefaults
192
186
 
193
187
  # The debug mode (boolean). The default value is +false+.
194
188
  #
@@ -200,7 +194,7 @@ module Net
200
194
  #
201
195
  # *NOTE:* Versioned default configs inherit #debug from Config.global, and
202
196
  # #load_defaults will not override #debug.
203
- attr_accessor :debug, type: :boolean
197
+ attr_accessor :debug, type: :boolean, default: false
204
198
 
205
199
  # method: debug?
206
200
  # :call-seq: debug? -> boolean
@@ -218,7 +212,7 @@ module Net
218
212
  # See Net::IMAP.new and Net::IMAP#starttls.
219
213
  #
220
214
  # The default value is +30+ seconds.
221
- attr_accessor :open_timeout, type: Integer
215
+ attr_accessor :open_timeout, type: Integer, default: 30
222
216
 
223
217
  # Seconds to wait until an IDLE response is received, after
224
218
  # the client asks to leave the IDLE state.
@@ -226,7 +220,7 @@ module Net
226
220
  # See Net::IMAP#idle and Net::IMAP#idle_done.
227
221
  #
228
222
  # The default value is +5+ seconds.
229
- attr_accessor :idle_response_timeout, type: Integer
223
+ attr_accessor :idle_response_timeout, type: Integer, default: 5
230
224
 
231
225
  # Whether to use the +SASL-IR+ extension when the server and \SASL
232
226
  # mechanism both support it. Can be overridden by the +sasl_ir+ keyword
@@ -242,7 +236,10 @@ module Net
242
236
  #
243
237
  # [+true+ <em>(default since +v0.4+)</em>]
244
238
  # Use +SASL-IR+ when it is supported by the server and the mechanism.
245
- attr_accessor :sasl_ir, type: :boolean
239
+ attr_accessor :sasl_ir, type: :boolean, defaults: {
240
+ 0.0r => false,
241
+ 0.4r => true,
242
+ }
246
243
 
247
244
  # Controls the behavior of Net::IMAP#login when the +LOGINDISABLED+
248
245
  # capability is present. When enforced, Net::IMAP will raise a
@@ -266,7 +263,10 @@ module Net
266
263
  #
267
264
  attr_accessor :enforce_logindisabled, type: Enum[
268
265
  false, :when_capabilities_cached, true
269
- ]
266
+ ], defaults: {
267
+ 0.0r => false,
268
+ 0.5r => true,
269
+ }
270
270
 
271
271
  # The maximum allowed server response size. When +nil+, there is no limit
272
272
  # on response size.
@@ -300,7 +300,10 @@ module Net
300
300
  #
301
301
  # * original: +nil+ <em>(no limit)</em>
302
302
  # * +0.5+: 512 MiB
303
- attr_accessor :max_response_size, type: Integer?
303
+ attr_accessor :max_response_size, type: Integer?, defaults: {
304
+ 0.0r => nil,
305
+ 0.5r => 512 << 20, # 512 MiB
306
+ }
304
307
 
305
308
  # Controls the behavior of Net::IMAP#responses when called without any
306
309
  # arguments (+type+ or +block+).
@@ -330,7 +333,11 @@ module Net
330
333
  # Note: #responses_without_args is an alias for #responses_without_block.
331
334
  attr_accessor :responses_without_block, type: Enum[
332
335
  :silence_deprecation_warning, :warn, :frozen_dup, :raise,
333
- ]
336
+ ], defaults: {
337
+ 0.0r => :silence_deprecation_warning,
338
+ 0.5r => :warn,
339
+ 0.6r => :frozen_dup,
340
+ }
334
341
 
335
342
  alias responses_without_args responses_without_block # :nodoc:
336
343
  alias responses_without_args= responses_without_block= # :nodoc:
@@ -375,7 +382,11 @@ module Net
375
382
  # ResponseParser _only_ uses AppendUIDData and CopyUIDData.
376
383
  attr_accessor :parser_use_deprecated_uidplus_data, type: Enum[
377
384
  true, :up_to_max_size, false
378
- ]
385
+ ], defaults: {
386
+ 0.0r => true,
387
+ 0.5r => :up_to_max_size,
388
+ 0.6r => false,
389
+ }
379
390
 
380
391
  # The maximum +uid-set+ size that ResponseParser will parse into
381
392
  # deprecated UIDPlusData. This limit only applies when
@@ -399,7 +410,13 @@ module Net
399
410
  # * +0.5+: <tt>100</tt>
400
411
  # * +0.6+: <tt>0</tt>
401
412
  #
402
- attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer
413
+ attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer,
414
+ defaults: {
415
+ 0.0r => 10_000,
416
+ 0.4r => 1_000,
417
+ 0.5r => 100,
418
+ 0.6r => 0,
419
+ }
403
420
 
404
421
  # Creates a new config object and initialize its attribute with +attrs+.
405
422
  #
@@ -474,79 +491,10 @@ module Net
474
491
  to_h.reject {|k,v| DEFAULT_TO_INHERIT.include?(k) }
475
492
  end
476
493
 
477
- @default = new(
478
- debug: false,
479
- open_timeout: 30,
480
- idle_response_timeout: 5,
481
- sasl_ir: true,
482
- enforce_logindisabled: true,
483
- max_response_size: 512 << 20, # 512 MiB
484
- responses_without_block: :warn,
485
- parser_use_deprecated_uidplus_data: :up_to_max_size,
486
- parser_max_deprecated_uidplus_data_size: 100,
487
- ).freeze
488
-
489
- @global = default.new
490
-
491
- version_defaults[:default] = Config[default.send(:defaults_hash)]
492
-
493
- version_defaults[0r] = Config[:default].dup.update(
494
- sasl_ir: false,
495
- responses_without_block: :silence_deprecation_warning,
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,
500
- ).freeze
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]
505
-
506
- version_defaults[0.4r] = Config[0.3r].dup.update(
507
- sasl_ir: true,
508
- parser_max_deprecated_uidplus_data_size: 1000,
509
- ).freeze
494
+ @default = AttrVersionDefaults.compile_default!
495
+ @global = default.new
496
+ AttrVersionDefaults.compile_version_defaults!
510
497
 
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
518
-
519
- version_defaults[0.6r] = Config[0.5r].dup.update(
520
- responses_without_block: :frozen_dup,
521
- parser_use_deprecated_uidplus_data: false,
522
- parser_max_deprecated_uidplus_data_size: 0,
523
- ).freeze
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]
541
-
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
550
498
  end
551
499
  end
552
500
  end
@@ -307,6 +307,14 @@ module Net
307
307
  # because the server doesn't allow deletion of mailboxes with children.
308
308
  # #data is +nil+.
309
309
  #
310
+ # === <tt>QUOTA=RES-*</tt> response codes
311
+ # See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
312
+ # * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
313
+ # with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
314
+ # the command would put the target mailbox over any quota, and with an
315
+ # untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
316
+ # be external events). #data is +nil+.
317
+ #
310
318
  # === +CONDSTORE+ extension
311
319
  # See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
312
320
  # * +NOMODSEQ+, when selecting a mailbox that does not support
@@ -384,14 +392,23 @@ module Net
384
392
  # and MailboxQuota objects.
385
393
  #
386
394
  # == Required capability
395
+ #
387
396
  # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
388
- # capability.
397
+ # or <tt>QUOTA=RES-STORAGE</tt>
398
+ # [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
389
399
  class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
390
400
  ##
391
401
  # method: mailbox
392
402
  # :call-seq: mailbox -> string
393
403
  #
394
- # The mailbox with the associated quota.
404
+ # The quota root with the associated quota.
405
+ #
406
+ # NOTE: this was mistakenly named "mailbox". But the quota root's name may
407
+ # differ from the mailbox. A single quota root may cover multiple
408
+ # mailboxes, and a single mailbox may be governed by multiple quota roots.
409
+
410
+ # The quota root with the associated quota.
411
+ alias quota_root mailbox
395
412
 
396
413
  ##
397
414
  # method: usage
@@ -403,7 +420,7 @@ module Net
403
420
  # method: quota
404
421
  # :call-seq: quota -> Integer
405
422
  #
406
- # Quota limit imposed on the mailbox.
423
+ # Storage limit imposed on the mailbox.
407
424
  #
408
425
  end
409
426
 
@@ -8,6 +8,7 @@ module Net
8
8
 
9
9
  def initialize(client, sock)
10
10
  @client, @sock = client, sock
11
+ @buff = @literal_size = nil
11
12
  end
12
13
 
13
14
  def read_response_buffer
@@ -15,13 +16,13 @@ module Net
15
16
  catch :eof do
16
17
  while true
17
18
  read_line
18
- break unless (@literal_size = get_literal_size)
19
+ break unless literal_size
19
20
  read_literal
20
21
  end
21
22
  end
22
23
  buff
23
24
  ensure
24
- @buff = nil
25
+ @buff = @literal_size = nil
25
26
  end
26
27
 
27
28
  private
@@ -30,13 +31,18 @@ module Net
30
31
 
31
32
  def bytes_read = buff.bytesize
32
33
  def empty? = buff.empty?
33
- def done? = line_done? && !get_literal_size
34
+ def done? = line_done? && !literal_size
34
35
  def line_done? = buff.end_with?(CRLF)
35
- def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
36
+
37
+ def get_literal_size(buff)
38
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
39
+ end
36
40
 
37
41
  def read_line
38
- buff << (@sock.gets(CRLF, read_limit) or throw :eof)
42
+ line = (@sock.gets(CRLF, read_limit) or throw :eof)
43
+ buff << line
39
44
  max_response_remaining! unless line_done?
45
+ @literal_size = get_literal_size(line)
40
46
  end
41
47
 
42
48
  def read_literal
@@ -75,13 +75,19 @@ module Net
75
75
  # * #password ― Password or passphrase associated with this #username.
76
76
  # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
77
77
  # * _optional_ #min_iterations - Overrides the default value (4096).
78
+ # * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
78
79
  #
79
80
  # Any other keyword parameters are quietly ignored.
81
+ #
82
+ # *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
83
+ # and maximum iteration counts that are appropriate for their security
84
+ # context.
80
85
  def initialize(username_arg = nil, password_arg = nil,
81
86
  authcid: nil, username: nil,
82
87
  authzid: nil,
83
88
  password: nil, secret: nil,
84
89
  min_iterations: 4096, # see both RFC5802 and RFC7677
90
+ max_iterations: 2**31 - 1, # max int32
85
91
  cnonce: nil, # must only be set in tests
86
92
  **options)
87
93
  @username = username || username_arg || authcid or
@@ -94,7 +100,22 @@ module Net
94
100
  @min_iterations.positive? or
95
101
  raise ArgumentError, "min_iterations must be positive"
96
102
 
103
+ @max_iterations = Integer max_iterations.to_int
104
+ @min_iterations <= @max_iterations or
105
+ raise ArgumentError, "max_iterations must be more than min_iterations"
106
+
97
107
  @cnonce = cnonce || SecureRandom.base64(32)
108
+
109
+ # These attrs are set from the server challenges
110
+ @server_first_message = @snonce = @salt = @iterations = nil
111
+ @server_error = nil
112
+
113
+ # Memoized after @salt and @iterations have been sent.
114
+ @salted_password = @client_key = @server_key = nil
115
+
116
+ # These values are created and cached in response to server challenges
117
+ @client_first_message_bare = nil
118
+ @client_final_message_without_proof = nil
98
119
  end
99
120
 
100
121
  # Authentication identity: the identity that matches the #password.
@@ -127,8 +148,43 @@ module Net
127
148
 
128
149
  # The minimal allowed iteration count. Lower #iterations will raise an
129
150
  # Error.
151
+ #
152
+ # *WARNING:* The default value (4096) is set to match guidance from
153
+ # both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
154
+ # and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
155
+ # {modern recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
156
+ # are significantly higher.
157
+ #
158
+ # It is ultimately the server's responsibility to securely store
159
+ # password hashes. While this parameter can alert the user to
160
+ # insecure password storage and prevent insecure authentication
161
+ # exchange, updating the iteration count generally requires resetting
162
+ # the password on the server.
130
163
  attr_reader :min_iterations
131
164
 
165
+ # The maximal allowed iteration count. Higher #iterations will raise an
166
+ # Error.
167
+ #
168
+ # As noted in {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
169
+ # >>>
170
+ # A hostile server can perform a computational denial-of-service
171
+ # attack on clients by sending a big iteration count value.
172
+ #
173
+ # *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
174
+ # 32-bit integer. This is large enough for the computation to take
175
+ # several minutes, and insufficient protection against hostile servers.
176
+ #
177
+ # Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
178
+ # blocking C function, and cannot be interrupted by +Timeout+ or
179
+ # <tt>Thread.raise</tt>. And it keeps the Global VM lock, as of v4.0 of
180
+ # the +openssl+ gem, so other ruby threads will not be able to run.
181
+ #
182
+ # <em>To prevent a denial of service attack,</em> this must be set to a
183
+ # safe value, depending on hardware and version of OpenSSL. <em>It is
184
+ # the user's responsibility</em> to enforce minimum and maximum
185
+ # iteration counts that are appropriate for their security context.
186
+ attr_reader :max_iterations
187
+
132
188
  # The client nonce, generated by SecureRandom
133
189
  attr_reader :cnonce
134
190
 
@@ -147,6 +203,15 @@ module Net
147
203
  # Net::IMAP::NoResponseError.
148
204
  attr_reader :server_error
149
205
 
206
+ # Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
207
+ def salted_password; @salted_password ||= compute_salted { super } end
208
+
209
+ # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
210
+ def client_key; @client_key ||= compute_salted { super } end
211
+
212
+ # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
213
+ def server_key; @server_key ||= compute_salted { super } end
214
+
150
215
  # Returns a new OpenSSL::Digest object, set to the appropriate hash
151
216
  # function for the chosen mechanism.
152
217
  #
@@ -186,6 +251,13 @@ module Net
186
251
 
187
252
  private
188
253
 
254
+ # Checks for +salt+ and +iterations+ before yielding
255
+ def compute_salted
256
+ salt in String or raise Error, "unknown salt"
257
+ iterations in Integer or raise Error, "unknown iterations"
258
+ yield
259
+ end
260
+
189
261
  # Need to store this for auth_message
190
262
  attr_reader :server_first_message
191
263
 
@@ -202,6 +274,8 @@ module Net
202
274
  raise Error, "server did not send iteration count"
203
275
  min_iterations <= iterations or
204
276
  raise Error, "too few iterations: %d" % [iterations]
277
+ max_iterations.nil? || iterations <= max_iterations or
278
+ raise Error, "too many iterations: %d" % [iterations]
205
279
  mext = sparams["m"] and
206
280
  raise Error, "mandatory extension: %p" % [mext]
207
281
  snonce.start_with? cnonce or
@@ -755,7 +755,13 @@ module Net
755
755
  # Related: #min, #minmax, #slice
756
756
  def max(count = nil, star: :*)
757
757
  if count
758
- slice(-[count, size].min..) || remain_frozen_empty
758
+ # n.b: #cardinality has not been backported to 0.5
759
+ cardinality = @tuples.sum(@tuples.count) { _2 - _1 }
760
+ if cardinality <= count
761
+ frozen? ? self : dup
762
+ else
763
+ slice(-count..) || remain_frozen_empty
764
+ end
759
765
  elsif (val = @tuples.last&.last)
760
766
  val == STAR_INT ? star : val
761
767
  end
@@ -1024,8 +1030,9 @@ module Net
1024
1030
  # Related: #delete, #delete_at, #subtract, #difference, #disjoint?
1025
1031
  def delete?(element)
1026
1032
  modifying! # short-circuit before input_to_tuple
1033
+ element = input_try_convert(element)
1027
1034
  tuple = input_to_tuple element
1028
- if tuple.first == tuple.last
1035
+ if number_input?(element)
1029
1036
  return unless include_tuple? tuple
1030
1037
  tuple_subtract tuple
1031
1038
  normalize!
@@ -1790,6 +1797,14 @@ module Net
1790
1797
  input
1791
1798
  end
1792
1799
 
1800
+ # NOTE: input_try_convert must be called on input first
1801
+ def number_input?(input)
1802
+ case input
1803
+ when *STARS, Integer then true
1804
+ when String then !input.include?(/[:,]/)
1805
+ end
1806
+ end
1807
+
1793
1808
  def range_to_tuple(range)
1794
1809
  first = to_tuple_int(range.begin || 1)
1795
1810
  last = to_tuple_int(range.end || :*)
data/lib/net/imap.rb CHANGED
@@ -462,6 +462,9 @@ module Net
462
462
  # +LITERAL-+, and +SPECIAL-USE+.</em>
463
463
  #
464
464
  # ==== RFC2087: +QUOTA+
465
+ # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
466
+ # - Obsoleted by <tt>QUOTA=RES-*</tt> [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]],
467
+ # although the commands are backward compatible.
465
468
  # - #getquota: returns the resource usage and limits for a quota root
466
469
  # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
467
470
  # their resource usage and limits.
@@ -578,6 +581,16 @@ module Net
578
581
  # See FetchData#emailid and FetchData#emailid.
579
582
  # - Updates #status with support for the +MAILBOXID+ status attribute.
580
583
  #
584
+ # ==== RFC9208: <tt>QUOTA=RES-*</tt>
585
+ # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
586
+ # - Obsoletes the +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
587
+ # extension and provides strict semantics for different resource types.
588
+ # - #getquota: returns the resource usage and limits for a quota root
589
+ # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
590
+ # their resource usage and limits.
591
+ # - #setquota: sets the resource limits for a given quota root.
592
+ # - Updates #status with <tt>"DELETED"</tt> and +DELETED-STORAGE+ attributes.
593
+ #
581
594
  # ==== RFC9394: +PARTIAL+
582
595
  # - Updates #search, #uid_search with the +PARTIAL+ return option which adds
583
596
  # ESearchResult#partial return data.
@@ -698,13 +711,12 @@ module Net
698
711
  #
699
712
  # === \IMAP Extensions
700
713
  #
701
- # [QUOTA[https://www.rfc-editor.org/rfc/rfc9208]]::
702
- # Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
703
- # March 2022, <https://www.rfc-editor.org/info/rfc9208>.
714
+ # [QUOTA[https://www.rfc-editor.org/rfc/rfc2087]]::
715
+ # Myers, J., "IMAP4 QUOTA extension", RFC 2087, DOI 10.17487/RFC2087,
716
+ # January 1997, <https://www.rfc-editor.org/info/rfc2087>.
704
717
  #
705
- # <em>Note: obsoletes</em>
706
- # RFC-2087[https://www.rfc-editor.org/rfc/rfc2087]<em> (January 1997)</em>.
707
- # <em>Net::IMAP does not fully support the RFC9208 updates yet.</em>
718
+ # *NOTE*: _obsoleted_ by RFC9208[https://www.rfc-editor.org/rfc/rfc9208]
719
+ # (March 2022).
708
720
  # [IDLE[https://www.rfc-editor.org/rfc/rfc2177]]::
709
721
  # Leiba, B., "IMAP4 IDLE command", RFC 2177, DOI 10.17487/RFC2177,
710
722
  # June 1997, <https://www.rfc-editor.org/info/rfc2177>.
@@ -756,6 +768,11 @@ module Net
756
768
  # Gondwana, B., Ed., "IMAP Extension for Object Identifiers",
757
769
  # RFC 8474, DOI 10.17487/RFC8474, September 2018,
758
770
  # <https://www.rfc-editor.org/info/rfc8474>.
771
+ # [{QUOTA=RES-*}[https://www.rfc-editor.org/rfc/rfc9208]]::
772
+ # Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
773
+ # March 2022, <https://www.rfc-editor.org/info/rfc9208>.
774
+ #
775
+ # Obsoletes RFC2087[https://www.rfc-editor.org/rfc/rfc2087].
759
776
  # [PARTIAL[https://www.rfc-editor.org/info/rfc9394]]::
760
777
  # Melnikov, A., Achuthan, A., Nagulakonda, V., and L. Alves,
761
778
  # "IMAP PARTIAL Extension for Paged SEARCH and FETCH", RFC 9394,
@@ -769,6 +786,7 @@ module Net
769
786
  #
770
787
  # === IANA registries
771
788
  # * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities]
789
+ # * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
772
790
  # * {IMAP Response Codes}[https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml]
773
791
  # * {IMAP Mailbox Name Attributes}[https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml]
774
792
  # * {IMAP and JMAP Keywords}[https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml]
@@ -779,8 +797,8 @@ module Net
779
797
  # * {GSSAPI/Kerberos/SASL Service Names}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml]:
780
798
  # +imap+
781
799
  # * {Character sets}[https://www.iana.org/assignments/character-sets/character-sets.xhtml]
800
+ #
782
801
  # ==== For currently unsupported features:
783
- # * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
784
802
  # * {LIST-EXTENDED options and responses}[https://www.iana.org/assignments/imap-list-extended/imap-list-extended.xhtml]
785
803
  # * {IMAP METADATA Server Entry and Mailbox Entry Registries}[https://www.iana.org/assignments/imap-metadata/imap-metadata.xhtml]
786
804
  # * {IMAP ANNOTATE Extension Entries and Attributes}[https://www.iana.org/assignments/imap-annotate-extension/imap-annotate-extension.xhtml]
@@ -788,7 +806,7 @@ module Net
788
806
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
789
807
  #
790
808
  class IMAP < Protocol
791
- VERSION = "0.5.12"
809
+ VERSION = "0.5.14"
792
810
 
793
811
  # Aliases for supported capabilities, to be used with the #enable command.
794
812
  ENABLE_ALIASES = {
@@ -1394,9 +1412,11 @@ module Net
1394
1412
  #
1395
1413
  def starttls(**options)
1396
1414
  @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
1415
+ handled = false
1397
1416
  error = nil
1398
1417
  ok = send_command("STARTTLS") do |resp|
1399
1418
  if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1419
+ handled = true
1400
1420
  clear_cached_capabilities
1401
1421
  clear_responses
1402
1422
  start_tls_session
@@ -1408,6 +1428,13 @@ module Net
1408
1428
  disconnect
1409
1429
  raise error
1410
1430
  end
1431
+ unless handled
1432
+ disconnect
1433
+ raise InvalidResponseError,
1434
+ "STARTTLS handler was bypassed, although server responded %p" % [
1435
+ ok.raw_data.chomp
1436
+ ]
1437
+ end
1411
1438
  ok
1412
1439
  end
1413
1440
 
@@ -1828,12 +1855,18 @@ module Net
1828
1855
  # to both admin and user. If this mailbox exists, it returns an array
1829
1856
  # containing objects of type MailboxQuotaRoot and MailboxQuota.
1830
1857
  #
1858
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1859
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1860
+ # with UntaggedResponse#raw_data.
1861
+ #
1831
1862
  # Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
1832
1863
  #
1833
1864
  # ==== Capabilities
1834
1865
  #
1835
- # The server's capabilities must include +QUOTA+
1836
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1866
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1867
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1868
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1869
+ # resource type.
1837
1870
  def getquotaroot(mailbox)
1838
1871
  synchronize do
1839
1872
  send_command("GETQUOTAROOT", mailbox)
@@ -1845,41 +1878,59 @@ module Net
1845
1878
  end
1846
1879
 
1847
1880
  # Sends a {GETQUOTA command [RFC2087 §4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
1848
- # along with specified +mailbox+. If this mailbox exists, then an array
1849
- # containing a MailboxQuota object is returned. This command is generally
1850
- # only available to server admin.
1881
+ # for the +quota_root+. If this quota root exists, then an array
1882
+ # containing a MailboxQuota object is returned.
1883
+ #
1884
+ # The names of quota roots that are applicable to a particular mailbox can
1885
+ # be discovered with #getquotaroot.
1886
+ #
1887
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1888
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1889
+ # with UntaggedResponse#raw_data.
1851
1890
  #
1852
1891
  # Related: #getquotaroot, #setquota, MailboxQuota
1853
1892
  #
1854
1893
  # ==== Capabilities
1855
1894
  #
1856
- # The server's capabilities must include +QUOTA+
1857
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1858
- def getquota(mailbox)
1895
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1896
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1897
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1898
+ # resource type.
1899
+ def getquota(quota_root)
1859
1900
  synchronize do
1860
- send_command("GETQUOTA", mailbox)
1901
+ send_command("GETQUOTA", quota_root)
1861
1902
  clear_responses("QUOTA")
1862
1903
  end
1863
1904
  end
1864
1905
 
1865
1906
  # Sends a {SETQUOTA command [RFC2087 §4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
1866
- # along with the specified +mailbox+ and +quota+. If +quota+ is nil, then
1867
- # +quota+ will be unset for that mailbox. Typically one needs to be logged
1868
- # in as a server admin for this to work.
1907
+ # along with the specified +quota_root+ and +storage_limit+. If
1908
+ # +storage_limit+ is +nil+, resource limits are unset for that quota root.
1909
+ # If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
1910
+ #
1911
+ # imap.setquota "#user/alice", 100
1912
+ # imap.getquota "#user/alice"
1913
+ # # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
1914
+ #
1915
+ # Typically one needs to be logged in as a server admin for this to work.
1916
+ #
1917
+ # *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
1869
1918
  #
1870
1919
  # Related: #getquota, #getquotaroot
1871
1920
  #
1872
1921
  # ==== Capabilities
1873
1922
  #
1874
- # The server's capabilities must include +QUOTA+
1875
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1876
- def setquota(mailbox, quota)
1877
- if quota.nil?
1878
- data = '()'
1923
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1924
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1925
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1926
+ # resource type.
1927
+ def setquota(quota_root, storage_limit)
1928
+ if storage_limit.nil?
1929
+ list = []
1879
1930
  else
1880
- data = '(STORAGE ' + quota.to_s + ')'
1931
+ list = ["STORAGE", Integer(storage_limit)]
1881
1932
  end
1882
- send_command("SETQUOTA", mailbox, RawData.new(data))
1933
+ send_command("SETQUOTA", quota_root, list)
1883
1934
  end
1884
1935
 
1885
1936
  # Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
@@ -1986,7 +2037,10 @@ module Net
1986
2037
  # <tt>STATUS=SIZE</tt>
1987
2038
  # {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
1988
2039
  #
1989
- # +DELETED+ requires the server's capabilities to include +IMAP4rev2+.
2040
+ # +DELETED+ must be supported when the server's capabilities includes
2041
+ # +IMAP4rev2+.
2042
+ # or <tt>QUOTA=RES-MESSAGES</tt>
2043
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
1990
2044
  #
1991
2045
  # +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
1992
2046
  # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
@@ -2267,11 +2321,11 @@ module Net
2267
2321
  # Encoded as an \IMAP date (see ::encode_date).
2268
2322
  #
2269
2323
  # [When +criteria+ is a String]
2270
- # +criteria+ will be sent directly to the server <em>without any
2271
- # validation or encoding</em>.
2324
+ # +criteria+ will be sent to the server <em>with minimal validation and no
2325
+ # encoding or formatting</em>.
2272
2326
  #
2273
- # <em>*WARNING:* This is vulnerable to injection attacks when external
2274
- # inputs are used.</em>
2327
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2328
+ # types of attribute injection attack if unvetted user input is used.</em>
2275
2329
  #
2276
2330
  # ==== Supported return options
2277
2331
  #
@@ -2592,6 +2646,13 @@ module Net
2592
2646
  #
2593
2647
  # +attr+ is a list of attributes to fetch; see FetchStruct documentation for
2594
2648
  # a list of supported attributes.
2649
+ # >>>
2650
+ # When +attr+ is a String, it will be sent <em>with minimal validation and
2651
+ # no encoding or formatting</em>. When +attr+ is an Array, each String in
2652
+ # +attr+ will be sent this way.
2653
+ #
2654
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2655
+ # types of attribute injection attack if unvetted user input is used.</em>
2595
2656
  #
2596
2657
  # +changedsince+ is an optional integer mod-sequence. It limits results to
2597
2658
  # messages with a mod-sequence greater than +changedsince+.
@@ -2675,6 +2736,7 @@ module Net
2675
2736
  # # fetch should return quickly and allocate little memory
2676
2737
  # results.size # => 0..500
2677
2738
  # break if results.empty?
2739
+ # results.sort_by!(&:uid) # server may return results out of order
2678
2740
  # next_uid_to_fetch = results.last.uid + 1
2679
2741
  # process results
2680
2742
  # end
@@ -3079,6 +3141,7 @@ module Net
3079
3141
 
3080
3142
  synchronize do
3081
3143
  tag = Thread.current[:net_imap_tag] = generate_tag
3144
+ guard_against_tagged_response_skipping_handler!(tag, "IDLE")
3082
3145
  put_string("#{tag} IDLE#{CRLF}")
3083
3146
 
3084
3147
  begin
@@ -3232,7 +3295,7 @@ module Net
3232
3295
  warn(RESPONSES_DEPRECATION_MSG, uplevel: 1, category: :deprecated)
3233
3296
  when :frozen_dup
3234
3297
  synchronize {
3235
- responses = @responses.transform_values(&:freeze)
3298
+ responses = @responses.transform_values { _1.dup.freeze }
3236
3299
  responses.default_proc = nil
3237
3300
  responses.default = [].freeze
3238
3301
  return responses.freeze
@@ -3543,6 +3606,7 @@ module Net
3543
3606
  put_string(" ")
3544
3607
  send_data(i, tag)
3545
3608
  end
3609
+ guard_against_tagged_response_skipping_handler!(tag, cmd)
3546
3610
  put_string(CRLF)
3547
3611
  if cmd == "LOGOUT"
3548
3612
  @logout_command_tag = tag
@@ -3558,6 +3622,19 @@ module Net
3558
3622
  end
3559
3623
  end
3560
3624
  end
3625
+ rescue InvalidResponseError
3626
+ disconnect
3627
+ raise
3628
+ end
3629
+
3630
+ def guard_against_tagged_response_skipping_handler!(tag, cmd)
3631
+ return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
3632
+ raise InvalidResponseError, format(
3633
+ "Received tagged 'OK' to incomplete %s command (tag=%s). " \
3634
+ "This could indicate a malicious server, a man-in-the-middle, or " \
3635
+ "client-side command injection. Disconnecting.",
3636
+ cmd, tag
3637
+ )
3561
3638
  end
3562
3639
 
3563
3640
  def generate_tag
@@ -3711,7 +3788,7 @@ module Net
3711
3788
  end
3712
3789
 
3713
3790
  def store_internal(cmd, set, attr, flags, unchangedsince: nil)
3714
- attr = RawData.new(attr) if attr.instance_of?(String)
3791
+ attr = Atom.new(attr) if attr.instance_of?(String)
3715
3792
  args = [SequenceSet.new(set)]
3716
3793
  args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
3717
3794
  args << attr << flags
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.12
4
+ version: 0.5.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -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/config/attr_version_defaults.rb
63
64
  - lib/net/imap/connection_state.rb
64
65
  - lib/net/imap/data_encoding.rb
65
66
  - lib/net/imap/data_lite.rb
@@ -129,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
130
  - !ruby/object:Gem::Version
130
131
  version: '0'
131
132
  requirements: []
132
- rubygems_version: 3.6.9
133
+ rubygems_version: 4.0.6
133
134
  specification_version: 4
134
135
  summary: Ruby client api for Internet Message Access Protocol
135
136
  test_files: []