net-imap 0.4.0 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 473842922edfdfd112a9cd3fa96df2831efa0d4640a38c0969725d6c04f45dbe
4
- data.tar.gz: 7f93b8a1882f2225f404db0d175c29ead931de531f918e17cbb6f8515300b9e9
3
+ metadata.gz: 90ea9e3ff227103752eda7658da755be8f4aabda8fdf60f33379f95c27a1a69c
4
+ data.tar.gz: b7878cec3bdd9216ee1eda01fe8276c1753f01de6bd875988415665150b5fb00
5
5
  SHA512:
6
- metadata.gz: d9470c52ca3b689a1dd314501a91f5651d1b11fd26cc5628cb6ccf4bf59aeb93ebdcca141ab12d0ec5e69b0e5aa5e29881c85433d9c6dcdeb5902591c1c62aaa
7
- data.tar.gz: 6caae2c7cf684d10ca54ac8b53b498acb5c8d210135d2f583d254fa3dbdc41b7e50382c43aeb05438713b723d9d5da7d48980569024e36d55683e9e5b041df8a
6
+ metadata.gz: df018d07e090469ff814af057d94ee4c8b9327071bc173f6f10aa106385071bd39076c334e195f163a670c756b3379a38ffcb00b46248a06b80b3d4aa3ed04c1
7
+ data.tar.gz: b6215f30153f034d04bfe6d0006da68cb0beee20ec15c2664283d2c766b5a37b9fddfaec82a0fa94a36233e175e81a492829a7094fda4581dc847573e572f0e9
@@ -29,8 +29,9 @@ module Net
29
29
  # this, see Net::IMAP#authenticate or your client's authentication
30
30
  # method.
31
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.
32
+ # ==== Parameters
33
+ #
34
+ # * _optional_ #anonymous_message — a message to send to the server.
34
35
  #
35
36
  # Any other keyword arguments are silently ignored.
36
37
  def initialize(anon_msg = nil, anonymous_message: nil, **)
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ module SASL
6
+
7
+ # This API is *experimental*, and may change.
8
+ #
9
+ # TODO: catch exceptions in #process and send #cancel_response.
10
+ # TODO: raise an error if the command succeeds after being canceled.
11
+ # TODO: use with more clients, to verify the API can accommodate them.
12
+ #
13
+ # Create an AuthenticationExchange from a client adapter and a mechanism
14
+ # authenticator:
15
+ # def authenticate(mechanism, ...)
16
+ # authenticator = SASL.authenticator(mechanism, ...)
17
+ # SASL::AuthenticationExchange.new(
18
+ # sasl_adapter, mechanism, authenticator
19
+ # ).authenticate
20
+ # end
21
+ #
22
+ # private
23
+ #
24
+ # def sasl_adapter = MyClientAdapter.new(self, &method(:send_command))
25
+ #
26
+ # Or delegate creation of the authenticator to ::build:
27
+ # def authenticate(...)
28
+ # SASL::AuthenticationExchange.build(sasl_adapter, ...)
29
+ # .authenticate
30
+ # end
31
+ #
32
+ # As a convenience, ::authenticate combines ::build and #authenticate:
33
+ # def authenticate(...)
34
+ # SASL::AuthenticationExchange.authenticate(sasl_adapter, ...)
35
+ # end
36
+ #
37
+ # Likewise, ClientAdapter#authenticate delegates to #authenticate:
38
+ # def authenticate(...) = sasl_adapter.authenticate(...)
39
+ #
40
+ class AuthenticationExchange
41
+ # Convenience method for <tt>build(...).authenticate</tt>
42
+ def self.authenticate(...) build(...).authenticate end
43
+
44
+ # Use +registry+ to override the global Authenticators registry.
45
+ def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block)
46
+ authenticator = SASL.authenticator(mechanism, *args, **kwargs, &block)
47
+ new(client, mechanism, authenticator, sasl_ir: sasl_ir)
48
+ end
49
+
50
+ attr_reader :mechanism, :authenticator
51
+
52
+ def initialize(client, mechanism, authenticator, sasl_ir: true)
53
+ @client = client
54
+ @mechanism = -mechanism.to_s.upcase.tr(?_, ?-)
55
+ @authenticator = authenticator
56
+ @sasl_ir = sasl_ir
57
+ @processed = false
58
+ end
59
+
60
+ # Call #authenticate to execute an authentication exchange for #client
61
+ # using #authenticator. Authentication failures will raise an
62
+ # exception. Any exceptions other than those in RESPONSE_ERRORS will
63
+ # drop the connection.
64
+ def authenticate
65
+ client.run_command(mechanism, initial_response) { process _1 }
66
+ .tap { raise AuthenticationIncomplete, _1 unless done? }
67
+ rescue *client.response_errors
68
+ raise # but don't drop the connection
69
+ rescue
70
+ client.drop_connection
71
+ raise
72
+ rescue Exception # rubocop:disable Lint/RescueException
73
+ client.drop_connection!
74
+ raise
75
+ end
76
+
77
+ def send_initial_response?
78
+ @sasl_ir &&
79
+ authenticator.respond_to?(:initial_response?) &&
80
+ authenticator.initial_response? &&
81
+ client.sasl_ir_capable? &&
82
+ client.auth_capable?(mechanism)
83
+ end
84
+
85
+ def done?
86
+ authenticator.respond_to?(:done?) ? authenticator.done? : @processed
87
+ end
88
+
89
+ private
90
+
91
+ attr_reader :client
92
+
93
+ def initial_response
94
+ return unless send_initial_response?
95
+ client.encode_ir authenticator.process nil
96
+ end
97
+
98
+ def process(challenge)
99
+ client.encode authenticator.process client.decode challenge
100
+ ensure
101
+ @processed = true
102
+ end
103
+
104
+ end
105
+ end
106
+ end
107
+ end
@@ -65,7 +65,7 @@ module Net::IMAP::SASL
65
65
  # lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
