net-imap 0.3.9 → 0.4.0

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.

Files changed (47) 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/errors.rb +0 -34
  14. data/lib/net/imap/response_data.rb +46 -41
  15. data/lib/net/imap/response_parser/parser_utils.rb +230 -0
  16. data/lib/net/imap/response_parser.rb +667 -649
  17. data/lib/net/imap/sasl/anonymous_authenticator.rb +68 -0
  18. data/lib/net/imap/sasl/authenticators.rb +112 -0
  19. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +15 -9
  20. data/lib/net/imap/{authenticators/digest_md5.rb → sasl/digest_md5_authenticator.rb} +74 -21
  21. data/lib/net/imap/sasl/external_authenticator.rb +62 -0
  22. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  23. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +19 -14
  24. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +164 -0
  25. data/lib/net/imap/sasl/plain_authenticator.rb +93 -0
  26. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  27. data/lib/net/imap/sasl/scram_authenticator.rb +278 -0
  28. data/lib/net/imap/sasl/stringprep.rb +6 -66
  29. data/lib/net/imap/sasl/xoauth2_authenticator.rb +88 -0
  30. data/lib/net/imap/sasl.rb +139 -44
  31. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  32. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  33. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  34. data/lib/net/imap/stringprep/tables.rb +146 -0
  35. data/lib/net/imap/stringprep/trace.rb +85 -0
  36. data/lib/net/imap/stringprep.rb +159 -0
  37. data/lib/net/imap.rb +987 -690
  38. data/net-imap.gemspec +1 -1
  39. data/rakelib/saslprep.rake +4 -4
  40. data/rakelib/string_prep_tables_generator.rb +82 -60
  41. metadata +30 -13
  42. data/lib/net/imap/authenticators/plain.rb +0 -41
  43. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  44. data/lib/net/imap/response_reader.rb +0 -75
  45. data/lib/net/imap/sasl/saslprep.rb +0 -55
  46. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  47. 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