Skip to content

Commit a4cd82a

Browse files
authored
Merge pull request #31 from tmtm/add_tls_and_starttls_arguments
Add tls and starttls arguments
2 parents 8354c45 + fed2a9d commit a4cd82a

4 files changed

Lines changed: 204 additions & 43 deletions

File tree

NEWS.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# NEWS
22

3+
## Version 0.3.0
4+
5+
### Improvements
6+
7+
* Add `tls`, `starttls` keyword arguments.
8+
```
9+
# always use TLS connection for port 465.
10+
Net::SMTP.start(hostname, 465, tls: true)
11+
12+
# do not use starttls for localhost
13+
Net::SMTP.start('localhost', starttls: false)
14+
```
15+
16+
### Incompatible changes
17+
18+
* The tls_* paramter has been moved from start() to initialize().
19+
320
## Version 0.2.2 (2021-10-09)
421

522
* Add `response` to SMTPError exceptions.

lib/net/smtp.rb

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class SMTPUnsupportedCommand < ProtocolError
179179
# user: 'Your Account', secret: 'Your Password', authtype: :cram_md5)
180180
#
181181
class SMTP < Protocol
182-
VERSION = "0.2.2"
182+
VERSION = "0.3.0"
183183

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

@@ -215,11 +215,23 @@ def SMTP.default_ssl_context(ssl_context_params = nil)
215215
# server. +port+ is the port to connect to; it defaults to
216216
# port 25.
217217
#
218+
# If +tls+ is true, enable TLS. The default is false.
219+
# If +starttls+ is :always, enable STARTTLS, if +:auto+, use STARTTLS when the server supports it,
220+
# if false, disable STARTTLS.
221+
#
222+
# If +tls_verify+ is true, verify the server's certificate. The default is true.
223+
# If the hostname in the server certificate is different from +address+,
224+
# it can be specified with +tls_hostname+.
225+
#
226+
# Additional SSLContext params can be added to +ssl_context_params+ hash argument and are passed to
227+
# +OpenSSL::SSL::SSLContext#set_params+
228+
#
229+
# +tls_verify: true+ is equivalent to +ssl_context_params: { verify_mode: OpenSSL::SSL::VERIFY_PEER }+.
218230
# This method does not open the TCP connection. You can use
219231
# SMTP.start instead of SMTP.new if you want to do everything
220232
# at once. Otherwise, follow SMTP.new with SMTP#start.
221233
#
222-
def initialize(address, port = nil)
234+
def initialize(address, port = nil, tls: false, starttls: :auto, tls_verify: true, tls_hostname: nil, ssl_context_params: nil)
223235
@address = address
224236
@port = (port || SMTP.default_port)
225237
@esmtp = true
@@ -230,10 +242,13 @@ def initialize(address, port = nil)
230242
@read_timeout = 60
231243
@error_occurred = false
232244
@debug_output = nil
233-
@tls = false
234-
@starttls = :auto
245+
@tls = tls
246+
@starttls = starttls
235247
@ssl_context_tls = nil
236248
@ssl_context_starttls = nil
249+
@tls_verify = tls_verify
250+
@tls_hostname = tls_hostname
251+
@ssl_context_params = ssl_context_params
237252
end
238253

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

