net-imap 0.4.23 → 0.4.25

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: a15f49a20256632f692729c868dfb503e025f999bd2f520270674218869aae77
4
- data.tar.gz: dd5daa492ba441af22036909b46dc57b40ab6f1d26742f5487756af89befe858
3
+ metadata.gz: bf05b4f7aec9127531e83134b740620da216f847851f8ca464ef54e71e3431ad
4
+ data.tar.gz: 18361d6ee25983da0e4814d42a0ec851e10ce88789559e30778a945a7024a297
5
5
  SHA512:
6
- metadata.gz: 245c92501ec0e7b3d48dc59537f72cee04609c83c402bdbdc49c315cf021087e02180a2bf6ba165b9dbf3afd5d3998c4f7cd7112293143f1b188d9d87c2b2445
7
- data.tar.gz: b23b4c65e667f681d25511f611791b14654de6222f4dd3aad6dcb1b134f003332f6d0984135bbda0acef0448a4b288bbd4855f0c2c26910110efab1f3e15799e
6
+ metadata.gz: 697214c436f129746ad8986dde31e4b819854cf131472158c4aacf5a35ef665b8529528bd0a843ba17d9be105711b4163ecccfa525989d9fe0b3f807f4ae9d58
7
+ data.tar.gz: ef657a1dbfd5bc73fa317248ae19589a842f87eabfe966b43b026e9dceff68d510621d3e17136b266d22f68b85d20fc22b4ea67a482fdca193d8a5290c643abf
@@ -14,17 +14,19 @@ module Net
14
14
  when nil
15
15
  when String
16
16
  when Integer
17
- NumValidator.ensure_number(data)
17
+ # Covers modseq-valzer, which is the largest valid IMAP integer
18
+ if data.negative?
19
+ raise DataFormatError, "Integer argument must be unsigned: #{data}"
20
+ elsif 0xffff_ffff_ffff_ffff < data
21
+ raise DataFormatError, "Integer argument must fit in 64 bits: #{data}"
22
+ end
18
23
  when Array
19
- if data[0] == 'CHANGEDSINCE'
20
- NumValidator.ensure_mod_sequence_value(data[1])
21
- else
22
- data.each do |i|
23
- validate_data(i)
24
- end
24
+ data.each do |i|
25
+ validate_data(i)
25
26
  end
26
27
  when Time, Date, DateTime
27
28
  when Symbol
29
+ Flag.validate(data)
28
30
  else
29
31
  data.validate
30
32
  end
@@ -45,7 +47,7 @@ module Net
45
47
  when Date
46
48
  send_date_data(data)
47
49
  when Symbol
48
- send_symbol_data(data)
50
+ Flag[data].send_data(self, tag)
49
51
  else
50
52
  data.send_data(self, tag)
51
53
  end
@@ -77,9 +79,31 @@ module Net
77
79
  put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
78
80
  end
79
81
 
80
- def send_literal(str, tag = nil)
82
+ def send_binary_literal(*a, **kw) send_literal(*a, **kw, binary: true) end
83
+
84
+ # `non_sync` is an optional tri-state flag:
85
+ # * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
86
+ # NOTE: raises DataFormatError when server doesn't support
87
+ # non-synchronizing literal, or literal is too large for LITERAL-.
88
+ # * `false` -> Force normal synchronizing literal behavior.
89
+ # * `nil` -> (default) Currently behaves like `false` (will be dynamic).
90
+ # TODO: Dynamic, based on capabilities and bytesize.
91
+ def send_literal(str, tag = nil, binary: false, non_sync: nil)
92
+ bytesize = str.bytesize
81
93
  synchronize do
82
- put_string("{" + str.bytesize.to_s + "}" + CRLF)
94
+ if non_sync && !non_sync_literal_allowed?(bytesize)
95
+ # TODO: check in Printer, so we don't need to close the connection.
96
+ @sock.close
97
+ raise DataFormatError, "Connection closed: " \
98
+ "Cannot send non-synchronizing literal without known server support"
99
+ end
100
+ prefix = "~" if binary
101
+ plus = "+" if non_sync
102
+ put_string("#{prefix}{#{bytesize}#{plus}}\r\n")
103
+ if non_sync
104
+ put_string(str)
105
+ return
106
+ end
83
107
  @continued_command_tag = tag
84
108
  @continuation_request_exception = nil
85
109
  begin
@@ -94,8 +118,18 @@ module Net
94
118
  end
95
119
  end
96
120
 
121
+ def non_sync_literal_allowed?(bytesize)
122
+ return unless capabilities_cached?
123
+ return "+" if capable?("LITERAL+")
124
+ return "-" if capable_literal_minus? && bytesize <= 4096
125
+ false
126
+ end
127
+
128
+ def capable_literal_minus?; capable?("LITERAL-") || capable?("IMAP4rev2") end
129
+
130
+ # NOTE: +num+ should already be an Integer
97
131
  def send_number_data(num)
98
- put_string(num.to_s)
132
+ put_string(Integer(num).to_s)
99
133
  end
