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