net-imap 0.5.13 → 0.5.15

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: 1da7db7eb1545ee4a81555ef7f46c65e82e48ad8e4b64a4112881698df4ebc5d
4
- data.tar.gz: b3a668019d4d36c2a9cfc8d41c5e65d1054a630dc2c5cb8ea7050fbaaf264f6b
3
+ metadata.gz: '0917528437aff2a931fa3b40d476ffbad713cf46655163dc21a6a7a2333f7a49'
4
+ data.tar.gz: 90b3bf050a4f403408e30eecfaf449069e55bc293297b482e627201821997e0c
5
5
  SHA512:
6
- metadata.gz: 731921ff022993cd51ada037fa764067d4da07e6a8119545f7b225354b32f19c3936a8c807c3badd3664dde424c924842ecb0ac9851d74bb9bf568ebbbb800e6
7
- data.tar.gz: da993296fd57d59c56c483920d901c9b6b44329c0bb5812c495de2024f01e449b0117be5900013875902ff1101ac0dfeaf177f407ea34e69ae210bd87f91e86a
6
+ metadata.gz: c2aaf64941ac0537b7ba6da23ea501dacf438d9eb99581a2ca9906ad150bf5f55a7702141cb573e38306e3eb16000388acb4eb0dbc478727488bc21fa1c61288
7
+ data.tar.gz: 9d6b93b6c963e4e9c226044e1d8f9c4c5e09365ab57beaf1157128f85cc511402927d50033b0ad154b75914cf7191f2ec111dd240c566907203e4263a24f5e49
@@ -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
 
@@ -15,17 +17,19 @@ module Net
15
17
  when nil
16
18
  when String
17
19
  when Integer
18
- NumValidator.ensure_number(data)
20
+ # Covers modseq-valzer, which is the largest valid IMAP integer
21
+ if data.negative?
22
+ raise DataFormatError, "Integer argument must be unsigned: #{data}"
23
+ elsif 0xffff_ffff_ffff_ffff < data
24
+ raise DataFormatError, "Integer argument must fit in 64 bits: #{data}"
25
+ end
19
26
  when Array
20
- if data[0] == 'CHANGEDSINCE'
21
- NumValidator.ensure_mod_sequence_value(data[1])
22
- else
23
- data.each do |i|
24
- validate_data(i)
25
- end
27
+ data.each do |i|
28
+ validate_data(i)
26
29
  end
27
30
  when Time, Date, DateTime
28
31
  when Symbol
32
+ Flag.validate(data)
29
33
  else
30
34
  data.validate
31
35
  end
@@ -46,7 +50,7 @@ module Net
46
50
  when Date
47
51
  send_date_data(data)
48
52
  when Symbol
49
- send_symbol_data(data)
53
+ Flag[data].send_data(self, tag)
50
54
  else
51
55
  data.send_data(self, tag)
52
56
  end
@@ -78,9 +82,31 @@ module Net
78
82
  put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
79
83
  end
80
84
 
81
- def send_literal(str, tag = nil)
85
+ def send_binary_literal(*a, **kw); send_literal(*a, **kw, binary: true) end
86
+
87
+ # `non_sync` is an optional tri-state flag:
88
+ # * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
89
+ # NOTE: raises DataFormatError when server doesn't support
90
+ # non-synchronizing literal, or literal is too large for LITERAL-.
91
+ # * `false` -> Force normal synchronizing literal behavior.
92
+ # * `nil` -> (default) Currently behaves like `false` (will be dynamic).
93
+ # TODO: Dynamic, based on capabilities and bytesize.
94
+ def send_literal(str, tag = nil, binary: false, non_sync: nil)
95
+ bytesize = str.bytesize
82
96
  synchronize do
