Skip to content

Commit cada6d8

Browse files
committed
Import net-smtp-0.2.0 from https://github.com/ruby/net-smtp
1 parent fcc88da commit cada6d8

File tree

4 files changed

+297
-28
lines changed

4 files changed

+297
-28
lines changed

lib/net/smtp.rb

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ class SMTPUnsupportedCommand < ProtocolError
168168
# user: 'Your Account', secret: 'Your Password', authtype: :cram_md5)
169169
#
170170
class SMTP < Protocol
171-
VERSION = "0.1.0"
171+
VERSION = "0.2.0"
172172

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

@@ -191,8 +191,13 @@ class << self
191191
alias default_ssl_port default_tls_port
192192
end
193193

194-
def SMTP.default_ssl_context
195-
OpenSSL::SSL::SSLContext.new
194+
def SMTP.default_ssl_context(verify_peer=true)
195+
context = OpenSSL::SSL::SSLContext.new
196+
context.verify_mode = verify_peer ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
197+
store = OpenSSL::X509::Store.new
198+
store.set_default_paths
199+
context.cert_store = store
200+
context
196201
end
197202

198203
#
@@ -218,8 +223,9 @@ def initialize(address, port = nil)
218223
@error_occurred = false
219224
@debug_output = nil
220225
@tls = false
221-
@starttls = false
222-
@ssl_context = nil
226+
@starttls = :auto
227+
@ssl_context_tls = nil
228+
@ssl_context_starttls = nil
223229
end
224230

225231
# Provide human-readable stringification of class state.
@@ -294,11 +300,11 @@ def tls?
294300
# Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for
295301
# this object. Must be called before the connection is established
296302
# to have any effect. +context+ is a OpenSSL::SSL::SSLContext object.
297-
def enable_tls(context = SMTP.default_ssl_context)
303+
def enable_tls(context = nil)
298304
raise 'openssl library not installed' unless defined?(OpenSSL)
299-
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls
305+
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls == :always
300306
@tls = true
301-
@ssl_context = context
307+
@ssl_context_tls = context
302308
end
303309

304310
alias enable_ssl enable_tls
@@ -307,7 +313,7 @@ def enable_tls(context = SMTP.default_ssl_context)
307313
# connection is established to have any effect.
308314
def disable_tls
309315
@tls = false
310-
@ssl_context = nil
316+
@ssl_context_tls = nil
311317
end
312318

313319
alias disable_ssl disable_tls
@@ -331,27 +337,27 @@ def starttls_auto?
331337

332338
# Enables SMTP/TLS (STARTTLS) for this object.
333339
# +context+ is a OpenSSL::SSL::SSLContext object.
334-
def enable_starttls(context = SMTP.default_ssl_context)
340+
def enable_starttls(context = nil)
335341
raise 'openssl library not installed' unless defined?(OpenSSL)
336342
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
337343
@starttls = :always
338-
@ssl_context = context
344+
@ssl_context_starttls = context
339345
end
340346

341347
# Enables SMTP/TLS (STARTTLS) for this object if server accepts.
342348
# +context+ is a OpenSSL::SSL::SSLContext object.
343-
def enable_starttls_auto(context = SMTP.default_ssl_context)
349+
def enable_starttls_auto(context = nil)
344350
raise 'openssl library not installed' unless defined?(OpenSSL)
345351
raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls
346352
@starttls = :auto
347-
@ssl_context = context
353+
@ssl_context_starttls = context
348354
end
349355

350356
# Disables SMTP/TLS (STARTTLS) for this object. Must be called
351357
# before the connection is established to have any effect.
352358
def disable_starttls
353359
@starttls = false
354-
@ssl_context = nil
360+
@ssl_context_starttls = nil
355361
end
356362

357363
# The address of the SMTP server to connect to.
@@ -403,14 +409,14 @@ def debug_output=(arg)
403409

404410
#
405411
# :call-seq:
406-
# start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil) { |smtp| ... }
412+
# start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil) { |smtp| ... }
407413
# start(address, port = nil, helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... }
408414
#
409415
# Creates a new Net::SMTP object and connects to the server.
410416
#
411417
# This method is equivalent to:
412418
#
413-
# Net::SMTP.new(address, port).start(helo: helo_domain, user: account, secret: password, authtype: authtype)
419+
# Net::SMTP.new(address, port).start(helo: helo_domain, user: account, secret: password, authtype: authtype, tls_verify: flag, tls_hostname: hostname)
414420
#
415421
# === Example
416422
#
@@ -440,6 +446,9 @@ def debug_output=(arg)
440446
# or other authentication token; and +authtype+ is the authentication
441447
# type, one of :plain, :login, or :cram_md5. See the discussion of
442448
# SMTP Authentication in the overview notes.
449+
# If +tls_verify+ is true, verify the server's certificate. The default is true.
450+
# If the hostname in the server certificate is different from +address+,
451+
# it can be specified with +tls_hostname+.
443452
#
444453
# === Errors
445454
#
@@ -456,13 +465,14 @@ def debug_output=(arg)
456465
#
457466
def SMTP.start(address, port = nil, *args, helo: nil,
458467
user: nil, secret: nil, password: nil, authtype: nil,
468+
tls_verify: true, tls_hostname: nil,
459469
&block)
460470
raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 1..6)" if args.size > 4
461471
helo ||= args[0] || 'localhost'
462472
user ||= args[1]
463473
secret ||= password || args[2]
464474
authtype ||= args[3]
465-
new(address, port).start(helo: helo, user: user, secret: secret, authtype: authtype, &block)
475+
new(address, port).start(helo: helo, user: user, secret: secret, authtype: authtype, tls_verify: tls_verify, tls_hostname: tls_hostname, &block)
466476
end
467477

