net-imap 0.5.13 → 0.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1da7db7eb1545ee4a81555ef7f46c65e82e48ad8e4b64a4112881698df4ebc5d
4
- data.tar.gz: b3a668019d4d36c2a9cfc8d41c5e65d1054a630dc2c5cb8ea7050fbaaf264f6b
3
+ metadata.gz: 33bccbb75eba778cb42fc5340afc2f10a899ca671123e8b538a39acbdf16bd1b
4
+ data.tar.gz: c4252164f38a0f36b827fb32247500e95293880e2f8026f4b7ed04926614df41
5
5
  SHA512:
6
- metadata.gz: 731921ff022993cd51ada037fa764067d4da07e6a8119545f7b225354b32f19c3936a8c807c3badd3664dde424c924842ecb0ac9851d74bb9bf568ebbbb800e6
7
- data.tar.gz: da993296fd57d59c56c483920d901c9b6b44329c0bb5812c495de2024f01e449b0117be5900013875902ff1101ac0dfeaf177f407ea34e69ae210bd87f91e86a
6
+ metadata.gz: e682041f5c1f0e071578c0910f3eace9438064741cce16b148870c73137fd38926154269907f4948dec365e83b9115facff5ec21ac23072270ef39bab64cea10
7
+ data.tar.gz: 8a04cac2ad54cd0bc4b2e2f5855bac16f571c3ab6aa0183caa1427ad7c420f8c3920e74b85871c22454dbd827c7772b24cfe96bcaf262222664c05f7483f5dbd
@@ -5,6 +5,8 @@ require "date"
5
5
  require_relative "errors"
6
6
  require_relative "data_lite"
7
7
 
8
+ # :enddoc:
9
+
8
10
  module Net
9
11
  class IMAP < Protocol
10
12
 
@@ -26,6 +28,7 @@ module Net
26
28
  end
27
29
  when Time, Date, DateTime
28
30
  when Symbol
31
+ Flag.validate(data)
29
32
  else
30
33
  data.validate
31
34
  end
@@ -46,7 +49,7 @@ module Net
46
49
  when Date
47
50
  send_date_data(data)
48
51
  when Symbol
49
- send_symbol_data(data)
52
+ Flag[data].send_data(self, tag)
50
53
  else
51
54
  data.send_data(self, tag)
52
55
  end
@@ -78,9 +81,23 @@ module Net
78
81
  put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
79
82
  end
80
83
 
81
- def send_literal(str, tag = nil)
84
+ def send_binary_literal(*a, **kw); send_literal(*a, **kw, binary: true) end
85
+
86
+ # `non_sync` is an optional tri-state flag:
87
+ # * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
88
+ # TODO: raise or warn when capabilities don't allow non_sync.
89
+ # * `false` -> Force normal synchronizing literal behavior.
90
+ # * `nil` -> (default) Currently behaves like `false` (will be dynamic).
91
+ # TODO: Dynamic, based on capabilities and bytesize.
92
+ def send_literal(str, tag = nil, binary: false, non_sync: nil)
82
93
  synchronize do
83
- put_string("{" + str.bytesize.to_s + "}" + CRLF)
94
+ prefix = "~" if binary
95
+ plus = "+" if non_sync
96
+ put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
97
+ if non_sync
98
+ put_string(str)
99
+ return
100
+ end
84
101
  @continued_command_tag = tag
85
102
  @continuation_request_exception = nil
86
103
  begin
@@ -116,11 +133,13 @@ module Net
116
133
  def send_date_data(date) put_string Net::IMAP.encode_date(date) end
117
134
  def send_time_data(time) put_string Net::IMAP.encode_time(time) end
118
135
 
119
- def send_symbol_data(symbol)
120
- put_string("\\" + symbol.to_s)
121
- end
122
-
123
136
  CommandData = Data.define(:data) do # :nodoc:
137
+ def self.validate(...)
138
+ data = new(...)
139
+ data.validate
140
+ data
141
+ end
142
+
124
143
  def send_data(imap, tag)
125
144
  raise NoMethodError, "#{self.class} must implement #{__method__}"
126
145
  end
@@ -129,15 +148,109 @@ module Net
129
148
  end
130
149
  end
131
150
 