66
66
  # preserved and non-alphanumeric characters are removed..
67
67
  def add_authenticator(name, authenticator = nil)
68
- key = name.upcase.to_sym
68
+ key = -name.to_s.upcase.tr(?_, ?-)
69
69
  authenticator ||= begin
70
70
  class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym
71
71
  auth_class = nil
@@ -79,10 +79,15 @@ module Net::IMAP::SASL
79
79
 
80
80
  # Removes the authenticator registered for +name+
81
81
  def remove_authenticator(name)
82
- key = name.upcase.to_sym
82
+ key = -name.to_s.upcase.tr(?_, ?-)
83
83
  @authenticators.delete(key)
84
84
  end
85
85
 
86
+ def mechanism?(name)
87
+ key = -name.to_s.upcase.tr(?_, ?-)
88
+ @authenticators.key?(key)
89
+ end
90
+
86
91
  # :call-seq:
87
92
  # authenticator(mechanism, ...) -> auth_session
88
93
  #
@@ -100,8 +105,9 @@ module Net::IMAP::SASL
100
105
  # only. Protocol client users should see refer to their client's
101
106
  # documentation, e.g. Net::IMAP#authenticate.
102
107
  def authenticator(mechanism, ...)
103
- auth = @authenticators.fetch(mechanism.upcase.to_sym) do
104
- raise ArgumentError, 'unknown auth type - "%s"' % mechanism
108
+ key = -mechanism.to_s.upcase.tr(?_, ?-)
109
+ auth = @authenticators.fetch(key) do
110
+ raise ArgumentError, 'unknown auth type - "%s"' % key
105
111
  end
106
112
  auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
107
113
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ module SASL
6
+
7
+ # This API is *experimental*, and may change.
8
+ #
9
+ # TODO: use with more clients, to verify the API can accommodate them.
10
+ #
11
+ # An abstract base class for implementing a SASL authentication exchange.
12
+ # Different clients will each have their own adapter subclass, overridden
13
+ # to match their needs.
14
+ #
15
+ # Although the default implementations _may_ be sufficient, subclasses
16
+ # will probably need to override some methods. Additionally, subclasses
17
+ # may need to include a protocol adapter mixin, if the default
18
+ # ProtocolAdapters::Generic isn't sufficient.
19
+ class ClientAdapter
20
+ include ProtocolAdapters::Generic
21
+
22
+ attr_reader :client, :command_proc
23
+
24
+ # +command_proc+ can used to avoid exposing private methods on #client.
25
+ # It should run a command with the arguments sent to it, yield each
26
+ # continuation payload, respond to the server with the result of each
27
+ # yield, and return the result. Non-successful results *MUST* raise an
28
+ # exception. Exceptions in the block *MUST* cause the command to fail.
29
+ #
30
+ # Subclasses that override #run_command may use #command_proc for
31
+ # other purposes.
32
+ def initialize(client, &command_proc)
33
+ @client, @command_proc = client, command_proc
34
+ end
35
+
36
+ # Delegates to AuthenticationExchange.authenticate.
37
+ def authenticate(...) AuthenticationExchange.authenticate(self, ...) end
38
+
39
+ # Do the protocol and server both support an initial response?
40
+ def sasl_ir_capable?; client.sasl_ir_capable? end
41
+
42
+ # Does the server advertise support for the mechanism?
43
+ def auth_capable?(mechanism); client.auth_capable?(mechanism) end
44
+
45
+ # Runs the authenticate command with +mechanism+ and +initial_response+.
46
+ # When +initial_response+ is nil, an initial response must NOT be sent.
47
+ #
48
+ # Yields each continuation payload, responds to the server with the
49
+ # result of each yield, and returns the result. Non-successful results
50
+ # *MUST* raise an exception. Exceptions in the block *MUST* cause the
51
+ # command to fail.
52
+ #
53
+ # Subclasses that override this may use #command_proc differently.
54
+ def run_command(mechanism, initial_response = nil, &block)
55
+ command_proc or raise Error, "initialize with block or override"
56
+ args = [command_name, mechanism, initial_response].compact
57
+ command_proc.call(*args, &block)
58
+ end
59
+
60
+ # Returns an array of server responses errors raised by run_command.
61
+ # Exceptions in this array won't drop the connection.
62
+ def response_errors; [] end
63
+
64
+ # Drop the connection gracefully.
65
+ def drop_connection; client.drop_connection end
66
+
67
+ # Drop the connection abruptly.
68
+ def drop_connection!; client.drop_connection! end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -14,13 +14,17 @@
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
16
  class Net::IMAP::SASL::CramMD5Authenticator
17
- def initialize(user, password, warn_deprecation: true, **_ignored)
17
+ def initialize(user = nil, pass = nil,
18
+ authcid: nil, username: nil,
19
+ password: nil, secret: nil,
20
+ warn_deprecation: true,
21
+ **)
18
22
  if warn_deprecation
