From 628f11326a0ed281037b62c12c10354054c93bb7 Mon Sep 17 00:00:00 2001 From: nick evans Date: Mon, 9 Oct 2023 14:49:56 -0400 Subject: [PATCH] Add forward-compatible `auth` kwarg to `start` The pre-existing `start` keyword params will retain their existing behavior as positional `user` and `secret` args to `#authenticate`. This adds a new `auth` keyword` param that is used to pass any arbitratry keyword params to `#authenticate`. The current #start API can't fully support every SASL mechanism. Some SASL mechanisms do not require a +username+ (OAUTHBEARER, EXTERNAL, ANONYMOUS) or a +secret+ (EXTERNAL, ANONYMOUS). Many SASL mechanisms should oe able to take extra arguments (e.g: `authzid` for many mechanisms, `warn_deprecations` for deprecated mechanisms, `min_iterations` for SCRAM-*, `anonymous_message` for ANONYMOUS), and so on. And, although it is convenient to use +username+ as an alias for +authcid+ or +authzid+ and +secret+ as an alias for +password+ or +oauth2_token+, it can also be useful to have keyword parameters that keep stable semantics across many different mechanisms. --- lib/net/smtp.rb | 30 ++++++++++++++++++++---------- test/net/smtp/test_smtp.rb | 9 +++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb index f774381..adda973 100644 --- a/lib/net/smtp.rb +++ b/lib/net/smtp.rb @@ -452,6 +452,7 @@ def debug_output=(arg) # # :call-seq: + # start(address, port = nil, helo: 'localhost', auth: nil, tls: false, starttls: :auto, tls_verify: true, tls_hostname: nil, ssl_context_params: nil) { |smtp| ... } # start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil, tls: false, starttls: :auto, tls_verify: true, tls_hostname: nil, ssl_context_params: nil) { |smtp| ... } # start(address, port = nil, helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... } # @@ -502,7 +503,8 @@ def debug_output=(arg) # identity (depending on +authtype+); +secret+ or +password+ is your # password or other authentication token; and +authtype+ is the SASL # authentication mechanism. These will be used as regular positional - # arguments to #authenticate. See the discussion of SMTP Authentication + # arguments to #authenticate, and +auth+ is a hash of arbitrary keyword + # arguments for #authenticate. See the discussion of SMTP Authentication # in the overview notes. # # === Errors @@ -520,7 +522,7 @@ def debug_output=(arg) # def SMTP.start(address, port = nil, *args, helo: nil, user: nil, secret: nil, password: nil, authtype: nil, - username: nil, + username: nil, auth: nil, tls: false, starttls: :auto, tls_verify: true, tls_hostname: nil, ssl_context_params: nil, &block) @@ -529,7 +531,7 @@ def SMTP.start(address, port = nil, *args, helo: nil, user ||= username || args[1] secret ||= password || args[2] authtype ||= args[3] - new(address, port, tls: tls, starttls: starttls, tls_verify: tls_verify, tls_hostname: tls_hostname, ssl_context_params: ssl_context_params).start(helo: helo, user: user, secret: secret, authtype: authtype, &block) + new(address, port, tls: tls, starttls: starttls, tls_verify: tls_verify, tls_hostname: tls_hostname, ssl_context_params: ssl_context_params).start(helo: helo, user: user, secret: secret, authtype: authtype, auth: auth, &block) end # +true+ if the SMTP session has been started. @@ -541,6 +543,7 @@ def started? # :call-seq: # start(helo: 'localhost', user: nil, secret: nil, authtype: nil) { |smtp| ... } # start(helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... } + # start(helo = 'localhost', auth: {type: nil, **auth_kwargs}) { |smtp| ... } # # Opens a TCP connection and starts the SMTP session. # @@ -554,7 +557,8 @@ def started? # identity (depending on +authtype+); +secret+ or +password+ is your # password or other authentication token; and +authtype+ is the SASL # authentication mechanism. These will be used as regular positional - # arguments to #authenticate. See the discussion of SMTP Authentication + # arguments to #authenticate, and +auth+ is a hash of arbitrary keyword + # arguments for #authenticate. See the discussion of SMTP Authentication # in the overview notes. # # === Block Usage @@ -594,13 +598,15 @@ def started? # * Net::ReadTimeout # * IOError # - def start(*args, helo: nil, user: nil, secret: nil, password: nil, authtype: nil, - username: nil) + def start(*args, helo: nil, + user: nil, username: nil, secret: nil, password: nil, + authtype: nil, auth: nil) raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..4)" if args.size > 4 helo ||= args[0] || 'localhost' user ||= username || args[1] secret ||= password || args[2] authtype ||= args[3] + auth ||= {} if defined?(OpenSSL::VERSION) ssl_context_params = @ssl_context_params || {} unless ssl_context_params.has_key?(:verify_mode) @@ -615,13 +621,13 @@ def start(*args, helo: nil, user: nil, secret: nil, password: nil, authtype: nil end if block_given? begin - do_start helo, user, secret, authtype + do_start helo, user, secret, authtype, **auth return yield(self) ensure do_finish end else - do_start helo, user, secret, authtype + do_start helo, user, secret, authtype, **auth return self end end @@ -639,7 +645,7 @@ def tcp_socket(address, port) TCPSocket.open address, port end - def do_start(helo_domain, user, secret, authtype) + def do_start(helo_domain, user, secret, authtype, **auth) raise IOError, 'SMTP session already started' if @started if user or secret check_auth_method(authtype || DEFAULT_AUTH_TYPE) @@ -661,7 +667,11 @@ def do_start(helo_domain, user, secret, authtype) # helo response may be different after STARTTLS do_helo helo_domain end - authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user + if user or secret + authenticate(user, secret, authtype, **auth) + elsif authtype or auth.any? + authenticate(authtype, **auth) + end @started = true ensure unless @started diff --git a/test/net/smtp/test_smtp.rb b/test/net/smtp/test_smtp.rb index 70063aa..f7a38b8 100644 --- a/test/net/smtp/test_smtp.rb +++ b/test/net/smtp/test_smtp.rb @@ -480,6 +480,15 @@ def test_start_auth_plain port = fake_server_start(auth: 'plain') Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :plain){} + port = fake_server_start(auth: 'plain') + Net::SMTP.start('localhost', port, authtype: "PLAIN", + auth: {username: 'account', password: 'password'}){} + + port = fake_server_start(auth: 'plain') + Net::SMTP.start('localhost', port, auth: {username: 'account', + password: 'password', + type: :plain}){} + port = fake_server_start(auth: 'plain') assert_raise Net::SMTPAuthenticationError do Net::SMTP.start('localhost', port, user: 'account', password: 'invalid', authtype: :plain){}