100
134
 
101
135
  def send_list_data(list, tag = nil)
@@ -115,67 +149,233 @@ module Net
115
149
  def send_date_data(date) put_string Net::IMAP.encode_date(date) end
116
150
  def send_time_data(time) put_string Net::IMAP.encode_time(time) end
117
151
 
118
- def send_symbol_data(symbol)
119
- put_string("\\" + symbol.to_s)
120
- end
152
+ # simplistic emulation of CommandData = Data.define(:data)
153
+ class CommandData # :nodoc:
154
+ class << self
155
+ def new(arg = nil, data: arg) super(data: data) end
156
+ alias :[] :new
157
+ end
158
+
159
+ def initialize(data:)
160
+ @data = data
161
+ freeze
162
+ end
163
+
164
+ attr_reader :data
165
+
166
+ def to_h(&block) block ? to_h.to_h(&block) : { data: data } end
167
+ def ==(other) self.class === other && to_h == other.to_h end
168
+ def eql?(other) self.class === other && to_h.eql?(other.to_h) end
169
+
170
+ # following class definition goes beyond the basic Data.define(:data)
171
+ ##
172
+
173
+ def self.validate(...)
174
+ data = new(...)
175
+ data.validate
176
+ data
177
+ end
121
178
 
122
- class RawData # :nodoc:
123
179
  def send_data(imap, tag)
124
- imap.__send__(:put_string, @data)
180
+ raise NoMethodError, "#{self.class} must implement #{__method__}"
125
181
  end
126
182
 
127
183
  def validate
128
184
  end
185
+ end
129
186
 
130
- private
187
+ # Represents IMAP +text+ or +quoted+ data, which share the same
188
+ # validations of decoded #data, and differ only in how they are formatted.
189
+ #
190
+ # +data+ may contain any 7-bit ASCII character except +NULL+, +CR+, or +LF+.
191
+ # Any multibyte +UTF-8+ character is also allowed when the connection
192
+ # supports UTF8: either +UTF8=ACCEPT+ or +IMAP4rev2+ have been enabled, or
193
+ # the server supports only +IMAP4rev2+ and not earlier IMAP revisions, or
194
+ # the server advertises +UTF8=ONLY+.
195
+ #
196
+ # NOTE: This does not verify whether the connection supports UTF-8, but that
197
+ # may change in future versions.
198
+ #
199
+ # The string's bytes must be valid ASCII or valid UTF-8. The string's
200
+ # reported encoding is ignored, but the string is _not_ transcoded.
201
+ class ValidNonLiteralData < CommandData
202
+ def initialize(data:)
203
+ data = String(data.to_str)
204
+ unless [Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
205
+ data = data.dup.force_encoding(data.ascii_only? ? "ASCII" : "UTF-8")
206
+ end
207
+ data = -data
208
+ super
209
+ validate
210
+ end
131
211
 
132
- def initialize(data)
133
- @data = data
212
+ def validate
213
+ if ![Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
214
+ raise DataFormatError, "must use ASCII or UTF-8 encoding"
215
+ elsif !data.valid_encoding?
216
+ raise DataFormatError, "invalid UTF-8 must be literal encoded"
217
+ elsif data.include?("\0")
218
+ raise DataFormatError, "NULL byte must be binary literal encoded"
219
+ elsif /[\r\n]/.match?(data)
220
+ raise DataFormatError, "CR and LF bytes must be literal encoded"
221
+ end
134
222
  end
223
+
224
+ def ascii_only?; data.ascii_only? end
225
+
226
+ def send_data(imap, tag = nil) imap.__send__(:put_string, formatted) end
135
227
  end
136
228
 
137
- class Atom # :nodoc:
138
- def send_data(imap, tag)
139
- imap.__send__(:put_string, @data)
229
+ # Represents IMAP +text+ data, which covers everything in the IMAP grammar,
230
+ # except for +literal+, +literal8+, and the concluding +CRLF+.
231
+ #
232
+ # NOTE: The current implementation does not verify that the connection
233
+ # supports UTF-8. Future versions may validate this.
234
+ class RawText < ValidNonLiteralData # :nodoc:
235
+ # raw: no formatting necessary
236
+ alias formatted data
237
+ end
238
+
239
+ class RawData < CommandData # :nodoc:
240
+ def initialize(data:)
241
+ case data
242
+ when String
243
+ data = self.class.split(data)
244
+ when Array
245
+ unless data.all? { |part| RawText === part || Literal === part }
246
+ raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
247
+ end
248
+ else
249
+ raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
250
+ end
251
+ super
252
+ validate
140
253
  end
141
254
 
255
+ def send_data(imap, tag) data.each do _1.send_data(imap, tag) end end
256
+
142
257
  def validate
258
+ return unless RawText === data.last
259
+ text = data.last.data
260
+ if text.rindex(/\{\d+\+?\}\z/n)
261
+ raise DataFormatError, "RawData cannot end with literal continuation"
262
+ end
143
263
  end
144
264
 
145
- private
265
+ # Splits an input +string+ into an array of RawText and Literal/Literal8.
266
+ #
267
+ # NOTE: unlike RawData#validate, this does not prevent the final RawText
268
+ # from ending with a literal prefix.
269
+ def self.split(data)
270
+ data = data.b # dups and ensures BINARY encoding
271
+ parts = []
272
+ while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
273
+ text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
274
+ bytesize = Integer bytesize, 10
275
+ parts << RawText[text] unless text.empty?
276
+ parts << extract_literal(data,
277
+ binary: binary,
278
+ bytesize: bytesize,
279
+ non_sync: non_sync)
280
+ data[0, bytesize] = ""
281
+ end
282
+ parts << RawText[data] unless data.empty?
283
+ parts
284
+ end
146
285
 
147
- def initialize(data)
148
- @data = data
286
+ def self.extract_literal(data, binary:, bytesize:, non_sync:)
287
+ if data.bytesize < bytesize
288
+ raise DataFormatError, "Too few bytes in string for literal, " \
289
+ "expected: %s, remaining: %s" % [bytesize, data.bytesize]
290
+ end
291
+ literal = data.byteslice(0, bytesize)
292
+ (binary ? Literal8 : Literal).new(data: literal, non_sync: non_sync)
149
293
  end
294
+ private_class_method :extract_literal
150
295
  end
151
296
 
152
- class QuotedString # :nodoc:
153
- def send_data(imap, tag)
154
- imap.__send__(:send_quoted_string, @data)
297
+ class Atom < CommandData # :nodoc:
298
+ def initialize(**)
299
+ super
300
+ validate
155
301
  end
156
302
 
157
303
  def validate
304
+ data.to_s.ascii_only? \
305
+ or raise DataFormatError, "#{self.class} must be ASCII only"
306
+ data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
307
+ and raise DataFormatError, "#{self.class} must not contain atom-specials"
308
+ data.empty? \
309
+ and raise DataFormatError, "#{self.class} must not be empty"
158
310
  end
159
311
 
160
- private
312
+ def send_data(imap, tag)
313
+ imap.__send__(:put_string, data.to_s)
314
+ end
315
+ end
161
316
 
162
- def initialize(data)
163
- @data = data
317
+ class Flag < Atom # :nodoc:
318
+ def send_data(imap, tag)
319
+ imap.__send__(:put_string, "\\#{data}")
164
320
  end
165
321
  end
166
322
 
323
+ # Represents a IMAP +quoted+ string, which can encode any valid ASCII or
324
+ # UTF-8 string, unless it contains any +CR+, +LF+, or +NULL+ bytes.
325
+ #
326
+ # NOTE: The current implementation does not verify that the connection
327
+ # supports UTF-8. Future versions may validate this.
328
+ class QuotedString < ValidNonLiteralData # :nodoc:
329
+ def formatted; %("#{data.gsub(/["\\]/, "\\\\\\&")}") end
330
+ end
331
+
167
332
  class Literal # :nodoc:
168
- def send_data(imap, tag)
169
- imap.__send__(:send_literal, @data, tag)
333
+ class << self
334
+ def new(_data = nil, _non_sync = nil, data: _data, non_sync: _non_sync)
335
+ super(data: data, non_sync: non_sync)
336
+ end
337
+ alias :[] :new
338
+ end
339
+
340
+ attr_reader :data, :non_sync
341
+
342
+ def to_h(&block) block ? to_h.to_h(&block) : { data: data, non_sync: non_sync } end
343
+ def ==(other) self.class === other && to_h == other.to_h end
344
+ def eql?(other) self.class === other && to_h.eql?(other.to_h) end
345
+
346
+ def initialize(data:, non_sync: nil)
347
+ data = -String(data.to_str).b or
348
+ raise DataFormatError, "#{self.class} expects string input"
349
+ @data, @non_sync = data, non_sync
350
+ validate
351
+ freeze
170
352
  end
171
353
 
354
+ def self.validate(...)
355
+ data = new(...)
356
+ data.validate
357
+ data
358
+ end
359
+
360
+ def bytesize; data.bytesize end
361
+
172
362
  def validate
363
+ if data.include?("\0")
364
+ raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
365
+ "Use #{Literal8} or a null-safe encoding."
366
+ end
173
367
  end
174
368
 
175
- private
369
+ def send_data(imap, tag)
370
+ imap.__send__(:send_literal, data, tag, non_sync: non_sync)
371
+ end
372
+ end
176
373
 
177
- def initialize(data)
178
- @data = data
374
+ class Literal8 < Literal # :nodoc:
375
+ def validate; nil end # all bytes are okay
376
+
377
+ def send_data(imap, tag)
378
+ imap.__send__(:send_binary_literal, data, tag, non_sync: non_sync)
179
379
  end
180
380
  end
181
381
 
@@ -295,6 +295,14 @@ module Net
295
295
  # because the server doesn't allow deletion of mailboxes with children.
296
296
  # #data is +nil+.
297
297
  #