468478
# +true+ if the SMTP session has been started.
@@ -472,7 +482,7 @@ def started?
472482

473483
#
474484
# :call-seq:
475-
# start(helo: 'localhost', user: nil, secret: nil, authtype: nil) { |smtp| ... }
485+
# start(helo: 'localhost', user: nil, secret: nil, authtype: nil, tls_verify: true, tls_hostname: nil) { |smtp| ... }
476486
# start(helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... }
477487
#
478488
# Opens a TCP connection and starts the SMTP session.
@@ -487,6 +497,9 @@ def started?
487497
# the type of authentication to attempt; it must be one of
488498
# :login, :plain, and :cram_md5. See the notes on SMTP Authentication
489499
# in the overview.
500+
# If +tls_verify+ is true, verify the server's certificate. The default is true.
501+
# If the hostname in the server certificate is different from +address+,
502+
# it can be specified with +tls_hostname+.
490503
#
491504
# === Block Usage
492505
#
@@ -526,12 +539,19 @@ def started?
526539
# * IOError
527540
#
528541
def start(*args, helo: nil,
529-
user: nil, secret: nil, password: nil, authtype: nil)
542+
user: nil, secret: nil, password: nil, authtype: nil, tls_verify: true, tls_hostname: nil)
530543
raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..4)" if args.size > 4
531544
helo ||= args[0] || 'localhost'
532545
user ||= args[1]
533546
secret ||= password || args[2]
534547
authtype ||= args[3]
548+
if @tls && @ssl_context_tls.nil?
549+
@ssl_context_tls = SMTP.default_ssl_context(tls_verify)
550+
end
551+
if @starttls && @ssl_context_starttls.nil?
552+
@ssl_context_starttls = SMTP.default_ssl_context(tls_verify)
553+
end
554+
@tls_hostname = tls_hostname
535555
if block_given?
536556
begin
537557
do_start helo, user, secret, authtype
@@ -568,16 +588,16 @@ def do_start(helo_domain, user, secret, authtype)
568588
tcp_socket(@address, @port)
569589
end
570590
logging "Connection opened: #{@address}:#{@port}"
571-
@socket = new_internet_message_io(tls? ? tlsconnect(s) : s)
591+
@socket = new_internet_message_io(tls? ? tlsconnect(s, @ssl_context_tls) : s)
572592
check_response critical { recv_response() }
573593
do_helo helo_domain
574-
if starttls_always? or (capable_starttls? and starttls_auto?)
594+
if ! tls? and (starttls_always? or (capable_starttls? and starttls_auto?))
575595
unless capable_starttls?
576596
raise SMTPUnsupportedCommand,
577597
"STARTTLS is not supported on this server"
578598
end
579599
starttls
580-
@socket = new_internet_message_io(tlsconnect(s))
600+
@socket = new_internet_message_io(tlsconnect(s, @ssl_context_starttls))
581601
# helo response may be different after STARTTLS
582602
do_helo helo_domain
583603
end
@@ -595,15 +615,15 @@ def ssl_socket(socket, context)
595615
OpenSSL::SSL::SSLSocket.new socket, context
596616
end
597617

598-
def tlsconnect(s)
618+
def tlsconnect(s, context)
599619
verified = false
600-
s = ssl_socket(s, @ssl_context)
620+
s = ssl_socket(s, context)
601621
logging "TLS connection started"
602622
s.sync_close = true
603-
s.hostname = @address if s.respond_to? :hostname=
623+
s.hostname = @tls_hostname || @address if s.respond_to? :hostname=
604624
ssl_socket_connect(s, @open_timeout)
605-
if @ssl_context.verify_mode && @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
606-
s.post_connection_check(@address)
625+
if context.verify_mode && context.verify_mode != OpenSSL::SSL::VERIFY_NONE
626+
s.post_connection_check(@tls_hostname || @address)
607627
end
608628
verified = true
609629
s

