diff --git a/lib/net/imap.rb b/lib/net/imap.rb
index 754d3282..43f4b477 100644
--- a/lib/net/imap.rb
+++ b/lib/net/imap.rb
@@ -1015,7 +1015,7 @@ def starttls(options = {}, verify = true)
# +PLAIN+:: See PlainAuthenticator.
# Login using clear-text username and password.
#
- # +XOAUTH2+:: See XOauth2Authenticator.
+ # +XOAUTH2+:: See XOAuth2Authenticator.
# Login using a username and OAuth2 access token.
# Non-standard and obsoleted by +OAUTHBEARER+, but widely
# supported.
@@ -1074,10 +1074,7 @@ def starttls(options = {}, verify = true)
# completes. If the TaggedResponse to #authenticate includes updated
# capabilities, they will be cached.
def authenticate(mechanism, *creds, sasl_ir: true, **props, &callback)
- authenticator = self.class.authenticator(mechanism,
- *creds,
- **props,
- &callback)
+ authenticator = SASL.authenticator(mechanism, *creds, **props, &callback)
cmdargs = ["AUTHENTICATE", mechanism]
if sasl_ir && capable?("SASL-IR") && auth_capable?(mechanism) &&
SASL.initial_response?(authenticator)
diff --git a/lib/net/imap/authenticators.rb b/lib/net/imap/authenticators.rb
index 3104a36c..8ecb3851 100644
--- a/lib/net/imap/authenticators.rb
+++ b/lib/net/imap/authenticators.rb
@@ -1,64 +1,37 @@
# frozen_string_literal: true
-# Registry for SASL authenticators used by Net::IMAP.
+# Backward compatible delegators from Net::IMAP to Net::IMAP::SASL.
module Net::IMAP::Authenticators
- # Adds an authenticator for Net::IMAP#authenticate to use. +mechanism+ is the
- # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
- # implemented by +authenticator+ (for instance, "PLAIN").
- #
- # The +authenticator+ must respond to +#new+ (or #call), receiving the
- # authenticator configuration and return a configured authentication session.
- # The authenticator session must respond to +#process+, receiving the server's
- # challenge and returning the client's response.
- #
- # See PlainAuthenticator, XOauth2Authenticator, and DigestMD5Authenticator for
- # examples.
- def add_authenticator(auth_type, authenticator)
- authenticators[auth_type] = authenticator
+ # Deprecated. Use Net::IMAP::SASL.add_authenticator instead.
+ def add_authenticator(...)
+ warn(
+ "%s.%s is deprecated. Use %s.%s instead." % [
+ Net::IMAP, __method__, Net::IMAP::SASL, __method__
+ ],
+ uplevel: 1
+ )
+ Net::IMAP::SASL.add_authenticator(...)
end
- # :call-seq:
- # authenticator(mechanism, ...) -> authenticator
- # authenticator(mech, *creds, **props) {|prop, auth| val } -> authenticator
- # authenticator(mechanism, authnid, creds, authzid=nil) -> authenticator
- # authenticator(mechanism, **properties) -> authenticator
- # authenticator(mechanism) {|propname, authctx| value } -> authenticator
- #
- # Builds a new authentication session context for +mechanism+.
- #
- # [Note]
- # This method is intended for internal use by connection protocol code only.
- # Protocol client users should see refer to their client's documentation,
- # e.g. Net::IMAP#authenticate for Net::IMAP.
- #
- # The call signatures documented for this method are recommendations for
- # authenticator implementors. All arguments (other than +mechanism+) are
- # forwarded to the registered authenticator's +#new+ (or +#call+) method, and
- # each authenticator must document its own arguments.
- #
- # The returned object represents a single authentication exchange and must
- # not be reused for multiple authentication attempts.
- def authenticator(mechanism, ...)
- auth = authenticators.fetch(mechanism.upcase) do
- raise ArgumentError, 'unknown auth type - "%s"' % mechanism
- end
- auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
- end
-
- private
-
- def authenticators
- @authenticators ||= {}
+ # Deprecated. Use Net::IMAP::SASL.authenticator instead.
+ def authenticator(...)
+ warn(
+ "%s.%s is deprecated. Use %s.%s instead." % [
+ Net::IMAP, __method__, Net::IMAP::SASL, __method__
+ ],
+ uplevel: 1
+ )
+ Net::IMAP::SASL.authenticator(...)
end
+ Net::IMAP.extend self
end
-Net::IMAP.extend Net::IMAP::Authenticators
+class Net::IMAP
+ PlainAuthenticator = SASL::PlainAuthenticator # :nodoc:
+ deprecate_constant :PlainAuthenticator
-require_relative "authenticators/plain"
-
-require_relative "authenticators/login"
-require_relative "authenticators/cram_md5"
-require_relative "authenticators/digest_md5"
-require_relative "authenticators/xoauth2"
+ XOauth2Authenticator = SASL::XOAuth2Authenticator # :nodoc:
+ deprecate_constant :XOauth2Authenticator
+end
diff --git a/lib/net/imap/sasl.rb b/lib/net/imap/sasl.rb
index eca77bdd..6f2db923 100644
--- a/lib/net/imap/sasl.rb
+++ b/lib/net/imap/sasl.rb
@@ -32,6 +32,27 @@ module SASL
autoload :ProhibitedCodepoint, sasl_stringprep_rb
autoload :BidiStringError, sasl_stringprep_rb
+ sasl_dir = File.expand_path("sasl", __dir__)
+ autoload :Authenticators, "#{sasl_dir}/authenticators"
+
+ autoload :PlainAuthenticator, "#{sasl_dir}/plain_authenticator"
+ autoload :XOAuth2Authenticator, "#{sasl_dir}/xoauth2_authenticator"
+
+ autoload :CramMD5Authenticator, "#{sasl_dir}/cram_md5_authenticator"
+ autoload :DigestMD5Authenticator, "#{sasl_dir}/digest_md5_authenticator"
+ autoload :LoginAuthenticator, "#{sasl_dir}/login_authenticator"
+
+ # Returns the default global SASL::Authenticators instance.
+ def self.authenticators
+ @authenticators ||= Authenticators.new(use_defaults: true)
+ end
+
+ # Delegates to ::authenticators. See Authenticators#authenticator.
+ def self.authenticator(...) authenticators.authenticator(...) end
+
+ # Delegates to ::authenticators. See Authenticators#add_authenticator.
+ def self.add_authenticator(...) authenticators.add_authenticator(...) end
+
module_function
# See Net::IMAP::StringPrep::SASLprep#saslprep.
diff --git a/lib/net/imap/sasl/authenticators.rb b/lib/net/imap/sasl/authenticators.rb
new file mode 100644
index 00000000..779a8018
--- /dev/null
+++ b/lib/net/imap/sasl/authenticators.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module Net::IMAP::SASL
+
+ # Registry for SASL authenticators
+ #
+ # Registered authenticators must respond to +#new+ or +#call+ (e.g. a class or
+ # a proc), receiving any credentials and options and returning an
+ # authenticator instance. The returned object represents a single
+ # authentication exchange and must not be reused for multiple
+ # authentication attempts.
+ #
+ # An authenticator instance object must respond to +#process+, receiving the
+ # server's challenge and returning the client's response. Optionally, it may
+ # also respond to +#initial_response?+ and +#done?+. When
+ # +#initial_response?+ returns +true+, +#process+ may be called the first
+ # time with +nil+. When +#done?+ returns +false+, the exchange is incomplete
+ # and an exception should be raised if the exchange terminates prematurely.
+ #
+ # See the source for PlainAuthenticator, XOAuth2Authenticator, and
+ # ScramSHA1Authenticator for examples.
+ class Authenticators
+
+ # Create a new Authenticators registry.
+ #
+ # This class is usually not instantiated directly. Use SASL.authenticators
+ # to reuse the default global registry.
+ #
+ # By default, the registry will be empty--without any registrations. When
+ # +add_defaults+ is +true+, authenticators for all standard mechanisms will
+ # be registered.
+ #
+ def initialize(use_defaults: false)
+ @authenticators = {}
+ if use_defaults
+ add_authenticator "PLAIN", PlainAuthenticator
+ add_authenticator "XOAUTH2", XOAuth2Authenticator
+ add_authenticator "LOGIN", LoginAuthenticator # deprecated
+ add_authenticator "CRAM-MD5", CramMD5Authenticator # deprecated
+ add_authenticator "DIGEST-MD5", DigestMD5Authenticator # deprecated
+ end
+ end
+
+ # Returns the names of all registered SASL mechanisms.
+ def names; @authenticators.keys end
+
+ # :call-seq:
+ # add_authenticator(mechanism)
+ # add_authenticator(mechanism, authenticator_class)
+ # add_authenticator(mechanism, authenticator_proc)
+ #
+ # Registers an authenticator for #authenticator to use. +mechanism+ is the
+ # name of the
+ # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
+ # implemented by +authenticator_class+ (for instance, "PLAIN").
+ #
+ # If +mechanism+ refers to an existing authenticator, a warning will be
+ # printed and the old authenticator will be replaced.
+ #
+ # When only a single argument is given, the authenticator class will be
+ # lazily loaded from Net::IMAP::SASL::#{name}Authenticator (case is
+ # preserved and non-alphanumeric characters are removed..
+ def add_authenticator(auth_type, authenticator)
+ @authenticators[auth_type] = authenticator
+ end
+
+ # :call-seq:
+ # authenticator(mechanism, ...) -> auth_session
+ #
+ # Builds an authenticator instance using the authenticator registered to
+ # +mechanism+. The returned object represents a single authentication
+ # exchange and must not be reused for multiple authentication
+ # attempts.
+ #
+ # All arguments (except +mechanism+) are forwarded to the registered
+ # authenticator's +#new+ or +#call+ method. Each authenticator must
+ # document its own arguments.
+ #
+ # [Note]
+ # This method is intended for internal use by connection protocol code
+ # only. Protocol client users should see refer to their client's
+ # documentation, e.g. Net::IMAP#authenticate.
+ def authenticator(mechanism, ...)
+ auth = @authenticators.fetch(mechanism.upcase) do
+ raise ArgumentError, 'unknown auth type - "%s"' % mechanism
+ end
+ auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
+ end
+
+ end
+
+end
diff --git a/lib/net/imap/authenticators/cram_md5.rb b/lib/net/imap/sasl/cram_md5_authenticator.rb
similarity index 94%
rename from lib/net/imap/authenticators/cram_md5.rb
rename to lib/net/imap/sasl/cram_md5_authenticator.rb
index ed602655..3359f5a1 100644
--- a/lib/net/imap/authenticators/cram_md5.rb
+++ b/lib/net/imap/sasl/cram_md5_authenticator.rb
@@ -13,7 +13,7 @@
# Additionally, RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use
# of cleartext and recommends TLS version 1.2 or greater be used for all
# traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
-class Net::IMAP::CramMD5Authenticator
+class Net::IMAP::SASL::CramMD5Authenticator
def process(challenge)
digest = hmac_md5(challenge, @password)
return @user + " " + digest
@@ -47,5 +47,4 @@ def hmac_md5(text, key)
return Digest::MD5.hexdigest(k_opad + digest)
end
- Net::IMAP.add_authenticator "CRAM-MD5", self
end
diff --git a/lib/net/imap/authenticators/digest_md5.rb b/lib/net/imap/sasl/digest_md5_authenticator.rb
similarity index 97%
rename from lib/net/imap/authenticators/digest_md5.rb
rename to lib/net/imap/sasl/digest_md5_authenticator.rb
index 86aae605..e8f29a0a 100644
--- a/lib/net/imap/authenticators/digest_md5.rb
+++ b/lib/net/imap/sasl/digest_md5_authenticator.rb
@@ -8,7 +8,7 @@
# "+DIGEST-MD5+" has been deprecated by
# {RFC6331}[https://tools.ietf.org/html/rfc6331] and should not be relied on for
# security. It is included for compatibility with existing servers.
-class Net::IMAP::DigestMD5Authenticator
+class Net::IMAP::SASL::DigestMD5Authenticator
def process(challenge)
case @stage
when STAGE_ONE
@@ -111,5 +111,4 @@ def qdval(k, v)
end
end
- Net::IMAP.add_authenticator "DIGEST-MD5", self
end
diff --git a/lib/net/imap/authenticators/login.rb b/lib/net/imap/sasl/login_authenticator.rb
similarity index 94%
rename from lib/net/imap/authenticators/login.rb
rename to lib/net/imap/sasl/login_authenticator.rb
index 0096cbaf..9ee838b0 100644
--- a/lib/net/imap/authenticators/login.rb
+++ b/lib/net/imap/sasl/login_authenticator.rb
@@ -17,7 +17,7 @@
# compatibility with existing servers. See
# {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login]
# for both specification and deprecation.
-class Net::IMAP::LoginAuthenticator
+class Net::IMAP::SASL::LoginAuthenticator
def process(data)
case @state
when STATE_USER
@@ -42,5 +42,4 @@ def initialize(user, password, warn_deprecation: true, **_ignored)
@state = STATE_USER
end
- Net::IMAP.add_authenticator "LOGIN", self
end
diff --git a/lib/net/imap/authenticators/plain.rb b/lib/net/imap/sasl/plain_authenticator.rb
similarity index 95%
rename from lib/net/imap/authenticators/plain.rb
rename to lib/net/imap/sasl/plain_authenticator.rb
index e7fe07c4..18d763bb 100644
--- a/lib/net/imap/authenticators/plain.rb
+++ b/lib/net/imap/sasl/plain_authenticator.rb
@@ -9,7 +9,7 @@
# RFC8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
# greater be used for all traffic, and deprecate cleartext access ASAP. +PLAIN+
# can be secured by TLS encryption.
-class Net::IMAP::PlainAuthenticator
+class Net::IMAP::SASL::PlainAuthenticator
def initial_response?; true end
@@ -39,5 +39,4 @@ def initialize(username, password, authzid: nil)
@authzid = authzid
end
- Net::IMAP.add_authenticator "PLAIN", self
end
diff --git a/lib/net/imap/authenticators/xoauth2.rb b/lib/net/imap/sasl/xoauth2_authenticator.rb
similarity index 81%
rename from lib/net/imap/authenticators/xoauth2.rb
rename to lib/net/imap/sasl/xoauth2_authenticator.rb
index 67b6e5aa..9f5a99f8 100644
--- a/lib/net/imap/authenticators/xoauth2.rb
+++ b/lib/net/imap/sasl/xoauth2_authenticator.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Net::IMAP::XOauth2Authenticator
+class Net::IMAP::SASL::XOAuth2Authenticator
def initial_response?; true end
@@ -19,5 +19,4 @@ def build_oauth2_string(user, oauth2_token)
format("user=%s\1auth=Bearer %s\1\1", user, oauth2_token)
end
- Net::IMAP.add_authenticator 'XOAUTH2', self
end
diff --git a/test/net/imap/test_imap_authenticators.rb b/test/net/imap/test_imap_authenticators.rb
index cdf32e0a..93c85817 100644
--- a/test/net/imap/test_imap_authenticators.rb
+++ b/test/net/imap/test_imap_authenticators.rb
@@ -5,14 +5,20 @@
class IMAPAuthenticatorsTest < Test::Unit::TestCase
+ def test_net_imap_authenticator_deprecated
+ assert_warn(/Net::IMAP\.authenticator .+deprecated./) do
+ Net::IMAP.authenticator("PLAIN", "user", "pass")
+ end
+ end
+
# ----------------------
# PLAIN
# ----------------------
- def plain(...) Net::IMAP.authenticator("PLAIN", ...) end
+ def plain(...) Net::IMAP::SASL.authenticator("PLAIN", ...) end
def test_plain_authenticator_matches_mechanism
- assert_kind_of(Net::IMAP::PlainAuthenticator, plain("user", "pass"))
+ assert_kind_of(Net::IMAP::SASL::PlainAuthenticator, plain("user", "pass"))
end
def test_plain_supports_initial_response
@@ -36,10 +42,10 @@ def test_plain_no_null_chars
# XOAUTH2
# ----------------------
- def xoauth2(...) Net::IMAP.authenticator("XOAUTH2", ...) end
+ def xoauth2(...) Net::IMAP::SASL.authenticator("XOAUTH2", ...) end
def test_xoauth2_authenticator_matches_mechanism
- assert_kind_of(Net::IMAP::XOauth2Authenticator, xoauth2("user", "pass"))
+ assert_kind_of(Net::IMAP::SASL::XOAuth2Authenticator, xoauth2("user", "tok"))
end
def test_xoauth2
@@ -59,13 +65,13 @@ def test_xoauth2_supports_initial_response
# ----------------------
def login(*args, warn_deprecation: false, **kwargs, &block)
- Net::IMAP.authenticator(
+ Net::IMAP::SASL.authenticator(
"LOGIN", *args, warn_deprecation: warn_deprecation, **kwargs, &block
)
end
def test_login_authenticator_matches_mechanism
- assert_kind_of(Net::IMAP::LoginAuthenticator, login("n", "p"))
+ assert_kind_of(Net::IMAP::SASL::LoginAuthenticator, login("n", "p"))
end
def test_login_does_not_support_initial_response
@@ -74,7 +80,7 @@ def test_login_does_not_support_initial_response
def test_login_authenticator_deprecated
assert_warn(/LOGIN.+deprecated.+PLAIN/) do
- Net::IMAP.authenticator("LOGIN", "user", "pass")
+ Net::IMAP::SASL.authenticator("LOGIN", "user", "pass")
end
end
@@ -89,13 +95,13 @@ def test_login_responses
# ----------------------
def cram_md5(*args, warn_deprecation: false, **kwargs, &block)
- Net::IMAP.authenticator(
+ Net::IMAP::SASL.authenticator(
"CRAM-MD5", *args, warn_deprecation: warn_deprecation, **kwargs, &block
)
end
def test_cram_md5_authenticator_matches_mechanism
- assert_kind_of(Net::IMAP::CramMD5Authenticator, cram_md5("n", "p"))
+ assert_kind_of(Net::IMAP::SASL::CramMD5Authenticator, cram_md5("n", "p"))
end
def test_cram_md5_does_not_support_initial_response
@@ -104,7 +110,7 @@ def test_cram_md5_does_not_support_initial_response
def test_cram_md5_authenticator_deprecated
assert_warn(/CRAM-MD5.+deprecated./) do
- Net::IMAP.authenticator("CRAM-MD5", "user", "pass")
+ Net::IMAP::SASL.authenticator("CRAM-MD5", "user", "pass")
end
end
@@ -119,18 +125,19 @@ def test_cram_md5_authenticator
# ----------------------
def digest_md5(*args, warn_deprecation: false, **kwargs, &block)
- Net::IMAP.authenticator(
+ Net::IMAP::SASL.authenticator(
"DIGEST-MD5", *args, warn_deprecation: warn_deprecation, **kwargs, &block
)
end
def test_digest_md5_authenticator_matches_mechanism
- assert_kind_of(Net::IMAP::DigestMD5Authenticator, digest_md5("n", "p", "z"))
+ assert_kind_of(Net::IMAP::SASL::DigestMD5Authenticator,
+ digest_md5("n", "p", "z"))
end
def test_digest_md5_authenticator_deprecated
assert_warn(/DIGEST-MD5.+deprecated.+RFC6331/) do
- Net::IMAP.authenticator("DIGEST-MD5", "user", "pass")
+ Net::IMAP::SASL.authenticator("DIGEST-MD5", "user", "pass")
end
end
diff --git a/test/net/imap/test_regexps.rb b/test/net/imap/test_regexps.rb
index 2aa7d462..83d1db24 100644
--- a/test/net/imap/test_regexps.rb
+++ b/test/net/imap/test_regexps.rb
@@ -25,7 +25,11 @@ class IMAPRegexpsTest < Test::Unit::TestCase
RegexpCollector.new(
Net::IMAP,
exclude_map: {
- Net::IMAP => %i[BodyTypeAttachment BodyTypeExtension], # deprecated
+ Net::IMAP => %i[
+ BodyTypeAttachment BodyTypeExtension
+ PlainAuthenticator
+ XOauth2Authenticator
+ ], # deprecated
},
).to_h
)