net-imap 0.4.12 → 0.5.1
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.
Potentially problematic release.
This version of net-imap might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Gemfile +7 -1
- data/lib/net/imap/authenticators.rb +2 -2
- data/lib/net/imap/command_data.rb +13 -2
- data/lib/net/imap/config/attr_accessors.rb +75 -0
- data/lib/net/imap/config/attr_inheritance.rb +90 -0
- data/lib/net/imap/config/attr_type_coercion.rb +61 -0
- data/lib/net/imap/config.rb +400 -0
- data/lib/net/imap/data_encoding.rb +3 -3
- data/lib/net/imap/deprecated_client_options.rb +8 -5
- data/lib/net/imap/errors.rb +6 -0
- data/lib/net/imap/response_data.rb +6 -93
- data/lib/net/imap/response_parser/parser_utils.rb +6 -6
- data/lib/net/imap/response_parser.rb +9 -17
- data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
- data/lib/net/imap/sasl/authenticators.rb +8 -4
- data/lib/net/imap/sasl/client_adapter.rb +77 -26
- data/lib/net/imap/sasl/cram_md5_authenticator.rb +1 -1
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +213 -51
- data/lib/net/imap/sasl/login_authenticator.rb +2 -1
- data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
- data/lib/net/imap/sasl.rb +6 -3
- data/lib/net/imap/sasl_adapter.rb +0 -1
- data/lib/net/imap/sequence_set.rb +28 -24
- data/lib/net/imap.rb +467 -152
- data/net-imap.gemspec +3 -3
- data/rakelib/string_prep_tables_generator.rb +2 -0
- metadata +11 -10
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/pages.yml +0 -46
- data/.github/workflows/push_gem.yml +0 -48
- data/.github/workflows/test.yml +0 -31
- data/.gitignore +0 -12
- data/.mailmap +0 -13
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Net::IMAP authenticator for the
|
3
|
+
# Net::IMAP authenticator for the +DIGEST-MD5+ SASL mechanism type, specified
|
4
4
|
# in RFC-2831[https://tools.ietf.org/html/rfc2831]. See Net::IMAP#authenticate.
|
5
5
|
#
|
6
6
|
# == Deprecated
|
@@ -9,11 +9,32 @@
|
|
9
9
|
# RFC-6331[https://tools.ietf.org/html/rfc6331] and should not be relied on for
|
10
10
|
# security. It is included for compatibility with existing servers.
|
11
11
|
class Net::IMAP::SASL::DigestMD5Authenticator
|
12
|
+
DataFormatError = Net::IMAP::DataFormatError
|
13
|
+
ResponseParseError = Net::IMAP::ResponseParseError
|
14
|
+
private_constant :DataFormatError, :ResponseParseError
|
15
|
+
|
12
16
|
STAGE_ONE = :stage_one
|
13
17
|
STAGE_TWO = :stage_two
|
14
18
|
STAGE_DONE = :stage_done
|
15
19
|
private_constant :STAGE_ONE, :STAGE_TWO, :STAGE_DONE
|
16
20
|
|
21
|
+
# Directives which must not have multiples. The RFC states:
|
22
|
+
# >>>
|
23
|
+
# This directive may appear at most once; if multiple instances are present,
|
24
|
+
# the client should abort the authentication exchange.
|
25
|
+
NO_MULTIPLES = %w[nonce stale maxbuf charset algorithm].freeze
|
26
|
+
|
27
|
+
# Required directives which must occur exactly once. The RFC states: >>>
|
28
|
+
# This directive is required and MUST appear exactly once; if not present,
|
29
|
+
# or if multiple instances are present, the client should abort the
|
30
|
+
# authentication exchange.
|
31
|
+
REQUIRED = %w[nonce algorithm].freeze
|
32
|
+
|
33
|
+
# Directives which are composed of one or more comma delimited tokens
|
34
|
+
QUOTED_LISTABLE = %w[qop cipher].freeze
|
35
|
+
|
36
|
+
private_constant :NO_MULTIPLES, :REQUIRED, :QUOTED_LISTABLE
|
37
|
+
|
17
38
|
# Authentication identity: the identity that matches the #password.
|
18
39
|
#
|
19
40
|
# RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
|
@@ -42,6 +63,59 @@ class Net::IMAP::SASL::DigestMD5Authenticator
|
|
42
63
|
#
|
43
64
|
attr_reader :authzid
|
44
65
|
|
66
|
+
# A namespace or collection of identities which contains +username+.
|
67
|
+
#
|
68
|
+
# Used by DIGEST-MD5, GSS-API, and NTLM. This is often a domain name that
|
69
|
+
# contains the name of the host performing the authentication.
|
70
|
+
#
|
71
|
+
# <em>Defaults to the last realm in the server-provided list of
|
72
|
+
# realms.</em>
|
73
|
+
attr_reader :realm
|
74
|
+
|
75
|
+
# Fully qualified canonical DNS host name for the requested service.
|
76
|
+
#
|
77
|
+
# <em>Defaults to #realm.</em>
|
78
|
+
attr_reader :host
|
79
|
+
|
80
|
+
# The service protocol, a
|
81
|
+
# {registered GSSAPI service name}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml],
|
82
|
+
# e.g. "imap", "ldap", or "xmpp".
|
83
|
+
#
|
84
|
+
# For Net::IMAP, the default is "imap" and should not be overridden. This
|
85
|
+
# must be set appropriately to use authenticators in other protocols.
|
86
|
+
#
|
87
|
+
# If an IANA-registered name isn't available, GSS-API
|
88
|
+
# (RFC-2743[https://tools.ietf.org/html/rfc2743]) allows the generic name
|
89
|
+
# "host".
|
90
|
+
attr_reader :service
|
91
|
+
|
92
|
+
# The generic server name when the server is replicated.
|
93
|
+
#
|
94
|
+
# +service_name+ will be ignored when it is +nil+ or identical to +host+.
|
95
|
+
#
|
96
|
+
# From RFC-2831[https://tools.ietf.org/html/rfc2831]:
|
97
|
+
# >>>
|
98
|
+
# The service is considered to be replicated if the client's
|
99
|
+
# service-location process involves resolution using standard DNS lookup
|
100
|
+
# operations, and if these operations involve DNS records (such as SRV, or
|
101
|
+
# MX) which resolve one DNS name into a set of other DNS names. In this
|
102
|
+
# case, the initial name used by the client is the "serv-name", and the
|
103
|
+
# final name is the "host" component.
|
104
|
+
attr_reader :service_name
|
105
|
+
|
106
|
+
# Parameters sent by the server are stored in this hash.
|
107
|
+
attr_reader :sparams
|
108
|
+
|
109
|
+
# The charset sent by the server. "UTF-8" (case insensitive) is the only
|
110
|
+
# allowed value. +nil+ should be interpreted as ISO 8859-1.
|
111
|
+
attr_reader :charset
|
112
|
+
|
113
|
+
# nonce sent by the server
|
114
|
+
attr_reader :nonce
|
115
|
+
|
116
|
+
# qop-options sent by the server
|
117
|
+
attr_reader :qop
|
118
|
+
|
45
119
|
# :call-seq:
|
46
120
|
# new(username, password, authzid = nil, **options) -> authenticator
|
47
121
|
# new(username:, password:, authzid: nil, **options) -> authenticator
|
@@ -64,27 +138,59 @@ class Net::IMAP::SASL::DigestMD5Authenticator
|
|
64
138
|
# When +authzid+ is not set, the server should derive the authorization
|
65
139
|
# identity from the authentication identity.
|
66
140
|
#
|
141
|
+
# * _optional_ #realm — A namespace for the #username, e.g. a domain.
|
142
|
+
# <em>Defaults to the last realm in the server-provided realms list.</em>
|
143
|
+
# * _optional_ #host — FQDN for requested service.
|
144
|
+
# <em>Defaults to</em> #realm.
|
145
|
+
# * _optional_ #service_name — The generic host name when the server is
|
146
|
+
# replicated.
|
147
|
+
# * _optional_ #service — the registered service protocol. E.g. "imap",
|
148
|
+
# "smtp", "ldap", "xmpp".
|
149
|
+
# <em>For Net::IMAP, this defaults to "imap".</em>
|
150
|
+
#
|
67
151
|
# * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
|
68
152
|
#
|
69
153
|
# Any other keyword arguments are silently ignored.
|
70
154
|
def initialize(user = nil, pass = nil, authz = nil,
|
71
155
|
username: nil, password: nil, authzid: nil,
|
72
156
|
authcid: nil, secret: nil,
|
157
|
+
realm: nil, service: "imap", host: nil, service_name: nil,
|
73
158
|
warn_deprecation: true, **)
|
74
159
|
username = authcid || username || user or
|
75
160
|
raise ArgumentError, "missing username (authcid)"
|
76
161
|
password ||= secret || pass or raise ArgumentError, "missing password"
|
77
162
|
authzid ||= authz
|
78
163
|
if warn_deprecation
|
79
|
-
warn
|
80
|
-
|
164
|
+
warn("WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331.",
|
165
|
+
category: :deprecated)
|
81
166
|
end
|
167
|
+
|
82
168
|
require "digest/md5"
|
169
|
+
require "securerandom"
|
83
170
|
require "strscan"
|
84
171
|
@username, @password, @authzid = username, password, authzid
|
172
|
+
@realm = realm
|
173
|
+
@host = host
|
174
|
+
@service = service
|
175
|
+
@service_name = service_name
|
85
176
|
@nc, @stage = {}, STAGE_ONE
|
86
177
|
end
|
87
178
|
|
179
|
+
# From RFC-2831[https://tools.ietf.org/html/rfc2831]:
|
180
|
+
# >>>
|
181
|
+
# Indicates the principal name of the service with which the client wishes
|
182
|
+
# to connect, formed from the serv-type, host, and serv-name. For
|
183
|
+
# example, the FTP service on "ftp.example.com" would have a "digest-uri"
|
184
|
+
# value of "ftp/ftp.example.com"; the SMTP server from the example above
|
185
|
+
# would have a "digest-uri" value of "smtp/mail3.example.com/example.com".
|
186
|
+
def digest_uri
|
187
|
+
if service_name && service_name != host
|
188
|
+
"#{service}/#{host}/#{service_name}"
|
189
|
+
else
|
190
|
+
"#{service}/#{host}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
88
194
|
def initial_response?; false end
|
89
195
|
|
90
196
|
# Responds to server challenge in two stages.
|
@@ -92,78 +198,134 @@ class Net::IMAP::SASL::DigestMD5Authenticator
|
|
92
198
|
case @stage
|
93
199
|
when STAGE_ONE
|
94
200
|
@stage = STAGE_TWO
|
95
|
-
sparams =
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
if v =~ /,/
|
102
|
-
v = v.split(',')
|
103
|
-
end
|
104
|
-
end
|
105
|
-
sparams[k] = v
|
106
|
-
end
|
201
|
+
@sparams = parse_challenge(challenge)
|
202
|
+
@qop = sparams.key?("qop") ? ["auth"] : sparams["qop"].flatten
|
203
|
+
@nonce = sparams["nonce"] &.first
|
204
|
+
@charset = sparams["charset"]&.first
|
205
|
+
@realm ||= sparams["realm"] &.last
|
206
|
+
@host ||= realm
|
107
207
|
|
108
|
-
|
109
|
-
|
208
|
+
if !qop.include?("auth")
|
209
|
+
raise DataFormatError, "Server does not support auth (qop = %p)" % [
|
210
|
+
sparams["qop"]
|
211
|
+
]
|
212
|
+
elsif (emptykey = REQUIRED.find { sparams[_1].empty? })
|
213
|
+
raise DataFormatError, "Server didn't send %s (%p)" % [emptykey, challenge]
|
214
|
+
elsif (multikey = NO_MULTIPLES.find { sparams[_1].length > 1 })
|
215
|
+
raise DataFormatError, "Server sent multiple %s (%p)" % [multikey, challenge]
|
216
|
+
end
|
110
217
|
|
111
218
|
response = {
|
112
|
-
:nonce
|
113
|
-
:username
|
114
|
-
:realm
|
115
|
-
:
|
116
|
-
|
117
|
-
:
|
118
|
-
:
|
119
|
-
:
|
120
|
-
:charset
|
219
|
+
nonce: nonce,
|
220
|
+
username: username,
|
221
|
+
realm: realm,
|
222
|
+
cnonce: SecureRandom.base64(32),
|
223
|
+
"digest-uri": digest_uri,
|
224
|
+
qop: "auth",
|
225
|
+
maxbuf: 65535,
|
226
|
+
nc: "%08d" % nc(nonce),
|
227
|
+
charset: charset,
|
121
228
|
}
|
122
229
|
|
123
230
|
response[:authzid] = @authzid unless @authzid.nil?
|
124
231
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':')
|
129
|
-
a1 << ':' + response[:authzid] unless response[:authzid].nil?
|
130
|
-
|
131
|
-
a2 = "AUTHENTICATE:" + response[:'digest-uri']
|
132
|
-
a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
|
133
|
-
|
134
|
-
response[:response] = Digest::MD5.hexdigest(
|
135
|
-
[
|
136
|
-
Digest::MD5.hexdigest(a1),
|
137
|
-
response.values_at(:nonce, :nc, :cnonce, :qop),
|
138
|
-
Digest::MD5.hexdigest(a2)
|
139
|
-
].join(':')
|
140
|
-
)
|
141
|
-
|
142
|
-
return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
|
232
|
+
response[:response] = response_value(response)
|
233
|
+
format_response(response)
|
143
234
|
when STAGE_TWO
|
144
235
|
@stage = STAGE_DONE
|
145
|
-
|
146
|
-
if
|
147
|
-
return ''
|
148
|
-
else
|
149
|
-
raise ResponseParseError, challenge
|
150
|
-
end
|
236
|
+
raise ResponseParseError, challenge unless challenge =~ /rspauth=/
|
237
|
+
"" # if at the second stage, return an empty string
|
151
238
|
else
|
152
239
|
raise ResponseParseError, challenge
|
153
240
|
end
|
241
|
+
rescue => error
|
242
|
+
@stage = error
|
243
|
+
raise
|
154
244
|
end
|
155
245
|
|
156
246
|
def done?; @stage == STAGE_DONE end
|
157
247
|
|
158
248
|
private
|
159
249
|
|
250
|
+
LWS = /[\r\n \t]*/n # less strict than RFC, more strict than '\s'
|
251
|
+
TOKEN = /[^\x00-\x20\x7f()<>@,;:\\"\/\[\]?={}]+/n
|
252
|
+
QUOTED_STR = /"(?: [\t\x20-\x7e&&[^"]] | \\[\x00-\x7f] )*"/nx
|
253
|
+
LIST_DELIM = /(?:#{LWS} , )+ #{LWS}/nx
|
254
|
+
AUTH_PARAM = /
|
255
|
+
(#{TOKEN}) #{LWS} = #{LWS} (#{QUOTED_STR} | #{TOKEN}) #{LIST_DELIM}?
|
256
|
+
/nx
|
257
|
+
private_constant :LWS, :TOKEN, :QUOTED_STR, :LIST_DELIM, :AUTH_PARAM
|
258
|
+
|
259
|
+
def parse_challenge(challenge)
|
260
|
+
sparams = Hash.new {|h, k| h[k] = [] }
|
261
|
+
c = StringScanner.new(challenge)
|
262
|
+
c.skip LIST_DELIM
|
263
|
+
while c.scan AUTH_PARAM
|
264
|
+
k, v = c[1], c[2]
|
265
|
+
k = k.downcase
|
266
|
+
if v =~ /\A"(.*)"\z/mn
|
267
|
+
v = $1.gsub(/\\(.)/mn, '\1')
|
268
|
+
v = split_quoted_list(v, challenge) if QUOTED_LISTABLE.include? k
|
269
|
+
end
|
270
|
+
sparams[k] << v
|
271
|
+
end
|
272
|
+
if !c.eos?
|
273
|
+
raise DataFormatError, "Unparsable challenge: %p" % [challenge]
|
274
|
+
elsif sparams.empty?
|
275
|
+
raise DataFormatError, "Empty challenge: %p" % [challenge]
|
276
|
+
end
|
277
|
+
sparams
|
278
|
+
end
|
279
|
+
|
280
|
+
def split_quoted_list(value, challenge)
|
281
|
+
value.split(LIST_DELIM).reject(&:empty?).tap do
|
282
|
+
_1.any? or raise DataFormatError, "Bad Challenge: %p" % [challenge]
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
160
286
|
def nc(nonce)
|
161
287
|
if @nc.has_key? nonce
|
162
288
|
@nc[nonce] = @nc[nonce] + 1
|
163
289
|
else
|
164
290
|
@nc[nonce] = 1
|
165
291
|
end
|
166
|
-
|
292
|
+
end
|
293
|
+
|
294
|
+
def response_value(response)
|
295
|
+
a1 = compute_a1(response)
|
296
|
+
a2 = compute_a2(response)
|
297
|
+
Digest::MD5.hexdigest(
|
298
|
+
[
|
299
|
+
Digest::MD5.hexdigest(a1),
|
300
|
+
response.values_at(:nonce, :nc, :cnonce, :qop),
|
301
|
+
Digest::MD5.hexdigest(a2)
|
302
|
+
].join(":")
|
303
|
+
)
|
304
|
+
end
|
305
|
+
|
306
|
+
def compute_a0(response)
|
307
|
+
Digest::MD5.digest(
|
308
|
+
[ response.values_at(:username, :realm), password ].join(":")
|
309
|
+
)
|
310
|
+
end
|
311
|
+
|
312
|
+
def compute_a1(response)
|
313
|
+
a0 = compute_a0(response)
|
314
|
+
a1 = [ a0, response.values_at(:nonce, :cnonce) ].join(":")
|
315
|
+
a1 << ":#{response[:authzid]}" unless response[:authzid].nil?
|
316
|
+
a1
|
317
|
+
end
|
318
|
+
|
319
|
+
def compute_a2(response)
|
320
|
+
a2 = "AUTHENTICATE:#{response[:"digest-uri"]}"
|
321
|
+
if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
|
322
|
+
a2 << ":00000000000000000000000000000000"
|
323
|
+
end
|
324
|
+
a2
|
325
|
+
end
|
326
|
+
|
327
|
+
def format_response(response)
|
328
|
+
response.map {|k, v| qdval(k.to_s, v) }.join(",")
|
167
329
|
end
|
168
330
|
|
169
331
|
# some responses need quoting
|
@@ -29,7 +29,8 @@ class Net::IMAP::SASL::LoginAuthenticator
|
|
29
29
|
warn_deprecation: true,
|
30
30
|
**)
|
31
31
|
if warn_deprecation
|
32
|
-
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
|
32
|
+
warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead.",
|
33
|
+
category: :deprecated
|
33
34
|
end
|
34
35
|
@user = authcid || username || user
|
35
36
|
@password = password || secret || pass
|
@@ -4,16 +4,72 @@ module Net
|
|
4
4
|
class IMAP
|
5
5
|
module SASL
|
6
6
|
|
7
|
+
# SASL::ProtocolAdapters modules are meant to be used as mixins for
|
8
|
+
# SASL::ClientAdapter and its subclasses. Where the client adapter must
|
9
|
+
# be customized for each client library, the protocol adapter mixin
|
10
|
+
# handles \SASL requirements that are part of the protocol specification,
|
11
|
+
# but not specific to any particular client library. In particular, see
|
12
|
+
# {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4]
|
13
|
+
#
|
14
|
+
# === Interface
|
15
|
+
#
|
16
|
+
# >>>
|
17
|
+
# NOTE: This API is experimental, and may change.
|
18
|
+
#
|
19
|
+
# - {#command_name}[rdoc-ref:Generic#command_name] -- The name of the
|
20
|
+
# command used to to initiate an authentication exchange.
|
21
|
+
# - {#service}[rdoc-ref:Generic#service] -- The GSSAPI service name.
|
22
|
+
# - {#encode_ir}[rdoc-ref:Generic#encode_ir]--Encodes an initial response.
|
23
|
+
# - {#decode}[rdoc-ref:Generic#decode] -- Decodes a server challenge.
|
24
|
+
# - {#encode}[rdoc-ref:Generic#encode] -- Encodes a client response.
|
25
|
+
# - {#cancel_response}[rdoc-ref:Generic#cancel_response] -- The encoded
|
26
|
+
# client response used to cancel an authentication exchange.
|
27
|
+
#
|
28
|
+
# Other protocol requirements of the \SASL authentication exchange are
|
29
|
+
# handled by SASL::ClientAdapter.
|
30
|
+
#
|
31
|
+
# === Included protocol adapters
|
32
|
+
#
|
33
|
+
# - Generic -- a basic implementation of all of the methods listed above.
|
34
|
+
# - IMAP -- An adapter for the IMAP4 protocol.
|
35
|
+
# - SMTP -- An adapter for the \SMTP protocol with the +AUTH+ capability.
|
36
|
+
# - POP -- An adapter for the POP3 protocol with the +SASL+ capability.
|
7
37
|
module ProtocolAdapters
|
8
|
-
#
|
38
|
+
# See SASL::ProtocolAdapters@Interface.
|
9
39
|
module Generic
|
40
|
+
# The name of the protocol command used to initiate a \SASL
|
41
|
+
# authentication exchange.
|
42
|
+
#
|
43
|
+
# The generic implementation returns <tt>"AUTHENTICATE"</tt>.
|
10
44
|
def command_name; "AUTHENTICATE" end
|
11
|
-
|
12
|
-
|
13
|
-
|
45
|
+
|
46
|
+
# A service name from the {GSSAPI/Kerberos/SASL Service Names
|
47
|
+
# registry}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml].
|
48
|
+
#
|
49
|
+
# The generic implementation returns <tt>"host"</tt>, which is the
|
50
|
+
# generic GSSAPI host-based service name.
|
51
|
+
def service; "host" end
|
52
|
+
|
53
|
+
# Encodes an initial response string.
|
54
|
+
#
|
55
|
+
# The generic implementation returns the result of #encode, or returns
|
56
|
+
# <tt>"="</tt> when +string+ is empty.
|
14
57
|
def encode_ir(string) string.empty? ? "=" : encode(string) end
|
58
|
+
|
59
|
+
# Encodes a client response string.
|
60
|
+
#
|
61
|
+
# The generic implementation returns the Base64 encoding of +string+.
|
15
62
|
def encode(string) [string].pack("m0") end
|
63
|
+
|
64
|
+
# Decodes a server challenge string.
|
65
|
+
#
|
66
|
+
# The generic implementation returns the Base64 decoding of +string+.
|
16
67
|
def decode(string) string.unpack1("m0") end
|
68
|
+
|
69
|
+
# Returns the message used by the client to abort an authentication
|
70
|
+
# exchange.
|
71
|
+
#
|
72
|
+
# The generic implementation returns <tt>"*"</tt>.
|
17
73
|
def cancel_response; "*" end
|
18
74
|
end
|
19
75
|
|
data/lib/net/imap/sasl.rb
CHANGED
@@ -114,8 +114,8 @@ module Net
|
|
114
114
|
# messages has not passed integrity checks.
|
115
115
|
AuthenticationFailed = Class.new(Error)
|
116
116
|
|
117
|
-
# Indicates that authentication cannot proceed because
|
118
|
-
#
|
117
|
+
# Indicates that authentication cannot proceed because the server ended
|
118
|
+
# authentication prematurely.
|
119
119
|
class AuthenticationIncomplete < AuthenticationFailed
|
120
120
|
# The success response from the server
|
121
121
|
attr_reader :response
|
@@ -159,7 +159,10 @@ module Net
|
|
159
159
|
# Returns the default global SASL::Authenticators instance.
|
160
160
|
def self.authenticators; @authenticators ||= Authenticators.new end
|
161
161
|
|
162
|
-
#
|
162
|
+
# Creates a new SASL authenticator, using SASL::Authenticators#new.
|
163
|
+
#
|
164
|
+
# +registry+ defaults to SASL.authenticators. All other arguments are
|
165
|
+
# forwarded to to <tt>registry.new</tt>.
|
163
166
|
def self.authenticator(*args, registry: authenticators, **kwargs, &block)
|
164
167
|
registry.new(*args, **kwargs, &block)
|
165
168
|
end
|
@@ -12,7 +12,6 @@ module Net
|
|
12
12
|
|
13
13
|
def response_errors; RESPONSE_ERRORS end
|
14
14
|
def sasl_ir_capable?; client.capable?("SASL-IR") end
|
15
|
-
def auth_capable?(mechanism); client.auth_capable?(mechanism) end
|
16
15
|
def drop_connection; client.logout! end
|
17
16
|
def drop_connection!; client.disconnect end
|
18
17
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set" unless defined?(::Set)
|
4
|
+
|
3
5
|
module Net
|
4
6
|
class IMAP
|
5
7
|
|
@@ -14,13 +16,6 @@ module Net
|
|
14
16
|
# receive a SequenceSet as an argument, for example IMAP#search, IMAP#fetch,
|
15
17
|
# and IMAP#store.
|
16
18
|
#
|
17
|
-
# == EXPERIMENTAL API
|
18
|
-
#
|
19
|
-
# SequenceSet is currently experimental. Only two methods, ::[] and
|
20
|
-
# #valid_string, are considered stable. Although the API isn't expected to
|
21
|
-
# change much, any other methods may be removed or changed without
|
22
|
-
# deprecation.
|
23
|
-
#
|
24
19
|
# == Creating sequence sets
|
25
20
|
#
|
26
21
|
# SequenceSet.new with no arguments creates an empty sequence set. Note
|
@@ -37,7 +32,8 @@ module Net
|
|
37
32
|
#
|
38
33
|
# SequenceSet.new may receive a single optional argument: a non-zero 32 bit
|
39
34
|
# unsigned integer, a range, a <tt>sequence-set</tt> formatted string,
|
40
|
-
# another sequence set,
|
35
|
+
# another sequence set, a Set (containing only numbers or <tt>*</tt>), or an
|
36
|
+
# Array containing any of these (array inputs may be nested).
|
41
37
|
#
|
42
38
|
# set = Net::IMAP::SequenceSet.new(1)
|
43
39
|
# set.valid_string #=> "1"
|
@@ -286,11 +282,7 @@ module Net
|
|
286
282
|
|
287
283
|
# valid inputs for "*"
|
288
284
|
STARS = [:*, ?*, -1].freeze
|
289
|
-
private_constant :
|
290
|
-
|
291
|
-
COERCIBLE = ->{ _1.respond_to? :to_sequence_set }
|
292
|
-
ENUMABLE = ->{ _1.respond_to?(:each) && _1.respond_to?(:empty?) }
|
293
|
-
private_constant :COERCIBLE, :ENUMABLE
|
285
|
+
private_constant :STARS
|
294
286
|
|
295
287
|
class << self
|
296
288
|
|
@@ -304,7 +296,7 @@ module Net
|
|
304
296
|
# Use ::new to create a mutable or empty SequenceSet.
|
305
297
|
def [](first, *rest)
|
306
298
|
if rest.empty?
|
307
|
-
if first.is_a?(SequenceSet) &&
|
299
|
+
if first.is_a?(SequenceSet) && first.frozen? && first.valid?
|
308
300
|
first
|
309
301
|
else
|
310
302
|
new(first).validate.freeze
|
@@ -325,7 +317,7 @@ module Net
|
|
325
317
|
# raised.
|
326
318
|
def try_convert(obj)
|
327
319
|
return obj if obj.is_a?(SequenceSet)
|
328
|
-
return nil unless respond_to?(:to_sequence_set)
|
320
|
+
return nil unless obj.respond_to?(:to_sequence_set)
|
329
321
|
obj = obj.to_sequence_set
|
330
322
|
return obj if obj.is_a?(SequenceSet)
|
331
323
|
raise DataFormatError, "invalid object returned from to_sequence_set"
|
@@ -389,6 +381,10 @@ module Net
|
|
389
381
|
# Related: #valid_string, #normalized_string, #to_s
|
390
382
|
def string; @string ||= normalized_string if valid? end
|
391
383
|
|
384
|
+
# Returns an array with #normalized_string when valid and an empty array
|
385
|
+
# otherwise.
|
386
|
+
def deconstruct; valid? ? [normalized_string] : [] end
|
387
|
+
|
392
388
|
# Assigns a new string to #string and resets #elements to match. It
|
393
389
|
# cannot be set to an empty string—assign +nil+ or use #clear instead.
|
394
390
|
# The string is validated but not normalized.
|
@@ -682,6 +678,7 @@ module Net
|
|
682
678
|
# Unlike #add, #merge, or #union, the new value is appended to #string.
|
683
679
|
# This may result in a #string which has duplicates or is out-of-order.
|
684
680
|
def append(object)
|
681
|
+
modifying!
|
685
682
|
tuple = input_to_tuple object
|
686
683
|
entry = tuple_to_str tuple
|
687
684
|
tuple_add tuple
|
@@ -1271,7 +1268,8 @@ module Net
|
|
1271
1268
|
when *STARS, Integer, Range then [input_to_tuple(obj)]
|
1272
1269
|
when String then str_to_tuples obj
|
1273
1270
|
when SequenceSet then obj.tuples
|
1274
|
-
when
|
1271
|
+
when Set then obj.map { [to_tuple_int(_1)] * 2 }
|
1272
|
+
when Array then obj.flat_map { input_to_tuples _1 }
|
1275
1273
|
when nil then []
|
1276
1274
|
else
|
1277
1275
|
raise DataFormatError,
|
@@ -1284,8 +1282,7 @@ module Net
|
|
1284
1282
|
# String, Set, Array, or... any type of object.
|
1285
1283
|
def input_try_convert(input)
|
1286
1284
|
SequenceSet.try_convert(input) ||
|
1287
|
-
|
1288
|
-
input.respond_to?(:to_int) && Integer(input.to_int) ||
|
1285
|
+
Integer.try_convert(input) ||
|
1289
1286
|
String.try_convert(input) ||
|
1290
1287
|
input
|
1291
1288
|
end
|
@@ -1317,6 +1314,12 @@ module Net
|
|
1317
1314
|
range.include?(min) || range.include?(max) || (min..max).cover?(range)
|
1318
1315
|
end
|
1319
1316
|
|
1317
|
+
def modifying!
|
1318
|
+
if frozen?
|
1319
|
+
raise FrozenError, "can't modify frozen #{self.class}: %p" % [self]
|
1320
|
+
end
|
1321
|
+
end
|
1322
|
+
|
1320
1323
|
def tuples_add(tuples) tuples.each do tuple_add _1 end; self end
|
1321
1324
|
def tuples_subtract(tuples) tuples.each do tuple_subtract _1 end; self end
|
1322
1325
|
|
@@ -1331,6 +1334,7 @@ module Net
|
|
1331
1334
|
# ---------??===lower==|--|==|----|===upper===|-- join until upper
|
1332
1335
|
# ---------??===lower==|--|==|--|=====upper===|-- join to upper
|
1333
1336
|
def tuple_add(tuple)
|
1337
|
+
modifying!
|
1334
1338
|
min, max = tuple
|
1335
1339
|
lower, lower_idx = tuple_gte_with_index(min - 1)
|
1336
1340
|
if lower.nil? then tuples << tuple
|
@@ -1367,6 +1371,7 @@ module Net
|
|
1367
1371
|
# -------??=====lower====|--|====|---|====upper====|-- 7. delete until
|
1368
1372
|
# -------??=====lower====|--|====|--|=====upper====|-- 8. delete and trim
|
1369
1373
|
def tuple_subtract(tuple)
|
1374
|
+
modifying!
|
1370
1375
|
min, max = tuple
|
1371
1376
|
lower, idx = tuple_gte_with_index(min)
|
1372
1377
|
if lower.nil? then nil # case 1.
|
@@ -1407,12 +1412,11 @@ module Net
|
|
1407
1412
|
end
|
1408
1413
|
|
1409
1414
|
def nz_number(num)
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
num
|
1415
|
+
String === num && !/\A[1-9]\d*\z/.match?(num) and
|
1416
|
+
raise DataFormatError, "%p is not a valid nz-number" % [num]
|
1417
|
+
NumValidator.ensure_nz_number Integer num
|
1418
|
+
rescue TypeError # To catch errors from Integer()
|
1419
|
+
raise DataFormatError, $!.message
|
1416
1420
|
end
|
1417
1421
|
|
1418
1422
|
# intentionally defined after the class implementation
|