83
- put_string("{" + str.bytesize.to_s + "}" + CRLF)
97
+ if non_sync && !non_sync_literal_allowed?(bytesize)
98
+ # TODO: check in Printer, so we don't need to close the connection.
99
+ @sock.close
100
+ raise DataFormatError, "Connection closed: " \
101
+ "Cannot send non-synchronizing literal without known server support"
102
+ end
103
+ prefix = "~" if binary
104
+ plus = "+" if non_sync
105
+ put_string("#{prefix}{#{bytesize}#{plus}}\r\n")
106
+ if non_sync
107
+ put_string(str)
108
+ return
109
+ end
84
110
  @continued_command_tag = tag
85
111
  @continuation_request_exception = nil
86
112
  begin
@@ -95,8 +121,18 @@ module Net
95
121
  end
96
122
  end
97
123
 
124
+ def non_sync_literal_allowed?(bytesize)
125
+ return unless capabilities_cached?
126
+ return "+" if capable?("LITERAL+")
127
+ return "-" if capable_literal_minus? && bytesize <= 4096
128
+ false
129
+ end
130
+
131
+ def capable_literal_minus? = capable?("LITERAL-") || capable?("IMAP4rev2")
132
+
133
+ # NOTE: +num+ should already be an Integer
98
134
  def send_number_data(num)
99
- put_string(num.to_s)
135
+ put_string(Integer(num).to_s)
100
136
  end
101
137
 
102
138
  def send_list_data(list, tag = nil)
@@ -116,11 +152,13 @@ module Net
116
152
  def send_date_data(date) put_string Net::IMAP.encode_date(date) end
117
153
  def send_time_data(time) put_string Net::IMAP.encode_time(time) end
118
154
 
119
- def send_symbol_data(symbol)
120
- put_string("\\" + symbol.to_s)
121
- end
122
-
123
155
  CommandData = Data.define(:data) do # :nodoc:
156
+ def self.validate(...)
157
+ data = new(...)
158
+ data.validate
159
+ data
160
+ end
161
+
124
162
  def send_data(imap, tag)
125
163
  raise NoMethodError, "#{self.class} must implement #{__method__}"
126
164
  end
@@ -129,27 +167,176 @@ module Net
129
167
  end
130
168
  end
131
169
 
170
+ # Represents IMAP +text+ or +quoted+ data, which share the same
171
+ # validations of decoded #data, and differ only in how they are formatted.
172
+ #
173
+ # +data+ may contain any 7-bit ASCII character except +NULL+, +CR+, or +LF+.
174
+ # Any multibyte +UTF-8+ character is also allowed when the connection
175
+ # supports UTF8: either +UTF8=ACCEPT+ or +IMAP4rev2+ have been enabled, or
176
+ # the server supports only +IMAP4rev2+ and not earlier IMAP revisions, or
177
+ # the server advertises +UTF8=ONLY+.
178
+ #
179
+ # NOTE: This does not verify whether the connection supports UTF-8, but that
180
+ # may change in future versions.
181
+ #
182
+ # The string's bytes must be valid ASCII or valid UTF-8. The string's
183
+ # reported encoding is ignored, but the string is _not_ transcoded.
184
+ class ValidNonLiteralData < CommandData
185
+ def initialize(data:)
186
+ data = String(data.to_str)
187
+ unless data.encoding in Encoding::ASCII | Encoding::UTF_8
188
+ data = data.dup.force_encoding(data.ascii_only? ? "ASCII" : "UTF-8")
189
+ end
190
+ data = -data
191
+ super
192
+ validate
193
+ end
194
+
195
+ def validate
196
+ if !(data.encoding in Encoding::ASCII | Encoding::UTF_8)
197
+ raise DataFormatError, "must use ASCII or UTF-8 encoding"
198
+ elsif !data.valid_encoding?
199
+ raise DataFormatError, "invalid UTF-8 must be literal encoded"
200
+ elsif data.include?("\0")
201
+ raise DataFormatError, "NULL byte must be binary literal encoded"
202
+ elsif /[\r\n]/.match?(data)
203
+ raise DataFormatError, "CR and LF bytes must be literal encoded"
204
+ end
205
+ end
206
+
207
+ def ascii_only? = data.ascii_only?
208
+
209
+ def send_data(imap, tag = nil) = imap.__send__(:put_string, formatted)
210
+ end
211
+
212
+ # Represents IMAP +text+ data, which covers everything in the IMAP grammar,
213
+ # except for +literal+, +literal8+, and the concluding +CRLF+.
214
+ #
215
+ # NOTE: The current implementation does not verify that the connection
216
+ # supports UTF-8. Future versions may validate this.
217
+ class RawText < ValidNonLiteralData # :nodoc:
218
+ # raw: no formatting necessary
219
+ alias formatted data
220
+ end
221
+
132
222
  class RawData < CommandData # :nodoc:
133
- def send_data(imap, tag)
134
- imap.__send__(:put_string, data)
223
+ def initialize(data:)
224
+ case data
225
+ in String then data = self.class.split(data)
226
+ in Array if data.all? { _1 in RawText | Literal }
227
+ else
228
+ raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
229
+ end
230
+ super
231
+ validate
135
232
  end
233
+
234
+ def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
235
+
236
+ def validate
237
+ return unless data.last in RawText(data: text)
238
+ if text.rindex(/\{\d+\+?\}\z/n)
239
+ raise DataFormatError, "RawData cannot end with literal continuation"
240
+ end
241
+ end
242
+
243
+ # Splits an input +string+ into an array of RawText and Literal/Literal8.
244
+ #
245
+ # NOTE: unlike RawData#validate, this does not prevent the final RawText
246
+ # from ending with a literal prefix.
247
+ def self.split(data)
248
+ data = data.b # dups and ensures BINARY encoding
249
+ parts = []
250
+ while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
251
+ text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
252
+ bytesize = Integer bytesize, 10
253
+ parts << RawText[text] unless text.empty?
254
+ parts << extract_literal(data, binary:, bytesize:, non_sync:)
255
+ data[0, bytesize] = ""
256
+ end
257
+ parts << RawText[data] unless data.empty?
258
+ parts
259
+ end
260
+
261
+ def self.extract_literal(data, binary:, bytesize:, non_sync:)
262
+ if data.bytesize < bytesize
263
+ raise DataFormatError, "Too few bytes in string for literal, " \
264
+ "expected: %s, remaining: %s" % [bytesize, data.bytesize]
265
+ end
266
+ literal = data.byteslice(0, bytesize)
267
+ (binary ? Literal8 : Literal).new(data: literal, non_sync:)
268
+ end
269
+ private_class_method :extract_literal
136
270
  end
137
271
 
138
272
  class Atom < CommandData # :nodoc:
273
+ def initialize(**)
274
+ super
275
+ validate
276
+ end
277
+
278
+ def validate
279
+ data.to_s.ascii_only? \
280
+ or raise DataFormatError, "#{self.class} must be ASCII only"
281
+ data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
282
+ and raise DataFormatError, "#{self.class} must not contain atom-specials"
283
+ data.empty? \
284
+ and raise DataFormatError, "#{self.class} must not be empty"
285
+ end
286
+
139
287
  def send_data(imap, tag)
140
- imap.__send__(:put_string, data)
288
+ imap.__send__(:put_string, data.to_s)
141
289
  end
142
290
  end
143
291
 
144
- class QuotedString < CommandData # :nodoc:
292
+ class Flag < Atom # :nodoc:
145
293
  def send_data(imap, tag)
146
- imap.__send__(:send_quoted_string, data)
294
+ imap.__send__(:put_string, "\\#{data}")
147
295
  end
148
296
  end
149
297
 