19
23
  warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
20
24
  end
21
25
  require "digest/md5"
22
- @user = user
23
- @password = password
26
+ @user = authcid || username || user
27
+ @password = password || secret || pass
24
28
  @done = false
25
29
  end
26
30
 
@@ -20,8 +20,9 @@ class Net::IMAP::SASL::DigestMD5Authenticator
20
20
  # "Authentication identity" is the generic term used by
21
21
  # RFC-4422[https://tools.ietf.org/html/rfc4422].
22
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.
23
+ # this to +authcid+.
24
24
  attr_reader :username
25
+ alias authcid username
25
26
 
26
27
  # A password or passphrase that matches the #username.
27
28
  #
@@ -44,6 +45,7 @@ class Net::IMAP::SASL::DigestMD5Authenticator
44
45
  # :call-seq:
45
46
  # new(username, password, authzid = nil, **options) -> authenticator
46
47
  # new(username:, password:, authzid: nil, **options) -> authenticator
48
+ # new(authcid:, password:, authzid: nil, **options) -> authenticator
47
49
  #
48
50
  # Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
49
51
  #
@@ -51,17 +53,27 @@ class Net::IMAP::SASL::DigestMD5Authenticator
51
53
  #
52
54
  # ==== Parameters
53
55
  #
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.
56
+ # * #authcid ― Authentication identity that is associated with #password.
58
57
  #
59
- # See the documentation for each attribute for more details.
58
+ # #username An alias for +authcid+.
59
+ #
60
+ # * #password ― A password or passphrase associated with this #authcid.
61
+ #
62
+ # * _optional_ #authzid ― Authorization identity to act as or on behalf of.
63
+ #
64
+ # When +authzid+ is not set, the server should derive the authorization
65
+ # identity from the authentication identity.
66
+ #
67
+ # * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
68
+ #
69
+ # Any other keyword arguments are silently ignored.
60
70
  def initialize(user = nil, pass = nil, authz = nil,
61
71
  username: nil, password: nil, authzid: nil,
72
+ authcid: nil, secret: nil,
62
73
  warn_deprecation: true, **)
63
- username ||= user or raise ArgumentError, "missing username"
64
- password ||= pass or raise ArgumentError, "missing password"
74
+ username = authcid || username || user or
75
+ raise ArgumentError, "missing username (authcid)"
76
+ password ||= secret || pass or raise ArgumentError, "missing password"
65
77
  authzid ||= authz
66
78
  if warn_deprecation
67
79
  warn "WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331."
@@ -12,24 +12,45 @@ module Net
12
12
  # established external to SASL, for example by TLS certificate or IPsec.
13
13
  class ExternalAuthenticator
14
14
 
15
- # Authorization identity: an identity to act as or on behalf of.
15
+ # Authorization identity: an identity to act as or on behalf of. The
16
+ # identity form is application protocol specific. If not provided or
17
+ # left blank, the server derives an authorization identity from the
18
+ # authentication identity. The server is responsible for verifying the
19
+ # client's credentials and verifying that the identity it associates
20
+ # with the client's authentication identity is allowed to act as (or on
21
+ # behalf of) the authorization identity.
22
+ #
23
+ # For example, an administrator or superuser might take on another role:
24
+ #
25
+ # imap.authenticate "PLAIN", "root", passwd, authzid: "user"
16
26
  #
17
- # If not explicitly provided, the server defaults to using the identity
18
- # that was authenticated by the external credentials.
19
27
  attr_reader :authzid
28
+ alias username authzid
20
29
 
21
30
  # :call-seq:
22
31
  # new(authzid: nil, **) -> authenticator
32
+ # new(username: nil, **) -> authenticator
33
+ # new(username = nil, **) -> authenticator
23
34
  #
24
35
  # Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
25
36
  # specified in RFC-4422[https://tools.ietf.org/html/rfc4422]. To use
26
37
  # this, see Net::IMAP#authenticate or your client's authentication
27
38
  # method.
28
39
  #
29
- # #authzid is an optional identity to act as or on behalf of.
40
+ # ==== Parameters
41
+ #
42
+ # * _optional_ #authzid ― Authorization identity to act as or on behalf of.
43
+ #
44
+ # _optional_ #username ― An alias for #authzid.
45
+ #
46
+ # Note that, unlike some other authenticators, +username+ sets the
47
+ # _authorization_ identity and not the _authentication_ identity. The
48
+ # authentication identity is established for the client by the
49
+ # external credentials.
30
50
  #
31
51
  # Any other keyword parameters are quietly ignored.
32
- def initialize(authzid: nil, **)
52
+ def initialize(user = nil, authzid: nil, username: nil, **)
53
+ authzid ||= username || user
33
54
  @authzid = authzid&.to_str&.encode "UTF-8"
34
55
  if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
35
56
  raise ArgumentError, "contains NULL"
@@ -23,12 +23,16 @@ class Net::IMAP::SASL::LoginAuthenticator
23
23
  STATE_DONE = :DONE
24
24
  private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE
25
25
 
26
- def initialize(user, password, warn_deprecation: true, **_ignored)
26
+ def initialize(user = nil, pass = nil,
27
+ authcid: nil, username: nil,
28
+ password: nil, secret: nil,
29
+ warn_deprecation: true,
30
+ **)
27
31
  if warn_deprecation
