net-imap 0.4.23 → 0.4.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a15f49a20256632f692729c868dfb503e025f999bd2f520270674218869aae77
4
- data.tar.gz: dd5daa492ba441af22036909b46dc57b40ab6f1d26742f5487756af89befe858
3
+ metadata.gz: 9419630b908b12c7f89846682dae953e50376d89ebb3b7391b3426bb34480991
4
+ data.tar.gz: 4558a77d38a4def28af960c201ca9359fcc828d5a316f76f95cf3c0e575702b6
5
5
  SHA512:
6
- metadata.gz: 245c92501ec0e7b3d48dc59537f72cee04609c83c402bdbdc49c315cf021087e02180a2bf6ba165b9dbf3afd5d3998c4f7cd7112293143f1b188d9d87c2b2445
7
- data.tar.gz: b23b4c65e667f681d25511f611791b14654de6222f4dd3aad6dcb1b134f003332f6d0984135bbda0acef0448a4b288bbd4855f0c2c26910110efab1f3e15799e
6
+ metadata.gz: 5f4baf570c8ed5732493ba801121ae092177fca3ab5f9162ef66f0db2a23e91c09fe740d8ddba52977e65737906a6735a8405bb94e923111970621e79b813141
7
+ data.tar.gz: 624e1b8a76630bffa007391b303eab5729b20665d9a5242f58749e0127340ddd1a842eb94ca46da085ccfa91cc093367348586cde705b51fe3eff25882f8f096
@@ -25,6 +25,7 @@ module Net
25
25
  end
26
26
  when Time, Date, DateTime
27
27
  when Symbol
28
+ Flag.validate(data)
28
29
  else
29
30
  data.validate
30
31
  end
@@ -45,7 +46,7 @@ module Net
45
46
  when Date
46
47
  send_date_data(data)
47
48
  when Symbol
48
- send_symbol_data(data)
49
+ Flag[data].send_data(self, tag)
49
50
  else
50
51
  data.send_data(self, tag)
51
52
  end
@@ -77,9 +78,23 @@ module Net
77
78
  put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
78
79
  end
79
80
 
80
- def send_literal(str, tag = nil)
81
+ def send_binary_literal(*a, **kw) send_literal(*a, **kw, binary: true) end
82
+
83
+ # `non_sync` is an optional tri-state flag:
84
+ # * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
85
+ # TODO: raise or warn when capabilities don't allow non_sync.
86
+ # * `false` -> Force normal synchronizing literal behavior.
87
+ # * `nil` -> (default) Currently behaves like `false` (will be dynamic).
88
+ # TODO: Dynamic, based on capabilities and bytesize.
89
+ def send_literal(str, tag = nil, binary: false, non_sync: nil)
81
90
  synchronize do
82
- put_string("{" + str.bytesize.to_s + "}" + CRLF)
91
+ prefix = "~" if binary
92
+ plus = "+" if non_sync
93
+ put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
94
+ if non_sync
95
+ put_string(str)
96
+ return
97
+ end
83
98
  @continued_command_tag = tag
84
99
  @continuation_request_exception = nil
85
100
  begin
@@ -115,37 +130,148 @@ module Net
115
130
  def send_date_data(date) put_string Net::IMAP.encode_date(date) end
116
131
  def send_time_data(time) put_string Net::IMAP.encode_time(time) end
117
132
 
118
- def send_symbol_data(symbol)
119
- put_string("\\" + symbol.to_s)
120
- end
133
+ # simplistic emulation of CommandData = Data.define(:data)
134
+ class CommandData # :nodoc:
135
+ class << self
136
+ def new(arg = nil, data: arg) super(data: data) end
137
+ alias :[] :new
138
+ end
139
+
140
+ def initialize(data:)
141
+ @data = data
142
+ freeze
143
+ end
144
+
145
+ attr_reader :data
146
+
147
+ def to_h(&block) block ? to_h.to_h(&block) : { data: data } end
148
+ def ==(other) self.class === other && to_h == other.to_h end
149
+ def eql?(other) self.class === other && to_h.eql?(other.to_h) end
150
+
151
+ # following class definition goes beyond the basic Data.define(:data)
152
+ ##
153
+
154
+ def self.validate(...)
155
+ data = new(...)
156
+ data.validate
157
+ data
158
+ end
121
159
 
