net-imap 0.6.3 → 0.6.4.1

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.
@@ -75,9 +75,18 @@ module Net
75
75
  #
76
76
  # Net::IMAP::UnparsedData represents data for unknown response types or
77
77
  # unknown extensions to response types without a well-defined extension
78
- # grammar.
78
+ # grammar. UnparsedData represents the portion of the response which the
79
+ # parser has skipped over, without attempting to parse it.
79
80
  #
80
- # See also: UnparsedNumericResponseData, ExtensionData, IgnoredResponse
81
+ # parser = Net::IMAP::ResponseParser.new
82
+ # response = parser.parse "* X-UNKNOWN-TYPE can't parse this\r\n"
83
+ # response => Net::IMAP::UntaggedResponse(
84
+ # name: "X-UNKNOWN-TYPE",
85
+ # data: Net::IMAP::UnparsedData(unparsed_data: "can't parse this"),
86
+ # )
87
+ #
88
+ # See also: UnparsedNumericResponseData, ExtensionData, IgnoredResponse,
89
+ # InvalidParseData.
81
90
  class UnparsedData < Struct.new(:unparsed_data)
82
91
  ##
83
92
  # method: unparsed_data
@@ -86,6 +95,61 @@ module Net
86
95
  # The unparsed data
87
96
  end
88
97
 
98
+ # **Note:** This represents an intentionally _unstable_ API. Where
99
+ # instances of this class are returned, future releases may return a
100
+ # different (incompatible) object <em>without deprecation or warning</em>.
101
+ #
102
+ # When the response parser encounters a recoverable error,
103
+ # Net::IMAP::InvalidParseData represents that portion of the response which
104
+ # could not be parsed, allowing the parser to parse the remainder of the
105
+ # response. InvalidParseData is always associated with a ResponseParseError
106
+ # which has been rescued.
107
+ #
108
+ # This could be caused by a malformed server response, by a bug in
109
+ # Net::IMAP::ResponseParser, or by an unsupported extension to the response
110
+ # syntax. For example, if a server supports +UIDPLUS+, but sends an invalid
111
+ # +COPYUID+ response code:
112
+ #
113
+ # parser = Net::IMAP::ResponseParser.new
114
+ # parsed = parser.parse "* OK [COPYUID 701 ] copied one message\r\n"
115
+ # parsed => {
116
+ # data: Net::IMAP::ResponseText(
117
+ # code: Net::IMAP::ResponseCode(
118
+ # name: "COPYUID",
119
+ # data: Net::IMAP::InvalidParseData(
120
+ # parse_error: Net::IMAP::ResponseParseError,
121
+ # unparsed_data: "701 ",
122
+ # parsed_data: nil,
123
+ # )
124
+ # )
125
+ # )
126
+ # }
127
+ #
128
+ # In this example, although <tt>[COPYUID 701 ]</tt> uses valid syntax for a
129
+ # _generic_ ResponseCode, it is _invalid_ syntax for a +COPYUID+ response
130
+ # code.
131
+ #
132
+ # See also: UnparsedData, ExtensionData
133
+ class InvalidParseData < Data.define(:parse_error, :unparsed_data, :parsed_data)
134
+ ##
135
+ # method: parse_error
136
+ # :call-seq: parse_error -> ResponseParseError
137
+ #
138
+ # Returns the rescued ResponseParseError.
139
+
140
+ ##
141
+ # method: unparsed_data
142
+ # :call-seq: unparsed_data -> string
143
+ #
144
+ # Returns the raw string which was skipped over by the parser.
145
+
146
+ ##
147
+ # method: parsed_data
148
+ #
149
+ # May return a partial parse result for unparsed_data, which had already
150
+ # been parsed before the parse_error.
151
+ end
152
+
89
153
  # **Note:** This represents an intentionally _unstable_ API. Where
90
154
  # instances of this class are returned, future releases may return a
91
155
  # different (incompatible) object <em>without deprecation or warning</em>.