151
+ # Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
152
+ # except for +NULL+, +CR+, or +LF+. +text+ is extended to allow any
153
+ # multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
154
+ # been enabled, or when the server supports only +IMAP4rev2+ and not earlier
155
+ # IMAP revisions, or when the server advertises +UTF8=ONLY+.
156
+ #
157
+ # NOTE: The current implementation does not validate whether the connection
158
+ # currently supports UTF-8. Future versions may change.
159
+ #
160
+ # The string's bytes must be valid ASCII or valid UTF-8. The string's
161
+ # reported encoding is ignored, but the string is _not_ transcoded.
162
+ class RawText < CommandData # :nodoc:
163
+ def initialize(data:)
164
+ data = String(data.to_str)
165
+ data = if data.encoding in Encoding::ASCII | Encoding::UTF_8
166
+ -data
167
+ elsif data.ascii_only?
168
+ -(data.dup.force_encoding("ASCII"))
169
+ else
170
+ -(data.dup.force_encoding("UTF-8"))
171
+ end
172
+ super
173
+ validate
174
+ end
175
+
176
+ def validate
177
+ if data.include?("\0")
178
+ raise DataFormatError, "NULL byte must be binary literal encoded"
179
+ elsif !data.valid_encoding?
180
+ raise DataFormatError, "invalid UTF-8 must be literal encoded"
181
+ elsif /[\r\n]/.match?(data)
182
+ raise DataFormatError, "CR and LF bytes must be literal encoded"
183
+ end
184
+ end
185
+
186
+ def ascii_only? = data.ascii_only?
187
+
188
+ def send_data(imap, tag) = imap.__send__(:put_string, data)
189
+ end
190
+
132
191
  class RawData < CommandData # :nodoc:
133
- def send_data(imap, tag)
134
- imap.__send__(:put_string, data)
192
+ def initialize(data:)
193
+ data = split_parts(data)
194
+ super
195
+ validate
196
+ end
197
+
198
+ def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
199
+
200
+ def validate
201
+ return unless data.last in RawText(data: text)
202
+ if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
203
+ raise DataFormatError, "RawData cannot end with literal continuation"
204
+ end
205
+ end
206
+
207
+ private
208
+
209
+ def split_parts(data)
210
+ data = data.b # dups and ensures BINARY encoding
211
+ parts = []
212
+ while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
213
+ text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
214
+ bytesize = Integer bytesize, 10
215
+ parts << RawText[text] unless text.empty?
216
+ parts << extract_literal(data, binary:, bytesize:, non_sync:)
217
+ data[0, bytesize] = ""
218
+ end
219
+ parts << RawText[data] unless data.empty?
220
+ parts
221
+ end
222
+
223
+ def extract_literal(data, binary:, bytesize:, non_sync:)
224
+ if data.bytesize < bytesize
225
+ raise DataFormatError, "Too few bytes in string for literal, " \
226
+ "expected: %s, remaining: %s" % [bytesize, data.bytesize]
227
+ end
228
+ literal = data.byteslice(0, bytesize)
229
+ (binary ? Literal8 : Literal).new(data: literal, non_sync:)
135
230
  end
136
231
  end
137
232
 
138
233
  class Atom < CommandData # :nodoc:
234
+ def initialize(**)
235
+ super
236
+ validate
237
+ end
238
+
239
+ def validate
240
+ data.to_s.ascii_only? \
241
+ or raise DataFormatError, "#{self.class} must be ASCII only"
242
+ data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
243
+ and raise DataFormatError, "#{self.class} must not contain atom-specials"
244
+ end
245
+
246
+ def send_data(imap, tag)
247
+ imap.__send__(:put_string, data.to_s)
248
+ end
249
+ end
250
+
251
+ class Flag < Atom # :nodoc:
139
252
  def send_data(imap, tag)
140
- imap.__send__(:put_string, data)
253
+ imap.__send__(:put_string, "\\#{data}")
141
254
  end
142
255
  end
143
256
 
@@ -147,9 +260,39 @@ module Net
147
260
  end
148
261
  end
149
262
 
