Skip to content

Commit 220f87a

Browse files
committed
SMTP: extend dot-stuffing workaround to SMTPConnection delivery method
Refactor SMTP delivery method to use SMTPConnection, ensuring we don't end up with divergent SMTP delivery behaviors. References #683
1 parent 2091e3f commit 220f87a

File tree

3 files changed

+77
-62
lines changed

3 files changed

+77
-62
lines changed

lib/mail/network/delivery_methods/smtp.rb

Lines changed: 56 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -76,79 +76,74 @@ module Mail
7676
class SMTP
7777
attr_accessor :settings
7878

79+
DEFAULTS = {
80+
:address => 'localhost',
81+
:port => 25,
82+
:domain => 'localhost.localdomain',
83+
:user_name => nil,
84+
:password => nil,
85+
:authentication => nil,
86+
:enable_starttls => nil,
87+
:enable_starttls_auto => true,
88+
:openssl_verify_mode => nil,
89+
:ssl => nil,
90+
:tls => nil,
91+
:open_timeout => nil,
92+
:read_timeout => nil
93+
}
94+
7995
def initialize(values)
80-
self.settings = { :address => "localhost",
81-
:port => 25,
82-
:domain => 'localhost.localdomain',
83-
:user_name => nil,
84-
:password => nil,
85-
:authentication => nil,
86-
:enable_starttls => nil,
87-
:enable_starttls_auto => true,
88-
:openssl_verify_mode => nil,
89-
:ssl => nil,
90-
:tls => nil,
91-
:open_timeout => nil,
92-
:read_timeout => nil
93-
}.merge!(values)
96+
self.settings = DEFAULTS.merge(values)
9497
end
9598

96-
# Send the message via SMTP.
97-
# The from and to attributes are optional. If not set, they are retrieve from the Message.
9899
def deliver!(mail)
99-
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)
100-
101-
smtp = Net::SMTP.new(settings[:address], settings[:port])
102-
if settings[:tls] || settings[:ssl]
103-
if smtp.respond_to?(:enable_tls)
104-
smtp.enable_tls(ssl_context)
105-
end
106-
elsif settings[:enable_starttls]
107-
if smtp.respond_to?(:enable_starttls)
108-
smtp.enable_starttls(ssl_context)
109-
end
110-
elsif settings[:enable_starttls_auto]
111-
if smtp.respond_to?(:enable_starttls_auto)
112-
smtp.enable_starttls_auto(ssl_context)
113-
end
100+
response = start_smtp_session do |smtp|
101+
Mail::SMTPConnection.new(:connection => smtp, :return_response => true).deliver!(mail)
114102
end
115-
smtp.open_timeout = settings[:open_timeout] if settings[:open_timeout]
116-
smtp.read_timeout = settings[:read_timeout] if settings[:read_timeout]
117103

118-
response = nil
119-
smtp.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication]) do |smtp_obj|
120-
response = smtp_obj.sendmail(dot_stuff(message), smtp_from, smtp_to)
121-
end
122-
123-
if settings[:return_response]
124-
response
125-
else
126-
self
127-
end
104+
settings[:return_response] ? response : self
128105
end
129106

130107
private
108+
def start_smtp_session(&block)
109+
build_smtp_session.start(settings[:domain], settings[:user_name], settings[:password], settings[:authentication], &block)
110+
end
131111

132-
# This is Net::SMTP's job, but before Ruby 2.x it does not dot-stuff
133-
# an unterminated last line: https://bugs.ruby-lang.org/issues/9627
134-
def dot_stuff(message)
135-
message.gsub(/(\r\n\.)(\r\n|$)/, '\1.\2')
136-
end
137-
138-
# Allow SSL context to be configured via settings, for Ruby >= 1.9
139-
# Just returns openssl verify mode for Ruby 1.8.x
140-
def ssl_context
141-
openssl_verify_mode = settings[:openssl_verify_mode]
112+
def build_smtp_session
113+
Net::SMTP.new(settings[:address], settings[:port]).tap do |smtp|
114+
if settings[:tls] || settings[:ssl]
115+
if smtp.respond_to?(:enable_tls)
116+
smtp.enable_tls(ssl_context)
117+
end
118+
elsif settings[:enable_starttls]
119+
if smtp.respond_to?(:enable_starttls)
120+
smtp.enable_starttls(ssl_context)
121+
end
122+
elsif settings[:enable_starttls_auto]
123+
if smtp.respond_to?(:enable_starttls_auto)
124+
smtp.enable_starttls_auto(ssl_context)
125+
end
126+
end
142127

143-
if openssl_verify_mode.kind_of?(String)
144-
openssl_verify_mode = OpenSSL::SSL.const_get("VERIFY_#{openssl_verify_mode.upcase}")
128+
smtp.open_timeout = settings[:open_timeout] if settings[:open_timeout]
129+
smtp.read_timeout = settings[:read_timeout] if settings[:read_timeout]
130+
end
145131
end
146132

147-
context = Net::SMTP.default_ssl_context
148-
context.verify_mode = openssl_verify_mode if openssl_verify_mode
149-
context.ca_path = settings[:ca_path] if settings[:ca_path]
150-
context.ca_file = settings[:ca_file] if settings[:ca_file]
151-
context
152-
end
133+
# Allow SSL context to be configured via settings, for Ruby >= 1.9
134+
# Just returns openssl verify mode for Ruby 1.8.x
135+
def ssl_context
136+
openssl_verify_mode = settings[:openssl_verify_mode]
137+
138+
if openssl_verify_mode.kind_of?(String)
139+
openssl_verify_mode = OpenSSL::SSL.const_get("VERIFY_#{openssl_verify_mode.upcase}")
140+
end
141+
142+
context = Net::SMTP.default_ssl_context
143+
context.verify_mode = openssl_verify_mode if openssl_verify_mode
144+
context.ca_path = settings[:ca_path] if settings[:ca_path]
145+
context.ca_file = settings[:ca_file] if settings[:ca_file]
146+
context
147+
end
153148
end
154149
end

lib/mail/network/delivery_methods/smtp_connection.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,17 @@ def initialize(values)
5050
# The from and to attributes are optional. If not set, they are retrieve from the Message.
5151
def deliver!(mail)
5252
smtp_from, smtp_to, message = Mail::CheckDeliveryParams.check(mail)
53-
response = smtp.sendmail(message, smtp_from, smtp_to)
53+
54+
response = smtp.sendmail(dot_stuff(message), smtp_from, smtp_to)
5455

5556
settings[:return_response] ? response : self
5657
end
58+
59+
private
60+
# This is Net::SMTP's job, but before Ruby 2.x it does not dot-stuff
61+
# an unterminated last line: https://bugs.ruby-lang.org/issues/9627
62+
def dot_stuff(message)
63+
message.gsub(/(\r\n\.)(\r\n|$)/, '\1.\2')
64+
end
5765
end
5866
end

spec/mail/network/delivery_methods/smtp_connection_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
Mail.delivery_method.smtp.finish
1616
end
1717

18+
it "dot-stuff unterminated last line of the message" do
19+
mail = Mail.deliver do
20+
from 'from@example.com'
21+
to 'to@example.com'
22+
subject 'dot-stuff last line'
23+
body "this is a test\n.\nonly a test\n."
24+
end
25+
26+
message, from, to = MockSMTP.deliveries.first
27+
expect(Mail.new(message).decoded).to eq("this is a test\n..\nonly a test\n..")
28+
end
29+
1830
it "should send an email using open SMTP connection" do
1931
mail = Mail.deliver do
2032
from 'roger@test.lindsaar.net'

0 commit comments

Comments
 (0)