418433
#
419434
# :call-seq:
420-
# start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil, ssl_context_params: nil) { |smtp| ... }
435+
# 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| ... }
421436
# start(address, port = nil, helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... }
422437
#
423438
# Creates a new Net::SMTP object and connects to the server.
@@ -454,6 +469,11 @@ def debug_output=(arg)
454469
# or other authentication token; and +authtype+ is the authentication
455470
# type, one of :plain, :login, or :cram_md5. See the discussion of
456471
# SMTP Authentication in the overview notes.
472+
#
473+
# If +tls+ is true, enable TLS. The default is false.
474+
# If +starttls+ is :always, enable STARTTLS, if +:auto+, use STARTTLS when the server supports it,
475+
# if false, disable STARTTLS.
476+
#
457477
# If +tls_verify+ is true, verify the server's certificate. The default is true.
458478
# If the hostname in the server certificate is different from +address+,
459479
# it can be specified with +tls_hostname+.
@@ -478,14 +498,15 @@ def debug_output=(arg)
478498
#
479499
def SMTP.start(address, port = nil, *args, helo: nil,
480500
user: nil, secret: nil, password: nil, authtype: nil,
501+
tls: false, starttls: :auto,
481502
tls_verify: true, tls_hostname: nil, ssl_context_params: nil,
482503
&block)
483504
raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 1..6)" if args.size > 4
484505
helo ||= args[0] || 'localhost'
485506
user ||= args[1]
486507
secret ||= password || args[2]
487508
authtype ||= args[3]
488-
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)
509+
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)
489510
end
490511

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

496517
#
497518
# :call-seq:
498-
# start(helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil, ssl_context_params: nil) { |smtp| ... }
519+
# start(helo: 'localhost', user: nil, secret: nil, authtype: nil) { |smtp| ... }
499520
# start(helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... }
500521
#
501522
# Opens a TCP connection and starts the SMTP session.
@@ -510,14 +531,6 @@ def started?
510531
# the type of authentication to attempt; it must be one of
511532
# :login, :plain, and :cram_md5. See the notes on SMTP Authentication
512533
# in the overview.
513-
# If +tls_verify+ is true, verify the server's certificate. The default is true.
514-
# If the hostname in the server certificate is different from +address+,
515-
# it can be specified with +tls_hostname+.
516-
#
517-
# Additional SSLContext params can be added to +ssl_context_params+ hash argument and are passed to
518-
# +OpenSSL::SSL::SSLContext#set_params+
519-
#
520-
# +tls_verify: true+ is equivalent to +ssl_context_params: { verify_mode: OpenSSL::SSL::VERIFY_PEER }+.
521534
#
522535
# === Block Usage
523536
#
@@ -556,25 +569,23 @@ def started?
556569
# * Net::ReadTimeout
557570
# * IOError
558571
#
559-
def start(*args, helo: nil,
560-
user: nil, secret: nil, password: nil, authtype: nil, tls_verify: true, tls_hostname: nil, ssl_context_params: nil)
572+
def start(*args, helo: nil, user: nil, secret: nil, password: nil, authtype: nil)
561573
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..4)" if args.size > 4
562574
helo ||= args[0] || 'localhost'
563575
user ||= args[1]
564576
secret ||= password || args[2]
565577
authtype ||= args[3]
566578
if defined?(OpenSSL::VERSION)
567-
ssl_context_params = ssl_context_params ? ssl_context_params : {}
579+
ssl_context_params = @ssl_context_params || {}
568580
unless ssl_context_params.has_key?(:verify_mode)
569-
ssl_context_params[:verify_mode] = tls_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
581+
ssl_context_params[:verify_mode] = @tls_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
570582
end
571583
if @tls && @ssl_context_tls.nil?
572584
@ssl_context_tls = SMTP.default_ssl_context(ssl_context_params)
573585
end
574586
if @starttls && @ssl_context_starttls.nil?
575587
@ssl_context_starttls = SMTP.default_ssl_context(ssl_context_params)
576588
end
577-
@tls_hostname = tls_hostname
578589
end
579590
if block_given?
580591
begin

test/net/smtp/test_smtp.rb

Lines changed: 146 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,10 @@ def test_tls_connect
234234
sock.gets
235235
sock.write("221 localhost Service closing transmission channel\r\n")
236236
end
237-
smtp = Net::SMTP.new("localhost", servers[0].local_address.ip_port)
237+
smtp = Net::SMTP.new("localhost", servers[0].local_address.ip_port, tls_verify: false)
238238
smtp.enable_tls
239239
smtp.open_timeout = 1
240-
smtp.start(tls_verify: false) do
240+
smtp.start do
241241
end
242242
ensure
243243
sock&.close
@@ -287,6 +287,59 @@ def test_eof_error_backtrace
287287
end
288288
end
289289