150
- class Literal < CommandData # :nodoc:
263
+ class Literal < Data.define(:data, :non_sync) # :nodoc:
264
+ def self.validate(...)
265
+ data = new(...)
266
+ data.validate
267
+ data
268
+ end
269
+
270
+ def initialize(data:, non_sync: nil)
271
+ data = -String(data.to_str).b or
272
+ raise DataFormatError, "#{self.class} expects string input"
273
+ super
274
+ validate
275
+ end
276
+
277
+ def bytesize; data.bytesize end
278
+
279
+ def validate
280
+ if data.include?("\0")
281
+ raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
282
+ "Use #{Literal8} or a null-safe encoding."
283
+ end
284
+ end
285
+
286
+ def send_data(imap, tag)
287
+ imap.__send__(:send_literal, data, tag, non_sync:)
288
+ end
289
+ end
290
+
291
+ class Literal8 < Literal # :nodoc:
292
+ def validate; nil end # all bytes are okay
293
+
151
294
  def send_data(imap, tag)
152
- imap.__send__(:send_literal, data, tag)
295
+ imap.__send__(:send_binary_literal, data, tag, non_sync:)
153
296
  end
154
297
  end
155
298
 
@@ -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
 
@@ -8,6 +8,7 @@ module Net
8
8
 
9
9
  def initialize(client, sock)
10
10
  @client, @sock = client, sock
11
+ @buff = @literal_size = nil
11
12
  end
12
13
 
13
14
  def read_response_buffer
@@ -15,13 +16,13 @@ module Net
15
16
  catch :eof do
16
17
  while true
17
18
  read_line
18
- break unless (@literal_size = get_literal_size)
19
+ break unless literal_size
19
20
  read_literal
20
21
  end
21
22
  end
22
23
  buff
23
24
  ensure
24
- @buff = nil
25
+ @buff = @literal_size = nil
25
26
  end
26
27
 
27
28
  private
@@ -30,13 +31,18 @@ module Net
30
31
 
31
32
  def bytes_read = buff.bytesize
32
33
  def empty? = buff.empty?
33
- def done? = line_done? && !get_literal_size
34
+ def done? = line_done? && !literal_size
34
35
  def line_done? = buff.end_with?(CRLF)
35
- def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
36
+
37
+ def get_literal_size(buff)
38
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
39
+ end
36
40
 
37
41
  def read_line
38
- buff << (@sock.gets(CRLF, read_limit) or throw :eof)
42
+ line = (@sock.gets(CRLF, read_limit) or throw :eof)
43
+ buff << line
39
44
  max_response_remaining! unless line_done?
45
+ @literal_size = get_literal_size(line)
40
46
  end
41
47
 
42
48
  def read_literal
@@ -75,13 +75,19 @@ module Net
75
75
  # * #password ― Password or passphrase associated with this #username.
76
76
  # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
77
77
  # * _optional_ #min_iterations - Overrides the default value (4096).
78
+ # * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
78
79
  #
79
80
  # Any other keyword parameters are quietly ignored.
81
+ #
82
+ # *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
83
+ # and maximum iteration counts that are appropriate for their security
84
+ # context.
80
85
  def initialize(username_arg = nil, password_arg = nil,
81
86
  authcid: nil, username: nil,
82
87
  authzid: nil,
83
88
  password: nil, secret: nil,
84
89
  min_iterations: 4096, # see both RFC5802 and RFC7677
90
+ max_iterations: 2**31 - 1, # max int32
85
91
  cnonce: nil, # must only be set in tests
86
92
  **options)
87
93
  @username = username || username_arg || authcid or
@@ -94,7 +100,22 @@ module Net
94
100
  @min_iterations.positive? or
95
101
  raise ArgumentError, "min_iterations must be positive"
96
102
 
103
+ @max_iterations = Integer max_iterations.to_int
104
+ @min_iterations <= @max_iterations or
105
+ raise ArgumentError, "max_iterations must be more than min_iterations"
106
+
97
107
  @cnonce = cnonce || SecureRandom.base64(32)
108
+
109
+ # These attrs are set from the server challenges
110
+ @server_first_message = @snonce = @salt = @iterations = nil
111
+ @server_error = nil
112
+
113
+ # Memoized after @salt and @iterations have been sent.
114
+ @salted_password = @client_key = @server_key = nil
115
+
116
+ # These values are created and cached in response to server challenges
117
+ @client_first_message_bare = nil
118
+ @client_final_message_without_proof = nil
98
119
  end