28
32
  warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
29
33
  end
30
- @user = user
31
- @password = password
34
+ @user = authcid || username || user
35
+ @password = password || secret || pass
32
36
  @state = STATE_USER
33
37
  end
34
38
 
@@ -14,18 +14,25 @@ module Net
14
14
  class OAuthAuthenticator
15
15
  include GS2Header
16
16
 
17
- # Authorization identity: an identity to act as or on behalf of.
17
+ # Authorization identity: an identity to act as or on behalf of. The
18
+ # identity form is application protocol specific. If not provided or
19
+ # left blank, the server derives an authorization identity from the
20
+ # authentication identity. The server is responsible for verifying the
21
+ # client's credentials and verifying that the identity it associates
22
+ # with the client's authentication identity is allowed to act as (or on
23
+ # behalf of) the authorization identity.
24
+ #
25
+ # For example, an administrator or superuser might take on another role:
26
+ #
27
+ # imap.authenticate "PLAIN", "root", passwd, authzid: "user"
18
28
  #
19
- # If no explicit authorization identity is provided, it is usually
20
- # derived from the authentication identity. For the OAuth-based
21
- # mechanisms, the authentication identity is the identity established by
22
- # the OAuth credential.
23
29
  attr_reader :authzid
30
+ alias username authzid
24
31
 
25
- # Hostname to which the client connected.
32
+ # Hostname to which the client connected. (optional)
26
33
  attr_reader :host
27
34
 
28
- # Service port to which the client connected.
35
+ # Service port to which the client connected. (optional)
29
36
  attr_reader :port
30
37
 
31
38
  # HTTP method. (optional)
@@ -39,6 +46,7 @@ module Net
39
46
 
40
47
  # The query string. (optional)
41
48
  attr_reader :qs
49
+ alias query qs
42
50
 
43
51
  # Stores the most recent server "challenge". When authentication fails,
44
52
  # this may hold information about the failure reason, as JSON.
@@ -47,29 +55,42 @@ module Net
47
55
  # Creates an RFC7628[https://tools.ietf.org/html/rfc7628] OAuth
48
56
  # authenticator.
49
57
  #
50
- # === Options
58
+ # ==== Parameters
59
+ #
60
+ # See child classes for required parameter(s). The following parameters
61
+ # are all optional, but it is worth noting that <b>application protocols
62
+ # are allowed to require</b> #authzid (or other parameters, such as
63
+ # #host or #port) <b>as are specific server implementations</b>.
64
+ #
65
+ # * _optional_ #authzid ― Authorization identity to act as or on behalf of.
66
+ #
67
+ # _optional_ #username — An alias for #authzid.
51
68
  #
52
- # See child classes for required configuration parameter(s). The
53
- # following parameters are all optional, but protocols or servers may
54
- # add requirements for #authzid, #host, #port, or any other parameter.
69
+ # Note that, unlike some other authenticators, +username+ sets the
70
+ # _authorization_ identity and not the _authentication_ identity. The
71
+ # authentication identity is established for the client by the OAuth
72
+ # token.
55
73
  #
56
- # * #authzid Identity to act as or on behalf of.
57
- # * #hostHostname to which the client connected.
58
- # * #portService port to which the client connected.
59
- # * #mthd — HTTP method
60
- # * #path — HTTP path data
61
- # * #post — HTTP post data
62
- # * #qs — HTTP query string
74
+ # * _optional_ #host Hostname to which the client connected.
75
+ # * _optional_ #portService port to which the client connected.
76
+ # * _optional_ #mthdHTTP method
77
+ # * _optional_ #path — HTTP path data
78
+ # * _optional_ #post — HTTP post data
79
+ # * _optional_ #qs — HTTP query string
63
80
  #
81
+ # _optional_ #query — An alias for #qs
82
+ #
83
+ # Any other keyword parameters are quietly ignored.
64
84
  def initialize(authzid: nil, host: nil, port: nil,
85
+ username: nil, query: nil,
65
86
  mthd: nil, path: nil, post: nil, qs: nil, **)
66
- @authzid = authzid
87
+ @authzid = authzid || username
67
88
  @host = host
68
89
  @port = port
69
90
  @mthd = mthd
70
91
  @path = path
71
92
  @post = post
72
- @qs = qs
93
+ @qs = qs || query
73
94
  @done = false
74
95
  end
75
96
 
@@ -116,35 +137,49 @@ module Net
116
137
  # the bearer token.
117
138
  class OAuthBearerAuthenticator < OAuthAuthenticator
118
139
 
119
- # An OAuth2 bearer token, generally the access token.
140
+ # An OAuth 2.0 bearer token. See {RFC-6750}[https://www.rfc-editor.org/rfc/rfc6750]
120
141
  attr_reader :oauth2_token
142
+ alias secret oauth2_token
121
143
 
122
144
  # :call-seq:
123
- # new(oauth2_token, **options) -> authenticator
124
- # new(oauth2_token:, **options) -> authenticator
145
+ # new(oauth2_token, **options) -> authenticator
146
+ # new(authzid, oauth2_token, **options) -> authenticator
147
+ # new(oauth2_token:, **options) -> authenticator
125
148
  #
126
149
  # Creates an Authenticator for the "+OAUTHBEARER+" SASL mechanism.
127
150
  #
128
151
  # Called by Net::IMAP#authenticate and similar methods on other clients.
129
152
  #