150
- class Literal < CommandData # :nodoc:
298
+ # Represents a IMAP +quoted+ string, which can encode any valid ASCII or
299
+ # UTF-8 string, unless it contains any +CR+, +LF+, or +NULL+ bytes.
300
+ #
301
+ # NOTE: The current implementation does not verify that the connection
302
+ # supports UTF-8. Future versions may validate this.
303
+ class QuotedString < ValidNonLiteralData # :nodoc:
304
+ def formatted = %("#{data.gsub(/["\\]/, "\\\\\\&")}")
305
+ end
306
+
307
+ class Literal < Data.define(:data, :non_sync) # :nodoc:
308
+ def self.validate(...)
309
+ data = new(...)
310
+ data.validate
311
+ data
312
+ end
313
+
314
+ def initialize(data:, non_sync: nil)
315
+ data = -String(data.to_str).b or
316
+ raise DataFormatError, "#{self.class} expects string input"
317
+ super
318
+ validate
319
+ end
320
+
321
+ def bytesize; data.bytesize end
322
+
323
+ def validate
324
+ if data.include?("\0")
325
+ raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
326
+ "Use #{Literal8} or a null-safe encoding."
327
+ end
328
+ end
329
+
330
+ def send_data(imap, tag)
331
+ imap.__send__(:send_literal, data, tag, non_sync:)
332
+ end
333
+ end
334
+
335
+ class Literal8 < Literal # :nodoc:
336
+ def validate; nil end # all bytes are okay
337
+
151
338
  def send_data(imap, tag)
152
- imap.__send__(:send_literal, data, tag)
339
+ imap.__send__(:send_binary_literal, data, tag, non_sync:)
153
340
  end
154
341
  end
155
342
 
@@ -24,7 +24,7 @@ module Net
24
24
  VERSIONS = ((0.0r..FUTURE_VERSION) % 0.1r).to_a.freeze
25
25
 
26
26
  # See Config.version_defaults.
27
- singleton_class.attr_accessor :version_defaults
27
+ singleton_class.attr_reader :version_defaults
28
28
 
29
29
  @version_defaults = Hash.new {|h, k|
30
30
  # NOTE: String responds to both so the order is significant.
@@ -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
 
@@ -2189,10 +2189,7 @@ module Net
2189
2189
  if $1
2190
2190
  return Token.new(T_SPACE, $+)
2191
2191
  elsif $2
2192
- len = $+.to_i
2193
- val = @str[@pos, len]
2194
- @pos += len
2195
- return Token.new(T_LITERAL8, val)
2192
+ literal_token($+, T_LITERAL8)
2196
2193
  elsif $3 && $7
2197
2194
  # greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
2198
2195
  return Token.new(T_ATOM, $3)
@@ -2220,10 +2217,7 @@ module Net
2220
2217
  elsif $15
2221
2218
  return Token.new(T_RBRA, $+)
2222
2219
  elsif $16
2223
- len = $+.to_i
2224
- val = @str[@pos, len]
2225
- @pos += len
2226
- return Token.new(T_LITERAL, val)
2220
+ literal_token($+)
2227
2221
  elsif $17
2228
2222
  return Token.new(T_PERCENT, $+)
2229
2223
  elsif $18
@@ -2249,10 +2243,7 @@ module Net
2249
2243
  elsif $4
2250
2244
  return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
2251
2245
  elsif $5
2252
- len = $+.to_i
2253
- val = @str[@pos, len]
2254
- @pos += len
2255
- return Token.new(T_LITERAL, val)
2246
+ literal_token($+)
2256
2247
  elsif $6
2257
2248
  return Token.new(T_LPAR, $+)
2258
2249
  elsif $7
@@ -2267,6 +2258,23 @@ module Net
2267
2258
  else
2268
2259
  parse_error("invalid @lex_state - %s", @lex_state.inspect)
2269
2260
  end
2261
+ rescue DataFormatError => error
2262
+ parse_error error.message
2263
+ end
2264
+
2265
+ def literal_token(len, type = T_LITERAL)
2266
+ len = coerce_number64 len.to_i
2267
+ val = @str[@pos, len]
2268
+ @pos += len
2269
+ Token.new(type, val)
2270
+ end
2271
+
2272
+ # copied/adapted from NumValidator in v0.6
2273
+ def coerce_number64(num)
2274
+ int = num.to_i
2275
+ return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
2276
+ raise DataFormatError,
2277
+ "number64 must be unsigned 63-bit integer: #{num}"
2270
2278
  end
2271
2279
 
2272
2280
  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
32
35
  def empty? = buff.empty?
33
- def done? = line_done? && !get_literal_size
36
+ def done? = line_done? && !literal_size
34
37
  def line_done? = buff.end_with?(CRLF)
35
- def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
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
@@ -68,6 +79,14 @@ module Net
68
79
  )
