Skip to content

Commit 546dbf8

Browse files
committed
Config smtp-delivery via url
1 parent 800022a commit 546dbf8

File tree

4 files changed

+202
-2
lines changed

4 files changed

+202
-2
lines changed

CHANGELOG.rdoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Features:
1010

1111
* Updated README to improve around sending multipart mail @kapfenho
1212
* Add delivery_interceptors method to Mail class to fetch registered interceptors @ghousemohamed
13+
* Configure smtp-delivery using a url @eval
1314

1415
Code Improvements:
1516

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ mail.delivery_method :sendmail
277277
mail.deliver
278278
```
279279

280+
#### Sending via SMTP
281+
280282
Sending via smtp (for example to [mailcatcher](https://github.com/sj26/mailcatcher))
281283
```ruby
282284

@@ -285,6 +287,46 @@ Mail.defaults do
285287
end
286288
```
287289

290+
A url is also accepted:
291+
```ruby
292+
Mail.defaults do
293+
# The following (note the URL-encoded userinfo)...:
294+
delivery_method :smtp, url: 'smtp://user%40gmail.com:[email protected]'
295+
296+
# ...would be equivalent to:
297+
delivery_method :smtp, { :address => 'smtp.gmail.com',
298+
:port => 587,
299+
:user_name => '[email protected]',
300+
:password => 'app-password',
301+
:tls => false,
302+
:enable_starttls_auto => false,
303+
:enable_starttls => true }
304+
305+
# For localhost (or 127.0.0.1) it assumes no tls, nor starttls.
306+
# So the following would work using [mailcatcher](https://github.com/sj26/mailcatcher):
307+
delivery_method :smtp, url: 'smtp://127.0.0.1:1025'
308+
309+
# Scheme 'smtps' enables `tls` and uses port 465:
310+
delivery_method :smtp, url: 'smtps://user%40gmail.com:[email protected]'
311+
312+
# Attributes provided via the url take precedence over other provided attributes.
313+
# So in the following example, `address` will be set to 'smtp.gmail.com',
314+
# and `authentication` will be set to 'login':
315+
delivery_method :smtp, url: 'smtps://user%40gmail.com:[email protected]',
316+
address: 'ignored.org',
317+
authentication: 'login'
318+
319+
# Attributes can also be provided via the query string of the url.
320+
# So in the following example `authentication` will be set to 'login':
321+
delivery_method :smtp, url: 'smtps://user:[email protected]?authentication=login'
322+
323+
# Typically you would provide the url via an environment variable:
324+
delivery_method :smtp, url: ENV["MY_APP_SMTP_URL"]
325+
end
326+
```
327+
328+
NOTE attributes extracted from the url take precedence. So adding an 'address' attribute in the example above would have no effect.
329+
288330

289331
Exim requires its own delivery manager, and can be used like so:
290332

lib/mail/network/delivery_methods/smtp.rb

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22
require 'mail/smtp_envelope'
3+
require 'mail/utilities'
34

45
module Mail
56
# == Sending Email with SMTP
@@ -35,8 +36,21 @@ module Mail
3536
# :password => '<password>',
3637
# :authentication => 'plain',
3738
# :enable_starttls_auto => true }
39+
#
40+
# # ...or using the url-attribute (note the trailing 's' in the scheme
41+
# # to set `tls`, also note the URL-encoded userinfo):
42+
# delivery_method :smtp,
43+
# { :url => 'smtp://user%40gmail.com:[email protected]' }
3844
# end
3945
#
46+
# === Sending via Fastmail
47+
#
48+
# Mail.defaults do
49+
# delivery_method :smtp,
50+
# { :url => 'smtps://user%40fastmail.fm:[email protected]' }
51+
# end
52+
#
53+
#
4054
# === Certificate verification
4155
#
4256
# When using TLS, some mail servers provide certificates that are self-signed
@@ -74,6 +88,79 @@ module Mail
7488
#
7589
# mail.deliver!
7690
class SMTP
91+
class UrlResolver
92+
93+
DEFAULTS = {
94+
"smtp" => Hash.new({
95+
:port => 587,
96+
:tls => false,
97+
:enable_starttls => true,
98+
:enable_starttls_auto => false
99+
}).merge({
100+
"localhost" => {
101+
:port => 25,
102+
:tls => false,
103+
:enable_starttls => false,
104+
:enable_starttls_auto => false
105+
},
106+
"127.0.0.1" => {
107+
:port => 25,
108+
:tls => false,
109+
:enable_starttls => false,
110+
:enable_starttls_auto => false
111+
}}),
112+
"smtps" => Hash.new({
113+
:port => 465,
114+
:tls => true,
115+
:enable_starttls => false,
116+
:enable_starttls_auto => false
117+
})
118+
}
119+
120+
def initialize(url)
121+
@uri = url.is_a?(URI) ? url : uri_parser.parse(url)
122+
unless DEFAULTS.has_key?(@uri.scheme)
123+
raise ArgumentError, "#{url} is not a valid SMTP-url. Required format: smtp(s)://host?domain=sender.org"
124+
end
125+
@query = uri.query
126+
end
127+
128+
def to_hash
129+
config = raw_config
130+
config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String }
131+
config
132+
end
133+
134+
private
135+
attr_reader :uri
136+
137+
def raw_config
138+
scheme_defaults.merge(query_hash).merge({
139+
:address => uri.host,
140+
:port => uri.port,
141+
:user_name => uri.user,
142+
:password => uri.password
143+
}.delete_if {|_key, value| Utilities.blank?(value) })
144+
end
145+
146+
def uri_parser
147+
Utilities.uri_parser
148+
end
149+
150+
def query_hash
151+
@query_hash = begin
152+
result = Hash[(@query || "").split("&").map { |pair| k,v = pair.split("="); [k.to_sym, v] }]
153+
result[:open_timeout] &&= result[:open_timeout].to_i
154+
result[:read_timeout] &&= result[:read_timeout].to_i
155+
result
156+
end
157+
end
158+
159+
def scheme_defaults
160+
DEFAULTS[uri.scheme][uri.host]
161+
end
162+
end
163+
77164
attr_accessor :settings
78165

79166
DEFAULTS = {
@@ -92,8 +179,13 @@ class SMTP
92179
:read_timeout => 5
93180
}
94181

95-
def initialize(values)
96-
self.settings = DEFAULTS.merge(values)
182+
def initialize(config)
183+
settings = DEFAULTS.merge(config)
184+
185+
if config.has_key?(:url)
186+
settings = settings.merge(UrlResolver.new(config.delete(:url)).to_hash)
187+
end
188+
self.settings = settings
97189
end
98190

99191
def deliver!(mail)

spec/mail/network/delivery_methods/smtp_spec.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,71 @@
2525
MockSMTP.clear_deliveries
2626
end
2727

28+
describe 'settings via url' do
29+
def smtp_delivery_settings_for_url(url, options = {})
30+
Mail.defaults { delivery_method :smtp, {:url => url}.merge(options) }
31+
32+
Mail.delivery_method.settings
33+
end
34+
35+
it 'provides smtp defaults for localhost' do
36+
expect(smtp_delivery_settings_for_url('smtp://localhost')).to \
37+
include(:port => 25,
38+
:tls => false,
39+
:enable_starttls_auto => false)
40+
end
41+
42+
it 'provides smtp defaults for non-localhost' do
43+
expect(smtp_delivery_settings_for_url('smtp://gmail.com')).to \
44+
include(:port => 587,
45+
:enable_starttls => true,
46+
:enable_starttls_auto => false)
47+
end
48+
49+
it 'provides smtps defaults' do
50+
expect(smtp_delivery_settings_for_url('smtps://gmail.com')).to \
51+
include(:enable_starttls_auto => false,
52+
:port => 465,
53+
:tls => true)
54+
end
55+
56+
it 'allows for all attributes to be set' do
57+
url = 'smtp://mailer.org:12345?domain=sender.org&authentication=login&openssl_verify_mode=peer&tls=true&open_timeout=1&read_timeout=2'
58+
expect(smtp_delivery_settings_for_url(url)).to \
59+
include(:address => 'mailer.org',
60+
:port => 12345,
61+
:domain => 'sender.org',
62+
:authentication => 'login',
63+
:openssl_verify_mode => 'peer',
64+
:tls => 'true',
65+
:open_timeout => 1,
66+
:read_timeout => 2)
67+
end
68+
69+
it 'gives precedence to attributes from the url' do
70+
config = {:address => 'ignored', :domain => 'ignored'}
71+
expect(smtp_delivery_settings_for_url('smtp://user:pw@real-host?domain=real-domain.org', config)).not_to \
72+
include(config)
73+
end
74+
75+
it 'unescapes credentials' do
76+
expect(smtp_delivery_settings_for_url('smtps://user%40host:pw-with-%[email protected]')).to \
77+
include(:user_name => 'user@host',
78+
:password => 'pw-with-/')
79+
end
80+
81+
it 'accepts an URI' do
82+
expect(smtp_delivery_settings_for_url(URI.parse('smtps://host'))).to \
83+
include(:address => 'host')
84+
end
85+
86+
it 'should raise an error for url without smtp(s) scheme' do
87+
expect {
88+
smtp_delivery_settings_for_url('foo://host')
89+
}.to raise_error(/is not a valid SMTP-url/)
90+
end
91+
end
92+
2893
describe "general usage" do
2994
it "should send emails from given settings" do
3095

0 commit comments

Comments
 (0)