130
- # === Options
153
+ # ==== Parameters
154
+ #
155
+ # * #oauth2_token — An OAuth2 bearer token
156
+ #
157
+ # All other keyword parameters are passed to
158
+ # {super}[rdoc-ref:OAuthAuthenticator::new] (see OAuthAuthenticator).
159
+ # The most common ones are:
160
+ #
161
+ # * _optional_ #authzid ― Authorization identity to act as or on behalf of.
162
+ #
163
+ # _optional_ #username — An alias for #authzid.
131
164
  #
132
- # Only +oauth2_token+ is required by the mechanism, however protocols
133
- # and servers may add requirements for #authzid, #host, #port, or any
134
- # other parameter.
165
+ # Note that, unlike some other authenticators, +username+ sets the
166
+ # _authorization_ identity and not the _authentication_ identity. The
167
+ # authentication identity is established for the client by
168
+ # #oauth2_token.
135
169
  #
136
- # * #oauth2_tokenAn OAuth2 bearer token or access token. *Required.*
137
- # May be provided as either regular or keyword argument.
138
- # * #authzid ― Identity to act as or on behalf of.
139
- # * #host — Hostname to which the client connected.
140
- # * #port — Service port to which the client connected.
141
- # * See OAuthAuthenticator documentation for less common parameters.
170
+ # * _optional_ #hostHostname to which the client connected.
171
+ # * _optional_ #port Service port to which the client connected.
142
172
  #
143
- def initialize(oauth2_token_arg = nil, oauth2_token: nil, **args, &blk)
144
- super(**args, &blk) # handles authzid, host, port, etc
145
- oauth2_token && oauth2_token_arg and
146
- raise ArgumentError, "conflicting values for oauth2_token"
147
- @oauth2_token = oauth2_token || oauth2_token_arg or
173
+ # Although only oauth2_token is required by this mechanism, it is worth
174
+ # noting that <b><em>application protocols are allowed to
175
+ # require</em></b> #authzid (<em>or other parameters, such as</em> #host
176
+ # _or_ #port) <b><em>as are specific server implementations</em></b>.
177
+ def initialize(arg1 = nil, arg2 = nil,
178
+ oauth2_token: nil, secret: nil,
179
+ **args, &blk)
180
+ username, oauth2_token_arg = arg2.nil? ? [nil, arg1] : [arg1, arg2]
181
+ super(username: username, **args, &blk)
182
+ @oauth2_token = oauth2_token || secret || oauth2_token_arg or
148
183
  raise ArgumentError, "missing oauth2_token"
149
184
  end
150
185
 
@@ -22,9 +22,11 @@ class Net::IMAP::SASL::PlainAuthenticator
22
22
  # RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
23
23
  # this to +authcid+.
24
24
  attr_reader :username
25
+ alias authcid username
25
26
 
26
27
  # A password or passphrase that matches the #username.
27
28
  attr_reader :password
29
+ alias secret password
28
30
 
29
31
  # Authorization identity: an identity to act as or on behalf of. The identity
30
32
  # form is application protocol specific. If not provided or left blank, the
@@ -42,26 +44,32 @@ class Net::IMAP::SASL::PlainAuthenticator
42
44
  # :call-seq:
43
45
  # new(username, password, authzid: nil, **) -> authenticator
44
46
  # new(username:, password:, authzid: nil, **) -> authenticator
47
+ # new(authcid:, password:, authzid: nil, **) -> authenticator
45
48
  #
46
49
  # Creates an Authenticator for the "+PLAIN+" SASL mechanism.
47
50
  #
48
51
  # Called by Net::IMAP#authenticate and similar methods on other clients.
49
52
  #
50
- # === Parameters
53
+ # ==== Parameters
51
54
  #
52
- # * #usernameIdentity whose +password+ is used.
53
- # * #password ― Password or passphrase associated with this username+.
54
- # * #authzid ― Alternate identity to act as or on behalf of. Optional.
55
+ # * #authcidAuthentication identity that is associated with #password.
55
56
  #
56
- # See attribute documentation for more details.
57
+ # #username An alias for #authcid.
58
+ #
59
+ # * #password ― A password or passphrase associated with the #authcid.
60
+ #
61
+ # * _optional_ #authzid ― Authorization identity to act as or on behalf of.
62
+ #
63
+ # When +authzid+ is not set, the server should derive the authorization
64
+ # identity from the authentication identity.
65
+ #
66
+ # Any other keyword parameters are quietly ignored.
57
67
  def initialize(user = nil, pass = nil,
68
+ authcid: nil, secret: nil,
58
69
  username: nil, password: nil, authzid: nil, **)
59
- [username, user].compact.count == 1 or
60
- raise ArgumentError, "conflicting values for username"
61
- [password, pass].compact.count == 1 or
62
- raise ArgumentError, "conflicting values for password"
63
- username ||= user or raise ArgumentError, "missing username"
64
- password ||= pass or raise ArgumentError, "missing password"
70
+ username ||= authcid || user or
71
+ raise ArgumentError, "missing username (authcid)"
72
+ password ||= secret || pass or raise ArgumentError, "missing password"
65
73
  raise ArgumentError, "username contains NULL" if username.include?(NULL)
66
74
  raise ArgumentError, "password contains NULL" if password.include?(NULL)
67
75
  raise ArgumentError, "authzid contains NULL" if authzid&.include?(NULL)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ module SASL
