Skip to content

Commit d8810b0

Browse files
committed
Config smtp-delivery via url
1 parent d1d65b3 commit d8810b0

File tree

4 files changed

+262
-7
lines changed

4 files changed

+262
-7
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: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,14 +278,56 @@ mail.delivery_method :sendmail
278278
mail.deliver
279279
```
280280

281-
Sending via smtp (for example to [mailcatcher](https://github.com/sj26/mailcatcher))
282-
```ruby
281+
#### Sending via SMTP
283282

283+
Sending via smtp (for example to [mailcatcher](https://github.com/sj26/mailcatcher)):
284+
```ruby
284285
Mail.defaults do
285286
delivery_method :smtp, address: "localhost", port: 1025
286287
end
287288
```
288289

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+
# Settings from the url take precedence in case of clashes. Otherwise all unique values are combined:
305+
delivery_method :smtp, {
306+
url: 'smtps://smtp.gmail.com?domain=foo.org',
307+
address: 'bar.com', # <-- ignored
308+
# Added:
309+
user_name: '[email protected]',
310+
password: credentials.dig(:smtp, :password)
311+
}
312+
313+
# Provide additional settings or overrides via the query string of the url:
314+
delivery_method :smtp, url: '<above-url>?domain=foo.org&authentication=login'
315+
316+
# Note that the authentication can also be passed as part of the scheme:
317+
delivery_method :smtp, url: 'smtps+login://smtp.gmail.com?domain=foo.org'
318+
319+
# For localhost (or 127.0.0.1) it assumes no tls, nor starttls.
320+
# So the following would work using [mailcatcher](https://github.com/sj26/mailcatcher):
321+
delivery_method :smtp, url: 'smtp://127.0.0.1:1025'
322+
323+
# Scheme 'smtps' enables `tls` and uses port 465:
324+
delivery_method :smtp, url: 'smtps://user%40gmail.com:[email protected]'
325+
326+
# See Mail::SMTP::UrlResolver::DEFAULTS for the defaults used for various schemes and addresses.
327+
end
328+
```
329+
330+
#### Sending via Exim
289331

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

@@ -295,6 +337,8 @@ mail.delivery_method :exim, :location => "/usr/bin/exim"
295337
mail.deliver
296338
```
297339

340+
#### Logging
341+
298342
Mail may be "delivered" to a logfile, too, for development and testing:
299343

300344
```ruby

lib/mail/network/delivery_methods/smtp.rb

Lines changed: 133 additions & 5 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,6 +36,37 @@ module Mail
3536
# :password => '<password>',
3637
# :authentication => 'plain',
3738
# :enable_starttls => :auto }
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]' }
44+
# end
45+
#
46+
# See {Mail::SMTP::UrlResolver::DEFAULTS} what defaults are applied for certain schemes.
47+
# The scheme can also contain the authentication-type, e.g. `smtps+cram-md5://...`.
48+
#
49+
# === Sending via Fastmail
50+
#
51+
# Mail.defaults do
52+
# delivery_method :smtp,
53+
# { :url => 'smtps://user%40fastmail.fm:[email protected]' }
54+
#
55+
# # Settings from the url take precedence:
56+
# delivery_method :smtp, {
57+
# url: 'smtps://smtp.fastmail.com?domain=foo.org',
58+
# domain: 'domain.org', # <-- ignored
59+
# user_name: '[email protected]',
60+
# password: credentials.dig(:smtp, :password)
61+
# }
62+
# end
63+
#
64+
# === Local development
65+
#
66+
# Using {https://github.com/sj26/mailcatcher mailcatcher}:
67+
#
68+
# Mail.defaults do
69+
# delivery_method :smtp, url: 'smtp://127.0.0.1:1025'
3870
# end
3971
#
4072
# === Configuring TLS/SSL and STARTTLS
@@ -82,6 +114,97 @@ module Mail
82114
#
83115
# mail.deliver!
84116
class SMTP
117+
class UrlResolver
118+
119+
DEFAULTS = {
120+
"smtp" => Hash.new({
121+
:port => 587,
122+
:tls => false,
123+
:enable_starttls => :always
124+
}).merge({
125+
"localhost" => {
126+
:port => 25,
127+
:tls => false,
128+
:enable_starttls => false
129+
},
130+
"127.0.0.1" => {
131+
:port => 25,
132+
:tls => false,
133+
:enable_starttls => false
134+
}}),
135+
"smtps" => Hash.new({
136+
:port => 465,
137+
:tls => true,
138+
:enable_starttls => false
139+
})
140+
}
141+
142+
def initialize(url)
143+
@uri = url.is_a?(URI) ? url : uri_parser.parse(url)
144+
unless @uri.scheme && DEFAULTS.has_key?(uri_scheme)
145+
raise ArgumentError, "#{url} is not a valid SMTP-url. Required format: smtp(s)://host?domain=sender.org"
146+
end
147+
@query = uri.query
148+
end
149+
150+
def to_hash
151+
config = raw_config
152+
config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String }
153+
config
154+
end
155+
156+
private
157+
attr_reader :uri
158+
159+
def raw_config
160+
scheme_defaults.merge(query_hash).merge({
161+
:address => uri.host,
162+
:port => uri.port,
163+
:user_name => uri.user,
164+
:password => uri.password,
165+
:authentication => uri_scheme_authentication
166+
}.delete_if {|_key, value| Utilities.blank?(value) })
167+
end
168+
169+
def uri_parser
170+
Utilities.uri_parser
171+
end
172+
173+
def uri_scheme_and_authentication
174+
(uri.scheme.split("+") + [ nil ]).take(2)
175+
end
176+
177+
def uri_scheme
178+
uri_scheme_and_authentication.first
179+
end
180+
181+
def uri_scheme_authentication
182+
uri_scheme_and_authentication.last
183+
end
184+
185+
def query_hash
186+
@query_hash = begin
187+
result = Hash[(@query || "").split("&").map { |pair| k,v = pair.split("="); [k.to_sym, v] }]
188+
189+
result[:open_timeout] &&= result[:open_timeout].to_i
190+
result[:read_timeout] &&= result[:read_timeout].to_i
191+
result[:enable_starttls] &&= begin
192+
case result[:enable_starttls]
193+
when "always", "auto" then result[:enable_starttls].to_sym
194+
else
195+
result[:enable_starttls] != "false"
196+
end
197+
end
198+
result[:enable_starttls_auto] &&= result[:enable_starttls_auto] != 'false'
199+
result
200+
end
201+
end
202+
203+
def scheme_defaults
204+
DEFAULTS[uri_scheme][uri.host]
205+
end
206+
end
207+
85208
attr_accessor :settings
86209

87210
DEFAULTS = {
@@ -100,8 +223,13 @@ class SMTP
100223
:read_timeout => 5
101224
}
102225

103-
def initialize(values)
104-
self.settings = DEFAULTS.merge(values)
226+
def initialize(config)
227+
settings = DEFAULTS.merge(config)
228+
229+
if config[:url]
230+
settings = settings.merge(UrlResolver.new(config.delete(:url)).to_hash)
231+
end
232+
self.settings = settings
105233
end
106234

107235
def deliver!(mail)
@@ -126,7 +254,7 @@ def smtp_starttls
126254
return false if smtp_tls?
127255

128256
if setting_provided?(:enable_starttls) && settings[:enable_starttls]
129-
# enable_starttls: provided and truthy
257+
# enable_starttls: truthy
130258
case settings[:enable_starttls]
131259
when :auto then :auto
132260
when :always then :always
@@ -139,8 +267,8 @@ def smtp_starttls
139267
settings[:enable_starttls_auto] ? :auto : false
140268
else
141269
# enable_starttls_auto: not provided
142-
# enable_starttls: when provided then false
143-
# use :auto when neither enable_starttls* provided
270+
# enable_starttls: false when provided
271+
# use :auto as fallback when neither enable_starttls* provided
144272
setting_provided?(:enable_starttls) ? false : :auto
145273
end
146274
end

spec/mail/network/delivery_methods/smtp_spec.rb

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,88 @@
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 'ignores url when nil' do
42+
expect(smtp_delivery_settings_for_url(nil, {host: "gmail.com", port: 465})).to \
43+
include(:port => 465,
44+
:host => "gmail.com")
45+
end
46+
47+
it 'parses the url correctly and coerces where needed' do
48+
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'
49+
expect(smtp_delivery_settings_for_url(url)).to \
50+
include(:address => 'mailer.org',
51+
:port => 12345,
52+
:domain => 'sender.org',
53+
:authentication => 'login',
54+
:openssl_verify_mode => 'peer',
55+
:tls => false,
56+
:open_timeout => 1,
57+
:read_timeout => 2,
58+
:enable_starttls => :always,
59+
:enable_starttls_auto => true)
60+
end
61+
62+
it 'gives precedence to attributes from the url' do
63+
config = {:address => 'ignored', :domain => 'ignored'}
64+
expect(smtp_delivery_settings_for_url('smtp://user:pw@real-host?domain=real-domain.org', config)).not_to \
65+
include(config)
66+
end
67+
68+
it 'accepts additional attributes' do
69+
config = { password: "secr@t" }
70+
expect(smtp_delivery_settings_for_url('smtps://user@host', config)).to include(config)
71+
end
72+
73+
it 'unescapes credentials' do
74+
expect(smtp_delivery_settings_for_url('smtps://user%40host:pw-with-%[email protected]')).to \
75+
include(:user_name => 'user@host',
76+
:password => 'pw-with-/')
77+
end
78+
79+
it 'accepts an URI' do
80+
expect(smtp_delivery_settings_for_url(URI.parse('smtp://host?enable_starttls=auto'))).to \
81+
include(:address => 'host', :enable_starttls => :auto)
82+
end
83+
84+
it 'should raise an error for url without smtp(s) scheme' do
85+
expect {
86+
smtp_delivery_settings_for_url('foo://host')
87+
}.to raise_error(/is not a valid SMTP-url/)
88+
end
89+
90+
it 'allows for authencation to be provided as part of the scheme' do
91+
expect(smtp_delivery_settings_for_url(URI.parse('smtp+plain://host'))).to \
92+
include(authentication: "plain")
93+
end
94+
end
95+
1496
describe "general usage" do
1597
it "should send emails from given settings" do
1698

0 commit comments

Comments
 (0)