99
120
 
100
121
  # Authentication identity: the identity that matches the #password.
@@ -127,8 +148,43 @@ module Net
127
148
 
128
149
  # The minimal allowed iteration count. Lower #iterations will raise an
129
150
  # Error.
151
+ #
152
+ # *WARNING:* The default value (4096) is set to match guidance from
153
+ # both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
154
+ # and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
155
+ # {modern recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
156
+ # are significantly higher.
157
+ #
158
+ # It is ultimately the server's responsibility to securely store
159
+ # password hashes. While this parameter can alert the user to
160
+ # insecure password storage and prevent insecure authentication
161
+ # exchange, updating the iteration count generally requires resetting
162
+ # the password on the server.
130
163
  attr_reader :min_iterations
131
164
 
165
+ # The maximal allowed iteration count. Higher #iterations will raise an
166
+ # Error.
167
+ #
168
+ # As noted in {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
169
+ # >>>
170
+ # A hostile server can perform a computational denial-of-service
171
+ # attack on clients by sending a big iteration count value.
172
+ #
173
+ # *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
174
+ # 32-bit integer. This is large enough for the computation to take
175
+ # several minutes, and insufficient protection against hostile servers.
176
+ #
177
+ # Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
178
+ # blocking C function, and cannot be interrupted by +Timeout+ or
179
+ # <tt>Thread.raise</tt>. And it keeps the Global VM lock, as of v4.0 of
180
+ # the +openssl+ gem, so other ruby threads will not be able to run.
181
+ #
182
+ # <em>To prevent a denial of service attack,</em> this must be set to a
183
+ # safe value, depending on hardware and version of OpenSSL. <em>It is
184
+ # the user's responsibility</em> to enforce minimum and maximum
185
+ # iteration counts that are appropriate for their security context.
186
+ attr_reader :max_iterations
187
+
132
188
  # The client nonce, generated by SecureRandom
133
189
  attr_reader :cnonce
134
190
 
@@ -147,6 +203,15 @@ module Net
147
203
  # Net::IMAP::NoResponseError.
148
204
  attr_reader :server_error
149
205
 
206
+ # Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
207
+ def salted_password; @salted_password ||= compute_salted { super } end
208
+
209
+ # Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
210
+ def client_key; @client_key ||= compute_salted { super } end
211
+
212
+ # Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
213
+ def server_key; @server_key ||= compute_salted { super } end
214
+
150
215
  # Returns a new OpenSSL::Digest object, set to the appropriate hash
151
216
  # function for the chosen mechanism.
152
217
  #
@@ -186,6 +251,13 @@ module Net
186
251
 
187
252
  private
188
253
 
254
+ # Checks for +salt+ and +iterations+ before yielding
255
+ def compute_salted
256
+ salt in String or raise Error, "unknown salt"
257
+ iterations in Integer or raise Error, "unknown iterations"
258
+ yield
259
+ end
260
+
189
261
  # Need to store this for auth_message
190
262
  attr_reader :server_first_message
191
263
 
@@ -202,6 +274,8 @@ module Net
202
274
  raise Error, "server did not send iteration count"
203
275
  min_iterations <= iterations or
204
276
  raise Error, "too few iterations: %d" % [iterations]
277
+ max_iterations.nil? || iterations <= max_iterations or
278
+ raise Error, "too many iterations: %d" % [iterations]
205
279
  mext = sparams["m"] and
206
280
  raise Error, "mandatory extension: %p" % [mext]
207
281
  snonce.start_with? cnonce or
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.14"
792
810
 
793
811
  # Aliases for supported capabilities, to be used with the #enable command.
