diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb index cca06e6..178b949 100644 --- a/lib/net/smtp.rb +++ b/lib/net/smtp.rb @@ -183,7 +183,7 @@ class SMTPUnsupportedCommand < ProtocolError # # # PLAIN # Net::SMTP.start('your.smtp.server', 25, - # user: 'Your Account', secret: 'Your Password', authtype: :plain) + # username: 'Your Account', secret: 'Your Password', authtype: :plain) # # Support for other SASL mechanisms-such as +EXTERNAL+, +OAUTHBEARER+, # +SCRAM-SHA-256+, and +XOAUTH2+-will be added in a future release. @@ -459,15 +459,16 @@ def debug_output=(arg) # # :call-seq: - # 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| ... } + # 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', username: 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', username = nil, secret = nil, authtype = nil) { |smtp| ... } # # Creates a new Net::SMTP object and connects to the server. # # This method is equivalent to: # # Net::SMTP.new(address, port, tls_verify: flag, tls_hostname: hostname, ssl_context_params: nil) - # .start(helo: helo_domain, user: account, secret: password, authtype: authtype) + # .start(helo: helo_domain, username: account, secret: password, authtype: authtype) # # See also: Net::SMTP.new, #start # @@ -514,13 +515,15 @@ def debug_output=(arg) # # +authtype+ is the SASL authentication mechanism. # - # +user+ is the authentication or authorization identity. + # +username+ or +user+ is the authentication or authorization identity. # # +secret+ or +password+ is your password or other authentication token. # # These will be sent to #authenticate as positional arguments-the exact # semantics are dependent on the +authtype+. # + # +auth+ is an optional hash of keyword arguments for #authenticate. + # # See the discussion of Net::SMTP@SMTP+Authentication in the overview notes. # # === Errors @@ -538,15 +541,18 @@ def debug_output=(arg) # def SMTP.start(address, port = nil, *args, helo: nil, user: nil, secret: nil, password: nil, authtype: nil, + username: nil, + auth: nil, tls: false, starttls: :auto, tls_verify: true, tls_hostname: nil, ssl_context_params: nil, &block) raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 1..6)" if args.size > 4 helo ||= args[0] || 'localhost' - user ||= args[1] + username ||= 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, username: username, secret: secret, authtype: authtype, auth: auth, &block) end # +true+ if the \SMTP session has been started. @@ -556,8 +562,9 @@ 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', username: nil, secret: nil, authtype: nil) { |smtp| ... } + # start(helo = 'localhost', username = nil, secret = nil, authtype = nil) { |smtp| ... } + # start(helo = 'localhost', auth: {type: nil, **auth_kwargs}) { |smtp| ... } # # Opens a TCP connection and starts the SMTP session. # @@ -571,13 +578,15 @@ def started? # # +authtype+ is the SASL authentication mechanism. # - # +user+ is the authentication or authorization identity. + # +username+ or +user+ is the authentication or authorization identity. # # +secret+ or +password+ is your password or other authentication token. # # These will be sent to #authenticate as positional arguments-the exact # semantics are dependent on the +authtype+. # + # +auth+ is an optional hash of keyword arguments for #authenticate. + # # See the discussion of Net::SMTP@SMTP+Authentication in the overview notes. # # See also: Net::SMTP.start @@ -595,7 +604,7 @@ def started? # # require 'net/smtp' # smtp = Net::SMTP.new('smtp.mail.server', 25) - # smtp.start(helo: helo_domain, user: account, secret: password, authtype: authtype) do |smtp| + # smtp.start(helo: helo_domain, username: account, secret: password, authtype: authtype) do |smtp| # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] # end # @@ -619,12 +628,15 @@ 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, 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 ||= args[1] + username ||= user || 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) @@ -639,13 +651,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, username, secret, authtype, **auth return yield(self) ensure do_finish end else - do_start helo, user, secret, authtype + do_start helo, username, secret, authtype, **auth return self end end @@ -663,10 +675,10 @@ 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 || secret || authtype - check_auth_args authtype, user, secret + if user || secret || authtype || auth.any? + check_auth_args(authtype, user, secret, **auth) end s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do tcp_socket(@address, @port) @@ -684,7 +696,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 @@ -862,26 +878,39 @@ def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream DEFAULT_AUTH_TYPE = :plain + # call-seq: + # authenticate(type: DEFAULT_AUTH_TYPE, **, &) + # authenticate(type = DEFAULT_AUTH_TYPE, **, &) + # authenticate(username, secret, type: DEFAULT_AUTH_TYPE, **, &) + # authenticate(username, secret, type = DEFAULT_AUTH_TYPE, **, &) + # # Authenticates with the server, using the "AUTH" command. # - # +authtype+ is the name of a SASL authentication mechanism. + # +type+ is the name of a SASL authentication mechanism. # # All arguments-other than +authtype+-are forwarded to the authenticator. - # Different authenticators may interpret the +user+ and +secret+ + # Different authenticators may interpret the +username+ and +secret+ # arguments differently. - def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE) - check_auth_args authtype, user, secret + def authenticate(*args, **kwargs, &block) + case args.length + when 1, 3 then authtype = args.pop + when (4..) + raise ArgumentError, "wrong number of arguments " \ + "(given %d, expected 0..3)" % [args.length] + end + authtype, args, kwargs = check_auth_args authtype, *args, **kwargs authenticator = Authenticator.auth_class(authtype).new(self) - authenticator.auth(user, secret) + authenticator.auth(*args, **kwargs, &block) end private - def check_auth_args(type, *args, **kwargs) - type ||= DEFAULT_AUTH_TYPE + def check_auth_args(type_arg = nil, *args, type: nil, **kwargs) + type ||= type_arg || DEFAULT_AUTH_TYPE klass = Authenticator.auth_class(type) or raise ArgumentError, "wrong authentication type #{type}" klass.check_args(*args, **kwargs) + [type, args, kwargs] end # diff --git a/lib/net/smtp/auth_cram_md5.rb b/lib/net/smtp/auth_cram_md5.rb index 0490cd6..50994ee 100644 --- a/lib/net/smtp/auth_cram_md5.rb +++ b/lib/net/smtp/auth_cram_md5.rb @@ -9,7 +9,12 @@ class Net::SMTP class AuthCramMD5 < Net::SMTP::Authenticator auth_type :cram_md5 - def auth(user, secret) + def auth(user_arg = nil, secret_arg = nil, + authcid: nil, username: nil, user: nil, + secret: nil, password: nil, + **) + user = req_param authcid, username, user, user_arg, "username (authcid)" + secret = req_param password, secret, secret_arg, "secret (password)" challenge = continue('AUTH CRAM-MD5') crammed = cram_md5_response(secret, challenge.unpack1('m')) finish(base64_encode("#{user} #{crammed}")) diff --git a/lib/net/smtp/auth_login.rb b/lib/net/smtp/auth_login.rb index 545c1f9..174ab09 100644 --- a/lib/net/smtp/auth_login.rb +++ b/lib/net/smtp/auth_login.rb @@ -2,7 +2,12 @@ class Net::SMTP class AuthLogin < Net::SMTP::Authenticator auth_type :login - def auth(user, secret) + def auth(user_arg = nil, secret_arg = nil, + authcid: nil, username: nil, user: nil, + secret: nil, password: nil, + **) + user = req_param authcid, username, user, user_arg, "username (authcid)" + secret = req_param password, secret, secret_arg, "secret (password)" continue('AUTH LOGIN') continue(base64_encode(user)) finish(base64_encode(secret)) diff --git a/lib/net/smtp/auth_plain.rb b/lib/net/smtp/auth_plain.rb index 7fa1198..e778bad 100644 --- a/lib/net/smtp/auth_plain.rb +++ b/lib/net/smtp/auth_plain.rb @@ -2,7 +2,12 @@ class Net::SMTP class AuthPlain < Net::SMTP::Authenticator auth_type :plain - def auth(user, secret) + def auth(user_arg = nil, secret_arg = nil, + authcid: nil, username: nil, user: nil, + secret: nil, password: nil, + **) + user = req_param authcid, username, user, user_arg, "username (authcid)" + secret = req_param password, secret, secret_arg, "secret (password)" finish('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}")) end end diff --git a/lib/net/smtp/authenticator.rb b/lib/net/smtp/authenticator.rb index 4e91228..6e381af 100644 --- a/lib/net/smtp/authenticator.rb +++ b/lib/net/smtp/authenticator.rb @@ -15,11 +15,14 @@ def self.auth_class(type) Authenticator.auth_classes[type] end - def self.check_args(user_arg = nil, secret_arg = nil, *, **) - unless user_arg + def self.check_args(user_arg = nil, secret_arg = nil, *, + authcid: nil, username: nil, user: nil, + secret: nil, password: nil, + **) + unless authcid || username || user || user_arg raise ArgumentError, 'SMTP-AUTH requested but missing user name' end - unless secret_arg + unless password || secret || secret_arg raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase' end end @@ -52,6 +55,12 @@ def base64_encode(str) # expects "str" may not become too long [str].pack('m0') end + + def req_param(*args, name) + args.compact.first or + raise ArgumentError, "SMTP-AUTH requested but missing #{name}" + end + end end end diff --git a/test/net/smtp/test_smtp.rb b/test/net/smtp/test_smtp.rb index 3b9e245..dbe8980 100644 --- a/test/net/smtp/test_smtp.rb +++ b/test/net/smtp/test_smtp.rb @@ -470,6 +470,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){}