net-imap 0.3.8 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of net-imap might be problematic. Click here for more details.

Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +46 -0
  3. data/.github/workflows/test.yml +5 -12
  4. data/Gemfile +1 -0
  5. data/README.md +15 -4
  6. data/Rakefile +0 -7
  7. data/benchmarks/generate_parser_benchmarks +52 -0
  8. data/benchmarks/parser.yml +578 -0
  9. data/benchmarks/stringprep.yml +1 -1
  10. data/lib/net/imap/authenticators.rb +26 -57
  11. data/lib/net/imap/command_data.rb +13 -6
  12. data/lib/net/imap/deprecated_client_options.rb +139 -0
  13. data/lib/net/imap/response_data.rb +46 -41
  14. data/lib/net/imap/response_parser/parser_utils.rb +230 -0
  15. data/lib/net/imap/response_parser.rb +667 -649
  16. data/lib/net/imap/sasl/anonymous_authenticator.rb +68 -0
  17. data/lib/net/imap/sasl/authenticators.rb +112 -0
  18. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +15 -9
  19. data/lib/net/imap/{authenticators/digest_md5.rb → sasl/digest_md5_authenticator.rb} +74 -21
  20. data/lib/net/imap/sasl/external_authenticator.rb +62 -0
  21. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  22. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +19 -14
  23. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +164 -0
  24. data/lib/net/imap/sasl/plain_authenticator.rb +93 -0
  25. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  26. data/lib/net/imap/sasl/scram_authenticator.rb +278 -0
  27. data/lib/net/imap/sasl/stringprep.rb +6 -66
  28. data/lib/net/imap/sasl/xoauth2_authenticator.rb +88 -0
  29. data/lib/net/imap/sasl.rb +139 -44
  30. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  31. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  32. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  33. data/lib/net/imap/stringprep/tables.rb +146 -0
  34. data/lib/net/imap/stringprep/trace.rb +85 -0
  35. data/lib/net/imap/stringprep.rb +159 -0
  36. data/lib/net/imap.rb +967 -588
  37. data/net-imap.gemspec +1 -1
  38. data/rakelib/saslprep.rake +4 -4
  39. data/rakelib/string_prep_tables_generator.rb +82 -60
  40. metadata +30 -12
  41. data/lib/net/imap/authenticators/plain.rb +0 -41
  42. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  43. data/lib/net/imap/sasl/saslprep.rb +0 -55
  44. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  45. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP < Protocol