@@ -93,7 +157,17 @@ module Net
93
157
  # Net::IMAP::UnparsedNumericResponseData represents data for unhandled
94
158
  # response types with a numeric prefix. See the documentation for #number.
95
159
  #
96
- # See also: UnparsedData, ExtensionData, IgnoredResponse
160
+ # parser = Net::IMAP::ResponseParser.new
161
+ # response = parser.parse "* 123 X-UNKNOWN-TYPE can't parse this\r\n"
162
+ # response => Net::IMAP::UntaggedResponse(
163
+ # name: "X-UNKNOWN-TYPE",
164
+ # data: Net::IMAP::UnparsedNumericData(
165
+ # number: 123,
166
+ # unparsed_data: "can't parse this"
167
+ # ),
168
+ # )
169
+ #
170
+ # See also: UnparsedData, ExtensionData, IgnoredResponse, InvalidParseData
97
171
  class UnparsedNumericResponseData < Struct.new(:number, :unparsed_data)
98
172
  ##
99
173
  # method: number
@@ -306,6 +380,14 @@ module Net
306
380
  # because the server doesn't allow deletion of mailboxes with children.
307
381
  # #data is +nil+.
308
382
  #
383
+ # === <tt>QUOTA=RES-*</tt> response codes
384
+ # See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
385
+ # * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
386
+ # with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
387
+ # the command would put the target mailbox over any quota, and with an
388
+ # untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
389
+ # be external events). #data is +nil+.
390
+ #
309
391
  # === +CONDSTORE+ extension
310
392
  # See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
311
393
  # * +NOMODSEQ+, when selecting a mailbox that does not support
@@ -324,9 +406,10 @@ module Net
324
406
  #
325
407
  # Response codes are backwards compatible: Servers are allowed to send new
326
408
  # response codes even if the client has not enabled the extension that
327
- # defines them. When Net::IMAP does not know how to parse response
328
- # code text, #data returns the unparsed string.
329
- #
409
+ # defines them. When ResponseParser does not know how to parse the response
410
+ # code data, #data may return the unparsed string, ExtensionData, or
411
+ # UnparsedData. When ResponseParser attempts but fails to parse the
412
+ # response code data, #data returns InvalidParseData.
330
413
  class ResponseCode < Struct.new(:name, :data)
331
414
  ##
332
415
  # method: name
@@ -341,8 +424,13 @@ module Net
341
424
  #
342
425
  # Returns the parsed response code data, e.g: an array of capabilities
343
426
  # strings, an array of character set strings, a list of permanent flags,
344
- # an Integer, etc. The response #code determines what form the response
345
- # code data can take.
427
+ # an Integer, etc. The response #name determines what form the response
428
+ # code #data can take.
429
+ #
430
+ # When ResponseParser does not know how to parse the response code data,
431
+ # #data may return the unparsed string, ExtensionData, or UnparsedData.
432
+ # When ResponseParser attempts but fails to parse the response code data,
433
+ # #data returns InvalidParseData.
346
434
  end
347
435
 
348
436
  # MailboxList represents the data of an untagged +LIST+ response, for a
@@ -383,14 +471,23 @@ module Net
383
471
  # and MailboxQuota objects.
384
472
  #
385
473
  # == Required capability
474
+ #
386
475
  # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
387
- # capability.
476
+ # or <tt>QUOTA=RES-STORAGE</tt>
477
+ # [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
388
478
  class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
389
479
  ##
390
480
  # method: mailbox
391
481
  # :call-seq: mailbox -> string
392
482
  #
393
- # The mailbox with the associated quota.
483
+ # The quota root with the associated quota.
484
+ #
485
+ # NOTE: this was mistakenly named "mailbox". But the quota root's name may
486
+ # differ from the mailbox. A single quota root may cover multiple
487
+ # mailboxes, and a single mailbox may be governed by multiple quota roots.
488
+
489
+ # The quota root with the associated quota.
490
+ alias quota_root mailbox
394
491
 
395
492
  ##
