From f8a997ca307481a834fd2783a70fce3c79c47c42 Mon Sep 17 00:00:00 2001 From: nick evans Date: Thu, 28 Sep 2023 18:27:26 -0400 Subject: [PATCH] WIP: support arbitrary authenticators from start --- lib/net/smtp.rb | 35 +++++++++++++++++--------- lib/net/smtp/auth_sasl_adapter.rb | 41 ++++++++----------------------- 2 files changed, 33 insertions(+), 43 deletions(-) diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb index 740f4b2..cd0b2b1 100644 --- a/lib/net/smtp.rb +++ b/lib/net/smtp.rb @@ -486,6 +486,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| ... } # @@ -552,6 +553,7 @@ def debug_output=(arg) # def SMTP.start(address, port = nil, *args, helo: nil, user: nil, secret: nil, password: nil, authtype: nil, + auth: nil, tls: false, starttls: :auto, tls_verify: true, tls_hostname: nil, ssl_context_params: nil, &block) @@ -560,7 +562,7 @@ def SMTP.start(address, port = nil, *args, helo: nil, user ||= 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. @@ -572,6 +574,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', authtype: nil, auth: {...}) { |smtp| ... } # # Opens a TCP connection and starts the SMTP session. # @@ -580,11 +583,10 @@ def started? # +helo+ is the _HELO_ _domain_ that you'll dispatch mails from; see # the discussion in the overview notes. # - # If both of +user+ and +secret+ are given, SMTP authentication - # will be attempted using the AUTH command. +authtype+ specifies - # the type of authentication to attempt; it must be one of - # :login, :plain, and :cram_md5. See the notes on SMTP Authentication - # in the overview. + # If either +auth+ or +user+ and +secret+ are given, SMTP authentication + # will be attempted using the AUTH command. +authtype+ specifies the type + # of authentication to attempt; it must be one of :login, :plain, and + # :cram_md5. See the notes on SMTP Authentication in the overview. # # === Block Usage # @@ -623,12 +625,14 @@ def started? # * Net::ReadTimeout # * IOError # - def start(*args, helo: nil, user: nil, secret: nil, password: nil, authtype: nil) + def start(*args, helo: nil, + user: 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 ||= args[1] secret ||= password || args[2] authtype ||= args[3] + auth ||= [user, secret, authtype] if user || secret if defined?(OpenSSL::VERSION) ssl_context_params = @ssl_context_params || {} unless ssl_context_params.has_key?(:verify_mode) @@ -643,13 +647,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 @@ -667,7 +671,8 @@ 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 = nil) + # pp({method: __method__, user:, secret:, authtype:, auth:}.compact) raise IOError, 'SMTP session already started' if @started s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do tcp_socket(@address, @port) @@ -896,8 +901,14 @@ def authenticate(*args, **kwargs) # # All arguments besides +mechanism+ are forwarded directly to the # authenticator. - def auth(mechanism, ...) - critical { Authenticator::SASLAdapter.authenticate(self, mechanism, ...) } + def auth(...) + critical do + Authenticator::SASLAdapter.new(self).authenticate(...) + rescue SMTPServerBusy, SMTPSyntaxError, SMTPFatalError => error + raise SMTPAuthenticationError.new(error.response) + rescue Net::IMAP::SASL::AuthenticationIncomplete => error + raise error.response.exception_class.new(error.response) + end end # diff --git a/lib/net/smtp/auth_sasl_adapter.rb b/lib/net/smtp/auth_sasl_adapter.rb index 7f1bfdd..5e36a89 100644 --- a/lib/net/smtp/auth_sasl_adapter.rb +++ b/lib/net/smtp/auth_sasl_adapter.rb @@ -14,18 +14,11 @@ class Authenticator # Net::SMTP#authenticate still supports the old API, so v0.4.0 compatible # Authenticators can still be added and used with it. class CompatibilityAdapter - def initialize(mechanism) - @mechanism = mechanism.to_s.tr("_", "-").upcase - end - - def new(smtp) - @smtp = smtp - self - end - + def initialize(mechanism) @mechanism = mechanism end + def new(smtp) @smtp = smtp; self end def auth(*args, **kwargs, &block) args.pop while args.any? && args.last.nil? - SASLAdapter.authenticate(@smtp, @mechanism, *args, **kwargs, &block) + @smtp.auth(@mechanism, *args, **kwargs, &block) end end @@ -35,7 +28,8 @@ def auth(*args, **kwargs, &block) # # Initialize with a block that runs a command, yielding for continuations. class SASLAdapter < SASL::ClientAdapter - COMMAND_NAME = "AUTH" + include SASL::ProtocolAdapters::SMTP + RESPONSE_ERRORS = [ SMTPAuthenticationError, SMTPServerBusy, @@ -43,31 +37,16 @@ class SASLAdapter < SASL::ClientAdapter SMTPFatalError, ].freeze - def self.authenticate(client, mechanism, *args, **kwargs, &block) - sasl = SASL.authenticator(mechanism, *args, **kwargs, &block) - adapter = new(client, mechanism, sasl) - adapter.authenticate - end - def initialize(...) super @command_proc ||= client.method(:send_command_with_continuations) end - def supports_initial_response?; true end - def drop_connection; client.finish end - def drop_connection!; client.finish end - - def transform_exception(error) - case error - when SMTPServerBusy, SMTPSyntaxError, SMTPFatalError - SMTPAuthenticationError.new(error.response) - when SASL::AuthenticationIncomplete - error.response.exception_class.new(error.response) - else - super - end - end + def response_errors; RESPONSE_ERRORS end + def sasl_ir_capable?; true end # TODO + def auth_capable?(mechanism); client.auth_capable?(mechanism) end + def drop_connection; client.finish end + def drop_connection!; client.finish end # TODO end end