298
+ # ==== <tt>QUOTA=RES-*</tt> response codes
299
+ # See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
300
+ # * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
301
+ # with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
302
+ # the command would put the target mailbox over any quota, and with an
303
+ # untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
304
+ # be external events). #data is +nil+.
305
+ #
298
306
  # ==== +CONDSTORE+ extension
299
307
  # See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
300
308
  # * +NOMODSEQ+, when selecting a mailbox that does not support
@@ -369,12 +377,24 @@ module Net
369
377
  # Net::IMAP#getquotaroot returns an array containing both MailboxQuotaRoot
370
378
  # and MailboxQuota objects.
371
379
  #
380
+ # ==== Required capability
381
+ #
382
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
383
+ # or <tt>QUOTA=RES-STORAGE</tt>
384
+ # [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
372
385
  class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
373
386
  ##
374
387
  # method: mailbox
375
388
  # :call-seq: mailbox -> string
376
389
  #
377
- # The mailbox with the associated quota.
390
+ # The quota root with the associated quota.
391
+ #
392
+ # NOTE: this was mistakenly named "mailbox". But the quota root's name may
393
+ # differ from the mailbox. A single quota root may cover multiple
394
+ # mailboxes, and a single mailbox may be governed by multiple quota roots.
395
+
396
+ # The quota root with the associated quota.
397
+ alias quota_root mailbox
378
398
 
379
399
  ##
380
400
  # method: usage
@@ -386,7 +406,7 @@ module Net
386
406
  # method: quota
387
407
  # :call-seq: quota -> Integer
388
408
  #
389
- # Quota limit imposed on the mailbox.
409
+ # Storage limit imposed on the mailbox.
390
410
  #
391
411
  end
392
412
 
@@ -2055,10 +2055,7 @@ module Net
2055
2055
  if $1
2056
2056
  return Token.new(T_SPACE, $+)
2057
2057
  elsif $2
2058
- len = $+.to_i
2059
- val = @str[@pos, len]
2060
- @pos += len
2061
- return Token.new(T_LITERAL8, val)
2058
+ literal_token($+, T_LITERAL8)
2062
2059
  elsif $3 && $7
2063
2060
  # greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
2064
2061
  return Token.new(T_ATOM, $3)
@@ -2086,10 +2083,7 @@ module Net
2086
2083
  elsif $15
2087
2084
  return Token.new(T_RBRA, $+)
2088
2085
  elsif $16
2089
- len = $+.to_i
2090
- val = @str[@pos, len]
2091
- @pos += len
2092
- return Token.new(T_LITERAL, val)
2086
+ literal_token($+)
2093
2087
  elsif $17
2094
2088
  return Token.new(T_PERCENT, $+)
2095
2089
  elsif $18
@@ -2115,10 +2109,7 @@ module Net
2115
2109
  elsif $4
2116
2110
  return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
2117
2111
  elsif $5
2118
- len = $+.to_i
2119
- val = @str[@pos, len]
2120
- @pos += len
2121
- return Token.new(T_LITERAL, val)
2112
+ literal_token($+)
2122
2113
  elsif $6
2123
2114
  return Token.new(T_LPAR, $+)
2124
2115
  elsif $7
@@ -2133,6 +2124,23 @@ module Net
2133
2124
  else
2134
2125
  parse_error("invalid @lex_state - %s", @lex_state.inspect)
2135
2126
  end
2127
+ rescue DataFormatError => error
2128
+ parse_error error.message
2129
+ end
2130
+
2131
+ def literal_token(len, type = T_LITERAL)
2132
+ len = coerce_number64 len.to_i
2133
+ val = @str[@pos, len]
2134
+ @pos += len
2135
+ Token.new(type, val)
2136
+ end
2137
+
2138
+ # copied/adapted from NumValidator in v0.6
2139
+ def coerce_number64(num)
2140
+ int = num.to_i
2141
+ return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
2142
+ raise DataFormatError,
2143
+ "number64 must be unsigned 63-bit integer: #{num}"
2136
2144
  end
2137
2145
 
2138
2146
  end
@@ -4,10 +4,13 @@ module Net
4
4
  class IMAP
5
5
  # See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2
6
6
  class ResponseReader # :nodoc:
7
+ include NumValidator
8
+
7
9
  attr_reader :client
8
10
 
9
11
  def initialize(client, sock)
10
12
  @client, @sock = client, sock
13
+ @buff = @literal_size = nil
11
14
  end
12
15
 
13
16
  def read_response_buffer
@@ -15,13 +18,13 @@ module Net
15
18
  catch :eof do
16
19
  while true
17
20
  read_line
18
- break unless (@literal_size = get_literal_size)
21
+ break unless literal_size
19
22
  read_literal
20
23
  end
21
24
  end
22
25
  buff
23
26
  ensure
24
- @buff = nil
27
+ @buff = @literal_size = nil
25
28
  end
26
29
 
27
30
  private
@@ -30,13 +33,21 @@ module Net
30
33
 
31
34
  def bytes_read; buff.bytesize end