5
+ module SASL
6
+
7
+ # Authenticator for the "+ANONYMOUS+" SASL mechanism, as specified by
8
+ # RFC-4505[https://tools.ietf.org/html/rfc4505]. See
9
+ # Net::IMAP#authenticate.
10
+ class AnonymousAuthenticator
11
+
12
+ # An optional token sent for the +ANONYMOUS+ mechanism., up to 255 UTF-8
13
+ # characters in length.
14
+ #
15
+ # If it contains an "@" sign, the message must be a valid email address
16
+ # (+addr-spec+ from RFC-2822[https://tools.ietf.org/html/rfc2822]).
17
+ # Email syntax is _not_ validated by AnonymousAuthenticator.
18
+ #
19
+ # Otherwise, it can be any UTF8 string which is permitted by the
20
+ # StringPrep::Trace profile.
21
+ attr_reader :anonymous_message
22
+
23
+ # :call-seq:
24
+ # new(anonymous_message = "", **) -> authenticator
25
+ # new(anonymous_message: "", **) -> authenticator
26
+ #
27
+ # Creates an Authenticator for the "+ANONYMOUS+" SASL mechanism, as
28
+ # specified in RFC-4505[https://tools.ietf.org/html/rfc4505]. To use
29
+ # this, see Net::IMAP#authenticate or your client's authentication
30
+ # method.
31
+ #
32
+ # #anonymous_message is an optional message which is sent to the server.
33
+ # It may be sent as a positional argument or as a keyword argument.
34
+ #
35
+ # Any other keyword arguments are silently ignored.
36
+ def initialize(anon_msg = nil, anonymous_message: nil, **)
37
+ message = (anonymous_message || anon_msg || "").to_str
38
+ @anonymous_message = StringPrep::Trace.stringprep_trace message
39
+ if (size = @anonymous_message&.length)&.> 255
40
+ raise ArgumentError,
41
+ "anonymous_message is too long. (%d codepoints)" % [size]
42
+ end
43
+ @done = false
44
+ end
45
+
46
+ # :call-seq:
47
+ # initial_response? -> true
48
+ #
49
+ # +ANONYMOUS+ can send an initial client response.
50
+ def initial_response?; true end
51
+
52
+ # Returns #anonymous_message.
53
+ def process(_server_challenge_string)
54
+ anonymous_message
55
+ ensure
56
+ @done = true
57
+ end
58
+
59
+ # Returns true when the initial client response was sent.
60
+ #
61
+ # The authentication should not succeed unless this returns true, but it
62
+ # does *not* indicate success.
63
+ def done?; @done end
64
+
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net::IMAP::SASL
4
+
5
+ # Registry for SASL authenticators
6
+ #
7
+ # Registered authenticators must respond to +#new+ or +#call+ (e.g. a class or
8
+ # a proc), receiving any credentials and options and returning an
9
+ # authenticator instance. The returned object represents a single
10
+ # authentication exchange and <em>must not</em> be reused for multiple
11
+ # authentication attempts.
12
+ #
13
+ # An authenticator instance object must respond to +#process+, receiving the
14
+ # server's challenge and returning the client's response. Optionally, it may
15
+ # also respond to +#initial_response?+ and +#done?+. When
16
+ # +#initial_response?+ returns +true+, +#process+ may be called the first
17
+ # time with +nil+. When +#done?+ returns +false+, the exchange is incomplete
18
+ # and an exception should be raised if the exchange terminates prematurely.
19
+ #
20
+ # See the source for PlainAuthenticator, XOAuth2Authenticator, and
21
+ # ScramSHA1Authenticator for examples.
22
+ class Authenticators
23
+
24
+ # Create a new Authenticators registry.
25
+ #
26
+ # This class is usually not instantiated directly. Use SASL.authenticators
27
+ # to reuse the default global registry.
28
+ #
29
+ # When +use_defaults+ is +false+, the registry will start empty. When
30
+ # +use_deprecated+ is +false+, deprecated authenticators will not be
31
+ # included with the defaults.
32
+ def initialize(use_defaults: true, use_deprecated: true)
33
+ @authenticators = {}
34
+ return unless use_defaults
35
+ add_authenticator "Anonymous"
36
+ add_authenticator "External"
37
+ add_authenticator "OAuthBearer"
38
+ add_authenticator "Plain"
39
+ add_authenticator "Scram-SHA-1"
40
+ add_authenticator "Scram-SHA-256"
41
+ add_authenticator "XOAuth2"
42
+ return unless use_deprecated
43
+ add_authenticator "Login" # deprecated
44
+ add_authenticator "Cram-MD5" # deprecated
45
+ add_authenticator "Digest-MD5" # deprecated
46
+ end
47
+
48
+ # Returns the names of all registered SASL mechanisms.
49
+ def names; @authenticators.keys end
50
+
51
+ # :call-seq:
52
+ # add_authenticator(mechanism)
53
+ # add_authenticator(mechanism, authenticator_class)
54
+ # add_authenticator(mechanism, authenticator_proc)
55
+ #
56
+ # Registers an authenticator for #authenticator to use. +mechanism+ is the
57
+ # name of the
58
+ # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
59
+ # implemented by +authenticator_class+ (for instance, <tt>"PLAIN"</tt>).
60
+ #
61
+ # If +mechanism+ refers to an existing authenticator, a warning will be
62
+ # printed and the old authenticator will be replaced.
63
+ #
64
+ # When only a single argument is given, the authenticator class will be
65
+ # lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
66
+ # preserved and non-alphanumeric characters are removed..
67
+ def add_authenticator(name, authenticator = nil)
68
+ key = name.upcase.to_sym
69
+ authenticator ||= begin
70
+ class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym
71
+ auth_class = nil
72
+ ->(*creds, **props, &block) {
73
+ auth_class ||= Net::IMAP::SASL.const_get(class_name)
74
+ auth_class.new(*creds, **props, &block)
75
+ }
76
+ end
77
+ @authenticators[key] = authenticator
78
+ end
79
+
80
+ # Removes the authenticator registered for +name+
81
+ def remove_authenticator(name)
82
+ key = name.upcase.to_sym
83
+ @authenticators.delete(key)
84
+ end
85
+
86
+ # :call-seq:
87
+ # authenticator(mechanism, ...) -> auth_session
88
+ #
89
+ # Builds an authenticator instance using the authenticator registered to
90
+ # +mechanism+. The returned object represents a single authentication
91
+ # exchange and <em>must not</em> be reused for multiple authentication
92
+ # attempts.
93
+ #
94
+ # All arguments (except +mechanism+) are forwarded to the registered
95
+ # authenticator's +#new+ or +#call+ method. Each authenticator must
96
+ # document its own arguments.
97
+ #
98
+ # [Note]
99
+ # This method is intended for internal use by connection protocol code
100
+ # only. Protocol client users should see refer to their client's
101
+ # documentation, e.g. Net::IMAP#authenticate.
102
+ def authenticator(mechanism, ...)
103
+ auth = @authenticators.fetch(mechanism.upcase.to_sym) do
104
+ raise ArgumentError, 'unknown auth type - "%s"' % mechanism
105
+ end
106
+ auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
107
+ end
108
+ alias new authenticator
109
+
110
+ end
111
+
112
+ end
@@ -13,14 +13,7 @@
13
13
  # Additionally, RFC8314[https://tools.ietf.org/html/rfc8314] discourage the use
