net-imap 0.3.7 → 0.5.6

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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/BSDL +22 -0
  3. data/COPYING +56 -0
  4. data/Gemfile +14 -0
  5. data/LICENSE.txt +3 -22
  6. data/README.md +25 -8
  7. data/Rakefile +0 -7
  8. data/docs/styles.css +72 -23
  9. data/lib/net/imap/authenticators.rb +26 -57
  10. data/lib/net/imap/command_data.rb +74 -54
  11. data/lib/net/imap/config/attr_accessors.rb +75 -0
  12. data/lib/net/imap/config/attr_inheritance.rb +90 -0
  13. data/lib/net/imap/config/attr_type_coercion.rb +61 -0
  14. data/lib/net/imap/config.rb +470 -0
  15. data/lib/net/imap/data_encoding.rb +18 -6
  16. data/lib/net/imap/data_lite.rb +226 -0
  17. data/lib/net/imap/deprecated_client_options.rb +142 -0
  18. data/lib/net/imap/errors.rb +27 -1
  19. data/lib/net/imap/esearch_result.rb +180 -0
  20. data/lib/net/imap/fetch_data.rb +597 -0
  21. data/lib/net/imap/flags.rb +1 -1
  22. data/lib/net/imap/response_data.rb +250 -440
  23. data/lib/net/imap/response_parser/parser_utils.rb +245 -0
  24. data/lib/net/imap/response_parser.rb +1867 -1184
  25. data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
  26. data/lib/net/imap/sasl/authentication_exchange.rb +139 -0
  27. data/lib/net/imap/sasl/authenticators.rb +122 -0
  28. data/lib/net/imap/sasl/client_adapter.rb +123 -0
  29. data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +24 -14
  30. data/lib/net/imap/sasl/digest_md5_authenticator.rb +342 -0
  31. data/lib/net/imap/sasl/external_authenticator.rb +83 -0
  32. data/lib/net/imap/sasl/gs2_header.rb +80 -0
  33. data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +28 -18
  34. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
  35. data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
  36. data/lib/net/imap/sasl/protocol_adapters.rb +101 -0
  37. data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
  38. data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
  39. data/lib/net/imap/sasl/stringprep.rb +6 -66
  40. data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
  41. data/lib/net/imap/sasl.rb +148 -44
  42. data/lib/net/imap/sasl_adapter.rb +20 -0
  43. data/lib/net/imap/search_result.rb +146 -0
  44. data/lib/net/imap/sequence_set.rb +1565 -0
  45. data/lib/net/imap/stringprep/nameprep.rb +70 -0
  46. data/lib/net/imap/stringprep/saslprep.rb +69 -0
  47. data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
  48. data/lib/net/imap/stringprep/tables.rb +146 -0
  49. data/lib/net/imap/stringprep/trace.rb +85 -0
  50. data/lib/net/imap/stringprep.rb +159 -0
  51. data/lib/net/imap/uidplus_data.rb +244 -0
  52. data/lib/net/imap/vanished_data.rb +56 -0
  53. data/lib/net/imap.rb +2090 -823
  54. data/net-imap.gemspec +7 -8
  55. data/rakelib/benchmarks.rake +91 -0
  56. data/rakelib/rfcs.rake +2 -0
  57. data/rakelib/saslprep.rake +4 -4
  58. data/rakelib/string_prep_tables_generator.rb +84 -60
  59. data/sample/net-imap.rb +167 -0
  60. metadata +45 -49
  61. data/.github/dependabot.yml +0 -6
  62. data/.github/workflows/test.yml +0 -38
  63. data/.gitignore +0 -10
  64. data/benchmarks/stringprep.yml +0 -65
  65. data/benchmarks/table-regexps.yml +0 -39
  66. data/lib/net/imap/authenticators/digest_md5.rb +0 -115
  67. data/lib/net/imap/authenticators/plain.rb +0 -41
  68. data/lib/net/imap/authenticators/xoauth2.rb +0 -20
  69. data/lib/net/imap/sasl/saslprep.rb +0 -55
  70. data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
  71. data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "securerandom"