69
80
  end
70
81
 
82
+ # copied/adapted from NumValidator in v0.6
83
+ def coerce_number64(num)
84
+ int = num.to_i
85
+ return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
86
+ raise DataFormatError,
87
+ "number64 must be unsigned 63-bit integer: #{num}"
88
+ end
89
+
71
90
  end
72
91
  end
73
92
  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
+ 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
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.13"
809
+ VERSION = "0.5.15"
792
810
 
793
811
  # Aliases for supported capabilities, to be used with the #enable command.
794
812
  ENABLE_ALIASES = {
@@ -1131,22 +1149,24 @@ module Net
1131
1149
 
1132
1150
  # Disconnects from the server.
1133
1151
  #
1134
- # Waits for receiver thread to close before returning. Slow or stuck
1135
- # response handlers can cause #disconnect to hang until they complete.
1152
+ # Waits for receiver thread to close before returning, except when called
1153
+ # from inside the connection mutex such as from a response handler. Slow or
1154
+ # stuck response handlers can cause #disconnect to hang until they complete.
1136
1155
  #
1137
1156
  # Related: #logout, #logout!
1138
1157
  def disconnect
1139
1158
  in_logout_state = try_state_logout?
1140
1159
  return if disconnected?
1160
+ in_receiver_thread = Thread.current == @receiver_thread
1141
1161
  begin
1142
1162
  @sock.to_io.shutdown
1143
1163
  rescue Errno::ENOTCONN
1144
1164
  # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
1145
1165
  rescue Exception => e
1146
- @receiver_thread.raise(e)
1166
+ @receiver_thread.raise(e) unless in_receiver_thread
1147
1167
  end
1148
1168
  @sock.close
1149
- @receiver_thread.join
1169
+ @receiver_thread.join unless mon_owned? || in_receiver_thread
1150
1170
  raise e if e
1151
1171
  ensure
1152
1172
  # Try again after shutting down the receiver thread. With no reciever
@@ -1394,9 +1414,11 @@ module Net
1394
1414
  #
1395
1415
  def starttls(**options)
1396
1416
  @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
1417
+ handled = false
1397
1418
  error = nil
1398
1419
  ok = send_command("STARTTLS") do |resp|
1399
1420
  if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1421
+ handled = true
1400
1422
  clear_cached_capabilities
1401
1423
  clear_responses
1402
1424
  start_tls_session
@@ -1408,6 +1430,13 @@ module Net
1408
1430
  disconnect
1409
1431
  raise error
1410
1432
  end
1433
+ unless handled
1434
+ disconnect
1435
+ raise InvalidResponseError,
1436
+ "STARTTLS handler was bypassed, although server responded %p" % [
1437
+ ok.raw_data.chomp
1438
+ ]
1439
+ end
1411
1440
  ok
1412
1441
  end
1413
1442
 
@@ -1828,12 +1857,18 @@ module Net
1828
1857
  # to both admin and user. If this mailbox exists, it returns an array
1829
1858
  # containing objects of type MailboxQuotaRoot and MailboxQuota.
1830
1859
  #
1860
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1861
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1862
+ # with UntaggedResponse#raw_data.
1863
+ #
1831
1864
  # Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
1832
1865
  #
1833
1866
  # ==== Capabilities
1834
1867
  #
1835
- # The server's capabilities must include +QUOTA+
1836
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1868
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1869
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1870
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1871
+ # resource type.
1837
1872
  def getquotaroot(mailbox)
1838
1873
  synchronize do
1839
1874
  send_command("GETQUOTAROOT", mailbox)
@@ -1845,41 +1880,59 @@ module Net
1845
1880
  end
1846
1881
 
1847
1882
  # 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.
1883
+ # for the +quota_root+. If this quota root exists, then an array
1884
+ # containing a MailboxQuota object is returned.
1885
+ #
1886
+ # The names of quota roots that are applicable to a particular mailbox can
1887
+ # be discovered with #getquotaroot.
1888
+ #
1889
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1890
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1891
+ # with UntaggedResponse#raw_data.
1851
1892
  #
1852
1893
  # Related: #getquotaroot, #setquota, MailboxQuota
1853
1894
  #
1854
1895
  # ==== Capabilities
1855
1896
  #
1856
- # The server's capabilities must include +QUOTA+
1857
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1858
- def getquota(mailbox)
1897
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1898
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1899
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1900
+ # resource type.
1901
+ def getquota(quota_root)
1859
1902
  synchronize do
1860
- send_command("GETQUOTA", mailbox)
1903
+ send_command("GETQUOTA", quota_root)
1861
1904
  clear_responses("QUOTA")
1862
1905
  end
1863
1906
  end
1864
1907
 
1865
1908
  # 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.
1909
+ # along with the specified +quota_root+ and +storage_limit+. If
1910
+ # +storage_limit+ is +nil+, resource limits are unset for that quota root.
1911
+ # If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
1912
+ #
1913
+ # imap.setquota "#user/alice", 100
1914
+ # imap.getquota "#user/alice"
1915
+ # # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
1916
+ #
1917
+ # Typically one needs to be logged in as a server admin for this to work.
1918
+ #
1919
+ # *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
1869
1920
  #
1870
1921
  # Related: #getquota, #getquotaroot
1871
1922
  #
1872
1923
  # ==== Capabilities
1873
1924
  #
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 = '()'
1925
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1926
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1927
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1928
+ # resource type.
1929
+ def setquota(quota_root, storage_limit)
1930
+ if storage_limit.nil?
1931
+ list = []
1879
1932
  else
1880
- data = '(STORAGE ' + quota.to_s + ')'
1933
+ list = ["STORAGE", Integer(storage_limit)]
1881
1934
  end
1882
- send_command("SETQUOTA", mailbox, RawData.new(data))
1935
+ send_command("SETQUOTA", quota_root, list)
1883
1936
  end
1884
1937
 
1885
1938
  # Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
@@ -1986,7 +2039,10 @@ module Net
1986
2039
  # <tt>STATUS=SIZE</tt>
1987
2040
  # {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
1988
2041
  #
1989
- # +DELETED+ requires the server's capabilities to include +IMAP4rev2+.
2042
+ # +DELETED+ must be supported when the server's capabilities includes
2043
+ # +IMAP4rev2+.
2044
+ # or <tt>QUOTA=RES-MESSAGES</tt>
2045
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
1990
2046
  #
1991
2047
  # +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
1992
2048
  # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
@@ -2155,6 +2211,7 @@ module Net
2155
2211
  # provided as an array or a string.
2156
2212
  # See {"Argument translation"}[rdoc-ref:#search@Argument+translation]
2157
2213
  # and {"Search criteria"}[rdoc-ref:#search@Search+criteria], below.
2214
+ # <em>Please note</em> the warning for when +criteria+ is a String.
2158
2215
  #
2159
2216
  # +return+ options control what kind of information is returned about
2160
2217
  # messages matching the search +criteria+. Specifying +return+ should force
@@ -2267,11 +2324,11 @@ module Net
2267
2324
  # Encoded as an \IMAP date (see ::encode_date).
2268
2325
  #
2269
2326
  # [When +criteria+ is a String]
2270
- # +criteria+ will be sent directly to the server <em>without any
2271
- # validation or encoding</em>.
2327
+ # +criteria+ will be sent to the server <em>with minimal validation and no
2328
+ # encoding or formatting</em>.
2272
2329
  #
2273
- # <em>*WARNING:* This is vulnerable to injection attacks when external
2274
- # inputs are used.</em>
2330
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2331
+ # types of attribute injection attack if unvetted user input is used.</em>
2275
2332
  #
2276
2333
  # ==== Supported return options
2277
2334
  #
@@ -2565,7 +2622,8 @@ module Net
2565
2622
  # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
2566
2623
  # capability has been enabled.
2567
2624
  #
2568
- # See #search for documentation of parameters.
2625
+ # See #search for documentation of parameters. <em>Please note</em> the
2626
+ # warning for when +criteria+ is a String.
2569
2627
  #
2570
2628
  # ==== Capabilities
2571
2629
  #
@@ -2592,6 +2650,13 @@ module Net
2592
2650
  #
2593
2651
  # +attr+ is a list of attributes to fetch; see FetchStruct documentation for
2594
2652
  # a list of supported attributes.
2653
+ # >>>
2654
+ # When +attr+ is a String, it will be sent <em>with minimal validation and
2655
+ # no encoding or formatting</em>. When +attr+ is an Array, each String in
2656
+ # +attr+ will be sent this way.
2657
+ #
2658
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2659
+ # types of attribute injection attack if unvetted user input is used.</em>
2595
2660
  #
2596
2661
  # +changedsince+ is an optional integer mod-sequence. It limits results to
2597
2662
  # messages with a mod-sequence greater than +changedsince+.
@@ -2644,7 +2709,8 @@ module Net
2644
2709
  # {SequenceSet[...]}[rdoc-ref:SequenceSet@Creating+sequence+sets].
2645
2710
  # (For message sequence numbers, use #fetch instead.)
2646
2711
  #
2647
- # +attr+ behaves the same as with #fetch.
2712
+ # +attr+ behaves the same as with #fetch. <em>Please note</em> the #fetch
2713
+ # warning on the +attr+ argument.
2648
2714
  # >>>
2649
2715
  # *Note:* Servers _MUST_ implicitly include the +UID+ message data item as
2650
2716
  # part of any +FETCH+ response caused by a +UID+ command, regardless of
@@ -2856,8 +2922,10 @@ module Net
2856
2922
 
2857
2923
  # Sends a {SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2858
2924
  # to search a mailbox for messages that match +search_keys+ and return an
2859
- # array of message sequence numbers, sorted by +sort_keys+. +search_keys+
2860
- # are interpreted the same as for #search.
2925
+ # array of message sequence numbers, sorted by +sort_keys+.
2926
+ #
2927
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2928
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2861
2929
  #
2862
2930
  #--
2863
2931
  # TODO: describe +sort_keys+
@@ -2882,8 +2950,10 @@ module Net
2882
2950
 
2883
2951
  # Sends a {UID SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2884
2952
  # to search a mailbox for messages that match +search_keys+ and return an
2885
- # array of unique identifiers, sorted by +sort_keys+. +search_keys+ are
2886
- # interpreted the same as for #search.
2953
+ # array of unique identifiers, sorted by +sort_keys+.
2954
+ #
2955
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2956
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2887
2957
  #
2888
2958
  # Related: #sort, #search, #uid_search, #thread, #uid_thread
2889
2959
  #
@@ -2897,8 +2967,10 @@ module Net
2897
2967
 
2898
2968
  # Sends a {THREAD command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2899
2969
  # to search a mailbox and return message sequence numbers in threaded
2900
- # format, as a ThreadMember tree. +search_keys+ are interpreted the same as
2901
- # for #search.
2970
+ # format, as a ThreadMember tree.
2971
+ #
2972
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2973
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2902
2974
  #
2903
2975
  # The supported algorithms are:
2904
2976
  #
@@ -2924,6 +2996,9 @@ module Net
2924
2996
  # Similar to #thread, but returns unique identifiers instead of
2925
2997
  # message sequence numbers.
2926
2998
  #
2999
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
3000
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
3001
+ #
2927
3002
  # Related: #thread, #search, #uid_search, #sort, #uid_sort
2928
3003
  #
2929
3004
  # ==== Capabilities
@@ -3033,10 +3108,11 @@ module Net
3033
3108
  capabilities = capabilities
3034
3109
  .flatten
3035
3110
  .map {|e| ENABLE_ALIASES[e] || e }
3111
+ .flat_map { _1.is_a?(String) && !_1.empty? ? _1.split(/ /, -1) : [_1] }
3036
3112
  .uniq
3037
- .join(' ')
3113
+ .map { Atom[_1] }
3038
3114
  synchronize do
3039
- send_command("ENABLE #{capabilities}")
3115
+ send_command("ENABLE", *capabilities)
3040
3116
  result = clear_responses("ENABLED").last || []
3041
3117
  @utf8_strings ||= result.include? "UTF8=ACCEPT"
3042
3118
  @utf8_strings ||= result.include? "IMAP4REV2"
@@ -3080,6 +3156,7 @@ module Net
3080
3156
 
3081
3157
  synchronize do
3082
3158
  tag = Thread.current[:net_imap_tag] = generate_tag
3159
+ guard_against_tagged_response_skipping_handler!(tag, "IDLE")
3083
3160
  put_string("#{tag} IDLE#{CRLF}")
3084
3161
 
3085
3162
  begin
@@ -3544,6 +3621,7 @@ module Net
3544
3621
  put_string(" ")
3545
3622
  send_data(i, tag)
3546
3623
  end
3624
+ guard_against_tagged_response_skipping_handler!(tag, cmd)
3547
3625
  put_string(CRLF)
3548
3626
  if cmd == "LOGOUT"
3549
3627
  @logout_command_tag = tag
@@ -3559,6 +3637,19 @@ module Net
3559
3637
  end
3560
3638
  end
3561
3639
  end
3640
+ rescue InvalidResponseError
3641
+ disconnect
3642
+ raise
3643
+ end
3644
+
3645
+ def guard_against_tagged_response_skipping_handler!(tag, cmd)
3646
+ return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
3647
+ raise InvalidResponseError, format(
3648
+ "Received tagged 'OK' to incomplete %s command (tag=%s). " \
3649
+ "This could indicate a malicious server, a man-in-the-middle, or " \
3650
+ "client-side command injection. Disconnecting.",
3651
+ cmd, tag
3652
+ )
3562
3653
  end
3563
3654
 
3564
3655
  def generate_tag
@@ -3712,7 +3803,7 @@ module Net
3712
3803
  end
3713
3804
 
3714
3805
  def store_internal(cmd, set, attr, flags, unchangedsince: nil)
3715
- attr = RawData.new(attr) if attr.instance_of?(String)
3806
+ attr = Atom.new(attr) if attr.instance_of?(String)
3716
3807
  args = [SequenceSet.new(set)]
3717
3808
  args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
3718
3809
  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.13
4
+ version: 0.5.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -130,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
130
  - !ruby/object:Gem::Version
131
131
  version: '0'
132
132
  requirements: []
133
- rubygems_version: 3.6.9
133
+ rubygems_version: 4.0.10
134
134
  specification_version: 4
135
135
  summary: Ruby client api for Internet Message Access Protocol
136
136
  test_files: []