14
14
  # of cleartext and recommends TLS version 1.2 or greater be used for all
15
15
  # traffic. With TLS +CRAM-MD5+ is okay, but so is +PLAIN+
16
- class Net::IMAP::CramMD5Authenticator
17
- def process(challenge)
18
- digest = hmac_md5(challenge, @password)
19
- return @user + " " + digest
20
- end
21
-
22
- private
23
-
16
+ class Net::IMAP::SASL::CramMD5Authenticator
24
17
  def initialize(user, password, warn_deprecation: true, **_ignored)
25
18
  if warn_deprecation
26
19
  warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
@@ -28,8 +21,22 @@ class Net::IMAP::CramMD5Authenticator
28
21
  require "digest/md5"
29
22
  @user = user
30
23
  @password = password
24
+ @done = false
25
+ end
26
+
27
+ def initial_response?; false end
28
+
29
+ def process(challenge)
30
+ digest = hmac_md5(challenge, @password)
31
+ return @user + " " + digest
32
+ ensure
33
+ @done = true
31
34
  end
32
35
 
36
+ def done?; @done end
37
+
38
+ private
39
+
33
40
  def hmac_md5(text, key)
34
41
  if key.length > 64
35
42
  key = Digest::MD5.digest(key)
@@ -47,5 +54,4 @@ class Net::IMAP::CramMD5Authenticator
47
54
  return Digest::MD5.hexdigest(k_opad + digest)
48
55
  end
49
56
 
50
- Net::IMAP.add_authenticator "CRAM-MD5", self
51
57
  end
@@ -1,14 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Net::IMAP authenticator for the "`DIGEST-MD5`" SASL mechanism type, specified
4
- # in RFC2831(https://tools.ietf.org/html/rfc2831). See Net::IMAP#authenticate.
4
+ # in RFC-2831[https://tools.ietf.org/html/rfc2831]. See Net::IMAP#authenticate.
5
5
  #
6
6
  # == Deprecated
7
7
  #
8
8
  # "+DIGEST-MD5+" has been deprecated by
