net-imap 0.4.12 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|