6
+
7
+ module ProtocolAdapters
8
+ # This API is experimental, and may change.
9
+ module Generic
10
+ def command_name; "AUTHENTICATE" end
11
+ def service; raise "Implement in subclass or module" end
12
+ def host; client.host end
13
+ def port; client.port end
14
+ def encode_ir(string) string.empty? ? "=" : encode(string) end
15
+ def encode(string) [string].pack("m0") end
16
+ def decode(string) string.unpack1("m0") end
17
+ def cancel_response; "*" end
18
+ end
19
+
20
+ # See RFC-3501 (IMAP4rev1), RFC-4959 (SASL-IR capability),
21
+ # and RFC-9051 (IMAP4rev2).
22
+ module IMAP
23
+ include Generic
24
+ def service; "imap" end
25
+ end
26
+
27
+ # See RFC-4954 (AUTH capability).
28
+ module SMTP
29
+ include Generic
30
+ def command_name; "AUTH" end
31
+ def service; "smtp" end
32
+ end
33
+
34
+ # See RFC-5034 (SASL capability).
35
+ module POP
36
+ include Generic
37
+ def command_name; "AUTH" end
38
+ def service; "pop" end
39
+ end
40
+
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -60,6 +60,7 @@ module Net
60
60
  # :call-seq:
61
61
  # new(username, password, **options) -> auth_ctx
62
62
  # new(username:, password:, **options) -> auth_ctx
63
+ # new(authcid:, password:, **options) -> auth_ctx
63
64
  #
64
65
  # Creates an authenticator for one of the "+SCRAM-*+" SASL mechanisms.
65
66
  # Each subclass defines #digest to match a specific mechanism.
@@ -68,39 +69,47 @@ module Net
68
69
  #
69
70
  # === Parameters
70
71
  #
71
- # * #username ― Identity whose #password is used. Aliased as #authcid.
72
+ # * #authcid ― Identity whose #password is used.
73
+ #
74
+ # #username - An alias for #authcid.
72
75
  # * #password ― Password or passphrase associated with this #username.
73
- # * #authzid ― Alternate identity to act as or on behalf of. Optional.
74
- # * #min_iterations - Overrides the default value (4096). Optional.
76
+ # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
77
+ # * _optional_ #min_iterations - Overrides the default value (4096).
75
78
  #
76
- # See the documentation on the corresponding attributes for more.
79
+ # Any other keyword parameters are quietly ignored.
77
80
  def initialize(username_arg = nil, password_arg = nil,
78
- username: nil, password: nil, authcid: nil, authzid: nil,
81
+ authcid: nil, username: nil,
82
+ authzid: nil,
83
+ password: nil, secret: nil,
79
84
  min_iterations: 4096, # see both RFC5802 and RFC7677
80
85
  cnonce: nil, # must only be set in tests
81
86
  **options)
82
87
  @username = username || username_arg || authcid or
83
88
  raise ArgumentError, "missing username (authcid)"
84
- [username, username_arg, authcid].compact.count == 1 or
85
- raise ArgumentError, "conflicting values for username (authcid)"
86
- @password = password || password_arg or
89
+ @password = password || secret || password_arg or
87
90
  raise ArgumentError, "missing password"
88
- [password, password_arg].compact.count == 1 or
89
- raise ArgumentError, "conflicting values for password"
90
91
  @authzid = authzid
91
92
 
92
93
  @min_iterations = Integer min_iterations
93
94
  @min_iterations.positive? or
94
95
  raise ArgumentError, "min_iterations must be positive"
96
+
95
97
  @cnonce = cnonce || SecureRandom.base64(32)
96
98
  end
97
99
 
98
100
  # Authentication identity: the identity that matches the #password.
101
+ #
102
+ # RFC-2831[https://tools.ietf.org/html/rfc2831] uses the term +username+.
103
+ # "Authentication identity" is the generic term used by
104
+ # RFC-4422[https://tools.ietf.org/html/rfc4422].
105
+ # RFC-4616[https://tools.ietf.org/html/rfc4616] and many later RFCs abbreviate
106
+ # this to +authcid+.
99
107
  attr_reader :username
100
108
  alias authcid username
101
109
 
102
110
  # A password or passphrase that matches the #username.
103
111
  attr_reader :password
112
+ alias secret password
104
113
 
105
114
  # Authorization identity: an identity to act as or on behalf of. The
106
115
  # identity form is application protocol specific. If not provided or
@@ -6,9 +6,10 @@
6
6
  # Google[https://developers.google.com/gmail/imap/xoauth2-protocol] and
7
7
  # Microsoft[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth].
8
8
  #
9
- # This mechanism requires an OAuth2 +access_token+ which has been authorized
10
- # with the appropriate OAuth2 scopes to access IMAP. These scopes are not
11
- # standardized---consult each email service provider's documentation.
9
+ # This mechanism requires an OAuth2 access token which has been authorized
10
+ # with the appropriate OAuth2 scopes to access the user's services. Most of
11
+ # these scopes are not standardized---consult each service provider's
12
+ # documentation for their scopes.
12
13
  #
13
14
  # Although this mechanism was never standardized and has been obsoleted by
14
15
  # "+OAUTHBEARER+", it is still very widely supported.
@@ -19,21 +20,34 @@ class Net::IMAP::SASL::XOAuth2Authenticator
19
20
  # It is unclear from {Google's original XOAUTH2