5
+
6
+ require_relative "gs2_header"
7
+ require_relative "scram_algorithm"
8
+
9
+ module Net
10
+ class IMAP
11
+ module SASL
12
+
13
+ # Abstract base class for the "+SCRAM-*+" family of SASL mechanisms,
14
+ # defined in RFC5802[https://www.rfc-editor.org/rfc/rfc5802]. Use via
15
+ # Net::IMAP#authenticate.
16
+ #
17
+ # Directly supported:
18
+ # * +SCRAM-SHA-1+ --- ScramSHA1Authenticator
19
+ # * +SCRAM-SHA-256+ --- ScramSHA256Authenticator
20
+ #
21
+ # New +SCRAM-*+ mechanisms can easily be added for any hash algorithm
22
+ # supported by
23
+ # OpenSSL::Digest[https://ruby.github.io/openssl/OpenSSL/Digest.html].
24
+ # Subclasses need only set an appropriate +DIGEST_NAME+ constant.
25
+ #
26
+ # === SCRAM algorithm
27
+ #
28
+ # See the documentation and method definitions on ScramAlgorithm for an
29
+ # overview of the algorithm. The different mechanisms differ only by
30
+ # which hash function that is used (or by support for channel binding with
31
+ # +-PLUS+).
32
+ #
33
+ # See also the methods on GS2Header.
34
+ #
35
+ # ==== Server messages
36
+ #
37
+ # As server messages are received, they are validated and loaded into
38
+ # the various attributes, e.g: #snonce, #salt, #iterations, #verifier,
39
+ # #server_error, etc.
40
+ #
41
+ # Unlike many other SASL mechanisms, the +SCRAM-*+ family supports mutual
42
+ # authentication and can return server error data in the server messages.
43
+ # If #process raises an Error for the server-final-message, then
44
+ # server_error may contain error details.
45
+ #
46
+ # === TLS Channel binding
47
+ #
48
+ # <em>The <tt>SCRAM-*-PLUS</tt> mechanisms and channel binding are not
49
+ # supported yet.</em>
50
+ #
51
+ # === Caching SCRAM secrets
52
+ #
53
+ # <em>Caching of salted_password, client_key, stored_key, and server_key
54
+ # is not supported yet.</em>
55
+ #
56
+ class ScramAuthenticator
57
+ include GS2Header
58
+ include ScramAlgorithm
59
+
60
+ # :call-seq:
61
+ # new(username, password, **options) -> auth_ctx
62
+ # new(username:, password:, **options) -> auth_ctx
63
+ # new(authcid:, password:, **options) -> auth_ctx
64
+ #
65
+ # Creates an authenticator for one of the "+SCRAM-*+" SASL mechanisms.
66
+ # Each subclass defines #digest to match a specific mechanism.
67
+ #
68
+ # Called by Net::IMAP#authenticate and similar methods on other clients.
69
+ #
70
+ # === Parameters
71
+ #
72
+ # * #authcid ― Identity whose #password is used.
73
+ #
74
+ # #username - An alias for #authcid.
75
+ # * #password ― Password or passphrase associated with this #username.
76
+ # * _optional_ #authzid ― Alternate identity to act as or on behalf of.
77
+ # * _optional_ #min_iterations - Overrides the default value (4096).
78
+ #
79
+ # Any other keyword parameters are quietly ignored.
80
+ def initialize(username_arg = nil, password_arg = nil,
81
+ authcid: nil, username: nil,
82
+ authzid: nil,
83
+ password: nil, secret: nil,
84
+ min_iterations: 4096, # see both RFC5802 and RFC7677
85
+ cnonce: nil, # must only be set in tests
86
+ **options)
87
+ @username = username || username_arg || authcid or
88
+ raise ArgumentError, "missing username (authcid)"
89
+ @password = password || secret || password_arg or
90
+ raise ArgumentError, "missing password"
91
+ @authzid = authzid
92
+
93
+ @min_iterations = Integer min_iterations
94
+ @min_iterations.positive? or
95
+ raise ArgumentError, "min_iterations must be positive"
96
+
97
+ @cnonce = cnonce || SecureRandom.base64(32)
98
+ end
99
+
100
+ # Authentication identity: the identity that matches the #password.
101
+ #
102
+ # RFC-2831[https://www.rfc-editor.org/rfc/rfc2831] uses the term
103
+ # +username+. "Authentication identity" is the generic term used by
104
+ # RFC-4422[https://www.rfc-editor.org/rfc/rfc4422].
105
+ # RFC-4616[https://www.rfc-editor.org/rfc/rfc4616] and many later RFCs
106
+ # abbreviate this to +authcid+.
107
+ attr_reader :username
108
+ alias authcid username
109
+
110
+ # A password or passphrase that matches the #username.
111
+ attr_reader :password
112
+ alias secret password
113
+
114
+ # Authorization identity: an identity to act as or on behalf of. The
115
+ # identity form is application protocol specific. If not provided or
116
+ # left blank, the server derives an authorization identity from the
117
+ # authentication identity. For example, an administrator or superuser
118
+ # might take on another role:
119
+ #
120
+ # imap.authenticate "SCRAM-SHA-256", "root", passwd, authzid: "user"
121
+ #
122
+ # The server is responsible for verifying the client's credentials and
123
+ # verifying that the identity it associates with the client's
124
+ # authentication identity is allowed to act as (or on behalf of) the
125
+ # authorization identity.
126
+ attr_reader :authzid
127
+
128
+ # The minimal allowed iteration count. Lower #iterations will raise an
129
+ # Error.
130
+ attr_reader :min_iterations
131
+
132
+ # The client nonce, generated by SecureRandom
133
+ attr_reader :cnonce
134
+
135
+ # The server nonce, which must start with #cnonce
136
+ attr_reader :snonce
137
+
138
+ # The salt used by the server for this user
139
+ attr_reader :salt
140
+
141
+ # The iteration count for the selected hash function and user
142
+ attr_reader :iterations
143
+
144
+ # An error reported by the server during the \SASL exchange.
145
+ #
146
+ # Does not include errors reported by the protocol, e.g.
147
+ # Net::IMAP::NoResponseError.
148
+ attr_reader :server_error
149
+
150
+ # Returns a new OpenSSL::Digest object, set to the appropriate hash
151
+ # function for the chosen mechanism.
152
+ #
153
+ # <em>The class's +DIGEST_NAME+ constant must be set to the name of an
154
+ # algorithm supported by OpenSSL::Digest.</em>
155
+ def digest; OpenSSL::Digest.new self.class::DIGEST_NAME end
156
+
157
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
158
+ # +client-first-message+.
159
+ def initial_client_response
160
+ "#{gs2_header}#{client_first_message_bare}"
161
+ end
162
+
163
+ # responds to the server's challenges
164
+ def process(challenge)
165
+ case (@state ||= :initial_client_response)
166
+ when :initial_client_response
167
+ initial_client_response.tap { @state = :server_first_message }
168
+ when :server_first_message
169
+ recv_server_first_message challenge
170
+ final_message_with_proof.tap { @state = :server_final_message }
171
+ when :server_final_message
172
+ recv_server_final_message challenge
173
+ "".tap { @state = :done }
174
+ else
175
+ raise Error, "server sent after complete, %p" % [challenge]
176
+ end
177
+ rescue Exception => ex
178
+ @state = ex
179
+ raise
180
+ end
181
+
182
+ # Is the authentication exchange complete?
183
+ #
184
+ # If false, another server continuation is required.
185
+ def done?; @state == :done end
186
+
187
+ private
188
+
189
+ # Need to store this for auth_message
190
+ attr_reader :server_first_message
191
+
192
+ def format_message(hash) hash.map { _1.join("=") }.join(",") end
193
+
194
+ def recv_server_first_message(server_first_message)
195
+ @server_first_message = server_first_message
196
+ sparams = parse_challenge server_first_message
197
+ @snonce = sparams["r"] or
198
+ raise Error, "server did not send nonce"
199
+ @salt = sparams["s"]&.unpack1("m") or
200
+ raise Error, "server did not send salt"
201
+ @iterations = sparams["i"]&.then {|i| Integer i } or
202
+ raise Error, "server did not send iteration count"
203
+ min_iterations <= iterations or
204
+ raise Error, "too few iterations: %d" % [iterations]
205
+ mext = sparams["m"] and
206
+ raise Error, "mandatory extension: %p" % [mext]
207
+ snonce.start_with? cnonce or
208
+ raise Error, "invalid server nonce"
209
+ end
210
+
211
+ def recv_server_final_message(server_final_message)
212
+ sparams = parse_challenge server_final_message
213
+ @server_error = sparams["e"] and
214
+ raise Error, "server error: %s" % [server_error]
215
+ verifier = sparams["v"].unpack1("m") or
216
+ raise Error, "server did not send verifier"
217
+ verifier == server_signature or
218
+ raise Error, "server verify failed: %p != %p" % [
219
+ server_signature, verifier
220
+ ]
221
+ end
222
+
223
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
224
+ # +client-first-message-bare+.
225
+ def client_first_message_bare
226
+ @client_first_message_bare ||=
227
+ format_message(n: gs2_saslname_encode(SASL.saslprep(username)),
228
+ r: cnonce)
229
+ end
230
+
231
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
232
+ # +client-final-message+.
233
+ def final_message_with_proof
234
+ proof = [client_proof].pack("m0")
235
+ "#{client_final_message_without_proof},p=#{proof}"
236
+ end
237
+
238
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
239
+ # +client-final-message-without-proof+.
240
+ def client_final_message_without_proof
241
+ @client_final_message_without_proof ||=
242
+ format_message(c: [cbind_input].pack("m0"), # channel-binding
243
+ r: snonce) # nonce
244
+ end
245
+
246
+ # See {RFC5802 §7}[https://www.rfc-editor.org/rfc/rfc5802#section-7]
247
+ # +cbind-input+.
248
+ #
249
+ # >>>
250
+ # *TODO:* implement channel binding, appending +cbind-data+ here.
251
+ alias cbind_input gs2_header
252
+
253
+ # RFC5802 specifies "that the order of attributes in client or server
254
+ # messages is fixed, with the exception of extension attributes", but
255
+ # this parses it simply as a hash, without respect to order. Note that
256
+ # repeated keys (violating the spec) will use the last value.
257
+ def parse_challenge(challenge)
258
+ challenge.split(/,/).to_h {|pair| pair.split(/=/, 2) }
259
+ rescue ArgumentError
260
+ raise Error, "unparsable challenge: %p" % [challenge]
261
+ end
262
+
263
+ end
264
+
265
+ # Authenticator for the "+SCRAM-SHA-1+" SASL mechanism, defined in
266
+ # RFC5802[https://www.rfc-editor.org/rfc/rfc5802].
267
+ #
268
+ # Uses the "SHA-1" digest algorithm from OpenSSL::Digest.
269
+ #
270
+ # See ScramAuthenticator.
271
+ class ScramSHA1Authenticator < ScramAuthenticator
272
+ DIGEST_NAME = "SHA1"
273
+ end
274
+
275
+ # Authenticator for the "+SCRAM-SHA-256+" SASL mechanism, defined in
276
+ # RFC7677[https://www.rfc-editor.org/rfc/rfc7677].
277
+ #
278
+ # Uses the "SHA-256" digest algorithm from OpenSSL::Digest.
279
+ #
280
+ # See ScramAuthenticator.
281
+ class ScramSHA256Authenticator < ScramAuthenticator
282
+ DIGEST_NAME = "SHA256"
283
+ end
284
+
285
+ end
286
+ end
287
+ end
@@ -1,72 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "stringprep_tables"
4
-
5
3
  module Net::IMAP::SASL
