net-imap 0.4.23 → 0.4.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/net/imap/command_data.rb +182 -21
- data/lib/net/imap/response_data.rb +22 -2
- data/lib/net/imap/response_reader.rb +11 -4
- data/lib/net/imap/sasl/scram_authenticator.rb +74 -0
- data/lib/net/imap.rb +110 -29
- 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: 9419630b908b12c7f89846682dae953e50376d89ebb3b7391b3426bb34480991
|
|
4
|
+
data.tar.gz: 4558a77d38a4def28af960c201ca9359fcc828d5a316f76f95cf3c0e575702b6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f4baf570c8ed5732493ba801121ae092177fca3ab5f9162ef66f0db2a23e91c09fe740d8ddba52977e65737906a6735a8405bb94e923111970621e79b813141
|
|
7
|
+
data.tar.gz: 624e1b8a76630bffa007391b303eab5729b20665d9a5242f58749e0127340ddd1a842eb94ca46da085ccfa91cc093367348586cde705b51fe3eff25882f8f096
|
|
@@ -25,6 +25,7 @@ module Net
|
|
|
25
25
|
end
|
|
26
26
|
when Time, Date, DateTime
|
|
27
27
|
when Symbol
|
|
28
|
+
Flag.validate(data)
|
|
28
29
|
else
|
|
29
30
|
data.validate
|
|
30
31
|
end
|
|
@@ -45,7 +46,7 @@ module Net
|
|
|
45
46
|
when Date
|
|
46
47
|
send_date_data(data)
|
|
47
48
|
when Symbol
|
|
48
|
-
|
|
49
|
+
Flag[data].send_data(self, tag)
|
|
49
50
|
else
|
|
50
51
|
data.send_data(self, tag)
|
|
51
52
|
end
|
|
@@ -77,9 +78,23 @@ module Net
|
|
|
77
78
|
put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
|
|
78
79
|
end
|
|
79
80
|
|
|
80
|
-
def send_literal(
|
|
81
|
+
def send_binary_literal(*a, **kw) send_literal(*a, **kw, binary: true) end
|
|
82
|
+
|
|
83
|
+
# `non_sync` is an optional tri-state flag:
|
|
84
|
+
# * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
|
|
85
|
+
# TODO: raise or warn when capabilities don't allow non_sync.
|
|
86
|
+
# * `false` -> Force normal synchronizing literal behavior.
|
|
87
|
+
# * `nil` -> (default) Currently behaves like `false` (will be dynamic).
|
|
88
|
+
# TODO: Dynamic, based on capabilities and bytesize.
|
|
89
|
+
def send_literal(str, tag = nil, binary: false, non_sync: nil)
|
|
81
90
|
synchronize do
|
|
82
|
-
|
|
91
|
+
prefix = "~" if binary
|
|
92
|
+
plus = "+" if non_sync
|
|
93
|
+
put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
|
|
94
|
+
if non_sync
|
|
95
|
+
put_string(str)
|
|
96
|
+
return
|
|
97
|
+
end
|
|
83
98
|
@continued_command_tag = tag
|
|
84
99
|
@continuation_request_exception = nil
|
|
85
100
|
begin
|
|
@@ -115,37 +130,148 @@ module Net
|
|
|
115
130
|
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
|
|
116
131
|
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
|
|
117
132
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
# simplistic emulation of CommandData = Data.define(:data)
|
|
134
|
+
class CommandData # :nodoc:
|
|
135
|
+
class << self
|
|
136
|
+
def new(arg = nil, data: arg) super(data: data) end
|
|
137
|
+
alias :[] :new
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def initialize(data:)
|
|
141
|
+
@data = data
|
|
142
|
+
freeze
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
attr_reader :data
|
|
146
|
+
|
|
147
|
+
def to_h(&block) block ? to_h.to_h(&block) : { data: data } end
|
|
148
|
+
def ==(other) self.class === other && to_h == other.to_h end
|
|
149
|
+
def eql?(other) self.class === other && to_h.eql?(other.to_h) end
|
|
150
|
+
|
|
151
|
+
# following class definition goes beyond the basic Data.define(:data)
|
|
152
|
+
##
|
|
153
|
+
|
|
154
|
+
def self.validate(...)
|
|
155
|
+
data = new(...)
|
|
156
|
+
data.validate
|
|
157
|
+
data
|
|
158
|
+
end
|
|
121
159
|
|
|
122
|
-
class RawData # :nodoc:
|
|
123
160
|
def send_data(imap, tag)
|
|
124
|
-
|
|
161
|
+
raise NoMethodError, "#{self.class} must implement #{__method__}"
|
|
125
162
|
end
|
|
126
163
|
|
|
127
164
|
def validate
|
|
128
165
|
end
|
|
166
|
+
end
|
|
129
167
|
|
|
130
|
-
|
|
168
|
+
# Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
|
|
169
|
+
# except for +NULL+, +CR+, or +LF+. +text+ is extended to allow any
|
|
170
|
+
# multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
|
|
171
|
+
# been enabled, or when the server supports only +IMAP4rev2+ and not earlier
|
|
172
|
+
# IMAP revisions, or when the server advertises +UTF8=ONLY+.
|
|
173
|
+
#
|
|
174
|
+
# NOTE: The current implementation does not validate whether the connection
|
|
175
|
+
# currently supports UTF-8. Future versions may change.
|
|
176
|
+
#
|
|
177
|
+
# The string's bytes must be valid ASCII or valid UTF-8. The string's
|
|
178
|
+
# reported encoding is ignored, but the string is _not_ transcoded.
|
|
179
|
+
class RawText < CommandData # :nodoc:
|
|
180
|
+
def initialize(data:)
|
|
181
|
+
data = String(data.to_str)
|
|
182
|
+
data = if [Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
|
|
183
|
+
-data
|
|
184
|
+
elsif data.ascii_only?
|
|
185
|
+
-(data.dup.force_encoding("ASCII"))
|
|
186
|
+
else
|
|
187
|
+
-(data.dup.force_encoding("UTF-8"))
|
|
188
|
+
end
|
|
189
|
+
super
|
|
190
|
+
validate
|
|
191
|
+
end
|
|
131
192
|
|
|
132
|
-
def
|
|
133
|
-
|
|
193
|
+
def validate
|
|
194
|
+
if data.include?("\0")
|
|
195
|
+
raise DataFormatError, "NULL byte must be binary literal encoded"
|
|
196
|
+
elsif !data.valid_encoding?
|
|
197
|
+
raise DataFormatError, "invalid UTF-8 must be literal encoded"
|
|
198
|
+
elsif /[\r\n]/.match?(data)
|
|
199
|
+
raise DataFormatError, "CR and LF bytes must be literal encoded"
|
|
200
|
+
end
|
|
134
201
|
end
|
|
202
|
+
|
|
203
|
+
def ascii_only?; data.ascii_only? end
|
|
204
|
+
|
|
205
|
+
def send_data(imap, tag) imap.__send__(:put_string, data) end
|
|
135
206
|
end
|
|
136
207
|
|
|
137
|
-
class
|
|
138
|
-
def
|
|
139
|
-
|
|
208
|
+
class RawData < CommandData # :nodoc:
|
|
209
|
+
def initialize(data:)
|
|
210
|
+
data = split_parts(data)
|
|
211
|
+
super
|
|
212
|
+
validate
|
|
140
213
|
end
|
|
141
214
|
|
|
215
|
+
def send_data(imap, tag) data.each do _1.send_data(imap, tag) end end
|
|
216
|
+
|
|
142
217
|
def validate
|
|
218
|
+
return unless RawText === data.last
|
|
219
|
+
text = data.last.data
|
|
220
|
+
if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
|
|
221
|
+
raise DataFormatError, "RawData cannot end with literal continuation"
|
|
222
|
+
end
|
|
143
223
|
end
|
|
144
224
|
|
|
145
225
|
private
|
|
146
226
|
|
|
147
|
-
def
|
|
148
|
-
|
|
227
|
+
def split_parts(data)
|
|
228
|
+
data = data.b # dups and ensures BINARY encoding
|
|
229
|
+
parts = []
|
|
230
|
+
while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
|
|
231
|
+
text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
|
|
232
|
+
bytesize = Integer bytesize, 10
|
|
233
|
+
parts << RawText[text] unless text.empty?
|
|
234
|
+
parts << extract_literal(data,
|
|
235
|
+
binary: binary,
|
|
236
|
+
bytesize: bytesize,
|
|
237
|
+
non_sync: non_sync)
|
|
238
|
+
data[0, bytesize] = ""
|
|
239
|
+
end
|
|
240
|
+
parts << RawText[data] unless data.empty?
|
|
241
|
+
parts
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def extract_literal(data, binary:, bytesize:, non_sync:)
|
|
245
|
+
if data.bytesize < bytesize
|
|
246
|
+
raise DataFormatError, "Too few bytes in string for literal, " \
|
|
247
|
+
"expected: %s, remaining: %s" % [bytesize, data.bytesize]
|
|
248
|
+
end
|
|
249
|
+
literal = data.byteslice(0, bytesize)
|
|
250
|
+
(binary ? Literal8 : Literal).new(data: literal, non_sync: non_sync)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
class Atom < CommandData # :nodoc:
|
|
255
|
+
def initialize(**)
|
|
256
|
+
super
|
|
257
|
+
validate
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def validate
|
|
261
|
+
data.to_s.ascii_only? \
|
|
262
|
+
or raise DataFormatError, "#{self.class} must be ASCII only"
|
|
263
|
+
data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
|
|
264
|
+
and raise DataFormatError, "#{self.class} must not contain atom-specials"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def send_data(imap, tag)
|
|
268
|
+
imap.__send__(:put_string, data.to_s)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
class Flag < Atom # :nodoc:
|
|
273
|
+
def send_data(imap, tag)
|
|
274
|
+
imap.__send__(:put_string, "\\#{data}")
|
|
149
275
|
end
|
|
150
276
|
end
|
|
151
277
|
|
|
@@ -165,17 +291,52 @@ module Net
|
|
|
165
291
|
end
|
|
166
292
|
|
|
167
293
|
class Literal # :nodoc:
|
|
168
|
-
|
|
169
|
-
|
|
294
|
+
class << self
|
|
295
|
+
def new(_data = nil, _non_sync = nil, data: _data, non_sync: _non_sync)
|
|
296
|
+
super(data: data, non_sync: non_sync)
|
|
297
|
+
end
|
|
298
|
+
alias :[] :new
|
|
170
299
|
end
|
|
171
300
|
|
|
301
|
+
attr_reader :data, :non_sync
|
|
302
|
+
|
|
303
|
+
def to_h(&block) block ? to_h.to_h(&block) : { data: data, non_sync: non_sync } end
|
|
304
|
+
def ==(other) self.class === other && to_h == other.to_h end
|
|
305
|
+
def eql?(other) self.class === other && to_h.eql?(other.to_h) end
|
|
306
|
+
|
|
307
|
+
def initialize(data:, non_sync: nil)
|
|
308
|
+
data = -String(data.to_str).b or
|
|
309
|
+
raise DataFormatError, "#{self.class} expects string input"
|
|
310
|
+
@data, @non_sync = data, non_sync
|
|
311
|
+
validate
|
|
312
|
+
freeze
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def self.validate(...)
|
|
316
|
+
data = new(...)
|
|
317
|
+
data.validate
|
|
318
|
+
data
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def bytesize; data.bytesize end
|
|
322
|
+
|
|
172
323
|
def validate
|
|
324
|
+
if data.include?("\0")
|
|
325
|
+
raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
|
|
326
|
+
"Use #{Literal8} or a null-safe encoding."
|
|
327
|
+
end
|
|
173
328
|
end
|
|
174
329
|
|
|
175
|
-
|
|
330
|
+
def send_data(imap, tag)
|
|
331
|
+
imap.__send__(:send_literal, data, tag, non_sync: non_sync)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
176
334
|
|
|
177
|
-
|
|
178
|
-
|
|
335
|
+
class Literal8 < Literal # :nodoc:
|
|
336
|
+
def validate; nil end # all bytes are okay
|
|
337
|
+
|
|
338
|
+
def send_data(imap, tag)
|
|
339
|
+
imap.__send__(:send_binary_literal, data, tag, non_sync: non_sync)
|
|
179
340
|
end
|
|
180
341
|
end
|
|
181
342
|
|
|
@@ -295,6 +295,14 @@ module Net
|
|
|
295
295
|
# because the server doesn't allow deletion of mailboxes with children.
|
|
296
296
|
# #data is +nil+.
|
|
297
297
|
#
|
|
298
|
+
# ==== <tt>QUOTA=RES-*</tt> response codes
|
|
299
|
+
# See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
|
|
300
|
+
# * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
|
|
301
|
+
# with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
|
|
302
|
+
# the command would put the target mailbox over any quota, and with an
|
|
303
|
+
# untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
|
|
304
|
+
# be external events). #data is +nil+.
|
|
305
|
+
#
|
|
298
306
|
# ==== +CONDSTORE+ extension
|
|
299
307
|
# See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
|
|
300
308
|
# * +NOMODSEQ+, when selecting a mailbox that does not support
|
|
@@ -369,12 +377,24 @@ module Net
|
|
|
369
377
|
# Net::IMAP#getquotaroot returns an array containing both MailboxQuotaRoot
|
|
370
378
|
# and MailboxQuota objects.
|
|
371
379
|
#
|
|
380
|
+
# ==== Required capability
|
|
381
|
+
#
|
|
382
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
383
|
+
# or <tt>QUOTA=RES-STORAGE</tt>
|
|
384
|
+
# [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
|
|
372
385
|
class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
|
|
373
386
|
##
|
|
374
387
|
# method: mailbox
|
|
375
388
|
# :call-seq: mailbox -> string
|
|
376
389
|
#
|
|
377
|
-
# The
|
|
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
|
|
|
@@ -8,6 +8,7 @@ module Net
|
|
|
8
8
|
|
|
9
9
|
def initialize(client, sock)
|
|
10
10
|
@client, @sock = client, sock
|
|
11
|
+
@buff = @literal_size = nil
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def read_response_buffer
|
|
@@ -15,13 +16,13 @@ module Net
|
|
|
15
16
|
catch :eof do
|
|
16
17
|
while true
|
|
17
18
|
read_line
|
|
18
|
-
break unless
|
|
19
|
+
break unless literal_size
|
|
19
20
|
read_literal
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
buff
|
|
23
24
|
ensure
|
|
24
|
-
@buff = nil
|
|
25
|
+
@buff = @literal_size = nil
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
private
|
|
@@ -31,12 +32,18 @@ module Net
|
|
|
31
32
|
def bytes_read; buff.bytesize end
|
|
32
33
|
def empty?; buff.empty? end
|
|
33
34
|
def done?; line_done? && !get_literal_size end
|
|
35
|
+
def done?; line_done? && !literal_size end
|
|
34
36
|
def line_done?; buff.end_with?(CRLF) end
|
|
35
|
-
|
|
37
|
+
|
|
38
|
+
def get_literal_size(buff)
|
|
39
|
+
buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
|
|
40
|
+
end
|
|
36
41
|
|
|
37
42
|
def read_line
|
|
38
|
-
|
|
43
|
+
line = (@sock.gets(CRLF, read_limit) or throw :eof)
|
|
44
|
+
buff << line
|
|
39
45
|
max_response_remaining! unless line_done?
|
|
46
|
+
@literal_size = get_literal_size(line)
|
|
40
47
|
end
|
|
41
48
|
|
|
42
49
|
def read_literal
|
|
@@ -75,13 +75,19 @@ module Net
|
|
|
75
75
|
# * #password ― Password or passphrase associated with this #username.
|
|
76
76
|
# * _optional_ #authzid ― Alternate identity to act as or on behalf of.
|
|
77
77
|
# * _optional_ #min_iterations - Overrides the default value (4096).
|
|
78
|
+
# * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
|
|
78
79
|
#
|
|
79
80
|
# Any other keyword parameters are quietly ignored.
|
|
81
|
+
#
|
|
82
|
+
# *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
|
|
83
|
+
# and maximum iteration counts that are appropriate for their security
|
|
84
|
+
# context.
|
|
80
85
|
def initialize(username_arg = nil, password_arg = nil,
|
|
81
86
|
authcid: nil, username: nil,
|
|
82
87
|
authzid: nil,
|
|
83
88
|
password: nil, secret: nil,
|
|
84
89
|
min_iterations: 4096, # see both RFC5802 and RFC7677
|
|
90
|
+
max_iterations: 2**31 - 1, # max int32
|
|
85
91
|
cnonce: nil, # must only be set in tests
|
|
86
92
|
**options)
|
|
87
93
|
@username = username || username_arg || authcid or
|
|
@@ -94,7 +100,22 @@ module Net
|
|
|
94
100
|
@min_iterations.positive? or
|
|
95
101
|
raise ArgumentError, "min_iterations must be positive"
|
|
96
102
|
|
|
103
|
+
@max_iterations = Integer max_iterations.to_int
|
|
104
|
+
@min_iterations <= @max_iterations or
|
|
105
|
+
raise ArgumentError, "max_iterations must be more than min_iterations"
|
|
106
|
+
|
|
97
107
|
@cnonce = cnonce || SecureRandom.base64(32)
|
|
108
|
+
|
|
109
|
+
# These attrs are set from the server challenges
|
|
110
|
+
@server_first_message = @snonce = @salt = @iterations = nil
|
|
111
|
+
@server_error = nil
|
|
112
|
+
|
|
113
|
+
# Memoized after @salt and @iterations have been sent.
|
|
114
|
+
@salted_password = @client_key = @server_key = nil
|
|
115
|
+
|
|
116
|
+
# These values are created and cached in response to server challenges
|
|
117
|
+
@client_first_message_bare = nil
|
|
118
|
+
@client_final_message_without_proof = nil
|
|
98
119
|
end
|
|
99
120
|
|
|
100
121
|
# Authentication identity: the identity that matches the #password.
|
|
@@ -127,8 +148,43 @@ module Net
|
|
|
127
148
|
|
|
128
149
|
# The minimal allowed iteration count. Lower #iterations will raise an
|
|
129
150
|
# Error.
|
|
151
|
+
#
|
|
152
|
+
# *WARNING:* The default value (4096) is set to match guidance from
|
|
153
|
+
# both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
|
|
154
|
+
# and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
|
|
155
|
+
# {modern recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
|
|
156
|
+
# are significantly higher.
|
|
157
|
+
#
|
|
158
|
+
# It is ultimately the server's responsibility to securely store
|
|
159
|
+
# password hashes. While this parameter can alert the user to
|
|
160
|
+
# insecure password storage and prevent insecure authentication
|
|
161
|
+
# exchange, updating the iteration count generally requires resetting
|
|
162
|
+
# the password on the server.
|
|
130
163
|
attr_reader :min_iterations
|
|
131
164
|
|
|
165
|
+
# The maximal allowed iteration count. Higher #iterations will raise an
|
|
166
|
+
# Error.
|
|
167
|
+
#
|
|
168
|
+
# As noted in {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
|
|
169
|
+
# >>>
|
|
170
|
+
# A hostile server can perform a computational denial-of-service
|
|
171
|
+
# attack on clients by sending a big iteration count value.
|
|
172
|
+
#
|
|
173
|
+
# *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
|
|
174
|
+
# 32-bit integer. This is large enough for the computation to take
|
|
175
|
+
# several minutes, and insufficient protection against hostile servers.
|
|
176
|
+
#
|
|
177
|
+
# Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
|
|
178
|
+
# blocking C function, and cannot be interrupted by +Timeout+ or
|
|
179
|
+
# <tt>Thread.raise</tt>. And it keeps the Global VM lock, as of v4.0 of
|
|
180
|
+
# the +openssl+ gem, so other ruby threads will not be able to run.
|
|
181
|
+
#
|
|
182
|
+
# <em>To prevent a denial of service attack,</em> this must be set to a
|
|
183
|
+
# safe value, depending on hardware and version of OpenSSL. <em>It is
|
|
184
|
+
# the user's responsibility</em> to enforce minimum and maximum
|
|
185
|
+
# iteration counts that are appropriate for their security context.
|
|
186
|
+
attr_reader :max_iterations
|
|
187
|
+
|
|
132
188
|
# The client nonce, generated by SecureRandom
|
|
133
189
|
attr_reader :cnonce
|
|
134
190
|
|
|
@@ -147,6 +203,15 @@ module Net
|
|
|
147
203
|
# Net::IMAP::NoResponseError.
|
|
148
204
|
attr_reader :server_error
|
|
149
205
|
|
|
206
|
+
# Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
|
|
207
|
+
def salted_password; @salted_password ||= compute_salted { super } end
|
|
208
|
+
|
|
209
|
+
# Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
|
|
210
|
+
def client_key; @client_key ||= compute_salted { super } end
|
|
211
|
+
|
|
212
|
+
# Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
|
|
213
|
+
def server_key; @server_key ||= compute_salted { super } end
|
|
214
|
+
|
|
150
215
|
# Returns a new OpenSSL::Digest object, set to the appropriate hash
|
|
151
216
|
# function for the chosen mechanism.
|
|
152
217
|
#
|
|
@@ -186,6 +251,13 @@ module Net
|
|
|
186
251
|
|
|
187
252
|
private
|
|
188
253
|
|
|
254
|
+
# Checks for +salt+ and +iterations+ before yielding
|
|
255
|
+
def compute_salted
|
|
256
|
+
String === salt or raise Error, "unknown salt"
|
|
257
|
+
Integer === iterations or raise Error, "unknown iterations"
|
|
258
|
+
yield
|
|
259
|
+
end
|
|
260
|
+
|
|
189
261
|
# Need to store this for auth_message
|
|
190
262
|
attr_reader :server_first_message
|
|
191
263
|
|
|
@@ -202,6 +274,8 @@ module Net
|
|
|
202
274
|
raise Error, "server did not send iteration count"
|
|
203
275
|
min_iterations <= iterations or
|
|
204
276
|
raise Error, "too few iterations: %d" % [iterations]
|
|
277
|
+
max_iterations.nil? || iterations <= max_iterations or
|
|
278
|
+
raise Error, "too many iterations: %d" % [iterations]
|
|
205
279
|
mext = sparams["m"] and
|
|
206
280
|
raise Error, "mandatory extension: %p" % [mext]
|
|
207
281
|
snonce.start_with? cnonce or
|
data/lib/net/imap.rb
CHANGED
|
@@ -460,6 +460,9 @@ module Net
|
|
|
460
460
|
# +LITERAL-+, and +SPECIAL-USE+.</em>
|
|
461
461
|
#
|
|
462
462
|
# ==== RFC2087: +QUOTA+
|
|
463
|
+
# +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
|
|
464
|
+
# - Obsoleted by <tt>QUOTA=RES-*</tt> [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]],
|
|
465
|
+
# although the commands are backward compatible.
|
|
463
466
|
# - #getquota: returns the resource usage and limits for a quota root
|
|
464
467
|
# - #getquotaroot: returns the list of quota roots for a mailbox, as well as
|
|
465
468
|
# their resource usage and limits.
|
|
@@ -572,6 +575,16 @@ module Net
|
|
|
572
575
|
# See FetchData#emailid and FetchData#emailid.
|
|
573
576
|
# - Updates #status with support for the +MAILBOXID+ status attribute.
|
|
574
577
|
#
|
|
578
|
+
# ==== RFC9208: <tt>QUOTA=RES-*</tt>
|
|
579
|
+
# +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
|
|
580
|
+
# - Obsoletes the +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
581
|
+
# extension and provides strict semantics for different resource types.
|
|
582
|
+
# - #getquota: returns the resource usage and limits for a quota root
|
|
583
|
+
# - #getquotaroot: returns the list of quota roots for a mailbox, as well as
|
|
584
|
+
# their resource usage and limits.
|
|
585
|
+
# - #setquota: sets the resource limits for a given quota root.
|
|
586
|
+
# - Updates #status with <tt>"DELETED"</tt> and +DELETED-STORAGE+ attributes.
|
|
587
|
+
#
|
|
575
588
|
# == References
|
|
576
589
|
#
|
|
577
590
|
# [{IMAP4rev1}[https://www.rfc-editor.org/rfc/rfc3501.html]]::
|
|
@@ -681,14 +694,13 @@ module Net
|
|
|
681
694
|
#
|
|
682
695
|
# === \IMAP Extensions
|
|
683
696
|
#
|
|
684
|
-
# [QUOTA[https://
|
|
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.24"
|
|
765
783
|
|
|
766
784
|
# Aliases for supported capabilities, to be used with the #enable command.
|
|
767
785
|
ENABLE_ALIASES = {
|
|
@@ -1294,9 +1312,11 @@ module Net
|
|
|
1294
1312
|
#
|
|
1295
1313
|
def starttls(**options)
|
|
1296
1314
|
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
|
|
1315
|
+
handled = false
|
|
1297
1316
|
error = nil
|
|
1298
1317
|
ok = send_command("STARTTLS") do |resp|
|
|
1299
1318
|
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
|
|
1319
|
+
handled = true
|
|
1300
1320
|
clear_cached_capabilities
|
|
1301
1321
|
clear_responses
|
|
1302
1322
|
start_tls_session
|
|
@@ -1308,6 +1328,13 @@ module Net
|
|
|
1308
1328
|
disconnect
|
|
1309
1329
|
raise error
|
|
1310
1330
|
end
|
|
1331
|
+
unless handled
|
|
1332
|
+
disconnect
|
|
1333
|
+
raise InvalidResponseError,
|
|
1334
|
+
"STARTTLS handler was bypassed, although server responded %p" % [
|
|
1335
|
+
ok.raw_data.chomp
|
|
1336
|
+
]
|
|
1337
|
+
end
|
|
1311
1338
|
ok
|
|
1312
1339
|
end
|
|
1313
1340
|
|
|
@@ -1742,12 +1769,18 @@ module Net
|
|
|
1742
1769
|
# to both admin and user. If this mailbox exists, it returns an array
|
|
1743
1770
|
# containing objects of type MailboxQuotaRoot and MailboxQuota.
|
|
1744
1771
|
#
|
|
1772
|
+
# *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
|
|
1773
|
+
# resource type. This is usually +STORAGE+, but you may need to verify this
|
|
1774
|
+
# with UntaggedResponse#raw_data.
|
|
1775
|
+
#
|
|
1745
1776
|
# Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
|
|
1746
1777
|
#
|
|
1747
1778
|
# ===== Capabilities
|
|
1748
1779
|
#
|
|
1749
|
-
#
|
|
1750
|
-
#
|
|
1780
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
1781
|
+
# capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
|
|
1782
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
|
|
1783
|
+
# resource type.
|
|
1751
1784
|
def getquotaroot(mailbox)
|
|
1752
1785
|
synchronize do
|
|
1753
1786
|
send_command("GETQUOTAROOT", mailbox)
|
|
@@ -1759,41 +1792,59 @@ module Net
|
|
|
1759
1792
|
end
|
|
1760
1793
|
|
|
1761
1794
|
# Sends a {GETQUOTA command [RFC2087 §4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
|
|
1762
|
-
#
|
|
1763
|
-
# containing a MailboxQuota object is returned.
|
|
1764
|
-
#
|
|
1795
|
+
# for the +quota_root+. If this quota root exists, then an array
|
|
1796
|
+
# containing a MailboxQuota object is returned.
|
|
1797
|
+
#
|
|
1798
|
+
# The names of quota roots that are applicable to a particular mailbox can
|
|
1799
|
+
# be discovered with #getquotaroot.
|
|
1800
|
+
#
|
|
1801
|
+
# *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
|
|
1802
|
+
# resource type. This is usually +STORAGE+, but you may need to verify this
|
|
1803
|
+
# with UntaggedResponse#raw_data.
|
|
1765
1804
|
#
|
|
1766
1805
|
# Related: #getquotaroot, #setquota, MailboxQuota
|
|
1767
1806
|
#
|
|
1768
1807
|
# ===== Capabilities
|
|
1769
1808
|
#
|
|
1770
|
-
#
|
|
1771
|
-
#
|
|
1772
|
-
|
|
1809
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
1810
|
+
# capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
|
|
1811
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
|
|
1812
|
+
# resource type.
|
|
1813
|
+
def getquota(quota_root)
|
|
1773
1814
|
synchronize do
|
|
1774
|
-
send_command("GETQUOTA",
|
|
1815
|
+
send_command("GETQUOTA", quota_root)
|
|
1775
1816
|
clear_responses("QUOTA")
|
|
1776
1817
|
end
|
|
1777
1818
|
end
|
|
1778
1819
|
|
|
1779
1820
|
# Sends a {SETQUOTA command [RFC2087 §4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
|
|
1780
|
-
# along with the specified +
|
|
1781
|
-
# +
|
|
1782
|
-
#
|
|
1821
|
+
# along with the specified +quota_root+ and +storage_limit+. If
|
|
1822
|
+
# +storage_limit+ is +nil+, resource limits are unset for that quota root.
|
|
1823
|
+
# If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
|
|
1824
|
+
#
|
|
1825
|
+
# imap.setquota "#user/alice", 100
|
|
1826
|
+
# imap.getquota "#user/alice"
|
|
1827
|
+
# # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
|
|
1828
|
+
#
|
|
1829
|
+
# Typically one needs to be logged in as a server admin for this to work.
|
|
1830
|
+
#
|
|
1831
|
+
# *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
|
|
1783
1832
|
#
|
|
1784
1833
|
# Related: #getquota, #getquotaroot
|
|
1785
1834
|
#
|
|
1786
1835
|
# ===== Capabilities
|
|
1787
1836
|
#
|
|
1788
|
-
#
|
|
1789
|
-
#
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1837
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
1838
|
+
# capability, or both +QUOTASET+ and a capability prefixed with
|
|
1839
|
+
# <tt>QUOTA=RES-*</tt> {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208]
|
|
1840
|
+
# for each supported resource type.
|
|
1841
|
+
def setquota(quota_root, storage_limit)
|
|
1842
|
+
if storage_limit.nil?
|
|
1843
|
+
list = []
|
|
1793
1844
|
else
|
|
1794
|
-
|
|
1845
|
+
list = ["STORAGE", Integer(storage_limit)]
|
|
1795
1846
|
end
|
|
1796
|
-
send_command("SETQUOTA",
|
|
1847
|
+
send_command("SETQUOTA", quota_root, list)
|
|
1797
1848
|
end
|
|
1798
1849
|
|
|
1799
1850
|
# Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
|
|
@@ -1900,7 +1951,10 @@ module Net
|
|
|
1900
1951
|
# <tt>STATUS=SIZE</tt>
|
|
1901
1952
|
# {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
|
|
1902
1953
|
#
|
|
1903
|
-
# +DELETED+
|
|
1954
|
+
# +DELETED+ must be supported when the server's capabilities includes
|
|
1955
|
+
# +IMAP4rev2+.
|
|
1956
|
+
# or <tt>QUOTA=RES-MESSAGES</tt>
|
|
1957
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
|
|
1904
1958
|
#
|
|
1905
1959
|
# +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
|
|
1906
1960
|
# {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
|
|
@@ -2049,6 +2103,14 @@ module Net
|
|
|
2049
2103
|
#
|
|
2050
2104
|
# ===== Search criteria
|
|
2051
2105
|
#
|
|
2106
|
+
# >>>
|
|
2107
|
+
# When +criteria+ is an Array, elements in the array will be validated and
|
|
2108
|
+
# formatted. When +criteria+ is a String, it will be sent <em>with
|
|
2109
|
+
# minimal validation and no encoding or formatting</em>.
|
|
2110
|
+
#
|
|
2111
|
+
# <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
|
|
2112
|
+
# types of attribute injection attack if unvetted user input is used.</em>
|
|
2113
|
+
#
|
|
2052
2114
|
# For a full list of search criteria,
|
|
2053
2115
|
# see [{IMAP4rev1 §6.4.4}[https://www.rfc-editor.org/rfc/rfc3501.html#section-6.4.4]],
|
|
2054
2116
|
# or [{IMAP4rev2 §6.4.4}[https://www.rfc-editor.org/rfc/rfc9051.html#section-6.4.4]],
|
|
@@ -2136,6 +2198,13 @@ module Net
|
|
|
2136
2198
|
#
|
|
2137
2199
|
# +attr+ is a list of attributes to fetch; see the documentation
|
|
2138
2200
|
# for FetchData for a list of valid attributes.
|
|
2201
|
+
# >>>
|
|
2202
|
+
# When +attr+ is a String, it will be sent <em>with minimal validation and
|
|
2203
|
+
# no encoding or formatting</em>. When +attr+ is an Array, each String in
|
|
2204
|
+
# +attr+ will be sent this way.
|
|
2205
|
+
#
|
|
2206
|
+
# <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
|
|
2207
|
+
# types of attribute injection attack if unvetted user input is used.</em>
|
|
2139
2208
|
#
|
|
2140
2209
|
# +changedsince+ is an optional integer mod-sequence. It limits results to
|
|
2141
2210
|
# messages with a mod-sequence greater than +changedsince+.
|
|
@@ -2992,6 +3061,7 @@ module Net
|
|
|
2992
3061
|
put_string(" ")
|
|
2993
3062
|
send_data(i, tag)
|
|
2994
3063
|
end
|
|
3064
|
+
guard_against_tagged_response_skipping_handler!(tag)
|
|
2995
3065
|
put_string(CRLF)
|
|
2996
3066
|
if cmd == "LOGOUT"
|
|
2997
3067
|
@logout_command_tag = tag
|
|
@@ -3007,6 +3077,17 @@ module Net
|
|
|
3007
3077
|
end
|
|
3008
3078
|
end
|
|
3009
3079
|
end
|
|
3080
|
+
rescue InvalidResponseError
|
|
3081
|
+
disconnect
|
|
3082
|
+
raise
|
|
3083
|
+
end
|
|
3084
|
+
|
|
3085
|
+
def guard_against_tagged_response_skipping_handler!(tag)
|
|
3086
|
+
return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
|
|
3087
|
+
raise(InvalidResponseError,
|
|
3088
|
+
"Server sent tagged 'OK' before command was finished: %p. " \
|
|
3089
|
+
"This could indicate a malicious server or client-side " \
|
|
3090
|
+
"command injection. Disconnecting." % [resp.raw_data.chomp])
|
|
3010
3091
|
end
|
|
3011
3092
|
|
|
3012
3093
|
def generate_tag
|
|
@@ -3071,7 +3152,7 @@ module Net
|
|
|
3071
3152
|
end
|
|
3072
3153
|
|
|
3073
3154
|
def store_internal(cmd, set, attr, flags, unchangedsince: nil)
|
|
3074
|
-
attr =
|
|
3155
|
+
attr = Atom.new(attr) if attr.instance_of?(String)
|
|
3075
3156
|
args = [MessageSet.new(set)]
|
|
3076
3157
|
args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
|
|
3077
3158
|
args << attr << flags
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: net-imap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.24
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shugo Maeda
|
|
@@ -125,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
125
125
|
- !ruby/object:Gem::Version
|
|
126
126
|
version: '0'
|
|
127
127
|
requirements: []
|
|
128
|
-
rubygems_version:
|
|
128
|
+
rubygems_version: 4.0.6
|
|
129
129
|
specification_version: 4
|
|
130
130
|
summary: Ruby client api for Internet Message Access Protocol
|
|
131
131
|
test_files: []
|