20
21
  # documentation}[https://developers.google.com/gmail/imap/xoauth2-protocol],
21
22
  # whether "User" refers to the authentication identity (+authcid+) or the
22
- # authorization identity (+authzid+). It appears to behave as +authzid+.
23
+ # authorization identity (+authzid+). The authentication identity is
24
+ # established for the client by the OAuth token, so it seems that +username+
25
+ # must be the authorization identity.
23
26
  #
24
27
  # {Microsoft's documentation for shared
25
28
  # mailboxes}[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#sasl-xoauth2-authentication-for-shared-mailboxes-in-office-365]
26
- # clearly indicate that the Office 365 server interprets it as the
29
+ # _clearly_ indicates that the Office 365 server interprets it as the
27
30
  # authorization identity.
31
+ #
32
+ # Although they _should_ validate that the token has been authorized to access
33
+ # the service for +username+, _some_ servers appear to ignore this field,
34
+ # relying only the identity and scope authorized by the token.
28
35
  attr_reader :username
29
36
 
37
+ # Note that, unlike most other authenticators, #username is an alias for the
38
+ # authorization identity and not the authentication identity. The
39
+ # authenticated identity is established for the client by the #oauth2_token.
40
+ alias authzid username
41
+
30
42
  # An OAuth2 access token which has been authorized with the appropriate OAuth2
31
43
  # scopes to use the service for #username.
32
44
  attr_reader :oauth2_token
45
+ alias secret oauth2_token
33
46
 
34
47
  # :call-seq:
35
48
  # new(username, oauth2_token, **) -> authenticator
36
49
  # new(username:, oauth2_token:, **) -> authenticator
50
+ # new(authzid:, oauth2_token:, **) -> authenticator
37
51
  #
38
52
  # Creates an Authenticator for the "+XOAUTH2+" SASL mechanism, as specified by
39
53
  # Google[https://developers.google.com/gmail/imap/xoauth2-protocol],
@@ -43,26 +57,30 @@ class Net::IMAP::SASL::XOAuth2Authenticator
43
57
  # === Properties
44
58
  #
45
59
  # * #username --- the username for the account being accessed.
60
+ #
61
+ # #authzid --- an alias for #username.
62
+ #
63
+ # Note that, unlike some other authenticators, +username+ sets the
64
+ # _authorization_ identity and not the _authentication_ identity. The
65
+ # authenticated identity is established for the client with the OAuth token.
66
+ #
46
67
  # * #oauth2_token --- An OAuth2.0 access token which is authorized to access
47
68
  # the service for #username.
48
69
  #
49
- # See the documentation for each attribute for more details.
50
- def initialize(user = nil, token = nil, username: nil, oauth2_token: nil, **)
51
- @username = username || user or
52
- raise ArgumentError, "missing username"
53
- @oauth2_token = oauth2_token || token or
70
+ # Any other keyword parameters are quietly ignored.
71
+ def initialize(user = nil, token = nil, username: nil, oauth2_token: nil,
72
+ authzid: nil, secret: nil, **)
73
+ @username = authzid || username || user or
74
+ raise ArgumentError, "missing username (authzid)"
75
+ @oauth2_token = oauth2_token || secret || token or
54
76
  raise ArgumentError, "missing oauth2_token"
55
- [username, user].compact.count == 1 or
56
- raise ArgumentError, "conflicting values for username"
57
- [oauth2_token, token].compact.count == 1 or
58
- raise ArgumentError, "conflicting values for oauth2_token"
59
77
  @done = false
60
78
  end
61
79
 
62
80
  # :call-seq:
63
81
  # initial_response? -> true
64
82
  #
65
- # +PLAIN+ can send an initial client response.
83
+ # +XOAUTH2+ can send an initial client response.
66
84
  def initial_response?; true end
67
85
 
68
86
  # Returns the XOAUTH2 formatted response, which combines the +username+
data/lib/net/imap/sasl.rb CHANGED
@@ -135,6 +135,10 @@ module Net
135
135
  autoload :BidiStringError, sasl_stringprep_rb
136
136
 
137
137
  sasl_dir = File.expand_path("sasl", __dir__)
138
+ autoload :AuthenticationExchange, "#{sasl_dir}/authentication_exchange"
139
+ autoload :ClientAdapter, "#{sasl_dir}/client_adapter"
140
+ autoload :ProtocolAdapters, "#{sasl_dir}/protocol_adapters"
141
+
138
142
  autoload :Authenticators, "#{sasl_dir}/authenticators"
139
143
  autoload :GS2Header, "#{sasl_dir}/gs2_header"
140
144
  autoload :ScramAlgorithm, "#{sasl_dir}/scram_algorithm"
@@ -155,8 +159,10 @@ module Net
155
159
  # Returns the default global SASL::Authenticators instance.
156
160
  def self.authenticators; @authenticators ||= Authenticators.new end
157
161
 
158
- # Delegates to ::authenticators. See Authenticators#authenticator.
159
- def self.authenticator(...) authenticators.authenticator(...) end
162
+ # Delegates to <tt>registry.new</tt> See Authenticators#new.
163
+ def self.authenticator(*args, registry: authenticators, **kwargs, &block)
164
+ registry.new(*args, **kwargs, &block)
165
+ end
160
166
 
161
167
  # Delegates to ::authenticators. See Authenticators#add_authenticator.
