net-imap 0.3.4 → 0.5.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/BSDL +22 -0
- data/COPYING +56 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +3 -22
- data/README.md +25 -8
- data/Rakefile +0 -7
- data/docs/styles.css +72 -23
- data/lib/net/imap/authenticators.rb +26 -57
- data/lib/net/imap/command_data.rb +74 -54
- data/lib/net/imap/config/attr_accessors.rb +75 -0
- data/lib/net/imap/config/attr_inheritance.rb +90 -0
- data/lib/net/imap/config/attr_type_coercion.rb +61 -0
- data/lib/net/imap/config.rb +470 -0
- data/lib/net/imap/data_encoding.rb +21 -9
- data/lib/net/imap/data_lite.rb +226 -0
- data/lib/net/imap/deprecated_client_options.rb +142 -0
- data/lib/net/imap/errors.rb +27 -1
- data/lib/net/imap/esearch_result.rb +180 -0
- data/lib/net/imap/fetch_data.rb +597 -0
- data/lib/net/imap/flags.rb +1 -1
- data/lib/net/imap/response_data.rb +250 -440
- data/lib/net/imap/response_parser/parser_utils.rb +245 -0
- data/lib/net/imap/response_parser.rb +1867 -1184
- data/lib/net/imap/sasl/anonymous_authenticator.rb +69 -0
- data/lib/net/imap/sasl/authentication_exchange.rb +139 -0
- data/lib/net/imap/sasl/authenticators.rb +122 -0
- data/lib/net/imap/sasl/client_adapter.rb +123 -0
- data/lib/net/imap/{authenticators/cram_md5.rb → sasl/cram_md5_authenticator.rb} +24 -14
- data/lib/net/imap/sasl/digest_md5_authenticator.rb +342 -0
- data/lib/net/imap/sasl/external_authenticator.rb +83 -0
- data/lib/net/imap/sasl/gs2_header.rb +80 -0
- data/lib/net/imap/{authenticators/login.rb → sasl/login_authenticator.rb} +28 -18
- data/lib/net/imap/sasl/oauthbearer_authenticator.rb +199 -0
- data/lib/net/imap/sasl/plain_authenticator.rb +101 -0
- data/lib/net/imap/sasl/protocol_adapters.rb +101 -0
- data/lib/net/imap/sasl/scram_algorithm.rb +58 -0
- data/lib/net/imap/sasl/scram_authenticator.rb +287 -0
- data/lib/net/imap/sasl/stringprep.rb +6 -66
- data/lib/net/imap/sasl/xoauth2_authenticator.rb +106 -0
- data/lib/net/imap/sasl.rb +148 -44
- data/lib/net/imap/sasl_adapter.rb +20 -0
- data/lib/net/imap/search_result.rb +146 -0
- data/lib/net/imap/sequence_set.rb +1565 -0
- data/lib/net/imap/stringprep/nameprep.rb +70 -0
- data/lib/net/imap/stringprep/saslprep.rb +69 -0
- data/lib/net/imap/stringprep/saslprep_tables.rb +96 -0
- data/lib/net/imap/stringprep/tables.rb +146 -0
- data/lib/net/imap/stringprep/trace.rb +85 -0
- data/lib/net/imap/stringprep.rb +159 -0
- data/lib/net/imap/uidplus_data.rb +244 -0
- data/lib/net/imap/vanished_data.rb +56 -0
- data/lib/net/imap.rb +2090 -823
- data/net-imap.gemspec +7 -8
- data/rakelib/benchmarks.rake +91 -0
- data/rakelib/rfcs.rake +2 -0
- data/rakelib/saslprep.rake +4 -4
- data/rakelib/string_prep_tables_generator.rb +84 -60
- data/sample/net-imap.rb +167 -0
- metadata +45 -49
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/test.yml +0 -31
- data/.gitignore +0 -10
- data/benchmarks/stringprep.yml +0 -65
- data/benchmarks/table-regexps.yml +0 -39
- data/lib/net/imap/authenticators/digest_md5.rb +0 -115
- data/lib/net/imap/authenticators/plain.rb +0 -41
- data/lib/net/imap/authenticators/xoauth2.rb +0 -20
- data/lib/net/imap/sasl/saslprep.rb +0 -55
- data/lib/net/imap/sasl/saslprep_tables.rb +0 -98
- data/lib/net/imap/sasl/stringprep_tables.rb +0 -153
@@ -0,0 +1,69 @@
|
|
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://www.rfc-editor.org/rfc/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://www.rfc-editor.org/rfc/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://www.rfc-editor.org/rfc/rfc4505]. To use
|
29
|
+
# this, see Net::IMAP#authenticate or your client's authentication
|
30
|
+
# method.
|
31
|
+
#
|
32
|
+
# ==== Parameters
|
33
|
+
#
|
34
|
+
# * _optional_ #anonymous_message — a message to send to the server.
|
35
|
+
#
|
36
|
+
# Any other keyword arguments are silently ignored.
|
37
|
+
def initialize(anon_msg = nil, anonymous_message: nil, **)
|
38
|
+
message = (anonymous_message || anon_msg || "").to_str
|
39
|
+
@anonymous_message = StringPrep::Trace.stringprep_trace message
|
40
|
+
if (size = @anonymous_message&.length)&.> 255
|
41
|
+
raise ArgumentError,
|
42
|
+
"anonymous_message is too long. (%d codepoints)" % [size]
|
43
|
+
end
|
44
|
+
@done = false
|
45
|
+
end
|
46
|
+
|
47
|
+
# :call-seq:
|
48
|
+
# initial_response? -> true
|
49
|
+
#
|
50
|
+
# +ANONYMOUS+ can send an initial client response.
|
51
|
+
def initial_response?; true end
|
52
|
+
|
53
|
+
# Returns #anonymous_message.
|
54
|
+
def process(_server_challenge_string)
|
55
|
+
anonymous_message
|
56
|
+
ensure
|
57
|
+
@done = true
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns true when the initial client response was sent.
|
61
|
+
#
|
62
|
+
# The authentication should not succeed unless this returns true, but it
|
63
|
+
# does *not* indicate success.
|
64
|
+
def done?; @done end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP
|
5
|
+
module SASL
|
6
|
+
|
7
|
+
# AuthenticationExchange is used internally by Net::IMAP#authenticate.
|
8
|
+
# But the API is still *experimental*, and may change.
|
9
|
+
#
|
10
|
+
# TODO: catch exceptions in #process and send #cancel_response.
|
11
|
+
# TODO: raise an error if the command succeeds after being canceled.
|
12
|
+
# TODO: use with more clients, to verify the API can accommodate them.
|
13
|
+
# TODO: pass ClientAdapter#service to SASL.authenticator
|
14
|
+
#
|
15
|
+
# An AuthenticationExchange represents a single attempt to authenticate
|
16
|
+
# a SASL client to a SASL server. It is created from a client adapter, a
|
17
|
+
# mechanism name, and a mechanism authenticator. When #authenticate is
|
18
|
+
# called, it will send the appropriate authenticate command to the server,
|
19
|
+
# returning the client response on success and raising an exception on
|
20
|
+
# failure.
|
21
|
+
#
|
22
|
+
# In most cases, the client will not need to use
|
23
|
+
# SASL::AuthenticationExchange directly at all. Instead, use
|
24
|
+
# SASL::ClientAdapter#authenticate. If customizations are needed, the
|
25
|
+
# custom client adapter is probably the best place for that code.
|
26
|
+
#
|
27
|
+
# def authenticate(...)
|
28
|
+
# MyClient::SASLAdapter.new(self).authenticate(...)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# SASL::ClientAdapter#authenticate delegates to ::authenticate, like so:
|
32
|
+
#
|
33
|
+
# def authenticate(...)
|
34
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
35
|
+
# SASL::AuthenticationExchange.authenticate(sasl_adapter, ...)
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# ::authenticate simply delegates to ::build and #authenticate, like so:
|
39
|
+
#
|
40
|
+
# def authenticate(...)
|
41
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
42
|
+
# SASL::AuthenticationExchange
|
43
|
+
# .build(sasl_adapter, ...)
|
44
|
+
# .authenticate
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# And ::build delegates to SASL.authenticator and ::new, like so:
|
48
|
+
#
|
49
|
+
# def authenticate(mechanism, ...)
|
50
|
+
# sasl_adapter = MyClient::SASLAdapter.new(self)
|
51
|
+
# authenticator = SASL.authenticator(mechanism, ...)
|
52
|
+
# SASL::AuthenticationExchange
|
53
|
+
# .new(sasl_adapter, mechanism, authenticator)
|
54
|
+
# .authenticate
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
class AuthenticationExchange
|
58
|
+
# Convenience method for <tt>build(...).authenticate</tt>
|
59
|
+
#
|
60
|
+
# See also: SASL::ClientAdapter#authenticate
|
61
|
+
def self.authenticate(...) build(...).authenticate end
|
62
|
+
|
63
|
+
# Convenience method to combine the creation of a new authenticator and
|
64
|
+
# a new Authentication exchange.
|
65
|
+
#
|
66
|
+
# +client+ must be an instance of SASL::ClientAdapter.
|
67
|
+
#
|
68
|
+
# +mechanism+ must be a SASL mechanism name, as a string or symbol.
|
69
|
+
#
|
70
|
+
# +sasl_ir+ allows or disallows sending an "initial response", depending
|
71
|
+
# also on whether the server capabilities, mechanism authenticator, and
|
72
|
+
# client adapter all support it. Defaults to +true+.
|
73
|
+
#
|
74
|
+
# +mechanism+, +args+, +kwargs+, and +block+ are all forwarded to
|
75
|
+
# SASL.authenticator. Use the +registry+ kwarg to override the global
|
76
|
+
# SASL::Authenticators registry.
|
77
|
+
def self.build(client, mechanism, *args, sasl_ir: true, **kwargs, &block)
|
78
|
+
authenticator = SASL.authenticator(mechanism, *args, **kwargs, &block)
|
79
|
+
new(client, mechanism, authenticator, sasl_ir: sasl_ir)
|
80
|
+
end
|
81
|
+
|
82
|
+
attr_reader :mechanism, :authenticator
|
83
|
+
|
84
|
+
def initialize(client, mechanism, authenticator, sasl_ir: true)
|
85
|
+
@client = client
|
86
|
+
@mechanism = Authenticators.normalize_name(mechanism)
|
87
|
+
@authenticator = authenticator
|
88
|
+
@sasl_ir = sasl_ir
|
89
|
+
@processed = false
|
90
|
+
end
|
91
|
+
|
92
|
+
# Call #authenticate to execute an authentication exchange for #client
|
93
|
+
# using #authenticator. Authentication failures will raise an
|
94
|
+
# exception. Any exceptions other than those in RESPONSE_ERRORS will
|
95
|
+
# drop the connection.
|
96
|
+
def authenticate
|
97
|
+
client.run_command(mechanism, initial_response) { process _1 }
|
98
|
+
.tap { raise AuthenticationIncomplete, _1 unless done? }
|
99
|
+
rescue *client.response_errors
|
100
|
+
raise # but don't drop the connection
|
101
|
+
rescue
|
102
|
+
client.drop_connection
|
103
|
+
raise
|
104
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
105
|
+
client.drop_connection!
|
106
|
+
raise
|
107
|
+
end
|
108
|
+
|
109
|
+
def send_initial_response?
|
110
|
+
@sasl_ir &&
|
111
|
+
authenticator.respond_to?(:initial_response?) &&
|
112
|
+
authenticator.initial_response? &&
|
113
|
+
client.sasl_ir_capable? &&
|
114
|
+
client.auth_capable?(mechanism)
|
115
|
+
end
|
116
|
+
|
117
|
+
def done?
|
118
|
+
authenticator.respond_to?(:done?) ? authenticator.done? : @processed
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
attr_reader :client
|
124
|
+
|
125
|
+
def initial_response
|
126
|
+
return unless send_initial_response?
|
127
|
+
client.encode_ir authenticator.process nil
|
128
|
+
end
|
129
|
+
|
130
|
+
def process(challenge)
|
131
|
+
client.encode authenticator.process client.decode challenge
|
132
|
+
ensure
|
133
|
+
@processed = true
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,122 @@
|
|
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
|
+
# Normalize the mechanism name as an uppercase string, with underscores
|
25
|
+
# converted to dashes.
|
26
|
+
def self.normalize_name(mechanism) -(mechanism.to_s.upcase.tr(?_, ?-)) end
|
27
|
+
|
28
|
+
# Create a new Authenticators registry.
|
29
|
+
#
|
30
|
+
# This class is usually not instantiated directly. Use SASL.authenticators
|
31
|
+
# to reuse the default global registry.
|
32
|
+
#
|
33
|
+
# When +use_defaults+ is +false+, the registry will start empty. When
|
34
|
+
# +use_deprecated+ is +false+, deprecated authenticators will not be
|
35
|
+
# included with the defaults.
|
36
|
+
def initialize(use_defaults: true, use_deprecated: true)
|
37
|
+
@authenticators = {}
|
38
|
+
return unless use_defaults
|
39
|
+
add_authenticator "Anonymous"
|
40
|
+
add_authenticator "External"
|
41
|
+
add_authenticator "OAuthBearer"
|
42
|
+
add_authenticator "Plain"
|
43
|
+
add_authenticator "Scram-SHA-1"
|
44
|
+
add_authenticator "Scram-SHA-256"
|
45
|
+
add_authenticator "XOAuth2"
|
46
|
+
return unless use_deprecated
|
47
|
+
add_authenticator "Login" # deprecated
|
48
|
+
add_authenticator "Cram-MD5" # deprecated
|
49
|
+
add_authenticator "Digest-MD5" # deprecated
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the names of all registered SASL mechanisms.
|
53
|
+
def names; @authenticators.keys end
|
54
|
+
|
55
|
+
# :call-seq:
|
56
|
+
# add_authenticator(mechanism)
|
57
|
+
# add_authenticator(mechanism, authenticator_class)
|
58
|
+
# add_authenticator(mechanism, authenticator_proc)
|
59
|
+
#
|
60
|
+
# Registers an authenticator for #authenticator to use. +mechanism+ is the
|
61
|
+
# name of the
|
62
|
+
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
|
63
|
+
# implemented by +authenticator_class+ (for instance, <tt>"PLAIN"</tt>).
|
64
|
+
#
|
65
|
+
# If +mechanism+ refers to an existing authenticator,
|
66
|
+
# the old authenticator will be replaced.
|
67
|
+
#
|
68
|
+
# When only a single argument is given, the authenticator class will be
|
69
|
+
# lazily loaded from <tt>Net::IMAP::SASL::#{name}Authenticator</tt> (case is
|
70
|
+
# preserved and non-alphanumeric characters are removed..
|
71
|
+
def add_authenticator(name, authenticator = nil)
|
72
|
+
authenticator ||= begin
|
73
|
+
class_name = "#{name.gsub(/[^a-zA-Z0-9]/, "")}Authenticator".to_sym
|
74
|
+
auth_class = nil
|
75
|
+
->(*creds, **props, &block) {
|
76
|
+
auth_class ||= Net::IMAP::SASL.const_get(class_name)
|
77
|
+
auth_class.new(*creds, **props, &block)
|
78
|
+
}
|
79
|
+
end
|
80
|
+
key = Authenticators.normalize_name(name)
|
81
|
+
@authenticators[key] = authenticator
|
82
|
+
end
|
83
|
+
|
84
|
+
# Removes the authenticator registered for +name+
|
85
|
+
def remove_authenticator(name)
|
86
|
+
key = Authenticators.normalize_name(name)
|
87
|
+
@authenticators.delete(key)
|
88
|
+
end
|
89
|
+
|
90
|
+
def mechanism?(name)
|
91
|
+
key = Authenticators.normalize_name(name)
|
92
|
+
@authenticators.key?(key)
|
93
|
+
end
|
94
|
+
|
95
|
+
# :call-seq:
|
96
|
+
# authenticator(mechanism, ...) -> auth_session
|
97
|
+
#
|
98
|
+
# Builds an authenticator instance using the authenticator registered to
|
99
|
+
# +mechanism+. The returned object represents a single authentication
|
100
|
+
# exchange and <em>must not</em> be reused for multiple authentication
|
101
|
+
# attempts.
|
102
|
+
#
|
103
|
+
# All arguments (except +mechanism+) are forwarded to the registered
|
104
|
+
# authenticator's +#new+ or +#call+ method. Each authenticator must
|
105
|
+
# document its own arguments.
|
106
|
+
#
|
107
|
+
# [Note]
|
108
|
+
# This method is intended for internal use by connection protocol code
|
109
|
+
# only. Protocol client users should see refer to their client's
|
110
|
+
# documentation, e.g. Net::IMAP#authenticate.
|
111
|
+
def authenticator(mechanism, ...)
|
112
|
+
key = Authenticators.normalize_name(mechanism)
|
113
|
+
auth = @authenticators.fetch(key) do
|
114
|
+
raise ArgumentError, 'unknown auth type - "%s"' % key
|
115
|
+
end
|
116
|
+
auth.respond_to?(:new) ? auth.new(...) : auth.call(...)
|
117
|
+
end
|
118
|
+
alias new authenticator
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Net
|
6
|
+
class IMAP
|
7
|
+
module SASL
|
8
|
+
|
9
|
+
# This API is *experimental*, and may change.
|
10
|
+
#
|
11
|
+
# TODO: use with more clients, to verify the API can accommodate them.
|
12
|
+
#
|
13
|
+
# Represents the client to a SASL::AuthenticationExchange. By default,
|
14
|
+
# most methods simply delegate to #client. Clients should subclass
|
15
|
+
# SASL::ClientAdapter and override methods as needed to match the
|
16
|
+
# semantics of this API to their API.
|
17
|
+
#
|
18
|
+
# Subclasses should also include a protocol adapter mixin when the default
|
19
|
+
# ProtocolAdapters::Generic isn't sufficient.
|
20
|
+
#
|
21
|
+
# === Protocol Requirements
|
22
|
+
#
|
23
|
+
# {RFC4422 §4}[https://www.rfc-editor.org/rfc/rfc4422.html#section-4]
|
24
|
+
# lists requirements for protocol specifications to offer SASL. Where
|
25
|
+
# possible, ClientAdapter delegates the handling of these requirements to
|
26
|
+
# SASL::ProtocolAdapters.
|
27
|
+
class ClientAdapter
|
28
|
+
extend Forwardable
|
29
|
+
|
30
|
+
include ProtocolAdapters::Generic
|
31
|
+
|
32
|
+
# The client that handles communication with the protocol server.
|
33
|
+
#
|
34
|
+
# Most ClientAdapter methods are simply delegated to #client by default.
|
35
|
+
attr_reader :client
|
36
|
+
|
37
|
+
# +command_proc+ can used to avoid exposing private methods on #client.
|
38
|
+
# It's value is set by the block that is passed to ::new, and it is used
|
39
|
+
# by the default implementation of #run_command. Subclasses that
|
40
|
+
# override #run_command may use #command_proc for any other purpose they
|
41
|
+
# find useful.
|
42
|
+
#
|
43
|
+
# In the default implementation of #run_command, command_proc is called
|
44
|
+
# with the protocols authenticate +command+ name, the +mechanism+ name,
|
45
|
+
# an _optional_ +initial_response+ argument, and a +continuations+
|
46
|
+
# block. command_proc must run the protocol command with the arguments
|
47
|
+
# sent to it, _yield_ the payload of each continuation, respond to the
|
48
|
+
# continuation with the result of each _yield_, and _return_ the
|
49
|
+
# command's successful result. Non-successful results *MUST* raise
|
50
|
+
# an exception.
|
51
|
+
attr_reader :command_proc
|
52
|
+
|
53
|
+
# By default, this simply sets the #client and #command_proc attributes.
|
54
|
+
# Subclasses may override it, for example: to set the appropriate
|
55
|
+
# command_proc automatically.
|
56
|
+
def initialize(client, &command_proc)
|
57
|
+
@client, @command_proc = client, command_proc
|
58
|
+
end
|
59
|
+
|
60
|
+
# Attempt to authenticate #client to the server.
|
61
|
+
#
|
62
|
+
# By default, this simply delegates to
|
63
|
+
# AuthenticationExchange.authenticate.
|
64
|
+
def authenticate(...) AuthenticationExchange.authenticate(self, ...) end
|
65
|
+
|
66
|
+
##
|
67
|
+
# method: sasl_ir_capable?
|
68
|
+
# Do the protocol, server, and client all support an initial response?
|
69
|
+
def_delegator :client, :sasl_ir_capable?
|
70
|
+
|
71
|
+
##
|
72
|
+
# method: auth_capable?
|
73
|
+
# call-seq: auth_capable?(mechanism)
|
74
|
+
#
|
75
|
+
# Does the server advertise support for the +mechanism+?
|
76
|
+
def_delegator :client, :auth_capable?
|
77
|
+
|
78
|
+
# Calls command_proc with +command_name+ (see
|
79
|
+
# SASL::ProtocolAdapters::Generic#command_name),
|
80
|
+
# +mechanism+, +initial_response+, and a +continuations_handler+ block.
|
81
|
+
# The +initial_response+ is optional; when it's nil, it won't be sent to
|
82
|
+
# command_proc.
|
83
|
+
#
|
84
|
+
# Yields each continuation payload, responds to the server with the
|
85
|
+
# result of each yield, and returns the result. Non-successful results
|
86
|
+
# *MUST* raise an exception. Exceptions in the block *MUST* cause the
|
87
|
+
# command to fail.
|
88
|
+
#
|
89
|
+
# Subclasses that override this may use #command_proc differently.
|
90
|
+
def run_command(mechanism, initial_response = nil, &continuations_handler)
|
91
|
+
command_proc or raise Error, "initialize with block or override"
|
92
|
+
args = [command_name, mechanism, initial_response].compact
|
93
|
+
command_proc.call(*args, &continuations_handler)
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# method: host
|
98
|
+
# The hostname to which the client connected.
|
99
|
+
def_delegator :client, :host
|
100
|
+
|
101
|
+
##
|
102
|
+
# method: port
|
103
|
+
# The destination port to which the client connected.
|
104
|
+
def_delegator :client, :port
|
105
|
+
|
106
|
+
# Returns an array of server responses errors raised by run_command.
|
107
|
+
# Exceptions in this array won't drop the connection.
|
108
|
+
def response_errors; [] end
|
109
|
+
|
110
|
+
##
|
111
|
+
# method: drop_connection
|
112
|
+
# Drop the connection gracefully, sending a "LOGOUT" command as needed.
|
113
|
+
def_delegator :client, :drop_connection
|
114
|
+
|
115
|
+
##
|
116
|
+
# method: drop_connection!
|
117
|
+
# Drop the connection abruptly, closing the socket without logging out.
|
118
|
+
def_delegator :client, :drop_connection!
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -1,34 +1,45 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Authenticator for the "+CRAM-MD5+" SASL mechanism, specified in
|
4
|
-
# RFC2195[https://
|
4
|
+
# RFC2195[https://www.rfc-editor.org/rfc/rfc2195]. See Net::IMAP#authenticate.
|
5
5
|
#
|
6
6
|
# == Deprecated
|
7
7
|
#
|
8
8
|
# +CRAM-MD5+ is obsolete and insecure. It is included for compatibility with
|
9
9
|
# existing servers.
|
10
|
-
# {draft-ietf-sasl-crammd5-to-historic}[https://
|
10
|
+
# {draft-ietf-sasl-crammd5-to-historic}[https://www.rfc-editor.org/rfc/draft-ietf-sasl-crammd5-to-historic-00.html]
|
11
11
|
# recommends using +SCRAM-*+ or +PLAIN+ protected by TLS instead.
|
12
12
|
#
|
13
|
-
# Additionally, RFC8314[https://
|
13
|
+
# Additionally, RFC8314[https://www.rfc-editor.org/rfc/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
|
16
|
+
class Net::IMAP::SASL::CramMD5Authenticator
|
17
|
+
def initialize(user = nil, pass = nil,
|
18
|
+
authcid: nil, username: nil,
|
19
|
+
password: nil, secret: nil,
|
20
|
+
warn_deprecation: true,
|
21
|
+
**)
|
22
|
+
if warn_deprecation
|
23
|
+
warn "WARNING: CRAM-MD5 mechanism is deprecated.", category: :deprecated
|
24
|
+
end
|
25
|
+
require "digest/md5"
|
26
|
+
@user = authcid || username || user
|
27
|
+
@password = password || secret || pass
|
28
|
+
@done = false
|
29
|
+
end
|
30
|
+
|
31
|
+
def initial_response?; false end
|
32
|
+
|
17
33
|
def process(challenge)
|
18
34
|
digest = hmac_md5(challenge, @password)
|
19
35
|
return @user + " " + digest
|
36
|
+
ensure
|
37
|
+
@done = true
|
20
38
|
end
|
21
39
|
|
22
|
-
|
40
|
+
def done?; @done end
|
23
41
|
|
24
|
-
|
25
|
-
if warn_deprecation
|
26
|
-
warn "WARNING: CRAM-MD5 mechanism is deprecated." # TODO: recommend SCRAM
|
27
|
-
end
|
28
|
-
require "digest/md5"
|
29
|
-
@user = user
|
30
|
-
@password = password
|
31
|
-
end
|
42
|
+
private
|
32
43
|
|
33
44
|
def hmac_md5(text, key)
|
34
45
|
if key.length > 64
|
@@ -47,5 +58,4 @@ class Net::IMAP::CramMD5Authenticator
|
|
47
58
|
return Digest::MD5.hexdigest(k_opad + digest)
|
48
59
|
end
|
49
60
|
|
50
|
-
Net::IMAP.add_authenticator "CRAM-MD5", self
|
51
61
|
end
|