32
35
  def empty?; buff.empty? end
33
- def done?; line_done? && !get_literal_size end
36
+ def done?; line_done? && !literal_size end
34
37
  def line_done?; buff.end_with?(CRLF) end
35
- def get_literal_size; /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i end
38
+
39
+ def get_literal_size(buff)
40
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) &&
41
+ coerce_number64($1)
42
+ rescue DataFormatError
43
+ raise DataFormatError, format("invalid response literal size (%s)", $1)
44
+ end
36
45
 
37
46
  def read_line
38
- buff << (@sock.gets(CRLF, read_limit) or throw :eof)
47
+ line = (@sock.gets(CRLF, read_limit) or throw :eof)
48
+ buff << line
39
49
  max_response_remaining! unless line_done?
50
+ @literal_size = get_literal_size(line)
40
51
  end
41
52
 
42
53
  def read_literal
@@ -70,6 +81,14 @@ module Net
70
81
  )
71
82
  end
72
83
 
84
+ # copied/adapted from NumValidator in v0.6
85
+ def coerce_number64(num)
86
+ int = num.to_i
87
+ return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
88
+ raise DataFormatError,
89
+ "number64 must be unsigned 63-bit integer: #{num}"
90
+ end
91
+
73
92
  end
74
93
  end
75
94
  end
@@ -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
+ String === salt or raise Error, "unknown salt"
257
+ Integer === iterations 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
data/lib/net/imap.rb CHANGED
@@ -460,6 +460,9 @@ module Net
460
460
  # +LITERAL-+, and +SPECIAL-USE+.</em>
461
461
  #
462
462
  # ==== RFC2087: +QUOTA+
463
+ # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
464
+ # - Obsoleted by <tt>QUOTA=RES-*</tt> [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]],
465
+ # although the commands are backward compatible.
463
466
  # - #getquota: returns the resource usage and limits for a quota root
464
467
  # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
465
468
  # their resource usage and limits.
@@ -572,6 +575,16 @@ module Net
572
575
  # See FetchData#emailid and FetchData#emailid.
573
576
  # - Updates #status with support for the +MAILBOXID+ status attribute.
574
577
  #
578
+ # ==== RFC9208: <tt>QUOTA=RES-*</tt>
579
+ # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
580
+ # - Obsoletes the +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
581
+ # extension and provides strict semantics for different resource types.
582
+ # - #getquota: returns the resource usage and limits for a quota root
583
+ # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
584
+ # their resource usage and limits.
585
+ # - #setquota: sets the resource limits for a given quota root.
586
+ # - Updates #status with <tt>"DELETED"</tt> and +DELETED-STORAGE+ attributes.
587
+ #
575
588
  # == References
576
589
  #
577
590
  # [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501.html]]::
@@ -681,14 +694,13 @@ module Net
681
694
  #
682
695
  # === \IMAP Extensions
683
696
  #
684
- # [QUOTA[https://tools.ietf.org/html/rfc9208]]::
685
- # Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
686
- # March 2022, <https://www.rfc-editor.org/info/rfc9208>.
697
+ # [QUOTA[https://www.rfc-editor.org/rfc/rfc2087]]::
698
+ # Myers, J., "IMAP4 QUOTA extension", RFC 2087, DOI 10.17487/RFC2087,
699
+ # January 1997, <https://www.rfc-editor.org/info/rfc2087>.
687
700
  #
688
- # <em>Note: obsoletes</em>
689
- # RFC-2087[https://tools.ietf.org/html/rfc2087]<em> (January 1997)</em>.
690
- # <em>Net::IMAP does not fully support the RFC9208 updates yet.</em>
691
- # [IDLE[https://tools.ietf.org/html/rfc2177]]::
701
+ # *NOTE*: _obsoleted_ by RFC9208[https://www.rfc-editor.org/rfc/rfc9208]
702
+ # (March 2022).
703
+ # [IDLE[https://www.rfc-editor.org/rfc/rfc2177]]::
692
704
  # Leiba, B., "IMAP4 IDLE command", RFC 2177, DOI 10.17487/RFC2177,
693
705
  # June 1997, <https://www.rfc-editor.org/info/rfc2177>.
694
706
  # [NAMESPACE[https://tools.ietf.org/html/rfc2342]]::
@@ -739,9 +751,15 @@ module Net
739
751
  # Gondwana, B., Ed., "IMAP Extension for Object Identifiers",
740
752
  # RFC 8474, DOI 10.17487/RFC8474, September 2018,
741
753
  # <https://www.rfc-editor.org/info/rfc8474>.
754
+ # [{QUOTA=RES-*}[https://www.rfc-editor.org/rfc/rfc9208]]::
755
+ # Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
756
+ # March 2022, <https://www.rfc-editor.org/info/rfc9208>.
757
+ #
758
+ # Obsoletes RFC2087[https://www.rfc-editor.org/rfc/rfc2087].
742
759
  #
743
760
  # === IANA registries
744
761
  # * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities]
762
+ # * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
745
763
  # * {IMAP Response Codes}[https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml]