122
- class RawData # :nodoc:
123
160
  def send_data(imap, tag)
124
- imap.__send__(:put_string, @data)
161
+ raise NoMethodError, "#{self.class} must implement #{__method__}"
125
162
  end
126
163
 
127
164
  def validate
128
165
  end
166
+ end
129
167
 
130
- private
168
+ # Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
169
+ # except for +NULL+, +CR+, or +LF+. +text+ is extended to allow any
170
+ # multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
171
+ # been enabled, or when the server supports only +IMAP4rev2+ and not earlier
172
+ # IMAP revisions, or when the server advertises +UTF8=ONLY+.
173
+ #
174
+ # NOTE: The current implementation does not validate whether the connection
175
+ # currently supports UTF-8. Future versions may change.
176
+ #
177
+ # The string's bytes must be valid ASCII or valid UTF-8. The string's
178
+ # reported encoding is ignored, but the string is _not_ transcoded.
179
+ class RawText < CommandData # :nodoc:
180
+ def initialize(data:)
181
+ data = String(data.to_str)
182
+ data = if [Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
183
+ -data
184
+ elsif data.ascii_only?
185
+ -(data.dup.force_encoding("ASCII"))
186
+ else
187
+ -(data.dup.force_encoding("UTF-8"))
188
+ end
189
+ super
190
+ validate
191
+ end
131
192
 
132
- def initialize(data)
133
- @data = data
193
+ def validate
194
+ if data.include?("\0")
195
+ raise DataFormatError, "NULL byte must be binary literal encoded"
196
+ elsif !data.valid_encoding?
197
+ raise DataFormatError, "invalid UTF-8 must be literal encoded"
198
+ elsif /[\r\n]/.match?(data)
199
+ raise DataFormatError, "CR and LF bytes must be literal encoded"
200
+ end
134
201
  end
202
+
203
+ def ascii_only?; data.ascii_only? end
204
+
205
+ def send_data(imap, tag) imap.__send__(:put_string, data) end
135
206
  end
136
207
 
137
- class Atom # :nodoc:
138
- def send_data(imap, tag)
139
- imap.__send__(:put_string, @data)
208
+ class RawData < CommandData # :nodoc:
209
+ def initialize(data:)
210
+ data = split_parts(data)
211
+ super
212
+ validate
140
213
  end
141
214
 
215
+ def send_data(imap, tag) data.each do _1.send_data(imap, tag) end end
216
+
142
217
  def validate
218
+ return unless RawText === data.last
219
+ text = data.last.data
220
+ if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
221
+ raise DataFormatError, "RawData cannot end with literal continuation"
222
+ end
143
223
  end
144
224
 
145
225
  private
146
226
 
147
- def initialize(data)
148
- @data = data
227
+ def split_parts(data)
228
+ data = data.b # dups and ensures BINARY encoding
229
+ parts = []
230
+ while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
231
+ text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
232
+ bytesize = Integer bytesize, 10
233
+ parts << RawText[text] unless text.empty?
234
+ parts << extract_literal(data,
235
+ binary: binary,
236
+ bytesize: bytesize,
237
+ non_sync: non_sync)
238
+ data[0, bytesize] = ""
239
+ end
240
+ parts << RawText[data] unless data.empty?
241
+ parts
242
+ end
243
+
244
+ def extract_literal(data, binary:, bytesize:, non_sync:)
245
+ if data.bytesize < bytesize
246
+ raise DataFormatError, "Too few bytes in string for literal, " \
247
+ "expected: %s, remaining: %s" % [bytesize, data.bytesize]
248
+ end
249
+ literal = data.byteslice(0, bytesize)
250
+ (binary ? Literal8 : Literal).new(data: literal, non_sync: non_sync)
251
+ end
252
+ end
253
+
254
+ class Atom < CommandData # :nodoc:
255
+ def initialize(**)
256
+ super
257
+ validate
258
+ end
259
+
260
+ def validate
261
+ data.to_s.ascii_only? \
262
+ or raise DataFormatError, "#{self.class} must be ASCII only"
263
+ data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
264
+ and raise DataFormatError, "#{self.class} must not contain atom-specials"
265
+ end
266
+
267
+ def send_data(imap, tag)
268
+ imap.__send__(:put_string, data.to_s)
269
+ end
270
+ end
271
+
272
+ class Flag < Atom # :nodoc:
273
+ def send_data(imap, tag)
274
+ imap.__send__(:put_string, "\\#{data}")
149
275
  end
150
276
  end
151
277
 
@@ -165,17 +291,52 @@ module Net
165
291
  end
166
292
 
167
293
  class Literal # :nodoc:
168
- def send_data(imap, tag)
169
- imap.__send__(:send_literal, @data, tag)
294
+ class << self
295
+ def new(_data = nil, _non_sync = nil, data: _data, non_sync: _non_sync)
296
+ super(data: data, non_sync: non_sync)
297
+ end
298
+ alias :[] :new
170
299
  end
171
300
 
301
+ attr_reader :data, :non_sync
302
+
303
+ def to_h(&block) block ? to_h.to_h(&block) : { data: data, non_sync: non_sync } end
304
+ def ==(other) self.class === other && to_h == other.to_h end
305
+ def eql?(other) self.class === other && to_h.eql?(other.to_h) end
306
+
307
+ def initialize(data:, non_sync: nil)
308
+ data = -String(data.to_str).b or
309
+ raise DataFormatError, "#{self.class} expects string input"
310
+ @data, @non_sync = data, non_sync
311
+ validate
312
+ freeze
313
+ end
314
+
315
+ def self.validate(...)
316
+ data = new(...)
317
+ data.validate
318
+ data
319
+ end
320
+
321
+ def bytesize; data.bytesize end
322
+
172
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
173
328
  end
174
329
 
175
- private
330
+ def send_data(imap, tag)
331
+ imap.__send__(:send_literal, data, tag, non_sync: non_sync)
332
+ end
333
+ end
176
334
 
177
- def initialize(data)
178
- @data = data
335
+ class Literal8 < Literal # :nodoc:
336
+ def validate; nil end # all bytes are okay
337
+
338
+ def send_data(imap, tag)
339
+ imap.__send__(:send_binary_literal, data, tag, non_sync: non_sync)
179
340
  end
180
341
  end
181
342
 
@@ -295,6 +295,14 @@ module Net
295
295
  # because the server doesn't allow deletion of mailboxes with children.
296
296
  # #data is +nil+.
297
297
  #
298
+ # ==== <tt>QUOTA=RES-*</tt> response codes
299
+ # See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
300
+ # * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
301
+ # with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
302
+ # the command would put the target mailbox over any quota, and with an
303
+ # untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
304
+ # be external events). #data is +nil+.
305
+ #
298
306
  # ==== +CONDSTORE+ extension
299
307
  # See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
300
308
  # * +NOMODSEQ+, when selecting a mailbox that does not support
@@ -369,12 +377,24 @@ module Net
369
377
  # Net::IMAP#getquotaroot returns an array containing both MailboxQuotaRoot
370
378
  # and MailboxQuota objects.
371
379
  #
380
+ # ==== Required capability
381
+ #
382
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
383
+ # or <tt>QUOTA=RES-STORAGE</tt>
384
+ # [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
372
385
  class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
373
386
  ##
374
387
  # method: mailbox
375
388
  # :call-seq: mailbox -> string
376
389
  #
377
- # The mailbox with the associated quota.
390
+ # The quota root with the associated quota.
391
+ #
392
+ # NOTE: this was mistakenly named "mailbox". But the quota root's name may
393
+ # differ from the mailbox. A single quota root may cover multiple
394
+ # mailboxes, and a single mailbox may be governed by multiple quota roots.
395
+
396
+ # The quota root with the associated quota.
397
+ alias quota_root mailbox
378
398
 
379
399
  ##
380
400
  # method: usage
@@ -386,7 +406,7 @@ module Net
386
406
  # method: quota
387
407
  # :call-seq: quota -> Integer
388
408
  #
389
- # Quota limit imposed on the mailbox.
409
+ # Storage limit imposed on the mailbox.
390
410
  #
391
411
  end
392
412
 
@@ -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
@@ -31,12 +32,18 @@ module Net
31
32
  def bytes_read; buff.bytesize end
32
33
  def empty?; buff.empty? end
33
34
  def done?; line_done? && !get_literal_size end
35
+ def done?; line_done? && !literal_size end
34
36
  def line_done?; buff.end_with?(CRLF) end
35
- def get_literal_size; /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i end
37
+
38
+ def get_literal_size(buff)
39
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
40
+ end
36
41
 
37
42
  def read_line
38
- buff << (@sock.gets(CRLF, read_limit) or throw :eof)
43
+ line = (@sock.gets(CRLF, read_limit) or throw :eof)
44
+ buff << line
39
45
  max_response_remaining! unless line_done?
46
+ @literal_size = get_literal_size(line)
40
47
  end
41
48
 
42
49
  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
+ String === salt or raise Error, "unknown salt"
257
+ Integer === iterations or raise Error, "unknown iterations"
258
+ yield
259
+ end
260
+
189
261
  # Need to store this for auth_message
190
262
  attr_reader :server_first_message
191
263
 
@@ -202,6 +274,8 @@ module Net
202
274
  raise Error, "server did not send iteration count"
203
275
  min_iterations <= iterations or
204
276
  raise Error, "too few iterations: %d" % [iterations]
277
+ max_iterations.nil? || iterations <= max_iterations or
278
+ raise Error, "too many iterations: %d" % [iterations]
205
279
  mext = sparams["m"] and
206
280
  raise Error, "mandatory extension: %p" % [mext]
207
281
  snonce.start_with? cnonce or
data/lib/net/imap.rb CHANGED
@@ -460,6 +460,9 @@ module Net
460
460
  # +LITERAL-+, and +SPECIAL-USE+.</em>
461
461
  #
462
462
  # ==== RFC2087: +QUOTA+
463
+ # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
464
+ # - Obsoleted by <tt>QUOTA=RES-*</tt> [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]],
465
+ # although the commands are backward compatible.
463
466
  # - #getquota: returns the resource usage and limits for a quota root
464
467
  # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
465
468
  # their resource usage and limits.
@@ -572,6 +575,16 @@ module Net
572
575
  # See FetchData#emailid and FetchData#emailid.
573
576
  # - Updates #status with support for the +MAILBOXID+ status attribute.
574
577
  #
578
+ # ==== RFC9208: <tt>QUOTA=RES-*</tt>
579
+ # +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
580
+ # - Obsoletes the +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
581
+ # extension and provides strict semantics for different resource types.
582
+ # - #getquota: returns the resource usage and limits for a quota root
583
+ # - #getquotaroot: returns the list of quota roots for a mailbox, as well as
584
+ # their resource usage and limits.
585
+ # - #setquota: sets the resource limits for a given quota root.
586
+ # - Updates #status with <tt>"DELETED"</tt> and +DELETED-STORAGE+ attributes.
587
+ #
575
588
  # == References
576
589
  #
577
590
  # [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501.html]]::
@@ -681,14 +694,13 @@ module Net
681
694
  #
682
695
  # === \IMAP Extensions
683
696
  #
684
- # [QUOTA[https://tools.ietf.org/html/rfc9208]]::
685
- # Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
686
- # March 2022, <https://www.rfc-editor.org/info/rfc9208>.
697
+ # [QUOTA[https://www.rfc-editor.org/rfc/rfc2087]]::
698
+ # Myers, J., "IMAP4 QUOTA extension", RFC 2087, DOI 10.17487/RFC2087,
699
+ # January 1997, <https://www.rfc-editor.org/info/rfc2087>.
687
700
  #
688
- # <em>Note: obsoletes</em>
689
- # RFC-2087[https://tools.ietf.org/html/rfc2087]<em> (January 1997)</em>.
690
- # <em>Net::IMAP does not fully support the RFC9208 updates yet.</em>
691
- # [IDLE[https://tools.ietf.org/html/rfc2177]]::
701
+ # *NOTE*: _obsoleted_ by RFC9208[https://www.rfc-editor.org/rfc/rfc9208]
702
+ # (March 2022).
703
+ # [IDLE[https://www.rfc-editor.org/rfc/rfc2177]]::
692
704
  # Leiba, B., "IMAP4 IDLE command", RFC 2177, DOI 10.17487/RFC2177,
693
705
  # June 1997, <https://www.rfc-editor.org/info/rfc2177>.
694
706
  # [NAMESPACE[https://tools.ietf.org/html/rfc2342]]::
@@ -739,9 +751,15 @@ module Net
739
751
  # Gondwana, B., Ed., "IMAP Extension for Object Identifiers",
740
752
  # RFC 8474, DOI 10.17487/RFC8474, September 2018,
741
753
  # <https://www.rfc-editor.org/info/rfc8474>.
754
+ # [{QUOTA=RES-*}[https://www.rfc-editor.org/rfc/rfc9208]]::
755
+ # Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
756
+ # March 2022, <https://www.rfc-editor.org/info/rfc9208>.
757
+ #
758
+ # Obsoletes RFC2087[https://www.rfc-editor.org/rfc/rfc2087].
742
759
  #
743
760
  # === IANA registries
744
761
  # * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities]
762
+ # * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
745
763
  # * {IMAP Response Codes}[https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml]
746
764
  # * {IMAP Mailbox Name Attributes}[https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml]
747
765
  # * {IMAP and JMAP Keywords}[https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml]
@@ -761,7 +779,7 @@ module Net
761
779
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
762
780
  #
763
781
  class IMAP < Protocol
764
- VERSION = "0.4.23"
782
+ VERSION = "0.4.24"
765
783
 
766
784
  # Aliases for supported capabilities, to be used with the #enable command.
767
785
  ENABLE_ALIASES = {
@@ -1294,9 +1312,11 @@ module Net
1294
1312
  #
1295
1313
  def starttls(**options)
1296
1314
  @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
1315
+ handled = false
1297
1316
  error = nil
1298
1317
  ok = send_command("STARTTLS") do |resp|
1299
1318
  if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1319
+ handled = true
1300
1320
  clear_cached_capabilities
1301
1321
  clear_responses
1302
1322
  start_tls_session
@@ -1308,6 +1328,13 @@ module Net
1308
1328
  disconnect
1309
1329
  raise error
1310
1330
  end
1331
+ unless handled
1332
+ disconnect
1333
+ raise InvalidResponseError,
1334
+ "STARTTLS handler was bypassed, although server responded %p" % [
1335
+ ok.raw_data.chomp
1336
+ ]
1337
+ end
1311
1338
  ok
1312
1339
  end
1313
1340
 
@@ -1742,12 +1769,18 @@ module Net
1742
1769
  # to both admin and user. If this mailbox exists, it returns an array
1743
1770
  # containing objects of type MailboxQuotaRoot and MailboxQuota.
1744
1771
  #
1772
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1773
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1774
+ # with UntaggedResponse#raw_data.
1775
+ #
1745
1776
  # Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
1746
1777
  #
1747
1778
  # ===== Capabilities
1748
1779
  #
1749
- # The server's capabilities must include +QUOTA+
1750
- # [RFC2087[https://tools.ietf.org/html/rfc2087]].
1780
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1781
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1782
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1783
+ # resource type.
1751
1784
  def getquotaroot(mailbox)
1752
1785
  synchronize do
1753
1786
  send_command("GETQUOTAROOT", mailbox)
@@ -1759,41 +1792,59 @@ module Net
1759
1792
  end
1760
1793
 
1761
1794
  # Sends a {GETQUOTA command [RFC2087 §4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
1762
- # along with specified +mailbox+. If this mailbox exists, then an array
1763
- # containing a MailboxQuota object is returned. This command is generally
1764
- # only available to server admin.
1795
+ # for the +quota_root+. If this quota root exists, then an array
1796
+ # containing a MailboxQuota object is returned.
1797
+ #
1798
+ # The names of quota roots that are applicable to a particular mailbox can
1799
+ # be discovered with #getquotaroot.
1800
+ #
1801
+ # *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
1802
+ # resource type. This is usually +STORAGE+, but you may need to verify this
1803
+ # with UntaggedResponse#raw_data.
1765
1804
  #
1766
1805
  # Related: #getquotaroot, #setquota, MailboxQuota
1767
1806
  #
1768
1807
  # ===== Capabilities
1769
1808
  #
1770
- # The server's capabilities must include +QUOTA+
1771
- # [RFC2087[https://tools.ietf.org/html/rfc2087]].
1772
- def getquota(mailbox)
1809
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1810
+ # capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
1811
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
1812
+ # resource type.
1813
+ def getquota(quota_root)
1773
1814
  synchronize do
1774
- send_command("GETQUOTA", mailbox)
1815
+ send_command("GETQUOTA", quota_root)
1775
1816
  clear_responses("QUOTA")
1776
1817
  end
1777
1818
  end
1778
1819
 
1779
1820
  # Sends a {SETQUOTA command [RFC2087 §4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
1780
- # along with the specified +mailbox+ and +quota+. If +quota+ is nil, then
1781
- # +quota+ will be unset for that mailbox. Typically one needs to be logged
1782
- # in as a server admin for this to work.
1821
+ # along with the specified +quota_root+ and +storage_limit+. If
1822
+ # +storage_limit+ is +nil+, resource limits are unset for that quota root.
1823
+ # If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
1824
+ #
1825
+ # imap.setquota "#user/alice", 100
1826
+ # imap.getquota "#user/alice"
1827
+ # # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
1828
+ #
1829
+ # Typically one needs to be logged in as a server admin for this to work.
1830
+ #
1831
+ # *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
1783
1832
  #
1784
1833
  # Related: #getquota, #getquotaroot
1785
1834
  #
1786
1835
  # ===== Capabilities
1787
1836
  #
1788
- # The server's capabilities must include +QUOTA+
1789
- # [RFC2087[https://tools.ietf.org/html/rfc2087]].
1790
- def setquota(mailbox, quota)
1791
- if quota.nil?
1792
- data = '()'
1837
+ # Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
1838
+ # capability, or both +QUOTASET+ and a capability prefixed with
1839
+ # <tt>QUOTA=RES-*</tt> {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208]
1840
+ # for each supported resource type.
1841
+ def setquota(quota_root, storage_limit)
1842
+ if storage_limit.nil?
1843
+ list = []
1793
1844
  else
1794
- data = '(STORAGE ' + quota.to_s + ')'
1845
+ list = ["STORAGE", Integer(storage_limit)]
1795
1846
  end
1796
- send_command("SETQUOTA", mailbox, RawData.new(data))
1847
+ send_command("SETQUOTA", quota_root, list)
1797
1848
  end
1798
1849
 
1799
1850
  # Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
@@ -1900,7 +1951,10 @@ module Net
1900
1951
  # <tt>STATUS=SIZE</tt>
1901
1952
  # {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
1902
1953
  #
1903
- # +DELETED+ requires the server's capabilities to include +IMAP4rev2+.
1954
+ # +DELETED+ must be supported when the server's capabilities includes
1955
+ # +IMAP4rev2+.
1956
+ # or <tt>QUOTA=RES-MESSAGES</tt>
1957
+ # {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
1904
1958
  #
1905
1959
  # +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
1906
1960
  # {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
@@ -2049,6 +2103,14 @@ module Net
2049
2103
  #
2050
2104
  # ===== Search criteria
2051
2105
  #
2106
+ # >>>
2107
+ # When +criteria+ is an Array, elements in the array will be validated and
2108
+ # formatted. When +criteria+ is a String, it will be sent <em>with
2109
+ # minimal validation and no encoding or formatting</em>.
2110
+ #
2111
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2112
+ # types of attribute injection attack if unvetted user input is used.</em>
2113
+ #
2052
2114
  # For a full list of search criteria,
2053
2115
  # see [{IMAP4rev1 §6.4.4}[https://www.rfc-editor.org/rfc/rfc3501.html#section-6.4.4]],
2054
2116
  # or [{IMAP4rev2 §6.4.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-6.4.4]],
@@ -2136,6 +2198,13 @@ module Net
2136
2198
  #
2137
2199
  # +attr+ is a list of attributes to fetch; see the documentation
2138
2200
  # for FetchData for a list of valid attributes.
2201
+ # >>>
2202
+ # When +attr+ is a String, it will be sent <em>with minimal validation and
2203
+ # no encoding or formatting</em>. When +attr+ is an Array, each String in
2204
+ # +attr+ will be sent this way.
2205
+ #
2206
+ # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
2207
+ # types of attribute injection attack if unvetted user input is used.</em>
2139
2208
  #
2140
2209
  # +changedsince+ is an optional integer mod-sequence. It limits results to
2141
2210
  # messages with a mod-sequence greater than +changedsince+.
@@ -2992,6 +3061,7 @@ module Net
2992
3061
  put_string(" ")
2993
3062
  send_data(i, tag)
2994
3063
  end
3064
+ guard_against_tagged_response_skipping_handler!(tag)
2995
3065
  put_string(CRLF)
2996
3066
  if cmd == "LOGOUT"
2997
3067
  @logout_command_tag = tag
@@ -3007,6 +3077,17 @@ module Net
3007
3077
  end
3008
3078
  end
3009
3079
  end
3080
+ rescue InvalidResponseError
3081
+ disconnect
3082
+ raise
3083
+ end
3084
+
3085
+ def guard_against_tagged_response_skipping_handler!(tag)
3086
+ return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
3087
+ raise(InvalidResponseError,
3088
+ "Server sent tagged 'OK' before command was finished: %p. " \
3089
+ "This could indicate a malicious server or client-side " \
3090
+ "command injection. Disconnecting." % [resp.raw_data.chomp])
3010
3091
  end
3011
3092
 
3012
3093
  def generate_tag
@@ -3071,7 +3152,7 @@ module Net
3071
3152
  end
3072
3153
 
3073
3154
  def store_internal(cmd, set, attr, flags, unchangedsince: nil)
3074
- attr = RawData.new(attr) if attr.instance_of?(String)
3155
+ attr = Atom.new(attr) if attr.instance_of?(String)
3075
3156
  args = [MessageSet.new(set)]
3076
3157
  args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
3077
3158
  args << attr << flags
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.23
4
+ version: 0.4.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -125,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
125
  - !ruby/object:Gem::Version
126
126
  version: '0'
127
127
  requirements: []
128
- rubygems_version: 3.6.9
128
+ rubygems_version: 4.0.6
129
129
  specification_version: 4
130
130
  summary: Ruby client api for Internet Message Access Protocol
131
131
  test_files: []