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 +4 -4
- data/lib/net/imap/command_data.rb +209 -22
- data/lib/net/imap/config/attr_version_defaults.rb +1 -1
- data/lib/net/imap/response_data.rb +20 -3
- data/lib/net/imap/response_parser.rb +20 -12
- data/lib/net/imap/response_reader.rb +24 -5
- data/lib/net/imap/sasl/scram_authenticator.rb +74 -0
- data/lib/net/imap.rb +138 -47
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '0917528437aff2a931fa3b40d476ffbad713cf46655163dc21a6a7a2333f7a49'
|
|
4
|
+
data.tar.gz: 90b3bf050a4f403408e30eecfaf449069e55bc293297b482e627201821997e0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
134
|
-
|
|
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
|
|
292
|
+
class Flag < Atom # :nodoc:
|
|
145
293
|
def send_data(imap, tag)
|
|
146
|
-
imap.__send__(:
|
|
294
|
+
imap.__send__(:put_string, "\\#{data}")
|
|
147
295
|
end
|
|
148
296
|
end
|
|
149
297
|
|
|
150
|
-
|
|
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__(:
|
|
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.
|
|
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
|
-
#
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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? && !
|
|
36
|
+
def done? = line_done? && !literal_size
|
|
34
37
|
def line_done? = buff.end_with?(CRLF)
|
|
35
|
-
|
|
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
|
-
|
|
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/
|
|
702
|
-
#
|
|
703
|
-
#
|
|
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
|
-
#
|
|
706
|
-
#
|
|
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.
|
|
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
|
|
1135
|
-
#
|
|
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
|
-
#
|
|
1836
|
-
#
|
|
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
|
-
#
|
|
1849
|
-
# containing a MailboxQuota object is returned.
|
|
1850
|
-
#
|
|
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
|
-
#
|
|
1857
|
-
#
|
|
1858
|
-
|
|
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",
|
|
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 +
|
|
1867
|
-
# +
|
|
1868
|
-
#
|
|
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
|
-
#
|
|
1875
|
-
#
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
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
|
-
|
|
1933
|
+
list = ["STORAGE", Integer(storage_limit)]
|
|
1881
1934
|
end
|
|
1882
|
-
send_command("SETQUOTA",
|
|
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+
|
|
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
|
|
2271
|
-
#
|
|
2327
|
+
# +criteria+ will be sent to the server <em>with minimal validation and no
|
|
2328
|
+
# encoding or formatting</em>.
|
|
2272
2329
|
#
|
|
2273
|
-
# <em>*WARNING:*
|
|
2274
|
-
#
|
|
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+.
|
|
2860
|
-
#
|
|
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+.
|
|
2886
|
-
#
|
|
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.
|
|
2901
|
-
#
|
|
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
|
-
.
|
|
3113
|
+
.map { Atom[_1] }
|
|
3038
3114
|
synchronize do
|
|
3039
|
-
send_command("ENABLE
|
|
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 =
|
|
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.
|
|
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:
|
|
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: []
|