9
- # {RFC6331}[https://tools.ietf.org/html/rfc6331] and should not be relied on for
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
- class Net::IMAP::DigestMD5Authenticator
11
+ class Net::IMAP::SASL::DigestMD5Authenticator
12
+ STAGE_ONE = :stage_one
13
+ STAGE_TWO = :stage_two
14
+ STAGE_DONE = :stage_done
15
+ private_constant :STAGE_ONE, :STAGE_TWO, :STAGE_DONE
16
+
17
+ # Authentication identity: the identity that matches the #password.
18
+ #
19
+ # RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
20
+ # "Authentication identity" is the generic term used by
21
+ # RFC-4422[https://tools.ietf.org/html/rfc4422].
22
+ # RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
23
+ # that to +authcid+. So +authcid+ is available as an alias for #username.
24
+ attr_reader :username
25
+
26
+ # A password or passphrase that matches the #username.
27
+ #
28
+ # The +password+ will be used to create the response digest.
29
+ attr_reader :password
30
+
31
+ # Authorization identity: an identity to act as or on behalf of. The identity
32
+ # form is application protocol specific. If not provided or left blank, the
33
+ # server derives an authorization identity from the authentication identity.
34
+ # The server is responsible for verifying the client's credentials and
35
+ # verifying that the identity it associates with the client's authentication
36
+ # identity is allowed to act as (or on behalf of) the authorization identity.
37
+ #
38
+ # For example, an administrator or superuser might take on another role:
39
+ #
40
+ # imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
41
+ #
42
+ attr_reader :authzid
43
+
44
+ # :call-seq:
45
+ # new(username, password, authzid = nil, **options) -> authenticator
46
+ # new(username:, password:, authzid: nil, **options) -> authenticator
47
+ #
48
+ # Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
49
+ #
50
+ # Called by Net::IMAP#authenticate and similar methods on other clients.
51
+ #
52
+ # ==== Parameters
53
+ #
54
+ # * #username — Identity whose #password is used.
55
+ # * #password — A password or passphrase associated with this #username.
56
+ # * #authzid ― Alternate identity to act as or on behalf of. Optional.
57
+ # * +warn_deprecation+ — Set to +false+ to silence the warning.
58
+ #
59
+ # See the documentation for each attribute for more details.
60
+ def initialize(user = nil, pass = nil, authz = nil,
61
+ username: nil, password: nil, authzid: nil,
62
+ warn_deprecation: true, **)
63
+ username ||= user or raise ArgumentError, "missing username"
64
+ password ||= pass or raise ArgumentError, "missing password"
65
+ authzid ||= authz
66
+ if warn_deprecation
67
+ warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
68
+ # TODO: recommend SCRAM instead.
69
+ end
70
+ require "digest/md5"
71
+ require "strscan"
72
+ @username, @password, @authzid = username, password, authzid
73
+ @nc, @stage = {}, STAGE_ONE
74
+ end
75
+
76
+ def initial_response?; false end
77
+
78
+ # Responds to server challenge in two stages.
12
79
  def process(challenge)
13
80
  case @stage
14
81
  when STAGE_ONE
@@ -31,7 +98,7 @@ class Net::IMAP::DigestMD5Authenticator
31
98
 
32
99
  response = {
33
100
  :nonce => sparams['nonce'],
34
- :username => @user,
101
+ :username => @username,
35
102
  :realm => sparams['realm'],
36
103
  :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]),
37
104
  :'digest-uri' => 'imap/' + sparams['realm'],
@@ -41,7 +108,7 @@ class Net::IMAP::DigestMD5Authenticator
41
108
  :charset => sparams['charset'],
42
109
  }
43
110
 
44
- response[:authzid] = @authname unless @authname.nil?
111
+ response[:authzid] = @authzid unless @authzid.nil?
45
112
 
46
113
  # now, the real thing
47
114
  a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') )
@@ -62,7 +129,7 @@ class Net::IMAP::DigestMD5Authenticator
62
129
 
63
130
  return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',')
64
131
  when STAGE_TWO
65
- @stage = nil
132
+ @stage = STAGE_DONE
66
133
  # if at the second stage, return an empty string
67
134
  if challenge =~ /rspauth=/
68
135
  return ''
@@ -74,23 +141,10 @@ class Net::IMAP::DigestMD5Authenticator
74
141
  end
75
142
  end
