Skip to content

Commit 7360d84

Browse files
committed
Config smtp-delivery via url
1 parent 199a76b commit 7360d84

File tree

4 files changed

+207
-2
lines changed

4 files changed

+207
-2
lines changed

CHANGELOG.rdoc

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

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

1516
Code Improvements:
1617

README.md

Lines changed: 41 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,43 @@ 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 => :always }
303+
304+
# Provide additional settings or overrides via the query string of the url:
305+
delivery_method :smtp, url: '<above-url>?enable_starttls=auto&authentication=login'
306+
307+
# For localhost (or 127.0.0.1) it assumes no tls, nor starttls.
308+
# So the following would work using [mailcatcher](https://github.com/sj26/mailcatcher):
309+
delivery_method :smtp, url: 'smtp://127.0.0.1:1025'
310+
311+
# Scheme 'smtps' enables `tls` and uses port 465:
312+
delivery_method :smtp, url: 'smtps://user%40gmail.com:[email protected]'
313+
314+
# Attributes provided via the url take precedence over other provided attributes.
315+
# So in the following example, `address` will be set to 'smtp.gmail.com',
316+
# and `authentication` will be set to 'login':
317+
delivery_method :smtp, url: 'smtps://user%40gmail.com:[email protected]',
318+
address: 'ignored.org',
319+
authentication: 'login'
320+
321+
# Typically you would provide the url via an environment variable:
322+
delivery_method :smtp, url: ENV["MY_APP_SMTP_URL"]
323+
end
324+
```
325+
326+
#### Sending via Exim
288327

289328
Exim requires its own delivery manager, and can be used like so:
290329

@@ -294,6 +333,8 @@ mail.delivery_method :exim, :location => "/usr/bin/exim"
294333
mail.deliver
295334
```
296335

336+
#### Logging
337+
297338
Mail may be "delivered" to a logfile, too, for development and testing:
298339