290+
if defined? OpenSSL::VERSION
291+
def test_with_tls
292+
port = fake_server_start(tls: true)
293+
smtp = Net::SMTP.new('localhost', port, tls: true, tls_verify: false)
294+
assert_nothing_raised do
295+
smtp.start{}
296+
end
297+
298+
port = fake_server_start(tls: false)
299+
smtp = Net::SMTP.new('localhost', port, tls: false)
300+
assert_nothing_raised do
301+
smtp.start{}
302+
end
303+
end
304+
305+
def test_with_starttls_always
306+
port = fake_server_start(starttls: true)
307+
smtp = Net::SMTP.new('localhost', port, starttls: :always, tls_verify: false)
308+
smtp.start{}
309+
assert_equal(true, @starttls_started)
310+
311+
port = fake_server_start(starttls: false)
312+
smtp = Net::SMTP.new('localhost', port, starttls: :always, tls_verify: false)
313+
assert_raise Net::SMTPUnsupportedCommand do
314+
smtp.start{}
315+
end
316+
end
317+
318+
def test_with_starttls_auto
319+
port = fake_server_start(starttls: true)
320+
smtp = Net::SMTP.new('localhost', port, starttls: :auto, tls_verify: false)
321+
smtp.start{}
322+
assert_equal(true, @starttls_started)
323+
324+
port = fake_server_start(starttls: false)
325+
smtp = Net::SMTP.new('localhost', port, starttls: :auto, tls_verify: false)
326+
smtp.start{}
327+
assert_equal(false, @starttls_started)
328+
end
329+
330+
def test_with_starttls_false
331+
port = fake_server_start(starttls: true)
332+
smtp = Net::SMTP.new('localhost', port, starttls: false, tls_verify: false)
333+
smtp.start{}
334+
assert_equal(false, @starttls_started)
335+
336+
port = fake_server_start(starttls: false)
337+
smtp = Net::SMTP.new('localhost', port, starttls: false, tls_verify: false)
338+
smtp.start{}
339+
assert_equal(false, @starttls_started)
340+
end
341+
end
342+
290343
def test_start
291344
port = fake_server_start
292345
smtp = Net::SMTP.start('localhost', port)
@@ -318,6 +371,51 @@ def test_start_invalid_number_of_arguments
318371
assert_equal('wrong number of arguments (given 7, expected 1..6)', err.message)
319372
end
320373

374+
if defined? OpenSSL::VERSION
375+
def test_start_with_tls
376+
port = fake_server_start(tls: true)
377+
assert_nothing_raised do
378+
Net::SMTP.start('localhost', port, tls: true, tls_verify: false){}
379+
end
380+
381+
port = fake_server_start(tls: false)
382+
assert_nothing_raised do
383+
Net::SMTP.start('localhost', port, tls: false){}
384+
end
385+
end
386+
387+
def test_start_with_starttls_always
388+
port = fake_server_start(starttls: true)
389+
Net::SMTP.start('localhost', port, starttls: :always, tls_verify: false){}
390+
assert_equal(true, @starttls_started)
391+
392+
port = fake_server_start(starttls: false)
393+
assert_raise Net::SMTPUnsupportedCommand do
394+
Net::SMTP.start('localhost', port, starttls: :always, tls_verify: false){}
395+
end
396+
end
397+
398+
def test_start_with_starttls_auto
399+
port = fake_server_start(starttls: true)
400+
Net::SMTP.start('localhost', port, starttls: :auto, tls_verify: false){}
401+
assert_equal(true, @starttls_started)
402+
403+
port = fake_server_start(starttls: false)
404+
Net::SMTP.start('localhost', port, starttls: :auto, tls_verify: false){}
405+
assert_equal(false, @starttls_started)
406+
end
407+
408+
def test_start_with_starttls_false
409+
port = fake_server_start(starttls: true)
410+
Net::SMTP.start('localhost', port, starttls: false, tls_verify: false){}
411+
assert_equal(false, @starttls_started)
412+
413+
port = fake_server_start(starttls: false)
414+
Net::SMTP.start('localhost', port, starttls: false, tls_verify: false){}
415+
assert_equal(false, @starttls_started)
416+
end
417+
end
418+
321419
def test_start_instance
322420
port = fake_server_start
323421
smtp = Net::SMTP.new('localhost', port)
@@ -360,23 +458,58 @@ def accept(servers)
360458
Socket.accept_loop(servers) { |s, _| break s }
361459
end
362460