test/net/smtp/test_smtp.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def test_tls_connect
137137
smtp = Net::SMTP.new("localhost", servers[0].local_address.ip_port)
138138
smtp.enable_tls
139139
smtp.open_timeout = 1
140-
smtp.start do
140+
smtp.start(tls_verify: false) do
141141
end
142142
ensure
143143
sock.close if sock

test/net/smtp/test_sslcontext.rb

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
require 'net/smtp'
2+
require 'test/unit'
3+
4+
module Net
5+
class TestSSLContext < Test::Unit::TestCase
6+
class MySMTP < SMTP
7+
attr_reader :__ssl_context, :__tls_hostname
8+
9+
def initialize(socket)
10+
@fake_socket = socket
11+
super("smtp.example.com")
12+
end
13+
14+
def tcp_socket(*)
15+
@fake_socket
16+
end
17+
18+
def ssl_socket_connect(*)
19+
end
20+
21+
def tlsconnect(*)
22+
super
23+
@fake_socket
24+
end
25+
26+
def ssl_socket(socket, context)
27+
@__ssl_context = context
28+
s = super
29+
hostname = @__tls_hostname = ''
30+
s.define_singleton_method(:post_connection_check){ |name| hostname.replace(name) }
31+
s
32+
end
33+
end
34+
35+
def teardown
36+
@server_thread&.exit
37+
@server_socket&.close
38+
@client_socket&.close
39+
end
40+
41+
def start_smtpd(starttls)
42+
@server_socket, @client_socket = UNIXSocket.pair
43+
@starttls_executed = false
44+
@server_thread = Thread.new(@server_socket) do |s|
45+
s.puts "220 fakeserver\r\n"
46+
while cmd = s.gets&.chomp
47+
case cmd
48+
when /\AEHLO /
49+
s.puts "250-fakeserver\r\n"
50+
s.puts "250-STARTTLS\r\n" if starttls
51+
s.puts "250 8BITMIME\r\n"
52+
when /\ASTARTTLS/
53+
@starttls_executed = true
54+
s.puts "220 2.0.0 Ready to start TLS\r\n"
55+
else
56+
raise "unsupported command: #{cmd}"
57+
end
58+
end
59+
end
60+
@client_socket
61+
end
62+
63+
def test_default
64+
smtp = MySMTP.new(start_smtpd(true))
65+
smtp.start
66+
assert_equal(OpenSSL::SSL::VERIFY_PEER, smtp.__ssl_context.verify_mode)
67+
end
68+
69+
def test_enable_tls
70+
smtp = MySMTP.new(start_smtpd(true))
71+
context = OpenSSL::SSL::SSLContext.new
72+
smtp.enable_tls(context)
73+
smtp.start
74+
assert_equal(context, smtp.__ssl_context)
75+
end
76+
77+
def test_enable_tls_before_disable_starttls
78+
smtp = MySMTP.new(start_smtpd(true))
79+
context = OpenSSL::SSL::SSLContext.new
80+
smtp.enable_tls(context)
81+
smtp.disable_starttls
82+
smtp.start
83+
assert_equal(context, smtp.__ssl_context)
84+
end
85+
86+
def test_enable_starttls
87+
smtp = MySMTP.new(start_smtpd(true))
88+
context = OpenSSL::SSL::SSLContext.new
89+
smtp.enable_starttls(context)
90+
smtp.start
91+
assert_equal(context, smtp.__ssl_context)
92+
end
93+
94+
def test_enable_starttls_before_disable_tls
95+
smtp = MySMTP.new(start_smtpd(true))
96+
context = OpenSSL::SSL::SSLContext.new
97+
smtp.enable_starttls(context)
98+
smtp.disable_tls
99+
smtp.start
100+
assert_equal(context, smtp.__ssl_context)
101+
end
102+
103+
def test_start_with_tls_verify_true
104+
smtp = MySMTP.new(start_smtpd(true))
105+
smtp.start(tls_verify: true)
106+
assert_equal(OpenSSL::SSL::VERIFY_PEER, smtp.__ssl_context.verify_mode)
107+
end
108+
109+
def test_start_with_tls_verify_false
110+
smtp = MySMTP.new(start_smtpd(true))
111+
smtp.start(tls_verify: false)
112+
assert_equal(OpenSSL::SSL::VERIFY_NONE, smtp.__ssl_context.verify_mode)
113+
end
114+
115+
def test_start_with_tls_hostname
116+
smtp = MySMTP.new(start_smtpd(true))
117+
smtp.start(tls_hostname: "localhost")
118+
assert_equal("localhost", smtp.__tls_hostname)
119+
end
120+
121+
def test_start_without_tls_hostname
122+
smtp = MySMTP.new(start_smtpd(true))
123+
smtp.start
124+
assert_equal("smtp.example.com", smtp.__tls_hostname)
125+
end
126+
127+
end
128+
end

0 commit comments

Comments
 (0)