lib/net/smtp.rb in net-smtp-0.2.1 vs lib/net/smtp.rb in net-smtp-0.2.2

- old
+ new

@@ -29,10 +29,21 @@ # Module mixed in to all SMTP error classes module SMTPError # This *class* is a module for backward compatibility. # In later release, this module becomes a class. + + attr_reader :response + + def initialize(response, message: nil) + @response = response + @message = message + end + + def message + @message || response.message + end end # Represents an SMTP authentication error. class SMTPAuthenticationError < ProtoAuthError include SMTPError @@ -166,11 +177,11 @@ # # CRAM MD5 # Net::SMTP.start('your.smtp.server', 25 # user: 'Your Account', secret: 'Your Password', authtype: :cram_md5) # class SMTP < Protocol - VERSION = "0.2.1" + VERSION = "0.2.2" Revision = %q$Revision$.split[1] # The default SMTP port number, 25. def SMTP.default_port @@ -189,16 +200,13 @@ class << self alias default_ssl_port default_tls_port end - def SMTP.default_ssl_context(verify_peer=true) + def SMTP.default_ssl_context(ssl_context_params = nil) context = OpenSSL::SSL::SSLContext.new - context.verify_mode = verify_peer ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE - store = OpenSSL::X509::Store.new - store.set_default_paths - context.cert_store = store + context.set_params(ssl_context_params ? ssl_context_params : {}) context end # # Creates a new Net::SMTP object. @@ -299,11 +307,11 @@ # Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for # this object. Must be called before the connection is established # to have any effect. +context+ is a OpenSSL::SSL::SSLContext object. def enable_tls(context = nil) - raise 'openssl library not installed' unless defined?(OpenSSL) + raise 'openssl library not installed' unless defined?(OpenSSL::VERSION) raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls == :always @tls = true @ssl_context_tls = context end @@ -336,20 +344,20 @@ end # Enables SMTP/TLS (STARTTLS) for this object. # +context+ is a OpenSSL::SSL::SSLContext object. def enable_starttls(context = nil) - raise 'openssl library not installed' unless defined?(OpenSSL) + raise 'openssl library not installed' unless defined?(OpenSSL::VERSION) raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls @starttls = :always @ssl_context_starttls = context end # Enables SMTP/TLS (STARTTLS) for this object if server accepts. # +context+ is a OpenSSL::SSL::SSLContext object. def enable_starttls_auto(context = nil) - raise 'openssl library not installed' unless defined?(OpenSSL) + raise 'openssl library not installed' unless defined?(OpenSSL::VERSION) raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls @starttls = :auto @ssl_context_starttls = context end @@ -407,18 +415,18 @@ # SMTP session control # # # :call-seq: - # start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil) { |smtp| ... } + # start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil, ssl_context_params: nil) { |smtp| ... } # start(address, port = nil, helo = 'localhost', user = 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).start(helo: helo_domain, user: account, secret: password, authtype: authtype, tls_verify: flag, tls_hostname: hostname) + # Net::SMTP.new(address, port).start(helo: helo_domain, user: account, secret: password, authtype: authtype, tls_verify: flag, tls_hostname: hostname, ssl_context_params: nil) # # === Example # # Net::SMTP.start('your.smtp.server') do |smtp| # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] @@ -448,10 +456,15 @@ # SMTP Authentication in the overview notes. # If +tls_verify+ is true, verify the server's certificate. The default is true. # If the hostname in the server certificate is different from +address+, # it can be specified with +tls_hostname+. # + # Additional SSLContext params can be added to +ssl_context_params+ hash argument and are passed to + # +OpenSSL::SSL::SSLContext#set_params+ + # + # +tls_verify: true+ is equivalent to +ssl_context_params: { verify_mode: OpenSSL::SSL::VERIFY_PEER }+. + # # === Errors # # This method may raise: # # * Net::SMTPAuthenticationError @@ -463,28 +476,28 @@ # * Net::ReadTimeout # * IOError # def SMTP.start(address, port = nil, *args, helo: nil, user: nil, secret: nil, password: nil, authtype: nil, - tls_verify: true, tls_hostname: nil, + 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] secret ||= password || args[2] authtype ||= args[3] - new(address, port).start(helo: helo, user: user, secret: secret, authtype: authtype, tls_verify: tls_verify, tls_hostname: tls_hostname, &block) + new(address, port).start(helo: helo, user: user, secret: secret, authtype: authtype, tls_verify: tls_verify, tls_hostname: tls_hostname, ssl_context_params: ssl_context_params, &block) end # +true+ if the SMTP session has been started. def started? @started end # # :call-seq: - # start(helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil) { |smtp| ... } + # start(helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil, ssl_context_params: nil) { |smtp| ... } # start(helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... } # # Opens a TCP connection and starts the SMTP session. # # === Parameters @@ -499,10 +512,15 @@ # in the overview. # If +tls_verify+ is true, verify the server's certificate. The default is true. # If the hostname in the server certificate is different from +address+, # it can be specified with +tls_hostname+. # + # Additional SSLContext params can be added to +ssl_context_params+ hash argument and are passed to + # +OpenSSL::SSL::SSLContext#set_params+ + # + # +tls_verify: true+ is equivalent to +ssl_context_params: { verify_mode: OpenSSL::SSL::VERIFY_PEER }+. + # # === Block Usage # # When this methods is called with a block, the newly-started SMTP # object is yielded to the block, and automatically closed after # the block call finishes. Otherwise, it is the caller's @@ -537,23 +555,29 @@ # * Net::OpenTimeout # * Net::ReadTimeout # * IOError # def start(*args, helo: nil, - user: nil, secret: nil, password: nil, authtype: nil, tls_verify: true, tls_hostname: nil) + user: nil, secret: nil, password: nil, authtype: nil, tls_verify: true, tls_hostname: nil, ssl_context_params: 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] - if @tls && @ssl_context_tls.nil? - @ssl_context_tls = SMTP.default_ssl_context(tls_verify) + if defined?(OpenSSL::VERSION) + ssl_context_params = ssl_context_params ? ssl_context_params : {} + unless ssl_context_params.has_key?(:verify_mode) + ssl_context_params[:verify_mode] = tls_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE + end + if @tls && @ssl_context_tls.nil? + @ssl_context_tls = SMTP.default_ssl_context(ssl_context_params) + end + if @starttls && @ssl_context_starttls.nil? + @ssl_context_starttls = SMTP.default_ssl_context(ssl_context_params) + end + @tls_hostname = tls_hostname end - if @starttls && @ssl_context_starttls.nil? - @ssl_context_starttls = SMTP.default_ssl_context(tls_verify) - end - @tls_hostname = tls_hostname if block_given? begin do_start helo, user, secret, authtype return yield(self) ensure @@ -573,30 +597,32 @@ end private def tcp_socket(address, port) - TCPSocket.open address, port + begin + Socket.tcp address, port, nil, nil, connect_timeout: @open_timeout + rescue Errno::ETIMEDOUT #raise Net:OpenTimeout instead for compatibility with previous versions + raise Net::OpenTimeout, "Timeout to open TCP connection to "\ + "#{address}:#{port} (exceeds #{@open_timeout} seconds)" + end end def do_start(helo_domain, user, secret, authtype) raise IOError, 'SMTP session already started' if @started if user or secret check_auth_method(authtype || DEFAULT_AUTH_TYPE) check_auth_args user, secret end - s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do - tcp_socket(@address, @port) - end + s = tcp_socket(@address, @port) logging "Connection opened: #{@address}:#{@port}" @socket = new_internet_message_io(tls? ? tlsconnect(s, @ssl_context_tls) : s) check_response critical { recv_response() } do_helo helo_domain if ! tls? and (starttls_always? or (capable_starttls? and starttls_auto?)) unless capable_starttls? - raise SMTPUnsupportedCommand, - "STARTTLS is not supported on this server" + raise SMTPUnsupportedCommand.new(nil, message: "STARTTLS is not supported on this server") end starttls @socket = new_internet_message_io(tlsconnect(s, @ssl_context_starttls)) # helo response may be different after STARTTLS do_helo helo_domain @@ -618,15 +644,12 @@ def tlsconnect(s, context) verified = false s = ssl_socket(s, context) logging "TLS connection started" s.sync_close = true - s.hostname = @tls_hostname || @address if s.respond_to? :hostname= + s.hostname = @tls_hostname || @address ssl_socket_connect(s, @open_timeout) - if context.verify_mode && context.verify_mode != OpenSSL::SSL::VERIFY_NONE - s.post_connection_check(@tls_hostname || @address) - end verified = true s ensure s.close unless verified end @@ -998,28 +1021,28 @@ end end def check_response(res) unless res.success? - raise res.exception_class, res.message + raise res.exception_class.new(res) end end def check_continue(res) unless res.continue? - raise SMTPUnknownError, "could not get 3xx (#{res.status}: #{res.string})" + raise SMTPUnknownError.new(res, message: "could not get 3xx (#{res.status}: #{res.string})") end end def check_auth_response(res) unless res.success? - raise SMTPAuthenticationError, res.message + raise SMTPAuthenticationError.new(res) end end def check_auth_continue(res) unless res.continue? - raise res.exception_class, res.message + raise res.exception_class.new(res) end end # This class represents a response received by the SMTP server. Instances # of this class are created by the SMTP class; they should not be directly