6
4
 
7
- # Regexps and utility methods for implementing stringprep profiles. The
8
- # \StringPrep algorithm is defined by
9
- # {RFC-3454}[https://www.rfc-editor.org/rfc/rfc3454.html]. Each
10
- # codepoint table defined in the RFC-3454 appendices is matched by a Regexp
11
- # defined in this module.
12
- #--
13
- # TODO: generic StringPrep mapping (not needed for SASLprep implementation)
14
- #++
15
- module StringPrep
16
-
17
- # Returns a Regexp matching the given +table+ name.
18
- def self.[](table)
19
- TABLE_REGEXPS.fetch(table)
20
- end
21
-
22
- module_function
23
-
24
- # Checks +string+ for any codepoint in +tables+. Raises a
25
- # ProhibitedCodepoint describing the first matching table.
26
- #
27
- # Also checks bidirectional characters, when <tt>bidi: true</tt>, which may
28
- # raise a BidiStringError.
29
- #
30
- # +profile+ is an optional string which will be added to any exception that
31
- # is raised (it does not affect behavior).
32
- def check_prohibited!(string, *tables, bidi: false, profile: nil)
33
- tables = TABLE_TITLES.keys.grep(/^C/) if tables.empty?
34
- tables |= %w[C.8] if bidi
35
- table = tables.find {|t| TABLE_REGEXPS[t].match?(string) }
36
- raise ProhibitedCodepoint.new(
37
- table, string: string, profile: nil
38
- ) if table
39
- check_bidi!(string, profile: profile) if bidi
40
- end
41
-
42
- # Checks that +string+ obeys all of the "Bidirectional Characters"
43
- # requirements in RFC-3454, §6:
44
- #
45
- # * The characters in \StringPrep\[\"C.8\"] MUST be prohibited
46
- # * If a string contains any RandALCat character, the string MUST NOT
47
- # contain any LCat character.
48
- # * If a string contains any RandALCat character, a RandALCat
49
- # character MUST be the first character of the string, and a
50
- # RandALCat character MUST be the last character of the string.
51
- #
52
- # This is usually combined with #check_prohibited!, so table "C.8" is only
53
- # checked when <tt>c_8: true</tt>.
54
- #
55
- # Raises either ProhibitedCodepoint or BidiStringError unless all
56
- # requirements are met. +profile+ is an optional string which will be
57
- # added to any exception that is raised (it does not affect behavior).
58
- def check_bidi!(string, c_8: false, profile: nil)
59
- check_prohibited!(string, "C.8", profile: profile) if c_8
60
- if BIDI_FAILS_REQ2.match?(string)
61
- raise BidiStringError.new(
62
- BIDI_DESC_REQ2, string: string, profile: profile,
63
- )
64
- elsif BIDI_FAILS_REQ3.match?(string)
65
- raise BidiStringError.new(
66
- BIDI_DESC_REQ3, string: string, profile: profile,
67
- )
68
- end
69
- end
5
+ # Alias for Net::IMAP::StringPrep::SASLprep.
6
+ SASLprep = Net::IMAP::StringPrep::SASLprep
7
+ StringPrep = Net::IMAP::StringPrep # :nodoc:
8
+ BidiStringError = Net::IMAP::StringPrep::BidiStringError # :nodoc:
9
+ ProhibitedCodepoint = Net::IMAP::StringPrep::ProhibitedCodepoint # :nodoc:
10
+ StringPrepError = Net::IMAP::StringPrep::StringPrepError # :nodoc:
70
11
 