299340
```ruby

lib/mail/network/delivery_methods/smtp.rb

Lines changed: 99 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 => 'smtps://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,84 @@ 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 => :always
98+
}).merge({
99+
"localhost" => {
100+
:port => 25,
101+
:tls => false,
102+
:enable_starttls => false
103+
},
104+
"127.0.0.1" => {
105+
:port => 25,
106+
:tls => false,
107+
:enable_starttls => false
108+
}}),
109+
"smtps" => Hash.new({
110+
:port => 465,
111+
:tls => true,
112+
:enable_starttls => false
113+
})
114+
}
115+
116+
def initialize(url)
117+
@uri = url.is_a?(URI) ? url : uri_parser.parse(url)
118+
unless DEFAULTS.has_key?(@uri.scheme)
119+
raise ArgumentError, "#{url} is not a valid SMTP-url. Required format: smtp(s)://host?domain=sender.org"
120+
end
121+
@query = uri.query
122+
end
123+
124+
def to_hash
125+
config = raw_config
126+
config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String }
127+
config
128+
end
129+
130+
private
131+
attr_reader :uri
132+
133+
def raw_config
134+
scheme_defaults.merge(query_hash).merge({
135+
:address => uri.host,
136+
:port => uri.port,
137+
:user_name => uri.user,
138+
:password => uri.password
139+
}.delete_if {|_key, value| Utilities.blank?(value) })
140+
end
141+
142+
def uri_parser
143+
Utilities.uri_parser
144+
end
145+
146+
def query_hash
147+
@query_hash = begin
148+
result = Hash[(@query || "").split("&").map { |pair| k,v = pair.split("="); [k.to_sym, v] }]
149+
150+
result[:open_timeout] &&= result[:open_timeout].to_i
151+
result[:read_timeout] &&= result[:read_timeout].to_i
152+
result[:enable_starttls] &&= begin
153+
case result[:enable_starttls]
154+
when "always", "auto" then result[:enable_starttls].to_sym
155+
else
156+
result[:enable_starttls] != "false"
157+
end
158+
end
159+
result[:enable_starttls_auto] &&= result[:enable_starttls_auto] != 'false'
160+
result
161+
end
162+
end
163+
164+
def scheme_defaults
165+
DEFAULTS[uri.scheme][uri.host]
166+
end
167+
end
168+
77169
attr_accessor :settings
78170

79171
DEFAULTS = {
@@ -92,8 +184,13 @@ class SMTP
92184
:read_timeout => 5
93185
}
94186

95-
def initialize(values)
96-
self.settings = DEFAULTS.merge(values)
187+
def initialize(config)
188+
settings = DEFAULTS.merge(config)
189+
190+
if config.has_key?(:url)
191+
settings = settings.merge(UrlResolver.new(config.delete(:url)).to_hash)
192+
end
193+
self.settings = settings
97194
end
98195

99196
def deliver!(mail)

spec/mail/network/delivery_methods/smtp_spec.rb

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,72 @@
1111
Mail.defaults { delivery_method :smtp, {} }
1212
end
1313

14+
describe 'settings via url' do
15+
def smtp_delivery_settings_for_url(url, options = {})
16+
Mail.defaults { delivery_method :smtp, {:url => url}.merge(options) }
17+
18+
Mail.delivery_method.settings
19+
end
20+
21+
it 'provides smtp defaults for localhost' do
22+
expect(smtp_delivery_settings_for_url('smtp://localhost')).to \
23+
include(:port => 25,
24+
:tls => false,
25+
:enable_starttls => false)
26+
end
27+
28+
it 'provides smtp defaults for non-localhost' do
29+
expect(smtp_delivery_settings_for_url('smtp://gmail.com')).to \
30+
include(:port => 587,
31+
:enable_starttls => :always)
32+
end
33+
34+
it 'provides smtps defaults' do
35+
expect(smtp_delivery_settings_for_url('smtps://gmail.com')).to \
36+
include(:port => 465,
37+
:tls => true,
38+
:enable_starttls => false)
39+
end
40+
41+
it 'parses the url correctly and coerces where needed' do
42+
url = 'smtp://mailer.org:12345?domain=sender.org&authentication=login&openssl_verify_mode=peer&open_timeout=1&read_timeout=2&enable_starttls=always&enable_starttls_auto=true'
43+
expect(smtp_delivery_settings_for_url(url)).to \
44+
include(:address => 'mailer.org',
45+
:port => 12345,
46+
:domain => 'sender.org',
47+
:authentication => 'login',
48+
:openssl_verify_mode => 'peer',
49+
:tls => false,
50+
:open_timeout => 1,
51+
:read_timeout => 2,
52+
:enable_starttls => :always,
53+
:enable_starttls_auto => true)
54+
end
55+
56+
it 'gives precedence to attributes from the url' do
57+
config = {:address => 'ignored', :domain => 'ignored'}
58+
expect(smtp_delivery_settings_for_url('smtp://user:pw@real-host?domain=real-domain.org', config)).not_to \
59+
include(config)
60+
end
61+
62+
it 'unescapes credentials' do
63+
expect(smtp_delivery_settings_for_url('smtps://user%40host:pw-with-%[email protected]')).to \
64+
include(:user_name => 'user@host',
65+
:password => 'pw-with-/')
66+
end
67+
68+
it 'accepts an URI' do
69+
expect(smtp_delivery_settings_for_url(URI.parse('smtp://host?enable_starttls=auto'))).to \
70+
include(:address => 'host', :enable_starttls => :auto)
71+
end
72+
73+
it 'should raise an error for url without smtp(s) scheme' do
74+
expect {
75+
smtp_delivery_settings_for_url('foo://host')
76+
}.to raise_error(/is not a valid SMTP-url/)
77+
end
78+
end
79+
1480
describe "general usage" do
1581
it "should send emails from given settings" do
1682

0 commit comments

Comments
 (0)