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,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Net::IMAP authenticator for the +DIGEST-MD5+ SASL mechanism type, specified
4
+ # in RFC-2831[https://www.rfc-editor.org/rfc/rfc2831]. See Net::IMAP#authenticate.
5
+ #
6
+ # == Deprecated
7
+ #
8
+ # "+DIGEST-MD5+" has been deprecated by
9
+ # RFC-6331[https://www.rfc-editor.org/rfc/rfc6331] and should not be relied on for
10
+ # security. It is included for compatibility with existing servers.
11
+ class Net::IMAP::SASL::DigestMD5Authenticator
12
+ DataFormatError = Net::IMAP::DataFormatError
13
+ ResponseParseError = Net::IMAP::ResponseParseError
14
+ private_constant :DataFormatError, :ResponseParseError
15
+
16
+ STAGE_ONE = :stage_one
17
+ STAGE_TWO = :stage_two
18
+ STAGE_DONE = :stage_done
19
+ private_constant :STAGE_ONE, :STAGE_TWO, :STAGE_DONE
20
+
21
+ # Directives which must not have multiples. The RFC states:
22
+ # >>>
23
+ # This directive may appear at most once; if multiple instances are present,
24
+ # the client should abort the authentication exchange.
25
+ NO_MULTIPLES = %w[nonce stale maxbuf charset algorithm].freeze
26
+
27
+ # Required directives which must occur exactly once. The RFC states: >>>
28
+ # This directive is required and MUST appear exactly once; if not present,
29
+ # or if multiple instances are present, the client should abort the
30
+ # authentication exchange.
31
+ REQUIRED = %w[nonce algorithm].freeze
32
+
33
+ # Directives which are composed of one or more comma delimited tokens
34
+ QUOTED_LISTABLE = %w[qop cipher].freeze
35
+
36
+ private_constant :NO_MULTIPLES, :REQUIRED, :QUOTED_LISTABLE
37
+
38
+ # Authentication identity: the identity that matches the #password.
39
+ #
40
+ # RFC-2831[https://www.rfc-editor.org/rfc/rfc2831] uses the term +username+.
41
+ # "Authentication identity" is the generic term used by
42
+ # RFC-4422[https://www.rfc-editor.org/rfc/rfc4422].
43
+ # RFC-4616[https://www.rfc-editor.org/rfc/rfc4616] and many later RFCs abbreviate
44
+ # this to +authcid+.
45
+ attr_reader :username
46
+ alias authcid username
47
+
48
+ # A password or passphrase that matches the #username.
49
+ #
50
+ # The +password+ will be used to create the response digest.
51
+ attr_reader :password
52
+
53
+ # Authorization identity: an identity to act as or on behalf of. The identity
54
+ # form is application protocol specific. If not provided or left blank, the
55
+ # server derives an authorization identity from the authentication identity.
56
+ # The server is responsible for verifying the client's credentials and
57
+ # verifying that the identity it associates with the client's authentication
58
+ # identity is allowed to act as (or on behalf of) the authorization identity.
59
+ #
60
+ # For example, an administrator or superuser might take on another role:
61
+ #
62
+ # imap.authenticate "DIGEST-MD5", "root", ->{passwd}, authzid: "user"
63
+ #
64
+ attr_reader :authzid
65
+
66
+ # A namespace or collection of identities which contains +username+.
67
+ #
68
+ # Used by DIGEST-MD5, GSS-API, and NTLM. This is often a domain name that
69
+ # contains the name of the host performing the authentication.
70
+ #
71
+ # <em>Defaults to the last realm in the server-provided list of
72
+ # realms.</em>
73
+ attr_reader :realm
74
+
75
+ # Fully qualified canonical DNS host name for the requested service.
76
+ #
77
+ # <em>Defaults to #realm.</em>
78
+ attr_reader :host
79
+
80
+ # The service protocol, a
81
+ # {registered GSSAPI service name}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml],
82
+ # e.g. "imap", "ldap", or "xmpp".
83
+ #
84
+ # For Net::IMAP, the default is "imap" and should not be overridden. This
85
+ # must be set appropriately to use authenticators in other protocols.
86
+ #
87
+ # If an IANA-registered name isn't available, GSS-API
88
+ # (RFC-2743[https://www.rfc-editor.org/rfc/rfc2743]) allows the generic name
89
+ # "host".
90
+ attr_reader :service
91
+
92
+ # The generic server name when the server is replicated.
93
+ #
94
+ # +service_name+ will be ignored when it is +nil+ or identical to +host+.
95
+ #
96
+ # From RFC-2831[https://www.rfc-editor.org/rfc/rfc2831]:
97
+ # >>>
98
+ # The service is considered to be replicated if the client's
99
+ # service-location process involves resolution using standard DNS lookup
100
+ # operations, and if these operations involve DNS records (such as SRV, or
101
+ # MX) which resolve one DNS name into a set of other DNS names. In this
102
+ # case, the initial name used by the client is the "serv-name", and the
103
+ # final name is the "host" component.
104
+ attr_reader :service_name
105
+
106
+ # Parameters sent by the server are stored in this hash.
107
+ attr_reader :sparams
108
+
109
+ # The charset sent by the server. "UTF-8" (case insensitive) is the only
110
+ # allowed value. +nil+ should be interpreted as ISO 8859-1.
111
+ attr_reader :charset
112
+
113
+ # nonce sent by the server
114
+ attr_reader :nonce
115
+
116
+ # qop-options sent by the server
117
+ attr_reader :qop
118
+
119
+ # :call-seq:
120
+ # new(username, password, authzid = nil, **options) -> authenticator
121
+ # new(username:, password:, authzid: nil, **options) -> authenticator
122
+ # new(authcid:, password:, authzid: nil, **options) -> authenticator
123
+ #
124
+ # Creates an Authenticator for the "+DIGEST-MD5+" SASL mechanism.
125
+ #
126
+ # Called by Net::IMAP#authenticate and similar methods on other clients.
127
+ #
128
+ # ==== Parameters
129
+ #
130
+ # * #authcid ― Authentication identity that is associated with #password.
131
+ #
132
+ # #username ― An alias for +authcid+.
133
+ #
134
+ # * #password ― A password or passphrase associated with this #authcid.
135
+ #
136
+ # * _optional_ #authzid ― Authorization identity to act as or on behalf of.
137
+ #
138
+ # When +authzid+ is not set, the server should derive the authorization
139
+ # identity from the authentication identity.
140
+ #
141
+ # * _optional_ #realm — A namespace for the #username, e.g. a domain.
142
+ # <em>Defaults to the last realm in the server-provided realms list.</em>
143
+ # * _optional_ #host — FQDN for requested service.
144
+ # <em>Defaults to</em> #realm.
145
+ # * _optional_ #service_name — The generic host name when the server is
146
+ # replicated.
147
+ # * _optional_ #service — the registered service protocol. E.g. "imap",
148
+ # "smtp", "ldap", "xmpp".
149
+ # <em>For Net::IMAP, this defaults to "imap".</em>
150
+ #
151
+ # * _optional_ +warn_deprecation+ — Set to +false+ to silence the warning.
152
+ #
153
+ # Any other keyword arguments are silently ignored.
154
+ def initialize(user = nil, pass = nil, authz = nil,
155
+ username: nil, password: nil, authzid: nil,
156
+ authcid: nil, secret: nil,
157
+ realm: nil, service: "imap", host: nil, service_name: nil,
158
+ warn_deprecation: true, **)
159
+ username = authcid || username || user or
160
+ raise ArgumentError, "missing username (authcid)"
161
+ password ||= secret || pass or raise ArgumentError, "missing password"
162
+ authzid ||= authz
163
+ if warn_deprecation
164
+ warn("WARNING: DIGEST-MD5 SASL mechanism was deprecated by RFC6331.",
165
+ category: :deprecated)
166
+ end
167
+
168
+ require "digest/md5"
169
+ require "securerandom"
170
+ require "strscan"
171
+ @username, @password, @authzid = username, password, authzid
172
+ @realm = realm
173
+ @host = host
174
+ @service = service
175
+ @service_name = service_name
176
+ @nc, @stage = {}, STAGE_ONE
177
+ end
178
+
179
+ # From RFC-2831[https://www.rfc-editor.org/rfc/rfc2831]:
180
+ # >>>
181
+ # Indicates the principal name of the service with which the client wishes
182
+ # to connect, formed from the serv-type, host, and serv-name. For
183
+ # example, the FTP service on "ftp.example.com" would have a "digest-uri"
184
+ # value of "ftp/ftp.example.com"; the SMTP server from the example above
185
+ # would have a "digest-uri" value of "smtp/mail3.example.com/example.com".
186
+ def digest_uri
187
+ if service_name && service_name != host
188
+ "#{service}/#{host}/#{service_name}"
189
+ else
190
+ "#{service}/#{host}"
191
+ end
192
+ end
193
+
194
+ def initial_response?; false end
195
+
196
+ # Responds to server challenge in two stages.
197
+ def process(challenge)
198
+ case @stage
199
+ when STAGE_ONE
200
+ @stage = STAGE_TWO
201
+ @sparams = parse_challenge(challenge)
202
+ @qop = sparams.key?("qop") ? ["auth"] : sparams["qop"].flatten
203
+ @nonce = sparams["nonce"] &.first
204
+ @charset = sparams["charset"]&.first
205
+ @realm ||= sparams["realm"] &.last
206
+ @host ||= realm
207
+
208
+ if !qop.include?("auth")
209
+ raise DataFormatError, "Server does not support auth (qop = %p)" % [
210
+ sparams["qop"]
211
+ ]
212
+ elsif (emptykey = REQUIRED.find { sparams[_1].empty? })
213
+ raise DataFormatError, "Server didn't send %s (%p)" % [emptykey, challenge]
214
+ elsif (multikey = NO_MULTIPLES.find { sparams[_1].length > 1 })
215
+ raise DataFormatError, "Server sent multiple %s (%p)" % [multikey, challenge]
216
+ end
217
+
218
+ response = {
219
+ nonce: nonce,
220
+ username: username,
221
+ realm: realm,
222
+ cnonce: SecureRandom.base64(32),
223
+ "digest-uri": digest_uri,
224
+ qop: "auth",
225
+ maxbuf: 65535,
226
+ nc: "%08d" % nc(nonce),
227
+ charset: charset,
228
+ }
229
+
230
+ response[:authzid] = @authzid unless @authzid.nil?
231
+
232
+ response[:response] = response_value(response)
233
+ format_response(response)
234
+ when STAGE_TWO
235
+ @stage = STAGE_DONE
236
+ raise ResponseParseError, challenge unless challenge =~ /rspauth=/
237
+ "" # if at the second stage, return an empty string
238
+ else
239
+ raise ResponseParseError, challenge
240
+ end
241
+ rescue => error
242
+ @stage = error
243
+ raise
244
+ end
245
+
246
+ def done?; @stage == STAGE_DONE end
247
+
248
+ private
249
+
250
+ LWS = /[\r\n \t]*/n # less strict than RFC, more strict than '\s'
251
+ TOKEN = /[^\x00-\x20\x7f()<>@,;:\\"\/\[\]?={}]+/n
252
+ QUOTED_STR = /"(?: [\t\x20-\x7e&&[^"]] | \\[\x00-\x7f] )*"/nx
253
+ LIST_DELIM = /(?:#{LWS} , )+ #{LWS}/nx
254
+ AUTH_PARAM = /
255
+ (#{TOKEN}) #{LWS} = #{LWS} (#{QUOTED_STR} | #{TOKEN}) #{LIST_DELIM}?
256
+ /nx
257
+ private_constant :LWS, :TOKEN, :QUOTED_STR, :LIST_DELIM, :AUTH_PARAM
258
+
259
+ def parse_challenge(challenge)
260
+ sparams = Hash.new {|h, k| h[k] = [] }
261
+ c = StringScanner.new(challenge)
262
+ c.skip LIST_DELIM
263
+ while c.scan AUTH_PARAM
264
+ k, v = c[1], c[2]
265
+ k = k.downcase
266
+ if v =~ /\A"(.*)"\z/mn
267
+ v = $1.gsub(/\\(.)/mn, '\1')
268
+ v = split_quoted_list(v, challenge) if QUOTED_LISTABLE.include? k
269
+ end
270
+ sparams[k] << v
271
+ end
272
+ if !c.eos?
273
+ raise DataFormatError, "Unparsable challenge: %p" % [challenge]
274
+ elsif sparams.empty?
275
+ raise DataFormatError, "Empty challenge: %p" % [challenge]
276
+ end
277
+ sparams
278
+ end
279
+
280
+ def split_quoted_list(value, challenge)
281
+ value.split(LIST_DELIM).reject(&:empty?).tap do
282
+ _1.any? or raise DataFormatError, "Bad Challenge: %p" % [challenge]
283
+ end
284
+ end
285
+
286
+ def nc(nonce)
287
+ if @nc.has_key? nonce
288
+ @nc[nonce] = @nc[nonce] + 1
289
+ else
290
+ @nc[nonce] = 1
291
+ end
292
+ end
293
+
294
+ def response_value(response)
295
+ a1 = compute_a1(response)
296
+ a2 = compute_a2(response)
297
+ Digest::MD5.hexdigest(
298
+ [
299
+ Digest::MD5.hexdigest(a1),
300
+ response.values_at(:nonce, :nc, :cnonce, :qop),
301
+ Digest::MD5.hexdigest(a2)
302
+ ].join(":")
303
+ )
304
+ end
305
+
306
+ def compute_a0(response)
307
+ Digest::MD5.digest(
308
+ [ response.values_at(:username, :realm), password ].join(":")
309
+ )
310
+ end
311
+
312
+ def compute_a1(response)
313
+ a0 = compute_a0(response)
314
+ a1 = [ a0, response.values_at(:nonce, :cnonce) ].join(":")
315
+ a1 << ":#{response[:authzid]}" unless response[:authzid].nil?
316
+ a1
317
+ end
318
+
319
+ def compute_a2(response)
320
+ a2 = "AUTHENTICATE:#{response[:"digest-uri"]}"
321
+ if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/
322
+ a2 << ":00000000000000000000000000000000"
323
+ end
324
+ a2
325
+ end
326
+
327
+ def format_response(response)
328
+ response.map {|k, v| qdval(k.to_s, v) }.join(",")
329
+ end
330
+
331
+ # some responses need quoting
332
+ def qdval(k, v)
333
+ return if k.nil? or v.nil?
334
+ if %w"username authzid realm nonce cnonce digest-uri qop".include? k
335
+ v = v.gsub(/([\\"])/, "\\\1")
336
+ return '%s="%s"' % [k, v]
337
+ else
338
+ return '%s=%s' % [k, v]
339
+ end
340
+ end
341
+
342
+ end
@@ -0,0 +1,83 @@
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://www.rfc-editor.org/rfc/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. 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"
26
+ #
27
+ attr_reader :authzid
28
+ alias username authzid
29
+
30
+ # :call-seq:
31
+ # new(authzid: nil, **) -> authenticator
32
+ # new(username: nil, **) -> authenticator
33
+ # new(username = nil, **) -> authenticator
34
+ #
35
+ # Creates an Authenticator for the "+EXTERNAL+" SASL mechanism, as
36
+ # specified in RFC-4422[https://www.rfc-editor.org/rfc/rfc4422]. To use
37
+ # this, see Net::IMAP#authenticate or your client's authentication
38
+ # method.
39
+ #
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.
50
+ #
51
+ # Any other keyword parameters are quietly ignored.
52
+ def initialize(user = nil, authzid: nil, username: nil, **)
53
+ authzid ||= username || user
54
+ @authzid = authzid&.to_str&.encode "UTF-8"
55
+ if @authzid&.match?(/\u0000/u) # also validates UTF8 encoding
56
+ raise ArgumentError, "contains NULL"
57
+ end
58
+ @done = false
59
+ end
60
+
61
+ # :call-seq:
62
+ # initial_response? -> true
63
+ #
64
+ # +EXTERNAL+ can send an initial client response.
65
+ def initial_response?; true end
66
+
67
+ # Returns #authzid, or an empty string if there is no authzid.
68
+ def process(_)
69
+ authzid || ""
70
+ ensure
71
+ @done = true
72
+ end
73
+
74
+ # Returns true when the initial client response was sent.
75
+ #
76
+ # The authentication should not succeed unless this returns true, but it
77
+ # does *not* indicate success.
78
+ def done?; @done end
79
+
80
+ end
81
+ end
82
+ end
83
+ 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://www.rfc-editor.org/rfc/rfc5801],
9
+ # several different mechanisms start with a GS2 header:
10
+ # * +GS2-*+ --- RFC5801[https://www.rfc-editor.org/rfc/rfc5801]
11
+ # * +SCRAM-*+ --- RFC5802[https://www.rfc-editor.org/rfc/rfc5802]
12
+ # (ScramAuthenticator)
13
+ # * +SAML20+ --- RFC6595[https://www.rfc-editor.org/rfc/rfc6595]
14
+ # * +OPENID20+ --- RFC6616[https://www.rfc-editor.org/rfc/rfc6616]
15
+ # * +OAUTH10A+ --- RFC7628[https://www.rfc-editor.org/rfc/rfc7628]
16
+ # * +OAUTHBEARER+ --- RFC7628[https://www.rfc-editor.org/rfc/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
@@ -3,9 +3,9 @@
3
3
  # Authenticator for the "+LOGIN+" SASL mechanism. See Net::IMAP#authenticate.
4
4
  #
5
5
  # +LOGIN+ authentication sends the password in cleartext.
6
- # RFC3501[https://tools.ietf.org/html/rfc3501] encourages servers to disable
6
+ # RFC3501[https://www.rfc-editor.org/rfc/rfc3501] encourages servers to disable
7
7
  # cleartext authentication until after TLS has been negotiated.
8
- # RFC8314[https://tools.ietf.org/html/rfc8314] recommends TLS version 1.2 or
8
+ # RFC8314[https://www.rfc-editor.org/rfc/rfc8314] recommends TLS version 1.2 or
9
9
  # greater be used for all traffic, and deprecate cleartext access ASAP. +LOGIN+
10
10
  # can be secured by TLS encryption.
11
11
  #
@@ -17,30 +17,40 @@
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
20
+ class Net::IMAP::SASL::LoginAuthenticator
21
+ STATE_USER = :USER
22
+ STATE_PASSWORD = :PASSWORD
23
+ STATE_DONE = :DONE
24
+ private_constant :STATE_USER, :STATE_PASSWORD, :STATE_DONE
25
+
26
+ def initialize(user = nil, pass = nil,
27
+ authcid: nil, username: nil,
28
+ password: nil, secret: nil,
29
+ warn_deprecation: true,
30
+ **)
31
+ if warn_deprecation
32
+ warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead.",
33
+ category: :deprecated
34
+ end
35
+ @user = authcid || username || user
36
+ @password = password || secret || pass
37
+ @state = STATE_USER
38
+ end
39
+
40
+ def initial_response?; false end
41
+
21
42
  def process(data)
22
43
  case @state
23
44
  when STATE_USER
24
45
  @state = STATE_PASSWORD
25
46
  return @user
26
47
  when STATE_PASSWORD
48
+ @state = STATE_DONE
27
49
  return @password
50
+ when STATE_DONE
51
+ raise ResponseParseError, data
28
52
  end
29
53
  end
30
54
 
31
- private
32
-
33
- STATE_USER = :USER
34
- STATE_PASSWORD = :PASSWORD
35
-
36
- def initialize(user, password, warn_deprecation: true, **_ignored)
37
- if warn_deprecation
38
- warn "WARNING: LOGIN SASL mechanism is deprecated. Use PLAIN instead."
39
- end
40
- @user = user
41
- @password = password
42
- @state = STATE_USER
43
- end
44
-
45
- Net::IMAP.add_authenticator "LOGIN", self
55
+ def done?; @state == STATE_DONE end
46
56
  end