71
- end
72
12
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Authenticator for the "+XOAUTH2+" SASL mechanism. This mechanism was
4
+ # originally created for GMail and widely adopted by hosted email providers.
5
+ # +XOAUTH2+ has been documented by
6
+ # Google[https://developers.google.com/gmail/imap/xoauth2-protocol] and
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
+ #
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.
13
+ #
14
+ # Although this mechanism was never standardized and has been obsoleted by
15
+ # "+OAUTHBEARER+", it is still very widely supported.
16
+ #
17
+ # See Net::IMAP::SASL::OAuthBearerAuthenticator.
18
+ class Net::IMAP::SASL::XOAuth2Authenticator
19
+
20
+ # It is unclear from {Google's original XOAUTH2
21
+ # documentation}[https://developers.google.com/gmail/imap/xoauth2-protocol],
22
+ # whether "User" refers to the authentication identity (+authcid+) or the
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.
26
+ #
27
+ # {Microsoft's documentation for shared
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]
29
+ # _clearly_ indicates that the Office 365 server interprets it as the
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.
35
+ attr_reader :username
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
+
42
+ # An OAuth2 access token which has been authorized with the appropriate OAuth2
43
+ # scopes to use the service for #username.
44
+ attr_reader :oauth2_token
45
+ alias secret oauth2_token
46
+
47
+ # :call-seq:
48
+ # new(username, oauth2_token, **) -> authenticator
49
+ # new(username:, oauth2_token:, **) -> authenticator
50
+ # new(authzid:, oauth2_token:, **) -> authenticator
51
+ #
52
+ # Creates an Authenticator for the "+XOAUTH2+" SASL mechanism, as specified by
53
+ # Google[https://developers.google.com/gmail/imap/xoauth2-protocol],
54
+ # Microsoft[https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth]
55
+ # and Yahoo[https://senders.yahooinc.com/developer/documentation].
56
+ #
57
+ # === Properties
58
+ #
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
+ #
67
+ # * #oauth2_token --- An OAuth2.0 access token which is authorized to access
68
+ # the service for #username.
69
+ #
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
76
+ raise ArgumentError, "missing oauth2_token"
77
+ @done = false
78
+ end
79
+
80
+ # :call-seq:
81
+ # initial_response? -> true
82
+ #
83
+ # +XOAUTH2+ can send an initial client response.
84
+ def initial_response?; true end
85
+
86
+ # Returns the XOAUTH2 formatted response, which combines the +username+
87
+ # with the +oauth2_token+.
88
+ def process(_data)
89
+ build_oauth2_string(@username, @oauth2_token)
90
+ ensure
91
+ @done = true
92
+ end
93
+
94
+ # Returns true when the initial client response was sent.
95
+ #
96
+ # The authentication should not succeed unless this returns true, but it
97
+ # does *not* indicate success.
98
+ def done?; @done end
99
+
100
+ private
101
+
102
+ def build_oauth2_string(username, oauth2_token)
103
+ format("user=%s\1auth=Bearer %s\1\1", username, oauth2_token)
104
+ end
105
+
106
+ end