net-imap 0.4.23 → 0.4.25
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 +236 -36
- data/lib/net/imap/response_data.rb +22 -2
- 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 +142 -50
- 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: bf05b4f7aec9127531e83134b740620da216f847851f8ca464ef54e71e3431ad
|
|
4
|
+
data.tar.gz: 18361d6ee25983da0e4814d42a0ec851e10ce88789559e30778a945a7024a297
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 697214c436f129746ad8986dde31e4b819854cf131472158c4aacf5a35ef665b8529528bd0a843ba17d9be105711b4163ecccfa525989d9fe0b3f807f4ae9d58
|
|
7
|
+
data.tar.gz: ef657a1dbfd5bc73fa317248ae19589a842f87eabfe966b43b026e9dceff68d510621d3e17136b266d22f68b85d20fc22b4ea67a482fdca193d8a5290c643abf
|
|
@@ -14,17 +14,19 @@ module Net
|
|
|
14
14
|
when nil
|
|
15
15
|
when String
|
|
16
16
|
when Integer
|
|
17
|
-
|
|
17
|
+
# Covers modseq-valzer, which is the largest valid IMAP integer
|
|
18
|
+
if data.negative?
|
|
19
|
+
raise DataFormatError, "Integer argument must be unsigned: #{data}"
|
|
20
|
+
elsif 0xffff_ffff_ffff_ffff < data
|
|
21
|
+
raise DataFormatError, "Integer argument must fit in 64 bits: #{data}"
|
|
22
|
+
end
|
|
18
23
|
when Array
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
else
|
|
22
|
-
data.each do |i|
|
|
23
|
-
validate_data(i)
|
|
24
|
-
end
|
|
24
|
+
data.each do |i|
|
|
25
|
+
validate_data(i)
|
|
25
26
|
end
|
|
26
27
|
when Time, Date, DateTime
|
|
27
28
|
when Symbol
|
|
29
|
+
Flag.validate(data)
|
|
28
30
|
else
|
|
29
31
|
data.validate
|
|
30
32
|
end
|
|
@@ -45,7 +47,7 @@ module Net
|
|
|
45
47
|
when Date
|
|
46
48
|
send_date_data(data)
|
|
47
49
|
when Symbol
|
|
48
|
-
|
|
50
|
+
Flag[data].send_data(self, tag)
|
|
49
51
|
else
|
|
50
52
|
data.send_data(self, tag)
|
|
51
53
|
end
|
|
@@ -77,9 +79,31 @@ module Net
|
|
|
77
79
|
put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
|
|
78
80
|
end
|
|
79
81
|
|
|
80
|
-
def send_literal(
|
|
82
|
+
def send_binary_literal(*a, **kw) send_literal(*a, **kw, binary: true) end
|
|
83
|
+
|
|
84
|
+
# `non_sync` is an optional tri-state flag:
|
|
85
|
+
# * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
|
|
86
|
+
# NOTE: raises DataFormatError when server doesn't support
|
|
87
|
+
# non-synchronizing literal, or literal is too large for LITERAL-.
|
|
88
|
+
# * `false` -> Force normal synchronizing literal behavior.
|
|
89
|
+
# * `nil` -> (default) Currently behaves like `false` (will be dynamic).
|
|
90
|
+
# TODO: Dynamic, based on capabilities and bytesize.
|
|
91
|
+
def send_literal(str, tag = nil, binary: false, non_sync: nil)
|
|
92
|
+
bytesize = str.bytesize
|
|
81
93
|
synchronize do
|
|
82
|
-
|
|
94
|
+
if non_sync && !non_sync_literal_allowed?(bytesize)
|
|
95
|
+
# TODO: check in Printer, so we don't need to close the connection.
|
|
96
|
+
@sock.close
|
|
97
|
+
raise DataFormatError, "Connection closed: " \
|
|
98
|
+
"Cannot send non-synchronizing literal without known server support"
|
|
99
|
+
end
|
|
100
|
+
prefix = "~" if binary
|
|
101
|
+
plus = "+" if non_sync
|
|
102
|
+
put_string("#{prefix}{#{bytesize}#{plus}}\r\n")
|
|
103
|
+
if non_sync
|
|
104
|
+
put_string(str)
|
|
105
|
+
return
|
|
106
|
+
end
|
|
83
107
|
@continued_command_tag = tag
|
|
84
108
|
@continuation_request_exception = nil
|
|
85
109
|
begin
|
|
@@ -94,8 +118,18 @@ module Net
|
|
|
94
118
|
end
|
|
95
119
|
end
|
|
96
120
|
|
|
121
|
+
def non_sync_literal_allowed?(bytesize)
|
|
122
|
+
return unless capabilities_cached?
|
|
123
|
+
return "+" if capable?("LITERAL+")
|
|
124
|
+
return "-" if capable_literal_minus? && bytesize <= 4096
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def capable_literal_minus?; capable?("LITERAL-") || capable?("IMAP4rev2") end
|
|
129
|
+
|
|
130
|
+
# NOTE: +num+ should already be an Integer
|
|
97
131
|
def send_number_data(num)
|
|
98
|
-
put_string(num.to_s)
|
|
132
|
+
put_string(Integer(num).to_s)
|
|
99
133
|
end
|
|
100
134
|
|
|
101
135
|
def send_list_data(list, tag = nil)
|
|
@@ -115,67 +149,233 @@ module Net
|
|
|
115
149
|
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
|
|
116
150
|
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
|
|
117
151
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
152
|
+
# simplistic emulation of CommandData = Data.define(:data)
|
|
153
|
+
class CommandData # :nodoc:
|
|
154
|
+
class << self
|
|
155
|
+
def new(arg = nil, data: arg) super(data: data) end
|
|
156
|
+
alias :[] :new
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def initialize(data:)
|
|
160
|
+
@data = data
|
|
161
|
+
freeze
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
attr_reader :data
|
|
165
|
+
|
|
166
|
+
def to_h(&block) block ? to_h.to_h(&block) : { data: data } end
|
|
167
|
+
def ==(other) self.class === other && to_h == other.to_h end
|
|
168
|
+
def eql?(other) self.class === other && to_h.eql?(other.to_h) end
|
|
169
|
+
|
|
170
|
+
# following class definition goes beyond the basic Data.define(:data)
|
|
171
|
+
##
|
|
172
|
+
|
|
173
|
+
def self.validate(...)
|
|
174
|
+
data = new(...)
|
|
175
|
+
data.validate
|
|
176
|
+
data
|
|
177
|
+
end
|
|
121
178
|
|
|
122
|
-
class RawData # :nodoc:
|
|
123
179
|
def send_data(imap, tag)
|
|
124
|
-
|
|
180
|
+
raise NoMethodError, "#{self.class} must implement #{__method__}"
|
|
125
181
|
end
|
|
126
182
|
|
|
127
183
|
def validate
|
|
128
184
|
end
|
|
185
|
+
end
|
|
129
186
|
|
|
130
|
-
|
|
187
|
+
# Represents IMAP +text+ or +quoted+ data, which share the same
|
|
188
|
+
# validations of decoded #data, and differ only in how they are formatted.
|
|
189
|
+
#
|
|
190
|
+
# +data+ may contain any 7-bit ASCII character except +NULL+, +CR+, or +LF+.
|
|
191
|
+
# Any multibyte +UTF-8+ character is also allowed when the connection
|
|
192
|
+
# supports UTF8: either +UTF8=ACCEPT+ or +IMAP4rev2+ have been enabled, or
|
|
193
|
+
# the server supports only +IMAP4rev2+ and not earlier IMAP revisions, or
|
|
194
|
+
# the server advertises +UTF8=ONLY+.
|
|
195
|
+
#
|
|
196
|
+
# NOTE: This does not verify whether the connection supports UTF-8, but that
|
|
197
|
+
# may change in future versions.
|
|
198
|
+
#
|
|
199
|
+
# The string's bytes must be valid ASCII or valid UTF-8. The string's
|
|
200
|
+
# reported encoding is ignored, but the string is _not_ transcoded.
|
|
201
|
+
class ValidNonLiteralData < CommandData
|
|
202
|
+
def initialize(data:)
|
|
203
|
+
data = String(data.to_str)
|
|
204
|
+
unless [Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
|
|
205
|
+
data = data.dup.force_encoding(data.ascii_only? ? "ASCII" : "UTF-8")
|
|
206
|
+
end
|
|
207
|
+
data = -data
|
|
208
|
+
super
|
|
209
|
+
validate
|
|
210
|
+
end
|
|
131
211
|
|
|
132
|
-
def
|
|
133
|
-
|
|
212
|
+
def validate
|
|
213
|
+
if ![Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
|
|
214
|
+
raise DataFormatError, "must use ASCII or UTF-8 encoding"
|
|
215
|
+
elsif !data.valid_encoding?
|
|
216
|
+
raise DataFormatError, "invalid UTF-8 must be literal encoded"
|
|
217
|
+
elsif data.include?("\0")
|
|
218
|
+
raise DataFormatError, "NULL byte must be binary literal encoded"
|
|
219
|
+
elsif /[\r\n]/.match?(data)
|
|
220
|
+
raise DataFormatError, "CR and LF bytes must be literal encoded"
|
|
221
|
+
end
|
|
134
222
|
end
|
|
223
|
+
|
|
224
|
+
def ascii_only?; data.ascii_only? end
|
|
225
|
+
|
|
226
|
+
def send_data(imap, tag = nil) imap.__send__(:put_string, formatted) end
|
|
135
227
|
end
|
|
136
228
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
229
|
+
# Represents IMAP +text+ data, which covers everything in the IMAP grammar,
|
|
230
|
+
# except for +literal+, +literal8+, and the concluding +CRLF+.
|
|
231
|
+
#
|
|
232
|
+
# NOTE: The current implementation does not verify that the connection
|
|
233
|
+
# supports UTF-8. Future versions may validate this.
|
|
234
|
+
class RawText < ValidNonLiteralData # :nodoc:
|
|
235
|
+
# raw: no formatting necessary
|
|
236
|
+
alias formatted data
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
class RawData < CommandData # :nodoc:
|
|
240
|
+
def initialize(data:)
|
|
241
|
+
case data
|
|
242
|
+
when String
|
|
243
|
+
data = self.class.split(data)
|
|
244
|
+
when Array
|
|
245
|
+
unless data.all? { |part| RawText === part || Literal === part }
|
|
246
|
+
raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
|
|
247
|
+
end
|
|
248
|
+
else
|
|
249
|
+
raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
|
|
250
|
+
end
|
|
251
|
+
super
|
|
252
|
+
validate
|
|
140
253
|
end
|
|
141
254
|
|
|
255
|
+
def send_data(imap, tag) data.each do _1.send_data(imap, tag) end end
|
|
256
|
+
|
|
142
257
|
def validate
|
|
258
|
+
return unless RawText === data.last
|
|
259
|
+
text = data.last.data
|
|
260
|
+
if text.rindex(/\{\d+\+?\}\z/n)
|
|
261
|
+
raise DataFormatError, "RawData cannot end with literal continuation"
|
|
262
|
+
end
|
|
143
263
|
end
|
|
144
264
|
|
|
145
|
-
|
|
265
|
+
# Splits an input +string+ into an array of RawText and Literal/Literal8.
|
|
266
|
+
#
|
|
267
|
+
# NOTE: unlike RawData#validate, this does not prevent the final RawText
|
|
268
|
+
# from ending with a literal prefix.
|
|
269
|
+
def self.split(data)
|
|
270
|
+
data = data.b # dups and ensures BINARY encoding
|
|
271
|
+
parts = []
|
|
272
|
+
while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
|
|
273
|
+
text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
|
|
274
|
+
bytesize = Integer bytesize, 10
|
|
275
|
+
parts << RawText[text] unless text.empty?
|
|
276
|
+
parts << extract_literal(data,
|
|
277
|
+
binary: binary,
|
|
278
|
+
bytesize: bytesize,
|
|
279
|
+
non_sync: non_sync)
|
|
280
|
+
data[0, bytesize] = ""
|
|
281
|
+
end
|
|
282
|
+
parts << RawText[data] unless data.empty?
|
|
283
|
+
parts
|
|
284
|
+
end
|
|
146
285
|
|
|
147
|
-
def
|
|
148
|
-
|
|
286
|
+
def self.extract_literal(data, binary:, bytesize:, non_sync:)
|
|
287
|
+
if data.bytesize < bytesize
|
|
288
|
+
raise DataFormatError, "Too few bytes in string for literal, " \
|
|
289
|
+
"expected: %s, remaining: %s" % [bytesize, data.bytesize]
|
|
290
|
+
end
|
|
291
|
+
literal = data.byteslice(0, bytesize)
|
|
292
|
+
(binary ? Literal8 : Literal).new(data: literal, non_sync: non_sync)
|
|
149
293
|
end
|
|
294
|
+
private_class_method :extract_literal
|
|
150
295
|
end
|
|
151
296
|
|
|
152
|
-
class
|
|
153
|
-
def
|
|
154
|
-
|
|
297
|
+
class Atom < CommandData # :nodoc:
|
|
298
|
+
def initialize(**)
|
|
299
|
+
super
|
|
300
|
+
validate
|
|
155
301
|
end
|
|
156
302
|
|
|
157
303
|
def validate
|
|
304
|
+
data.to_s.ascii_only? \
|
|
305
|
+
or raise DataFormatError, "#{self.class} must be ASCII only"
|
|
306
|
+
data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
|
|
307
|
+
and raise DataFormatError, "#{self.class} must not contain atom-specials"
|
|
308
|
+
data.empty? \
|
|
309
|
+
and raise DataFormatError, "#{self.class} must not be empty"
|
|
158
310
|
end
|
|
159
311
|
|
|
160
|
-
|
|
312
|
+
def send_data(imap, tag)
|
|
313
|
+
imap.__send__(:put_string, data.to_s)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
161
316
|
|
|
162
|
-
|
|
163
|
-
|
|
317
|
+
class Flag < Atom # :nodoc:
|
|
318
|
+
def send_data(imap, tag)
|
|
319
|
+
imap.__send__(:put_string, "\\#{data}")
|
|
164
320
|
end
|
|
165
321
|
end
|
|
166
322
|
|
|
323
|
+
# Represents a IMAP +quoted+ string, which can encode any valid ASCII or
|
|
324
|
+
# UTF-8 string, unless it contains any +CR+, +LF+, or +NULL+ bytes.
|
|
325
|
+
#
|
|
326
|
+
# NOTE: The current implementation does not verify that the connection
|
|
327
|
+
# supports UTF-8. Future versions may validate this.
|
|
328
|
+
class QuotedString < ValidNonLiteralData # :nodoc:
|
|
329
|
+
def formatted; %("#{data.gsub(/["\\]/, "\\\\\\&")}") end
|
|
330
|
+
end
|
|
331
|
+
|
|
167
332
|
class Literal # :nodoc:
|
|
168
|
-
|
|
169
|
-
|
|
333
|
+
class << self
|
|
334
|
+
def new(_data = nil, _non_sync = nil, data: _data, non_sync: _non_sync)
|
|
335
|
+
super(data: data, non_sync: non_sync)
|
|
336
|
+
end
|
|
337
|
+
alias :[] :new
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
attr_reader :data, :non_sync
|
|
341
|
+
|
|
342
|
+
def to_h(&block) block ? to_h.to_h(&block) : { data: data, non_sync: non_sync } end
|
|
343
|
+
def ==(other) self.class === other && to_h == other.to_h end
|
|
344
|
+
def eql?(other) self.class === other && to_h.eql?(other.to_h) end
|
|
345
|
+
|
|
346
|
+
def initialize(data:, non_sync: nil)
|
|
347
|
+
data = -String(data.to_str).b or
|
|
348
|
+
raise DataFormatError, "#{self.class} expects string input"
|
|
349
|
+
@data, @non_sync = data, non_sync
|
|
350
|
+
validate
|
|
351
|
+
freeze
|
|
170
352
|
end
|
|
171
353
|
|
|
354
|
+
def self.validate(...)
|
|
355
|
+
data = new(...)
|
|
356
|
+
data.validate
|
|
357
|
+
data
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def bytesize; data.bytesize end
|
|
361
|
+
|
|
172
362
|
def validate
|
|
363
|
+
if data.include?("\0")
|
|
364
|
+
raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
|
|
365
|
+
"Use #{Literal8} or a null-safe encoding."
|
|
366
|
+
end
|
|
173
367
|
end
|
|
174
368
|
|
|
175
|
-
|
|
369
|
+
def send_data(imap, tag)
|
|
370
|
+
imap.__send__(:send_literal, data, tag, non_sync: non_sync)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
176
373
|
|
|
177
|
-
|
|
178
|
-
|
|
374
|
+
class Literal8 < Literal # :nodoc:
|
|
375
|
+
def validate; nil end # all bytes are okay
|
|
376
|
+
|
|
377
|
+
def send_data(imap, tag)
|
|
378
|
+
imap.__send__(:send_binary_literal, data, tag, non_sync: non_sync)
|
|
179
379
|
end
|
|
180
380
|
end
|
|
181
381
|
|
|
@@ -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
|
|
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
|
-
#
|
|
409
|
+
# Storage limit imposed on the mailbox.
|
|
390
410
|
#
|
|
391
411
|
end
|
|
392
412
|
|
|
@@ -2055,10 +2055,7 @@ module Net
|
|
|
2055
2055
|
if $1
|
|
2056
2056
|
return Token.new(T_SPACE, $+)
|
|
2057
2057
|
elsif $2
|
|
2058
|
-
|
|
2059
|
-
val = @str[@pos, len]
|
|
2060
|
-
@pos += len
|
|
2061
|
-
return Token.new(T_LITERAL8, val)
|
|
2058
|
+
literal_token($+, T_LITERAL8)
|
|
2062
2059
|
elsif $3 && $7
|
|
2063
2060
|
# greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
|
|
2064
2061
|
return Token.new(T_ATOM, $3)
|
|
@@ -2086,10 +2083,7 @@ module Net
|
|
|
2086
2083
|
elsif $15
|
|
2087
2084
|
return Token.new(T_RBRA, $+)
|
|
2088
2085
|
elsif $16
|
|
2089
|
-
|
|
2090
|
-
val = @str[@pos, len]
|
|
2091
|
-
@pos += len
|
|
2092
|
-
return Token.new(T_LITERAL, val)
|
|
2086
|
+
literal_token($+)
|
|
2093
2087
|
elsif $17
|
|
2094
2088
|
return Token.new(T_PERCENT, $+)
|
|
2095
2089
|
elsif $18
|
|
@@ -2115,10 +2109,7 @@ module Net
|
|
|
2115
2109
|
elsif $4
|
|
2116
2110
|
return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
|
|
2117
2111
|
elsif $5
|
|
2118
|
-
|
|
2119
|
-
val = @str[@pos, len]
|
|
2120
|
-
@pos += len
|
|
2121
|
-
return Token.new(T_LITERAL, val)
|
|
2112
|
+
literal_token($+)
|
|
2122
2113
|
elsif $6
|
|
2123
2114
|
return Token.new(T_LPAR, $+)
|
|
2124
2115
|
elsif $7
|
|
@@ -2133,6 +2124,23 @@ module Net
|
|
|
2133
2124
|
else
|
|
2134
2125
|
parse_error("invalid @lex_state - %s", @lex_state.inspect)
|
|
2135
2126
|
end
|
|
2127
|
+
rescue DataFormatError => error
|
|
2128
|
+
parse_error error.message
|
|
2129
|
+
end
|
|
2130
|
+
|
|
2131
|
+
def literal_token(len, type = T_LITERAL)
|
|
2132
|
+
len = coerce_number64 len.to_i
|
|
2133
|
+
val = @str[@pos, len]
|
|
2134
|
+
@pos += len
|
|
2135
|
+
Token.new(type, val)
|
|
2136
|
+
end
|
|
2137
|
+
|
|
2138
|
+
# copied/adapted from NumValidator in v0.6
|
|
2139
|
+
def coerce_number64(num)
|
|
2140
|
+
int = num.to_i
|
|
2141
|
+
return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
|
|
2142
|
+
raise DataFormatError,
|
|
2143
|
+
"number64 must be unsigned 63-bit integer: #{num}"
|
|
2136
2144
|
end
|
|
2137
2145
|
|
|
2138
2146
|
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 end
|
|
32
35
|
def empty?; buff.empty? end
|
|
33
|
-
def done?; line_done? && !
|
|
36
|
+
def done?; line_done? && !literal_size end
|
|
34
37
|
def line_done?; buff.end_with?(CRLF) end
|
|
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
|
|
@@ -70,6 +81,14 @@ module Net
|
|
|
70
81
|
)
|
|
71
82
|
end
|
|
72
83
|
|
|
84
|
+
# copied/adapted from NumValidator in v0.6
|
|
85
|
+
def coerce_number64(num)
|
|
86
|
+
int = num.to_i
|
|
87
|
+
return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
|
|
88
|
+
raise DataFormatError,
|
|
89
|
+
"number64 must be unsigned 63-bit integer: #{num}"
|
|
90
|
+
end
|
|
91
|
+
|
|
73
92
|
end
|
|
74
93
|
end
|
|
75
94
|
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
|
+
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://
|
|
685
|
-
#
|
|
686
|
-
#
|
|
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
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
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.
|
|
782
|
+
VERSION = "0.4.25"
|
|
765
783
|
|
|
766
784
|
# Aliases for supported capabilities, to be used with the #enable command.
|
|
767
785
|
ENABLE_ALIASES = {
|
|
@@ -1031,26 +1049,22 @@ module Net
|
|
|
1031
1049
|
|
|
1032
1050
|
# Disconnects from the server.
|
|
1033
1051
|
#
|
|
1052
|
+
# Waits for receiver thread to close before returning. Slow or stuck
|
|
1053
|
+
# response handlers can cause #disconnect to hang until they complete.
|
|
1054
|
+
#
|
|
1034
1055
|
# Related: #logout, #logout!
|
|
1035
1056
|
def disconnect
|
|
1036
1057
|
return if disconnected?
|
|
1058
|
+
in_receiver_thread = Thread.current == @receiver_thread
|
|
1037
1059
|
begin
|
|
1038
|
-
|
|
1039
|
-
# try to call SSL::SSLSocket#io.
|
|
1040
|
-
@sock.io.shutdown
|
|
1041
|
-
rescue NoMethodError
|
|
1042
|
-
# @sock is not an SSL::SSLSocket.
|
|
1043
|
-
@sock.shutdown
|
|
1044
|
-
end
|
|
1060
|
+
@sock.to_io.shutdown
|
|
1045
1061
|
rescue Errno::ENOTCONN
|
|
1046
1062
|
# ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
|
|
1047
1063
|
rescue Exception => e
|
|
1048
|
-
@receiver_thread.raise(e)
|
|
1049
|
-
end
|
|
1050
|
-
@receiver_thread.join
|
|
1051
|
-
synchronize do
|
|
1052
|
-
@sock.close
|
|
1064
|
+
@receiver_thread.raise(e) unless in_receiver_thread
|
|
1053
1065
|
end
|
|
1066
|
+
@sock.close
|
|
1067
|
+
@receiver_thread.join unless mon_owned? || in_receiver_thread
|
|
1054
1068
|
raise e if e
|
|
1055
1069
|
end
|
|
1056
1070
|
|
|
@@ -1294,9 +1308,11 @@ module Net
|
|
|
1294
1308
|
#
|
|
1295
1309
|
def starttls(**options)
|
|
1296
1310
|
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
|
|
1311
|
+
handled = false
|
|
1297
1312
|
error = nil
|
|
1298
1313
|
ok = send_command("STARTTLS") do |resp|
|
|
1299
1314
|
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
|
|
1315
|
+
handled = true
|
|
1300
1316
|
clear_cached_capabilities
|
|
1301
1317
|
clear_responses
|
|
1302
1318
|
start_tls_session
|
|
@@ -1308,6 +1324,13 @@ module Net
|
|
|
1308
1324
|
disconnect
|
|
1309
1325
|
raise error
|
|
1310
1326
|
end
|
|
1327
|
+
unless handled
|
|
1328
|
+
disconnect
|
|
1329
|
+
raise InvalidResponseError,
|
|
1330
|
+
"STARTTLS handler was bypassed, although server responded %p" % [
|
|
1331
|
+
ok.raw_data.chomp
|
|
1332
|
+
]
|
|
1333
|
+
end
|
|
1311
1334
|
ok
|
|
1312
1335
|
end
|
|
1313
1336
|
|
|
@@ -1742,12 +1765,18 @@ module Net
|
|
|
1742
1765
|
# to both admin and user. If this mailbox exists, it returns an array
|
|
1743
1766
|
# containing objects of type MailboxQuotaRoot and MailboxQuota.
|
|
1744
1767
|
#
|
|
1768
|
+
# *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
|
|
1769
|
+
# resource type. This is usually +STORAGE+, but you may need to verify this
|
|
1770
|
+
# with UntaggedResponse#raw_data.
|
|
1771
|
+
#
|
|
1745
1772
|
# Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
|
|
1746
1773
|
#
|
|
1747
1774
|
# ===== Capabilities
|
|
1748
1775
|
#
|
|
1749
|
-
#
|
|
1750
|
-
#
|
|
1776
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
1777
|
+
# capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
|
|
1778
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
|
|
1779
|
+
# resource type.
|
|
1751
1780
|
def getquotaroot(mailbox)
|
|
1752
1781
|
synchronize do
|
|
1753
1782
|
send_command("GETQUOTAROOT", mailbox)
|
|
@@ -1759,41 +1788,59 @@ module Net
|
|
|
1759
1788
|
end
|
|
1760
1789
|
|
|
1761
1790
|
# Sends a {GETQUOTA command [RFC2087 §4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
|
|
1762
|
-
#
|
|
1763
|
-
# containing a MailboxQuota object is returned.
|
|
1764
|
-
#
|
|
1791
|
+
# for the +quota_root+. If this quota root exists, then an array
|
|
1792
|
+
# containing a MailboxQuota object is returned.
|
|
1793
|
+
#
|
|
1794
|
+
# The names of quota roots that are applicable to a particular mailbox can
|
|
1795
|
+
# be discovered with #getquotaroot.
|
|
1796
|
+
#
|
|
1797
|
+
# *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
|
|
1798
|
+
# resource type. This is usually +STORAGE+, but you may need to verify this
|
|
1799
|
+
# with UntaggedResponse#raw_data.
|
|
1765
1800
|
#
|
|
1766
1801
|
# Related: #getquotaroot, #setquota, MailboxQuota
|
|
1767
1802
|
#
|
|
1768
1803
|
# ===== Capabilities
|
|
1769
1804
|
#
|
|
1770
|
-
#
|
|
1771
|
-
#
|
|
1772
|
-
|
|
1805
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
1806
|
+
# capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
|
|
1807
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
|
|
1808
|
+
# resource type.
|
|
1809
|
+
def getquota(quota_root)
|
|
1773
1810
|
synchronize do
|
|
1774
|
-
send_command("GETQUOTA",
|
|
1811
|
+
send_command("GETQUOTA", quota_root)
|
|
1775
1812
|
clear_responses("QUOTA")
|
|
1776
1813
|
end
|
|
1777
1814
|
end
|
|
1778
1815
|
|
|
1779
1816
|
# Sends a {SETQUOTA command [RFC2087 §4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
|
|
1780
|
-
# along with the specified +
|
|
1781
|
-
# +
|
|
1782
|
-
#
|
|
1817
|
+
# along with the specified +quota_root+ and +storage_limit+. If
|
|
1818
|
+
# +storage_limit+ is +nil+, resource limits are unset for that quota root.
|
|
1819
|
+
# If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
|
|
1820
|
+
#
|
|
1821
|
+
# imap.setquota "#user/alice", 100
|
|
1822
|
+
# imap.getquota "#user/alice"
|
|
1823
|
+
# # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
|
|
1824
|
+
#
|
|
1825
|
+
# Typically one needs to be logged in as a server admin for this to work.
|
|
1826
|
+
#
|
|
1827
|
+
# *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
|
|
1783
1828
|
#
|
|
1784
1829
|
# Related: #getquota, #getquotaroot
|
|
1785
1830
|
#
|
|
1786
1831
|
# ===== Capabilities
|
|
1787
1832
|
#
|
|
1788
|
-
#
|
|
1789
|
-
#
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1833
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
1834
|
+
# capability, or both +QUOTASET+ and a capability prefixed with
|
|
1835
|
+
# <tt>QUOTA=RES-*</tt> {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208]
|
|
1836
|
+
# for each supported resource type.
|
|
1837
|
+
def setquota(quota_root, storage_limit)
|
|
1838
|
+
if storage_limit.nil?
|
|
1839
|
+
list = []
|
|
1793
1840
|
else
|
|
1794
|
-
|
|
1841
|
+
list = ["STORAGE", Integer(storage_limit)]
|
|
1795
1842
|
end
|
|
1796
|
-
send_command("SETQUOTA",
|
|
1843
|
+
send_command("SETQUOTA", quota_root, list)
|
|
1797
1844
|
end
|
|
1798
1845
|
|
|
1799
1846
|
# Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
|
|
@@ -1900,7 +1947,10 @@ module Net
|
|
|
1900
1947
|
# <tt>STATUS=SIZE</tt>
|
|
1901
1948
|
# {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
|
|
1902
1949
|
#
|
|
1903
|
-
# +DELETED+
|
|
1950
|
+
# +DELETED+ must be supported when the server's capabilities includes
|
|
1951
|
+
# +IMAP4rev2+.
|
|
1952
|
+
# or <tt>QUOTA=RES-MESSAGES</tt>
|
|
1953
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
|
|
1904
1954
|
#
|
|
1905
1955
|
# +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
|
|
1906
1956
|
# {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
|
|
@@ -2041,6 +2091,8 @@ module Net
|
|
|
2041
2091
|
# string holding the entire search string, or a single-dimension array of
|
|
2042
2092
|
# search keywords and arguments.
|
|
2043
2093
|
#
|
|
2094
|
+
# <em>Please note</em> the warning (below) when +keys+ is a String.
|
|
2095
|
+
#
|
|
2044
2096
|
# Returns a SearchResult object. SearchResult inherits from Array (for
|
|
2045
2097
|
# backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
|
|
2046
2098
|
# capability has been enabled.
|
|
@@ -2049,6 +2101,14 @@ module Net
|
|
|
2049
2101
|
#
|
|
2050
2102
|
# ===== Search criteria
|
|
2051
2103
|
#
|
|
2104
|
+
# >>>
|
|
2105
|
+
# When +keys+ is an Array, elements in the array will be validated and
|
|
2106
|
+
# formatted. When +keys+ is a String, it will be sent <em>with
|
|
2107
|
+
# minimal validation and no encoding or formatting</em>.
|
|
2108
|
+
#
|
|
2109
|
+
# <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
|
|
2110
|
+
# types of attribute injection attack if unvetted user input is used.</em>
|
|
2111
|
+
#
|
|
2052
2112
|
# For a full list of search criteria,
|
|
2053
2113
|
# see [{IMAP4rev1 §6.4.4}[https://www.rfc-editor.org/rfc/rfc3501.html#section-6.4.4]],
|
|
2054
2114
|
# or [{IMAP4rev2 §6.4.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-6.4.4]],
|
|
@@ -2114,7 +2174,8 @@ module Net
|
|
|
2114
2174
|
# backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
|
|
2115
2175
|
# capability has been enabled.
|
|
2116
2176
|
#
|
|
2117
|
-
# See #search for documentation of
|
|
2177
|
+
# See #search for documentation of parameters. <em>Please note</em> the
|
|
2178
|
+
# warning for when +keys+ is a String.
|
|
2118
2179
|
def uid_search(keys, charset = nil)
|
|
2119
2180
|
return search_internal("UID SEARCH", keys, charset)
|
|
2120
2181
|
end
|
|
@@ -2136,6 +2197,13 @@ module Net
|
|
|
2136
2197
|
#
|
|
2137
2198
|
# +attr+ is a list of attributes to fetch; see the documentation
|
|
2138
2199
|
# for FetchData for a list of valid attributes.
|
|
2200
|
+
# >>>
|
|
2201
|
+
# When +attr+ is a String, it will be sent <em>with minimal validation and
|
|
2202
|
+
# no encoding or formatting</em>. When +attr+ is an Array, each String in
|
|
2203
|
+
# +attr+ will be sent this way.
|
|
2204
|
+
#
|
|
2205
|
+
# <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
|
|
2206
|
+
# types of attribute injection attack if unvetted user input is used.</em>
|
|
2139
2207
|
#
|
|
2140
2208
|
# +changedsince+ is an optional integer mod-sequence. It limits results to
|
|
2141
2209
|
# messages with a mod-sequence greater than +changedsince+.
|
|
@@ -2184,6 +2252,8 @@ module Net
|
|
|
2184
2252
|
# Similar to #fetch, but the +set+ parameter contains unique identifiers
|
|
2185
2253
|
# instead of message sequence numbers.
|
|
2186
2254
|
#
|
|
2255
|
+
# +attr+ behaves the same as with #fetch. <em>Please note</em> the #fetch
|
|
2256
|
+
# warning on the +attr+ argument.
|
|
2187
2257
|
# >>>
|
|
2188
2258
|
# *Note:* Servers _MUST_ implicitly include the +UID+ message data item as
|
|
2189
2259
|
# part of any +FETCH+ response caused by a +UID+ command, regardless of
|
|
@@ -2336,8 +2406,10 @@ module Net
|
|
|
2336
2406
|
|
|
2337
2407
|
# Sends a {SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
|
|
2338
2408
|
# to search a mailbox for messages that match +search_keys+ and return an
|
|
2339
|
-
# array of message sequence numbers, sorted by +sort_keys+.
|
|
2340
|
-
#
|
|
2409
|
+
# array of message sequence numbers, sorted by +sort_keys+.
|
|
2410
|
+
#
|
|
2411
|
+
# +search_keys+ are interpreted the same as the +criteria+ argument for
|
|
2412
|
+
# #search. <em>Please note</em> the #search warning for String +criteria+.
|
|
2341
2413
|
#
|
|
2342
2414
|
#--
|
|
2343
2415
|
# TODO: describe +sort_keys+
|
|
@@ -2362,8 +2434,10 @@ module Net
|
|
|
2362
2434
|
|
|
2363
2435
|
# Sends a {UID SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
|
|
2364
2436
|
# to search a mailbox for messages that match +search_keys+ and return an
|
|
2365
|
-
# array of unique identifiers, sorted by +sort_keys+.
|
|
2366
|
-
#
|
|
2437
|
+
# array of unique identifiers, sorted by +sort_keys+.
|
|
2438
|
+
#
|
|
2439
|
+
# +search_keys+ are interpreted the same as the +criteria+ argument for
|
|
2440
|
+
# #search. <em>Please note</em> the #search warning for String +criteria+.
|
|
2367
2441
|
#
|
|
2368
2442
|
# Related: #sort, #search, #uid_search, #thread, #uid_thread
|
|
2369
2443
|
#
|
|
@@ -2377,8 +2451,10 @@ module Net
|
|
|
2377
2451
|
|
|
2378
2452
|
# Sends a {THREAD command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
|
|
2379
2453
|
# to search a mailbox and return message sequence numbers in threaded
|
|
2380
|
-
# format, as a ThreadMember tree.
|
|
2381
|
-
#
|
|
2454
|
+
# format, as a ThreadMember tree.
|
|
2455
|
+
#
|
|
2456
|
+
# +search_keys+ are interpreted the same as the +criteria+ argument for
|
|
2457
|
+
# #search. <em>Please note</em> the #search warning for String +criteria+.
|
|
2382
2458
|
#
|
|
2383
2459
|
# The supported algorithms are:
|
|
2384
2460
|
#
|
|
@@ -2404,6 +2480,9 @@ module Net
|
|
|
2404
2480
|
# Similar to #thread, but returns unique identifiers instead of
|
|
2405
2481
|
# message sequence numbers.
|
|
2406
2482
|
#
|
|
2483
|
+
# +search_keys+ are interpreted the same as the +criteria+ argument for
|
|
2484
|
+
# #search. <em>Please note</em> the #search warning for String +criteria+.
|
|
2485
|
+
#
|
|
2407
2486
|
# Related: #thread, #search, #uid_search, #sort, #uid_sort
|
|
2408
2487
|
#
|
|
2409
2488
|
# ===== Capabilities
|
|
@@ -2491,10 +2570,11 @@ module Net
|
|
|
2491
2570
|
capabilities = capabilities
|
|
2492
2571
|
.flatten
|
|
2493
2572
|
.map {|e| ENABLE_ALIASES[e] || e }
|
|
2573
|
+
.flat_map { _1.is_a?(String) && !_1.empty? ? _1.split(/ /, -1) : [_1] }
|
|
2494
2574
|
.uniq
|
|
2495
|
-
.
|
|
2575
|
+
.map { Atom[_1] }
|
|
2496
2576
|
synchronize do
|
|
2497
|
-
send_command("ENABLE
|
|
2577
|
+
send_command("ENABLE", *capabilities)
|
|
2498
2578
|
result = clear_responses("ENABLED").last || []
|
|
2499
2579
|
@utf8_strings ||= result.include? "UTF8=ACCEPT"
|
|
2500
2580
|
@utf8_strings ||= result.include? "IMAP4REV2"
|
|
@@ -2992,6 +3072,7 @@ module Net
|
|
|
2992
3072
|
put_string(" ")
|
|
2993
3073
|
send_data(i, tag)
|
|
2994
3074
|
end
|
|
3075
|
+
guard_against_tagged_response_skipping_handler!(tag)
|
|
2995
3076
|
put_string(CRLF)
|
|
2996
3077
|
if cmd == "LOGOUT"
|
|
2997
3078
|
@logout_command_tag = tag
|
|
@@ -3007,6 +3088,17 @@ module Net
|
|
|
3007
3088
|
end
|
|
3008
3089
|
end
|
|
3009
3090
|
end
|
|
3091
|
+
rescue InvalidResponseError
|
|
3092
|
+
disconnect
|
|
3093
|
+
raise
|
|
3094
|
+
end
|
|
3095
|
+
|
|
3096
|
+
def guard_against_tagged_response_skipping_handler!(tag)
|
|
3097
|
+
return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
|
|
3098
|
+
raise(InvalidResponseError,
|
|
3099
|
+
"Server sent tagged 'OK' before command was finished: %p. " \
|
|
3100
|
+
"This could indicate a malicious server or client-side " \
|
|
3101
|
+
"command injection. Disconnecting." % [resp.raw_data.chomp])
|
|
3010
3102
|
end
|
|
3011
3103
|
|
|
3012
3104
|
def generate_tag
|
|
@@ -3071,7 +3163,7 @@ module Net
|
|
|
3071
3163
|
end
|
|
3072
3164
|
|
|
3073
3165
|
def store_internal(cmd, set, attr, flags, unchangedsince: nil)
|
|
3074
|
-
attr =
|
|
3166
|
+
attr = Atom.new(attr) if attr.instance_of?(String)
|
|
3075
3167
|
args = [MessageSet.new(set)]
|
|
3076
3168
|
args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
|
|
3077
3169
|
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.
|
|
4
|
+
version: 0.4.25
|
|
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:
|
|
128
|
+
rubygems_version: 4.0.10
|
|
129
129
|
specification_version: 4
|
|
130
130
|
summary: Ruby client api for Internet Message Access Protocol
|
|
131
131
|
test_files: []
|