76
143
 
77
- def initialize(user, password, authname = nil, warn_deprecation: true)
78
- if warn_deprecation
79
- warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
80
- # TODO: recommend SCRAM instead.
81
- end
82
- require "digest/md5"
83
- require "strscan"
84
- @user, @password, @authname = user, password, authname
85
- @nc, @stage = {}, STAGE_ONE
86
- end
87
-
144
+ def done?; @stage == STAGE_DONE end
88
145
 
89
146
  private
90
147
 
91
- STAGE_ONE = :stage_one
92
- STAGE_TWO = :stage_two
93
-
94
148
  def nc(nonce)
95
149
  if @nc.has_key? nonce
96
150
  @nc[nonce] = @nc[nonce] + 1
@@ -111,5 +165,4 @@ class Net::IMAP::DigestMD5Authenticator
111
165
  end
112
166
  end
113
167
 
114
- Net::IMAP.add_authenticator "DIGEST-MD5", self
115
168
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP < Protocol
5
+ module SASL
6
+
7
+ # Authenticator for the "+EXTERNAL+" SASL mechanism, as specified by
8
+ # RFC-4422[https://tools.ietf.org/html/rfc4422]. See
9
+ # Net::IMAP#authenticate.
10
+ #
11
+ # The EXTERNAL mechanism requests that the server use client credentials
12
+ # established external to SASL, for example by TLS certificate or IPsec.
13
+ class ExternalAuthenticator
14
+
15
+ # Authorization identity: an identity to act as or on behalf of.
16
+ #
17
+ # If not explicitly provided, the server defaults to using the identity
18
+ # that was authenticated by the external credentials.
19
+ attr_reader :authzid
20
+
21
+ # :call-seq:
22
+ # new(authzid: nil, **) -> authenticator
23
+ #
24
+ # Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
25
+ # specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
26
+ # this, see Net::IMAP#authenticate or your client's authentication
27
+ # method.
28
+ #
29
+ # #authzid is an optional identity to act as or on behalf of.
30
+ #
31
+ # Any other keyword parameters are quietly ignored.
32
+ def initialize(authzid: nil, **)
33
+ @authzid = authzid&.to_str&.encode "UTF-8"
34
+ if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
35
+ raise ArgumentError, "contains NULL"
36
+ end
37
+ @done = false
38
+ end
39
+
40
+ # :call-seq:
41
+ # initial_response? -> true
42
+ #
43
+ # +EXTERNAL+ can send an initial client response.
44
+ def initial_response?; true end
45
+
46
+ # Returns #authzid, or an empty string if there is no authzid.
47
+ def process(_)
48
+ authzid || ""
49
+ ensure
50
+ @done = true
51
+ end
52
+
53
+ # Returns true when the initial client response was sent.
54
+ #
55
+ # The authentication should not succeed unless this returns true, but it
56
+ # does *not* indicate success.
57
+ def done?; @done end
58
+
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP < Protocol
5
+ module SASL
6
+
7
+ # Originally defined for the GS2 mechanism family in
8
+ # RFC5801[https://tools.ietf.org/html/rfc5801],
9
+ # several different mechanisms start with a GS2 header:
10
+ # * +GS2-*+ --- RFC5801[https://tools.ietf.org/html/rfc5801]
11
+ # * +SCRAM-*+ --- RFC5802[https://tools.ietf.org/html/rfc5802]
12
+ # (ScramAuthenticator)
13
+ # * +SAML20+ --- RFC6595[https://tools.ietf.org/html/rfc6595]
14
+ # * +OPENID20+ --- RFC6616[https://tools.ietf.org/html/rfc6616]
15
+ # * +OAUTH10A+ --- RFC7628[https://tools.ietf.org/html/rfc7628]
16
+ # * +OAUTHBEARER+ --- RFC7628[https://tools.ietf.org/html/rfc7628]
17
+ # (OAuthBearerAuthenticator)
18
+ #
19
+ # Classes that include this module must implement +#authzid+.
20
+ module GS2Header
21
+ NO_NULL_CHARS = /\A[^\x00]+\z/u.freeze # :nodoc:
22
+
23
+ ##
24
+ # Matches {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
25
+ # +saslname+. The output from gs2_saslname_encode matches this Regexp.
26
+ RFC5801_SASLNAME = /\A(?:[^,=\x00]|=2C|=3D)+\z/u.freeze
27
+
28
+ # The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
29
+ # +gs2-header+, which prefixes the #initial_client_response.
30
+ #
31
+ # >>>
32
+ # <em>Note: the actual GS2 header includes an optional flag to
33
+ # indicate that the GSS mechanism is not "standard", but since all of
34
+ # the SASL mechanisms using GS2 are "standard", we don't include that
35
+ # flag. A class for a nonstandard GSSAPI mechanism should prefix with
36
+ # "+F,+".</em>
37
+ def gs2_header
38
+ "#{gs2_cb_flag},#{gs2_authzid},"
39
+ end
40
+
41
+ # The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
42
+ # +gs2-cb-flag+:
43
+ #
44
+ # "+n+":: The client doesn't support channel binding.
45
+ # "+y+":: The client does support channel binding
46
+ # but thinks the server does not.
47
+ # "+p+":: The client requires channel binding.
48
+ # The selected channel binding follows "+p=+".
49
+ #
50
+ # The default always returns "+n+". A mechanism that supports channel
51
+ # binding must override this method.
52
+ #
53
+ def gs2_cb_flag; "n" end
54
+
55
+ # The {RFC5801 §4}[https://www.rfc-editor.org/rfc/rfc5801#section-4]
56
+ # +gs2-authzid+ header, when +#authzid+ is not empty.
57
+ #
58
+ # If +#authzid+ is empty or +nil+, an empty string is returned.
59
+ def gs2_authzid
60
+ return "" if authzid.nil? || authzid == ""
61
+ "a=#{gs2_saslname_encode(authzid)}"
62
+ end
63
+
64
+ module_function
65
+
66
+ # Encodes +str+ to match RFC5801_SASLNAME.
67
+ def gs2_saslname_encode(str)
68
+ str = str.encode("UTF-8")
69
+ # Regexp#match raises "invalid byte sequence" for invalid UTF-8
70
+ NO_NULL_CHARS.match str or
71
+ raise ArgumentError, "invalid saslname: %p" % [str]
72
+ str
73
+ .gsub(?=, "=3D")
74
+ .gsub(?,, "=2C")
75
+ end
76
+
77
+ end
78
+ end
79
+ end
80
+ end
@@ -17,21 +17,11 @@
17
17
  # compatibility with existing servers. See