746
764
  # * {IMAP Mailbox Name Attributes}[https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml]
747
765
  # * {IMAP and JMAP Keywords}[https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml]
@@ -761,7 +779,7 @@ module Net
761
779
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
762
780
  #
763
781
  class IMAP < Protocol
764
- VERSION = "0.4.23"
782
+ VERSION = "0.4.25"
765
783
 
766
784
  # Aliases for supported capabilities, to be used with the #enable command.
767
785
  ENABLE_ALIASES = {
@@ -1031,26 +1049,22 @@ module Net
1031
1049
 
1032
1050
  # Disconnects from the server.
1033
1051
  #
1052
+ # Waits for receiver thread to close before returning. Slow or stuck
1053
+ # response handlers can cause #disconnect to hang until they complete.
1054
+ #
1034
1055
  # Related: #logout, #logout!
1035
1056
  def disconnect
1036
1057
  return if disconnected?
1058
+ in_receiver_thread = Thread.current == @receiver_thread
1037
1059
  begin
1038
- begin
1039
- # try to call SSL::SSLSocket#io.
1040
- @sock.io.shutdown
1041
- rescue NoMethodError
1042
- # @sock is not an SSL::SSLSocket.
1043
- @sock.shutdown
1044
- end
1060
+ @sock.to_io.shutdown
1045
1061
  rescue Errno::ENOTCONN
1046
1062
  # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
1047
1063
  rescue Exception => e
1048
- @receiver_thread.raise(e)
1049
- end
1050
- @receiver_thread.join
1051
- synchronize do
1052
- @sock.close
1064
+ @receiver_thread.raise(e) unless in_receiver_thread
1053
1065
  end
1066
+ @sock.close
1067
+ @receiver_thread.join unless mon_owned? || in_receiver_thread
1054
1068
  raise e if e
1055
1069
  end
1056
1070
 
@@ -1294,9 +1308,11 @@ module Net
1294
1308
  #
1295
1309
  def starttls(**options)
1296
1310
  @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
1311
+ handled = false
1297
1312
  error = nil
1298
1313
  ok = send_command("STARTTLS") do |resp|
1299
1314
  if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1315
+ handled = true
1300
1316
  clear_cached_capabilities
1301
1317
  clear_responses
1302
1318
  start_tls_session
@@ -1308,6 +1324,13 @@ module Net
1308
1324
  disconnect
1309
1325
  raise error
1310
1326
  end
1327
+ unless handled
1328
+ disconnect
1329
+ raise InvalidResponseError,
1330
+ "STARTTLS handler was bypassed, although server responded %p" % [
1331
+ ok.raw_data.chomp
1332
+ ]
1333
+ end
1311
1334
  ok
1312
1335
  end
1313
1336
 
@@ -1742,12 +1765,18 @@ module Net
1742
1765
  # to both admin and user. If this mailbox exists, it returns an array
1743
1766
  # containing objects of type MailboxQuotaRoot and MailboxQuota.
1744
1767
  #
1768
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1769
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1770
+ # with UntaggedResponse#raw_data.
1771
+ #
1745
1772
  # Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
1746
1773
  #
1747
1774
  # ===== Capabilities
1748
1775
  #
1749
- # The server's capabilities must include +QUOTA+
1750
- # [RFC2087[https://tools.ietf.org/html/rfc2087]].
1776
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1777
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1778
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1779
+ # resource type.
1751
1780
  def getquotaroot(mailbox)
1752
1781
  synchronize do
1753
1782
  send_command("GETQUOTAROOT", mailbox)
@@ -1759,41 +1788,59 @@ module Net
1759
1788
  end
1760
1789
 
1761
1790
  # Sends a {GETQUOTA command [RFC2087 §4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
1762
- # along with specified +mailbox+. If this mailbox exists, then an array
1763
- # containing a MailboxQuota object is returned. This command is generally
1764
- # only available to server admin.
1791
+ # for the +quota_root+. If this quota root exists, then an array
1792
+ # containing a MailboxQuota object is returned.
1793
+ #
1794
+ # The names of quota roots that are applicable to a particular mailbox can
1795
+ # be discovered with #getquotaroot.
1796
+ #
1797
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1798
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1799
+ # with UntaggedResponse#raw_data.
1765
1800
  #
1766
1801
  # Related: #getquotaroot, #setquota, MailboxQuota
1767
1802
  #
1768
1803
  # ===== Capabilities
1769
1804
  #
1770
- # The server's capabilities must include +QUOTA+
1771
- # [RFC2087[https://tools.ietf.org/html/rfc2087]].
1772
- def getquota(mailbox)
1805
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1806
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1807
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1808
+ # resource type.
1809
+ def getquota(quota_root)
1773
1810
  synchronize do
1774
- send_command("GETQUOTA", mailbox)
1811
+ send_command("GETQUOTA", quota_root)
1775
1812
  clear_responses("QUOTA")
1776
1813
  end
1777
1814
  end
1778
1815
 
1779
1816
  # Sends a {SETQUOTA command [RFC2087 §4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
1780
- # along with the specified +mailbox+ and +quota+. If +quota+ is nil, then
1781
- # +quota+ will be unset for that mailbox. Typically one needs to be logged
1782
- # in as a server admin for this to work.
1817
+ # along with the specified +quota_root+ and +storage_limit+. If
1818
+ # +storage_limit+ is +nil+, resource limits are unset for that quota root.
1819
+ # If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
1820
+ #
1821
+ # imap.setquota "#user/alice", 100
1822
+ # imap.getquota "#user/alice"
1823
+ # # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
1824
+ #
1825
+ # Typically one needs to be logged in as a server admin for this to work.
1826
+ #
1827
+ # *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
1783
1828
  #
1784
1829
  # Related: #getquota, #getquotaroot
1785
1830
  #
1786
1831
  # ===== Capabilities
1787
1832
  #
1788
- # The server's capabilities must include +QUOTA+
1789
- # [RFC2087[https://tools.ietf.org/html/rfc2087]].
1790
- def setquota(mailbox, quota)
1791
- if quota.nil?
1792
- data = '()'
1833
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1834
+ # capability, or both +QUOTASET+ and a capability prefixed with
1835
+ # <tt>QUOTA=RES-*</tt> {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208]
1836
+ # for each supported resource type.
1837
+ def setquota(quota_root, storage_limit)
1838
+ if storage_limit.nil?
1839
+ list = []
1793
1840
  else
1794
- data = '(STORAGE ' + quota.to_s + ')'
1841
+ list = ["STORAGE", Integer(storage_limit)]
1795
1842
  end
1796
- send_command("SETQUOTA", mailbox, RawData.new(data))
1843
+ send_command("SETQUOTA", quota_root, list)
1797
1844
  end
1798
1845
 
1799
1846
  # Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
@@ -1900,7 +1947,10 @@ module Net
1900
1947
  # <tt>STATUS=SIZE</tt>
1901
1948
  # {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
1902
1949
  #
1903
- # +DELETED+ requires the server's capabilities to include +IMAP4rev2+.
1950
+ # +DELETED+ must be supported when the server's capabilities includes
1951
+ # +IMAP4rev2+.
1952
+ # or <tt>QUOTA=RES-MESSAGES</tt>
1953
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
1904
1954
  #
1905
1955
  # +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
1906
1956
  # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
@@ -2041,6 +2091,8 @@ module Net
2041
2091
  # string holding the entire search string, or a single-dimension array of
2042
2092
  # search keywords and arguments.
2043
2093
  #
2094
+ # <em>Please note</em> the warning (below) when +keys+ is a String.
2095
+ #
2044
2096
  # Returns a SearchResult object. SearchResult inherits from Array (for
2045
2097
  # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
2046
2098
  # capability has been enabled.
@@ -2049,6 +2101,14 @@ module Net
2049
2101
  #
2050
2102
  # ===== Search criteria
2051
2103
  #
2104
+ # >>>
2105
+ # When +keys+ is an Array, elements in the array will be validated and
2106
+ # formatted. When +keys+ is a String, it will be sent <em>with
2107
+ # minimal validation and no encoding or formatting</em>.
2108
+ #
2109
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2110
+ # types of attribute injection attack if unvetted user input is used.</em>
2111
+ #
2052
2112
  # For a full list of search criteria,
2053
2113
  # see [{IMAP4rev1 §6.4.4}[https://www.rfc-editor.org/rfc/rfc3501.html#section-6.4.4]],
2054
2114
  # or [{IMAP4rev2 §6.4.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-6.4.4]],
@@ -2114,7 +2174,8 @@ module Net
2114
2174
  # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
2115
2175
  # capability has been enabled.
2116
2176
  #
2117
- # See #search for documentation of search criteria.
2177
+ # See #search for documentation of parameters. <em>Please note</em> the
2178
+ # warning for when +keys+ is a String.
2118
2179
  def uid_search(keys, charset = nil)
2119
2180
  return search_internal("UID SEARCH", keys, charset)
2120
2181
  end
@@ -2136,6 +2197,13 @@ module Net
2136
2197
  #
2137
2198
  # +attr+ is a list of attributes to fetch; see the documentation
2138
2199
  # for FetchData for a list of valid attributes.
2200
+ # >>>
2201
+ # When +attr+ is a String, it will be sent <em>with minimal validation and
2202
+ # no encoding or formatting</em>. When +attr+ is an Array, each String in
2203
+ # +attr+ will be sent this way.
2204
+ #
2205
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2206
+ # types of attribute injection attack if unvetted user input is used.</em>
2139
2207
  #
2140
2208
  # +changedsince+ is an optional integer mod-sequence. It limits results to
2141
2209
  # messages with a mod-sequence greater than +changedsince+.
@@ -2184,6 +2252,8 @@ module Net
2184
2252
  # Similar to #fetch, but the +set+ parameter contains unique identifiers
2185
2253
  # instead of message sequence numbers.
2186
2254
  #
2255
+ # +attr+ behaves the same as with #fetch. <em>Please note</em> the #fetch
2256
+ # warning on the +attr+ argument.
2187
2257
  # >>>
2188
2258
  # *Note:* Servers _MUST_ implicitly include the +UID+ message data item as
2189
2259
  # part of any +FETCH+ response caused by a +UID+ command, regardless of
@@ -2336,8 +2406,10 @@ module Net
2336
2406
 
2337
2407
  # Sends a {SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2338
2408
  # to search a mailbox for messages that match +search_keys+ and return an
2339
- # array of message sequence numbers, sorted by +sort_keys+. +search_keys+
2340
- # are interpreted the same as for #search.
2409
+ # array of message sequence numbers, sorted by +sort_keys+.
2410
+ #
2411
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2412
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2341
2413
  #
2342
2414
  #--
2343
2415
  # TODO: describe +sort_keys+
@@ -2362,8 +2434,10 @@ module Net
2362
2434
 
2363
2435
  # Sends a {UID SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2364
2436
  # to search a mailbox for messages that match +search_keys+ and return an
2365
- # array of unique identifiers, sorted by +sort_keys+. +search_keys+ are
2366
- # interpreted the same as for #search.
2437
+ # array of unique identifiers, sorted by +sort_keys+.
2438
+ #
2439
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2440
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2367
2441
  #
2368
2442
  # Related: #sort, #search, #uid_search, #thread, #uid_thread
2369
2443
  #
@@ -2377,8 +2451,10 @@ module Net
2377
2451
 
2378
2452
  # Sends a {THREAD command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2379
2453
  # to search a mailbox and return message sequence numbers in threaded
2380
- # format, as a ThreadMember tree. +search_keys+ are interpreted the same as
2381
- # for #search.
2454
+ # format, as a ThreadMember tree.
2455
+ #
2456
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2457
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2382
2458
  #
2383
2459
  # The supported algorithms are:
2384
2460
  #
@@ -2404,6 +2480,9 @@ module Net
2404
2480
  # Similar to #thread, but returns unique identifiers instead of
2405
2481
  # message sequence numbers.
2406
2482
  #
2483
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2484
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2485
+ #
2407
2486
  # Related: #thread, #search, #uid_search, #sort, #uid_sort
2408
2487
  #
2409
2488
  # ===== Capabilities
@@ -2491,10 +2570,11 @@ module Net
2491
2570
  capabilities = capabilities
2492
2571
  .flatten
2493
2572
  .map {|e| ENABLE_ALIASES[e] || e }
2573
+ .flat_map { _1.is_a?(String) && !_1.empty? ? _1.split(/ /, -1) : [_1] }
2494
2574
  .uniq
2495
- .join(' ')
2575
+ .map { Atom[_1] }
2496
2576
  synchronize do
2497
- send_command("ENABLE #{capabilities}")
2577
+ send_command("ENABLE", *capabilities)
2498
2578
  result = clear_responses("ENABLED").last || []
2499
2579
  @utf8_strings ||= result.include? "UTF8=ACCEPT"
2500
2580
  @utf8_strings ||= result.include? "IMAP4REV2"
@@ -2992,6 +3072,7 @@ module Net
2992
3072
  put_string(" ")
2993
3073
  send_data(i, tag)
2994
3074
  end
3075
+ guard_against_tagged_response_skipping_handler!(tag)
2995
3076
  put_string(CRLF)
2996
3077
  if cmd == "LOGOUT"
2997
3078
  @logout_command_tag = tag
@@ -3007,6 +3088,17 @@ module Net
3007
3088
  end
3008
3089
  end
3009
3090
  end
3091
+ rescue InvalidResponseError
3092
+ disconnect
3093
+ raise
3094
+ end
3095
+
3096
+ def guard_against_tagged_response_skipping_handler!(tag)
3097
+ return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
3098
+ raise(InvalidResponseError,
3099
+ "Server sent tagged 'OK' before command was finished: %p. " \
3100
+ "This could indicate a malicious server or client-side " \
3101
+ "command injection. Disconnecting." % [resp.raw_data.chomp])
3010
3102
  end
3011
3103
 
3012
3104
  def generate_tag
@@ -3071,7 +3163,7 @@ module Net
3071
3163
  end
3072
3164
 
3073
3165
  def store_internal(cmd, set, attr, flags, unchangedsince: nil)
3074
- attr = RawData.new(attr) if attr.instance_of?(String)
3166
+ attr = Atom.new(attr) if attr.instance_of?(String)
3075
3167
  args = [MessageSet.new(set)]
3076
3168
  args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
3077
3169
  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.4.23
4
+ version: 0.4.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -125,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
125
  - !ruby/object:Gem::Version
126
126
  version: '0'
127
127
  requirements: []
128
- rubygems_version: 3.6.9
128
+ rubygems_version: 4.0.10
129
129
  specification_version: 4
130
130
  summary: Ruby client api for Internet Message Access Protocol
131
131
  test_files: []