363-
def fake_server_start(helo: 'localhost', user: nil, password: nil)
461+
def fake_server_start(helo: 'localhost', user: nil, password: nil, tls: false, starttls: false)
462+
@starttls_started = false
364463
servers = Socket.tcp_server_sockets('localhost', 0)
365464
@server_threads << Thread.start do
366465
Thread.current.abort_on_exception = true
367466
sock = accept(servers)
467+
if tls || starttls
468+
ctx = OpenSSL::SSL::SSLContext.new
469+
ctx.ca_file = CA_FILE
470+
ctx.key = File.open(SERVER_KEY){|f| OpenSSL::PKey::RSA.new(f)}
471+
ctx.cert = File.open(SERVER_CERT){|f| OpenSSL::X509::Certificate.new(f)}
472+
end
473+
if tls
474+
sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
475+
sock.sync_close = true
476+
sock.accept
477+
end
368478
sock.puts "220 ready\r\n"
369-
assert_equal("EHLO #{helo}\r\n", sock.gets)
370-
sock.puts "220-servername\r\n220 AUTH PLAIN\r\n"
371-
if user
372-
credential = ["\0#{user}\0#{password}"].pack('m0')
373-
assert_equal("AUTH PLAIN #{credential}\r\n", sock.gets)
374-
sock.puts "235 2.7.0 Authentication successful\r\n"
479+
while comm = sock.gets
480+
case comm.chomp
481+
when /\AEHLO /
482+
assert_equal(helo, comm.split[1])
483+
sock.puts "220-servername\r\n"
484+
sock.puts "220-STARTTLS\r\n" if starttls
485+
sock.puts "220 AUTH PLAIN\r\n"
486+
when "STARTTLS"
487+
unless starttls
488+
sock.puts "502 5.5.1 Error: command not implemented\r\n"
489+
next
490+
end
491+
sock.puts "220 2.0.0 Ready to start TLS\r\n"
492+
sock = OpenSSL::SSL::SSLSocket.new(sock, ctx)
493+
sock.sync_close = true
494+
sock.accept
495+
@starttls_started = true
496+
when /\AAUTH PLAIN /
497+
unless user
498+
sock.puts "503 5.5.1 Error: authentication not enabled\r\n"
499+
next
500+
end
501+
credential = ["\0#{user}\0#{password}"].pack('m0')
502+
assert_equal(credential, comm.split[2])
503+
sock.puts "235 2.7.0 Authentication successful\r\n"
504+
when "QUIT"
505+
sock.puts "221 2.0.0 Bye\r\n"
506+
sock.close
507+
servers.each(&:close)
508+
break
509+
else
510+
sock.puts "502 5.5.2 Error: command not recognized\r\n"
511+
end
375512
end
376-
assert_equal("QUIT\r\n", sock.gets)
377-
sock.puts "221 2.0.0 Bye\r\n"
378-
sock.close
379-
servers.each(&:close)
380513
end
381514
port = servers[0].local_address.ip_port
382515
return port

0 commit comments

Comments
 (0)