396
493
  # method: usage
@@ -402,7 +499,7 @@ module Net
402
499
  # method: quota
403
500
  # :call-seq: quota -> Integer
404
501
  #
405
- # Quota limit imposed on the mailbox.
502
+ # Storage limit imposed on the mailbox.
406
503
  #
407
504
  end
408
505
 
@@ -691,6 +691,8 @@ module Net
691
691
  CRLF!
692
692
  EOF!
693
693
  resp
694
+ rescue SystemStackError
695
+ parse_error("response recursion too deep")
694
696
  end
695
697
 
696
698
  # RFC3501 & RFC9051:
@@ -1961,6 +1963,7 @@ module Net
1961
1963
  # resp-text-code =/ "UIDREQUIRED"
1962
1964
  def resp_text_code
1963
1965
  name = resp_text_code__name
1966
+ state = current_state
1964
1967
  data =
1965
1968
  case name
1966
1969
  when "CAPABILITY" then resp_code__capability
@@ -1983,8 +1986,18 @@ module Net
1983
1986
  when "MAILBOXID" then SP!; parens__objectid # RFC8474: OBJECTID
1984
1987
  when "UIDREQUIRED" then # RFC9586: UIDONLY
1985
1988
  else
1989
+ state = nil # don't backtrack
1986
1990
  SP? and text_chars_except_rbra
1987
1991
  end
1992
+ peek_rbra? or
1993
+ parse_error("expected resp-text-code %p to be complete", name)
1994
+ ResponseCode.new(name, data)
1995
+ rescue Net::IMAP::ResponseParseError => parse_error
1996
+ raise unless state
1997
+ raise if parse_error.message.include?("uid-set")
1998
+ restore_state state
1999
+ unparsed_data = SP? && text_chars_except_rbra
2000
+ data = InvalidParseData[parse_error:, unparsed_data:, parsed_data: data]
1988
2001
  ResponseCode.new(name, data)
1989
2002
  end
1990
2003
 
@@ -2199,10 +2212,7 @@ module Net
2199
2212
  if $1
2200
2213
  return Token.new(T_SPACE, $+)
2201
2214
  elsif $2
2202
- len = $+.to_i
2203
- val = @str[@pos, len]
2204
- @pos += len
2205
- return Token.new(T_LITERAL8, val)
2215
+ literal_token($+, T_LITERAL8)
2206
2216
  elsif $3 && $7
2207
2217
  # greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
2208
2218
  return Token.new(T_ATOM, $3)
@@ -2230,10 +2240,7 @@ module Net
2230
2240
  elsif $15
2231
2241
  return Token.new(T_RBRA, $+)
2232
2242
  elsif $16
2233
- len = $+.to_i
2234
- val = @str[@pos, len]
2235
- @pos += len
2236
- return Token.new(T_LITERAL, val)
2243
+ literal_token($+)
2237
2244
  elsif $17
2238
2245
  return Token.new(T_PERCENT, $+)
2239
2246
  elsif $18
@@ -2259,10 +2266,7 @@ module Net
2259
2266
  elsif $4
2260
2267
  return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
2261
2268
  elsif $5
2262
- len = $+.to_i
2263
- val = @str[@pos, len]
2264
- @pos += len
2265
- return Token.new(T_LITERAL, val)
2269
+ literal_token($+)
2266
2270
  elsif $6
2267
2271
  return Token.new(T_LPAR, $+)
2268
2272
  elsif $7
@@ -2277,6 +2281,15 @@ module Net
2277
2281
  else
2278
2282
  parse_error("invalid @lex_state - %s", @lex_state.inspect)
2279
2283
  end
2284
+ rescue DataFormatError => error
2285
+ parse_error error.message
2286
+ end
2287
+
2288
+ def literal_token(len, type = T_LITERAL)
2289
+ len = NumValidator.coerce_number64 len
2290
+ val = @str[@pos, len]
2291
+ @pos += len
2292
+ Token.new(type, val)
2280
2293
  end