162
168
  def self.add_authenticator(...) authenticators.add_authenticator(...) end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+
6
+ # Experimental
7
+ class SASLAdapter < SASL::ClientAdapter
8
+ include SASL::ProtocolAdapters::IMAP
9
+
10
+ RESPONSE_ERRORS = [NoResponseError, BadResponseError, ByeResponseError]
11
+ .freeze
12
+
13
+ def response_errors; RESPONSE_ERRORS end
14
+ def sasl_ir_capable?; client.capable?("SASL-IR") end
15
+ def auth_capable?(mechanism); client.auth_capable?(mechanism) end
16
+ def drop_connection; client.logout! end
17
+ def drop_connection!; client.disconnect end
18
+ end
19
+
20
+ end
21
+ end
data/lib/net/imap.rb CHANGED
@@ -127,7 +127,7 @@ module Net
127
127
  # end
128
128
  #
129
129
  # # Support for "UTF8=ACCEPT" implies support for "ENABLE"
130
- # imap.enable :utf8 if imap.auth_capable?("UTF8=ACCEPT")
130
+ # imap.enable :utf8 if imap.capable?("UTF8=ACCEPT")
131
131
  #
132
132
  # namespaces = imap.namespace if imap.capable?(:namespace)
133
133
  # mbox_prefix = namespaces&.personal&.first&.prefix || ""
@@ -662,7 +662,7 @@ module Net
662
662
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
663
663
  #
664
664
  class IMAP < Protocol
665
- VERSION = "0.4.0"
665
+ VERSION = "0.4.2"
666
666
 
667
667
  # Aliases for supported capabilities, to be used with the #enable command.
668
668
  ENABLE_ALIASES = {
@@ -670,8 +670,9 @@ module Net
670
670
  "UTF8=ONLY" => "UTF8=ACCEPT",
671
671
  }.freeze
672
672
 
673
- autoload :SASL, File.expand_path("imap/sasl", __dir__)
674
- autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)
673
+ autoload :SASL, File.expand_path("imap/sasl", __dir__)
674
+ autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__)
675
+ autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)
675
676
 
676
677
  include MonitorMixin
677
678
  if defined?(OpenSSL::SSL)
@@ -1142,7 +1143,10 @@ module Net
1142
1143
  end
1143
1144
 
1144
1145
  # :call-seq:
1145
- # authenticate(mechanism, *, sasl_ir: true, **, &) -> ok_resp
1146
+ # authenticate(mechanism, *,
1147
+ # sasl_ir: true,
1148
+ # registry: Net::IMAP::SASL.authenticators,
1149
+ # **, &) -> ok_resp
1146
1150
  #
1147
1151
  # Sends an {AUTHENTICATE command [IMAP4rev1 §6.2.2]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.2]
1148
1152
  # to authenticate the client. If successful, the connection enters the
@@ -1886,8 +1890,7 @@ module Net
1886
1890
  # +attr+ is a list of attributes to fetch; see the documentation
1887
1891
  # for FetchData for a list of valid attributes.
1888
1892
  #
1889
- # The return value is an array of FetchData or nil
1890
- # (instead of an empty array) if there is no matching message.
1893
+ # The return value is an array of FetchData.
1891
1894
  #
1892
1895
  # Related: #uid_search, FetchData
1893
1896
  #
@@ -2747,6 +2750,10 @@ module Net
2747
2750
  end
2748
2751
  end
2749
2752
 
2753
+ def sasl_adapter
2754
+ SASLAdapter.new(self, &method(:send_command_with_continuations))
2755
+ end
2756
+
2750
2757
  #--
2751
2758
  # We could get the saslprep method by extending the SASLprep module
2752
2759
  # directly. It's done indirectly, so SASLprep can be lazily autoloaded,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2023-10-04 00:00:00.000000000 Z
12
+ date: 2023-10-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: net-protocol
@@ -100,7 +100,9 @@ files:
100
100
  - lib/net/imap/response_parser/parser_utils.rb
101
101
  - lib/net/imap/sasl.rb
102
102
  - lib/net/imap/sasl/anonymous_authenticator.rb
103
+ - lib/net/imap/sasl/authentication_exchange.rb
103
104
  - lib/net/imap/sasl/authenticators.rb
105
+ - lib/net/imap/sasl/client_adapter.rb
104
106
  - lib/net/imap/sasl/cram_md5_authenticator.rb
105
107
  - lib/net/imap/sasl/digest_md5_authenticator.rb
106
108
  - lib/net/imap/sasl/external_authenticator.rb
@@ -108,10 +110,12 @@ files:
108
110
  - lib/net/imap/sasl/login_authenticator.rb
109
111
  - lib/net/imap/sasl/oauthbearer_authenticator.rb
110
112
  - lib/net/imap/sasl/plain_authenticator.rb
113
+ - lib/net/imap/sasl/protocol_adapters.rb
111
114
  - lib/net/imap/sasl/scram_algorithm.rb
112
115
  - lib/net/imap/sasl/scram_authenticator.rb
113
116
  - lib/net/imap/sasl/stringprep.rb
114
117
  - lib/net/imap/sasl/xoauth2_authenticator.rb
118
+ - lib/net/imap/sasl_adapter.rb
115
119
  - lib/net/imap/stringprep.rb
116
120
  - lib/net/imap/stringprep/nameprep.rb
117
121
  - lib/net/imap/stringprep/saslprep.rb