18
18
  # {draft-murchison-sasl-login}[https://www.iana.org/go/draft-murchison-sasl-login]
19
19
  # for both specification and deprecation.
20
- class Net::IMAP::LoginAuthenticator
21
- def process(data)
22
- case @state
23
- when STATE_USER
24
- @state = STATE_PASSWORD
25
- return @user
26
- when STATE_PASSWORD
27
- return @password
28
- end
29
- end
30
-
31
- private
32
-
20
+ class Net::IMAP::SASL::LoginAuthenticator
33
21
  STATE_USER = :USER
34
22
  STATE_PASSWORD = :PASSWORD
23
+ STATE_DONE = :DONE
24
+ private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE
35
25
 
36
26
  def initialize(user, password, warn_deprecation: true, **_ignored)
37
27
  if warn_deprecation
@@ -42,5 +32,20 @@ class Net::IMAP::LoginAuthenticator
42
32
  @state = STATE_USER
43
33
  end
44
34
 
45
- Net::IMAP.add_authenticator "LOGIN", self
35
+ def initial_response?; false end
36
+
37
+ def process(data)
38
+ case @state
39
+ when STATE_USER
40
+ @state = STATE_PASSWORD
41
+ return @user
42
+ when STATE_PASSWORD
43
+ @state = STATE_DONE
44
+ return @password
45
+ when STATE_DONE
46
+ raise ResponseParseError, data
47
+ end
48
+ end
49
+
50
+ def done?; @state == STATE_DONE end
46
51
  end