2281
2294
 
2282
2295
  end
@@ -4,55 +4,69 @@ 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
+ # cached config
14
+ @max_response_size = nil
15
+ # response buffer state
16
+ @buff = @literal_size = nil
11
17
  end
12
18
 
13
19
  def read_response_buffer
20
+ @max_response_size = client.max_response_size
14
21
  @buff = String.new
15
22
  catch :eof do
16
23
  while true
24
+ guard_response_too_large!
17
25
  read_line
18
- break unless (@literal_size = get_literal_size)
26
+ # check before allocating memory for literal
27
+ guard_response_too_large!
28
+ break unless literal_size
19
29
  read_literal
20
30
  end
21
31
  end
22
32
  buff
23
33
  ensure
24
- @buff = nil
34
+ @buff = @literal_size = nil
25
35
  end
26
36
 
27
37
  private
28
38
 
39
+ # cached config
40
+ attr_reader :max_response_size
41
+
42
+ # response buffer state
29
43
  attr_reader :buff, :literal_size
30
44
 
31
45
  def bytes_read = buff.bytesize
32
46
  def empty? = buff.empty?
33
- def done? = line_done? && !get_literal_size
47
+ def done? = line_done? && !literal_size
34
48
  def line_done? = buff.end_with?(CRLF)
35
- def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
49
+
50
+ def get_literal_size(buff)
51
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) &&
52
+ coerce_number64($1)
53
+ rescue DataFormatError
54
+ raise DataFormatError, format("invalid response literal size (%s)", $1)
55
+ end
36
56
 
37
57
  def read_line
38
- buff << (@sock.gets(CRLF, read_limit) or throw :eof)
39
- max_response_remaining! unless line_done?
58
+ line = (@sock.gets(CRLF, max_response_remaining) or throw :eof)
59
+ @literal_size = get_literal_size(line)
60
+ buff << line
40
61
  end
41
62
 
42
63
  def read_literal
43
- # check before allocating memory for literal
44
- max_response_remaining!
45
64
  literal = String.new(capacity: literal_size)
46
- buff << (@sock.read(read_limit(literal_size), literal) or throw :eof)
65
+ buff << (@sock.read(literal_size, literal) or throw :eof)
47
66
  ensure
48
67
  @literal_size = nil
49
68
  end
50
69
 
51
- def read_limit(limit = nil)
52
- [limit, max_response_remaining!].compact.min
53
- end
54
-
55
- def max_response_size = client.max_response_size
56
70
  def max_response_remaining = max_response_size &.- bytes_read
57
71
  def response_too_large? = max_response_size &.< min_response_size
58
72
  def min_response_size = bytes_read + min_response_remaining
@@ -61,8 +75,8 @@ module Net
61
75
  empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
62
76
  end
63
77
 
64
- def max_response_remaining!
65
- return max_response_remaining unless response_too_large?
78
+ def guard_response_too_large!
79
+ return unless response_too_large?
66
80
  raise ResponseTooLargeError.new(
67
81
  max_response_size:, bytes_read:, literal_size:,
68
82
  )
@@ -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 }
208
+
209
+ # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
210
+ def client_key = @client_key ||= compute_salted { super }
211
+
212
+ # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
213
+ def server_key = @server_key ||= compute_salted { super }
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
@@ -123,7 +123,11 @@ module Net
123
123
  # Net::IMAP::SearchResult[9, 1, 2, 4, 10, 12, 3, modseq: 123_456]
124
124
  # .to_sequence_set
125
125
  # # => Net::IMAP::SequenceSet["1:4,9:10,12"]
126
- def to_sequence_set; SequenceSet[*self] end
126
+ #
127
+ # >>>
128
+ # *NOTE:* +SORT+ order is not preserved. The result will be sorted.
129
+ #
130
+ def to_sequence_set; empty? ? SequenceSet.empty : SequenceSet[to_a] end
127
131
 
128
132
  def pretty_print(pp)
129
133
  return super if modseq.nil?