Skip to content

Commit

Permalink
Merge pull request #31 from tmtm/add_tls_and_starttls_arguments
Browse files Browse the repository at this point in the history
Add tls and starttls arguments
  • Loading branch information
tmtm committed Oct 14, 2021
2 parents 8354c45 + fed2a9d commit a4cd82a
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 43 deletions.
17 changes: 17 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# NEWS

## Version 0.3.0

### Improvements

* Add `tls`, `starttls` keyword arguments.
```
# always use TLS connection for port 465.
Net::SMTP.start(hostname, 465, tls: true)

# do not use starttls for localhost
Net::SMTP.start('localhost', starttls: false)
```

### Incompatible changes

* The tls_* paramter has been moved from start() to initialize().

## Version 0.2.2 (2021-10-09)

* Add `response` to SMTPError exceptions.
Expand Down
51 changes: 31 additions & 20 deletions lib/net/smtp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class SMTPUnsupportedCommand < ProtocolError
# user: 'Your Account', secret: 'Your Password', authtype: :cram_md5)
#
class SMTP < Protocol
VERSION = "0.2.2"
VERSION = "0.3.0"

Revision = %q$Revision$.split[1]

Expand Down Expand Up @@ -215,11 +215,23 @@ def SMTP.default_ssl_context(ssl_context_params = nil)
# server. +port+ is the port to connect to; it defaults to
# port 25.
#
# If +tls+ is true, enable TLS. The default is false.
# If +starttls+ is :always, enable STARTTLS, if +:auto+, use STARTTLS when the server supports it,
# if false, disable STARTTLS.
#
# 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 }+.
# This method does not open the TCP connection. You can use
# SMTP.start instead of SMTP.new if you want to do everything
# at once. Otherwise, follow SMTP.new with SMTP#start.
#
def initialize(address, port = nil)
def initialize(address, port = nil, tls: false, starttls: :auto, tls_verify: true, tls_hostname: nil, ssl_context_params: nil)
@address = address
@port = (port || SMTP.default_port)
@esmtp = true
Expand All @@ -230,10 +242,13 @@ def initialize(address, port = nil)
@read_timeout = 60
@error_occurred = false
@debug_output = nil
@tls = false
@starttls = :auto
@tls = tls
@starttls = starttls
@ssl_context_tls = nil
@ssl_context_starttls = nil
@tls_verify = tls_verify
@tls_hostname = tls_hostname
@ssl_context_params = ssl_context_params
end

# Provide human-readable stringification of class state.
Expand Down Expand Up @@ -417,7 +432,7 @@ def debug_output=(arg)

#
# :call-seq:
# 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, 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| ... }
#
# Creates a new Net::SMTP object and connects to the server.
Expand Down Expand Up @@ -454,6 +469,11 @@ def debug_output=(arg)
# or other authentication token; and +authtype+ is the authentication
# type, one of :plain, :login, or :cram_md5. See the discussion of
# SMTP Authentication in the overview notes.
#
# If +tls+ is true, enable TLS. The default is false.
# If +starttls+ is :always, enable STARTTLS, if +:auto+, use STARTTLS when the server supports it,
# if false, disable STARTTLS.
#
# 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+.
Expand All @@ -478,14 +498,15 @@ def debug_output=(arg)
#
def SMTP.start(address, port = nil, *args, helo: nil,
user: nil, secret: nil, password: nil, authtype: 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]
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, ssl_context_params: ssl_context_params, &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, &block)
end

# +true+ if the SMTP session has been started.
Expand All @@ -495,7 +516,7 @@ def started?