794
812
  ENABLE_ALIASES = {
@@ -1394,9 +1412,11 @@ module Net
1394
1412
  #
1395
1413
  def starttls(**options)
1396
1414
  @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
1415
+ handled = false
1397
1416
  error = nil
1398
1417
  ok = send_command("STARTTLS") do |resp|
1399
1418
  if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1419
+ handled = true
1400
1420
  clear_cached_capabilities
1401
1421
  clear_responses
1402
1422
  start_tls_session
@@ -1408,6 +1428,13 @@ module Net
1408
1428
  disconnect
1409
1429
  raise error
1410
1430
  end
1431
+ unless handled
1432
+ disconnect
1433
+ raise InvalidResponseError,
1434
+ "STARTTLS handler was bypassed, although server responded %p" % [
1435
+ ok.raw_data.chomp
1436
+ ]
1437
+ end
1411
1438
  ok
1412
1439
  end
1413
1440
 
@@ -1828,12 +1855,18 @@ module Net
1828
1855
  # to both admin and user. If this mailbox exists, it returns an array
1829
1856
  # containing objects of type MailboxQuotaRoot and MailboxQuota.
1830
1857
  #
1858
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1859
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1860
+ # with UntaggedResponse#raw_data.
1861
+ #
1831
1862
  # Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
1832
1863
  #
1833
1864
  # ==== Capabilities
1834
1865
  #
1835
- # The server's capabilities must include +QUOTA+
1836
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1866
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1867
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1868
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1869
+ # resource type.
1837
1870
  def getquotaroot(mailbox)
1838
1871
  synchronize do
1839
1872
  send_command("GETQUOTAROOT", mailbox)
@@ -1845,41 +1878,59 @@ module Net
1845
1878
  end
1846
1879
 
1847
1880
  # Sends a {GETQUOTA command [RFC2087 §4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
1848
- # along with specified +mailbox+. If this mailbox exists, then an array
1849
- # containing a MailboxQuota object is returned. This command is generally
1850
- # only available to server admin.
1881
+ # for the +quota_root+. If this quota root exists, then an array
1882
+ # containing a MailboxQuota object is returned.
1883
+ #
1884
+ # The names of quota roots that are applicable to a particular mailbox can
1885
+ # be discovered with #getquotaroot.
1886
+ #
1887
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1888
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1889
+ # with UntaggedResponse#raw_data.
1851
1890
  #
1852
1891
  # Related: #getquotaroot, #setquota, MailboxQuota
1853
1892
  #
1854
1893
  # ==== Capabilities
1855
1894
  #
1856
- # The server's capabilities must include +QUOTA+
1857
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1858
- def getquota(mailbox)
1895
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1896
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1897
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1898
+ # resource type.
1899
+ def getquota(quota_root)
1859
1900
  synchronize do
1860
- send_command("GETQUOTA", mailbox)
1901
+ send_command("GETQUOTA", quota_root)
1861
1902
  clear_responses("QUOTA")
1862
1903
  end
1863
1904
  end
1864
1905
 
1865
1906
  # Sends a {SETQUOTA command [RFC2087 §4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
1866
- # along with the specified +mailbox+ and +quota+. If +quota+ is nil, then
1867
- # +quota+ will be unset for that mailbox. Typically one needs to be logged
1868
- # in as a server admin for this to work.
1907
+ # along with the specified +quota_root+ and +storage_limit+. If
1908
+ # +storage_limit+ is +nil+, resource limits are unset for that quota root.
1909
+ # If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
1910
+ #
1911
+ # imap.setquota "#user/alice", 100
1912
+ # imap.getquota "#user/alice"
1913
+ # # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
1914
+ #
1915
+ # Typically one needs to be logged in as a server admin for this to work.
1916
+ #
1917
+ # *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
1869
1918
  #
1870
1919
  # Related: #getquota, #getquotaroot
1871
1920
  #
1872
1921
  # ==== Capabilities
1873
1922
  #
1874
- # The server's capabilities must include +QUOTA+
1875
- # [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]].
1876
- def setquota(mailbox, quota)
1877
- if quota.nil?
1878
- data = '()'
1923
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1924
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1925
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1926
+ # resource type.
1927
+ def setquota(quota_root, storage_limit)
1928
+ if storage_limit.nil?
1929
+ list = []
1879
1930
  else
1880
- data = '(STORAGE ' + quota.to_s + ')'
1931
+ list = ["STORAGE", Integer(storage_limit)]
1881
1932
  end
1882
- send_command("SETQUOTA", mailbox, RawData.new(data))
1933
+ send_command("SETQUOTA", quota_root, list)
1883
1934
  end
1884
1935
 
1885
1936
  # Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
@@ -1986,7 +2037,10 @@ module Net
1986
2037
  # <tt>STATUS=SIZE</tt>
1987
2038
  # {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
1988
2039
  #
1989
- # +DELETED+ requires the server's capabilities to include +IMAP4rev2+.
2040
+ # +DELETED+ must be supported when the server's capabilities includes
2041
+ # +IMAP4rev2+.
2042
+ # or <tt>QUOTA=RES-MESSAGES</tt>
2043
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
1990
2044
  #
1991
2045
  # +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
1992
2046
  # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
@@ -2267,11 +2321,11 @@ module Net
2267
2321
  # Encoded as an \IMAP date (see ::encode_date).
2268
2322
  #
2269
2323
  # [When +criteria+ is a String]
2270
- # +criteria+ will be sent directly to the server <em>without any
2271
- # validation or encoding</em>.
2324
+ # +criteria+ will be sent to the server <em>with minimal validation and no
2325
+ # encoding or formatting</em>.
2272
2326
  #
2273
- # <em>*WARNING:* This is vulnerable to injection attacks when external
2274
- # inputs are used.</em>
2327
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2328
+ # types of attribute injection attack if unvetted user input is used.</em>
2275
2329
  #
2276
2330
  # ==== Supported return options
2277
2331
  #
@@ -2592,6 +2646,13 @@ module Net
2592
2646
  #
2593
2647
  # +attr+ is a list of attributes to fetch; see FetchStruct documentation for
2594
2648
  # a list of supported attributes.
2649
+ # >>>
2650
+ # When +attr+ is a String, it will be sent <em>with minimal validation and
2651
+ # no encoding or formatting</em>. When +attr+ is an Array, each String in
2652
+ # +attr+ will be sent this way.
2653
+ #
2654
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2655
+ # types of attribute injection attack if unvetted user input is used.</em>
2595
2656
  #
2596
2657
  # +changedsince+ is an optional integer mod-sequence. It limits results to
2597
2658
  # messages with a mod-sequence greater than +changedsince+.
@@ -3080,6 +3141,7 @@ module Net
3080
3141
 
3081
3142
  synchronize do
3082
3143
  tag = Thread.current[:net_imap_tag] = generate_tag
3144
+ guard_against_tagged_response_skipping_handler!(tag, "IDLE")
3083
3145
  put_string("#{tag} IDLE#{CRLF}")
3084
3146
 
3085
3147
  begin
@@ -3544,6 +3606,7 @@ module Net
3544
3606
  put_string(" ")
3545
3607
  send_data(i, tag)
3546
3608
  end
3609
+ guard_against_tagged_response_skipping_handler!(tag, cmd)
3547
3610
  put_string(CRLF)
3548
3611
  if cmd == "LOGOUT"
3549
3612
  @logout_command_tag = tag
@@ -3559,6 +3622,19 @@ module Net
3559
3622
  end
3560
3623
  end
3561
3624
  end
3625
+ rescue InvalidResponseError
3626
+ disconnect
3627
+ raise
3628
+ end
3629
+
3630
+ def guard_against_tagged_response_skipping_handler!(tag, cmd)
3631
+ return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
3632
+ raise InvalidResponseError, format(
3633
+ "Received tagged 'OK' to incomplete %s command (tag=%s). " \
3634
+ "This could indicate a malicious server, a man-in-the-middle, or " \
3635
+ "client-side command injection. Disconnecting.",
3636
+ cmd, tag
3637
+ )
3562
3638
  end
3563
3639
 
3564
3640
  def generate_tag
@@ -3712,7 +3788,7 @@ module Net
3712
3788
  end
3713
3789
 
3714
3790
  def store_internal(cmd, set, attr, flags, unchangedsince: nil)
3715
- attr = RawData.new(attr) if attr.instance_of?(String)
3791
+ attr = Atom.new(attr) if attr.instance_of?(String)
3716
3792
  args = [SequenceSet.new(set)]
3717
3793
  args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
3718
3794
  args << attr << flags
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.13
4
+ version: 0.5.14
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.6
134
134
  specification_version: 4
135
135
  summary: Ruby client api for Internet Message Access Protocol
136
136
  test_files: []