#
# :call-seq:
# 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| ... }
# start(helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... }
#
# Opens a TCP connection and starts the SMTP session.
Expand All @@ -510,14 +531,6 @@ def started?
# 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 +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
#
Expand Down Expand Up @@ -556,25 +569,23 @@ def started?
# * Net::ReadTimeout
# * IOError
#
def start(*args, helo: nil,
user: nil, secret: nil, password: nil, authtype: nil, tls_verify: true, tls_hostname: nil, ssl_context_params: nil)
def start(*args, helo: nil, user: nil, secret: nil, password: nil, authtype: 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 defined?(OpenSSL::VERSION)
ssl_context_params = ssl_context_params ? 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
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 block_given?
begin
Expand Down
159 changes: 146 additions & 13 deletions test/net/smtp/test_smtp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,10 @@ def test_tls_connect
sock.gets
sock.write("221 localhost Service closing transmission channel\r\n")
end
smtp = Net::SMTP.new("localhost", servers[0].local_address.ip_port)
smtp = Net::SMTP.new("localhost", servers[0].local_address.ip_port, tls_verify: false)
smtp.enable_tls
smtp.open_timeout = 1
smtp.start(tls_verify: false) do
smtp.start do
end
ensure
sock&.close
Expand Down Expand Up @@ -287,6 +287,59 @@ def test_eof_error_backtrace
end
end

if defined? OpenSSL::VERSION
def test_with_tls
port = fake_server_start(tls: true)
smtp = Net::SMTP.new('localhost', port, tls: true, tls_verify: false)
assert_nothing_raised do
smtp.start{}
end

port = fake_server_start(tls: false)
smtp = Net::SMTP.new('localhost', port, tls: false)
assert_nothing_raised do
smtp.start{}
end
end

def test_with_starttls_always
port = fake_server_start(starttls: true)
smtp = Net::SMTP.new('localhost', port, starttls: :always, tls_verify: false)
smtp.start{}
assert_equal(true, @starttls_started)

port = fake_server_start(starttls: false)
smtp = Net::SMTP.new('localhost', port, starttls: :always, tls_verify: false)
assert_raise Net::SMTPUnsupportedCommand do
smtp.start{}
end
end

def test_with_starttls_auto
port = fake_server_start(starttls: true)
smtp = Net::SMTP.new('localhost', port, starttls: :auto, tls_verify: false)
smtp.start{}
assert_equal(true, @starttls_started)

port = fake_server_start(starttls: false)
smtp = Net::SMTP.new('localhost', port, starttls: :auto, tls_verify: false)
smtp.start{}
assert_equal(false, @starttls_started)
end

def test_with_starttls_false
port = fake_server_start(starttls: true)
smtp = Net::SMTP.new('localhost', port, starttls: false, tls_verify: false)
smtp.start{}
assert_equal(false, @starttls_started)

port = fake_server_start(starttls: false)
smtp = Net::SMTP.new('localhost', port, starttls: false, tls_verify: false)
smtp.start{}
assert_equal(false, @starttls_started)
end
end

def test_start
port = fake_server_start
smtp = Net::SMTP.start('localhost', port)
Expand Down Expand Up @@ -318,6 +371,51 @@ def test_start_invalid_number_of_arguments
assert_equal('wrong number of arguments (given 7, expected 1..6)', err.message)
end

if defined? OpenSSL::VERSION
def test_start_with_tls
port = fake_server_start(tls: true)
assert_nothing_raised do
Net::SMTP.start('localhost', port, tls: true, tls_verify: false){}
end

port = fake_server_start(tls: false)
assert_nothing_raised do
Net::SMTP.start('localhost', port, tls: false){}
end
end

def test_start_with_starttls_always
port = fake_server_start(starttls: true)
Net::SMTP.start('localhost', port, starttls: :always, tls_verify: false){}
assert_equal(true, @starttls_started)

port = fake_server_start(starttls: false)
assert_raise Net::SMTPUnsupportedCommand do
Net::SMTP.start('localhost', port, starttls: :always, tls_verify: false){}
end
end

def test_start_with_starttls_auto
port = fake_server_start(starttls: true)
Net::SMTP.start('localhost', port, starttls: :auto, tls_verify: false){}
assert_equal(true, @starttls_started)

port = fake_server_start(starttls: false)
Net::SMTP.start('localhost', port, starttls: :auto, tls_verify: false){}
assert_equal(false, @starttls_started)
end

def test_start_with_starttls_false
port = fake_server_start(starttls: true)
Net::SMTP.start('localhost', port, starttls: false, tls_verify: false){}
assert_equal(false, @starttls_started)

port = fake_server_start(starttls: false)
Net::SMTP.start('localhost', port, starttls: false, tls_verify: false){}
assert_equal(false, @starttls_started)
end
end

def test_start_instance
port = fake_server_start
smtp = Net::SMTP.new('localhost', port)
Expand Down Expand Up @@ -360,23 +458,58 @@ def accept(servers)
Socket.accept_loop(servers) { |s, _| break s }
end

def fake_server_start(helo: 'localhost', user: nil, password: nil)
def fake_server_start(helo: 'localhost', user: nil, password: nil, tls: false, starttls: false)
@starttls_started = false
servers = Socket.tcp_server_sockets('localhost', 0)
@server_threads << Thread.start do
Thread.current.abort_on_exception = true
sock = accept(servers)
if tls || starttls
ctx = OpenSSL::SSL::SSLContext.new
ctx.ca_file = CA_FILE
ctx.key = File.open(SERVER_KEY){|f| OpenSSL::PKey::RSA.new(f)}
ctx.cert = File.open(SERVER_CERT){|f| OpenSSL::X509::Certificate.new(f)}
end
if tls
sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
sock.sync_close = true
sock.accept
end
sock.puts "220 ready\r\n"
assert_equal("EHLO #{helo}\r\n", sock.gets)
sock.puts "220-servername\r\n220 AUTH PLAIN\r\n"
if user
credential = ["\0#{user}\0#{password}"].pack('m0')
assert_equal("AUTH PLAIN #{credential}\r\n", sock.gets)
sock.puts "235 2.7.0 Authentication successful\r\n"
while comm = sock.gets
case comm.chomp
when /\AEHLO /
assert_equal(helo, comm.split[1])
sock.puts "220-servername\r\n"
sock.puts "220-STARTTLS\r\n" if starttls
sock.puts "220 AUTH PLAIN\r\n"
when "STARTTLS"
unless starttls
sock.puts "502 5.5.1 Error: command not implemented\r\n"
next
end
sock.puts "220 2.0.0 Ready to start TLS\r\n"
sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
sock.sync_close = true
sock.accept
@starttls_started = true
when /\AAUTH PLAIN /
unless user
sock.puts "503 5.5.1 Error: authentication not enabled\r\n"
next
end
credential = ["\0#{user}\0#{password}"].pack('m0')
assert_equal(credential, comm.split[2])
sock.puts "235 2.7.0 Authentication successful\r\n"
when "QUIT"
sock.puts "221 2.0.0 Bye\r\n"
sock.close
servers.each(&:close)
break
else
sock.puts "502 5.5.2 Error: command not recognized\r\n"
end
end
assert_equal("QUIT\r\n", sock.gets)
sock.puts "221 2.0.0 Bye\r\n"
sock.close
servers.each(&:close)
end
port = servers[0].local_address.ip_port
return port
Expand Down
Loading

0 comments on commit a4cd82a

Please sign in to comment.