diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc index 4727518dd..faf6aaf04 100644 --- a/CHANGELOG.rdoc +++ b/CHANGELOG.rdoc @@ -11,6 +11,7 @@ Features: * Updated README to improve around sending multipart mail @kapfenho * Add delivery_interceptors method to Mail class to fetch registered interceptors @ghousemohamed +* Configure smtp-delivery using a url @eval Code Improvements: diff --git a/README.md b/README.md index dd0237501..75f445bd9 100644 --- a/README.md +++ b/README.md @@ -278,14 +278,56 @@ mail.delivery_method :sendmail mail.deliver ``` -Sending via smtp (for example to [mailcatcher](https://github.com/sj26/mailcatcher)) -```ruby +#### Sending via SMTP +Sending via smtp (for example to [mailcatcher](https://github.com/sj26/mailcatcher)): +```ruby Mail.defaults do delivery_method :smtp, address: "localhost", port: 1025 end ``` +A url is also accepted: +```ruby +Mail.defaults do + # The following (note the URL-encoded userinfo)...: + delivery_method :smtp, url: 'smtp://user%40gmail.com:app-password@smtp.gmail.com' + + # ...would be equivalent to: + delivery_method :smtp, { :address => 'smtp.gmail.com', + :port => 587, + :user_name => 'user@gmail.com', + :password => 'app-password', + :tls => false, + :enable_starttls => :always } + + # Settings from the url take precedence in case of clashes. Otherwise all unique values are combined: + delivery_method :smtp, { + url: 'smtps://smtp.gmail.com?domain=foo.org', + address: 'bar.com', # <-- ignored + # Added: + user_name: 'user@gmail.com', + password: credentials.dig(:smtp, :password) + } + + # Provide additional settings or overrides via the query string of the url: + delivery_method :smtp, url: '?domain=foo.org&authentication=login' + + # Note that the authentication can also be passed as part of the scheme: + delivery_method :smtp, url: 'smtps+login://smtp.gmail.com?domain=foo.org' + + # For localhost (or 127.0.0.1) it assumes no tls, nor starttls. + # So the following would work using [mailcatcher](https://github.com/sj26/mailcatcher): + delivery_method :smtp, url: 'smtp://127.0.0.1:1025' + + # Scheme 'smtps' enables `tls` and uses port 465: + delivery_method :smtp, url: 'smtps://user%40gmail.com:app-password@smtp.gmail.com' + + # See Mail::SMTP::UrlResolver::DEFAULTS for the defaults used for various schemes and addresses. +end +``` + +#### Sending via Exim Exim requires its own delivery manager, and can be used like so: @@ -295,6 +337,8 @@ mail.delivery_method :exim, :location => "/usr/bin/exim" mail.deliver ``` +#### Logging + Mail may be "delivered" to a logfile, too, for development and testing: ```ruby diff --git a/lib/mail/network/delivery_methods/smtp.rb b/lib/mail/network/delivery_methods/smtp.rb index 7ecdd19e6..4f38eb20f 100644 --- a/lib/mail/network/delivery_methods/smtp.rb +++ b/lib/mail/network/delivery_methods/smtp.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true require 'mail/smtp_envelope' +require 'mail/utilities' module Mail # == Sending Email with SMTP @@ -35,6 +36,37 @@ module Mail # :password => '', # :authentication => 'plain', # :enable_starttls => :auto } + # + # # ...or using the url-attribute (note the trailing 's' in the scheme + # # to set `tls`, also note the URL-encoded userinfo): + # delivery_method :smtp, + # { :url => 'smtps://user%40gmail.com:app-password@smtp.gmail.com' } + # end + # + # See {Mail::SMTP::UrlResolver::DEFAULTS} what defaults are applied for certain schemes. + # The scheme can also contain the authentication-type, e.g. `smtps+cram-md5://...`. + # + # === Sending via Fastmail + # + # Mail.defaults do + # delivery_method :smtp, + # { :url => 'smtps://user%40fastmail.fm:app-pw@smtp.fastmail.com' } + # + # # Settings from the url take precedence: + # delivery_method :smtp, { + # url: 'smtps://smtp.fastmail.com?domain=foo.org', + # domain: 'domain.org', # <-- ignored + # user_name: 'user@fastmail.fm', + # password: credentials.dig(:smtp, :password) + # } + # end + # + # === Local development + # + # Using {https://github.com/sj26/mailcatcher mailcatcher}: + # + # Mail.defaults do + # delivery_method :smtp, url: 'smtp://127.0.0.1:1025' # end # # === Configuring TLS/SSL and STARTTLS @@ -82,6 +114,97 @@ module Mail # # mail.deliver! class SMTP + class UrlResolver + + DEFAULTS = { + "smtp" => Hash.new({ + :port => 587, + :tls => false, + :enable_starttls => :always + }).merge({ + "localhost" => { + :port => 25, + :tls => false, + :enable_starttls => false + }, + "127.0.0.1" => { + :port => 25, + :tls => false, + :enable_starttls => false + }}), + "smtps" => Hash.new({ + :port => 465, + :tls => true, + :enable_starttls => false + }) + } + + def initialize(url) + @uri = url.is_a?(URI) ? url : uri_parser.parse(url) + unless @uri.scheme && DEFAULTS.has_key?(uri_scheme) + raise ArgumentError, "#{url} is not a valid SMTP-url. Required format: smtp(s)://host?domain=sender.org" + end + @query = uri.query + end + + def to_hash + config = raw_config + config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String } + config + end + + private + attr_reader :uri + + def raw_config + scheme_defaults.merge(query_hash).merge({ + :address => uri.host, + :port => uri.port, + :user_name => uri.user, + :password => uri.password, + :authentication => uri_scheme_authentication + }.delete_if {|_key, value| Utilities.blank?(value) }) + end + + def uri_parser + Utilities.uri_parser + end + + def uri_scheme_and_authentication + (uri.scheme.split("+") + [ nil ]).take(2) + end + + def uri_scheme + uri_scheme_and_authentication.first + end + + def uri_scheme_authentication + uri_scheme_and_authentication.last + end + + def query_hash + @query_hash = begin + result = Hash[(@query || "").split("&").map { |pair| k,v = pair.split("="); [k.to_sym, v] }] + + result[:open_timeout] &&= result[:open_timeout].to_i + result[:read_timeout] &&= result[:read_timeout].to_i + result[:enable_starttls] &&= begin + case result[:enable_starttls] + when "always", "auto" then result[:enable_starttls].to_sym + else + result[:enable_starttls] != "false" + end + end + result[:enable_starttls_auto] &&= result[:enable_starttls_auto] != 'false' + result + end + end + + def scheme_defaults + DEFAULTS[uri_scheme][uri.host] + end + end + attr_accessor :settings DEFAULTS = { @@ -100,8 +223,13 @@ class SMTP :read_timeout => 5 } - def initialize(values) - self.settings = DEFAULTS.merge(values) + def initialize(config) + settings = DEFAULTS.merge(config) + + if config[:url] + settings = settings.merge(UrlResolver.new(config.delete(:url)).to_hash) + end + self.settings = settings end def deliver!(mail) @@ -126,7 +254,7 @@ def smtp_starttls return false if smtp_tls? if setting_provided?(:enable_starttls) && settings[:enable_starttls] - # enable_starttls: provided and truthy + # enable_starttls: truthy case settings[:enable_starttls] when :auto then :auto when :always then :always @@ -139,8 +267,8 @@ def smtp_starttls settings[:enable_starttls_auto] ? :auto : false else # enable_starttls_auto: not provided - # enable_starttls: when provided then false - # use :auto when neither enable_starttls* provided + # enable_starttls: false when provided + # use :auto as fallback when neither enable_starttls* provided setting_provided?(:enable_starttls) ? false : :auto end end diff --git a/spec/mail/network/delivery_methods/smtp_spec.rb b/spec/mail/network/delivery_methods/smtp_spec.rb index 89262777b..78951e6b1 100644 --- a/spec/mail/network/delivery_methods/smtp_spec.rb +++ b/spec/mail/network/delivery_methods/smtp_spec.rb @@ -11,6 +11,88 @@ Mail.defaults { delivery_method :smtp, {} } end + describe 'settings via url' do + def smtp_delivery_settings_for_url(url, options = {}) + Mail.defaults { delivery_method :smtp, {:url => url}.merge(options) } + + Mail.delivery_method.settings + end + + it 'provides smtp defaults for localhost' do + expect(smtp_delivery_settings_for_url('smtp://localhost')).to \ + include(:port => 25, + :tls => false, + :enable_starttls => false) + end + + it 'provides smtp defaults for non-localhost' do + expect(smtp_delivery_settings_for_url('smtp://gmail.com')).to \ + include(:port => 587, + :enable_starttls => :always) + end + + it 'provides smtps defaults' do + expect(smtp_delivery_settings_for_url('smtps://gmail.com')).to \ + include(:port => 465, + :tls => true, + :enable_starttls => false) + end + + it 'ignores url when nil' do + expect(smtp_delivery_settings_for_url(nil, {host: "gmail.com", port: 465})).to \ + include(:port => 465, + :host => "gmail.com") + end + + it 'parses the url correctly and coerces where needed' do + 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' + expect(smtp_delivery_settings_for_url(url)).to \ + include(:address => 'mailer.org', + :port => 12345, + :domain => 'sender.org', + :authentication => 'login', + :openssl_verify_mode => 'peer', + :tls => false, + :open_timeout => 1, + :read_timeout => 2, + :enable_starttls => :always, + :enable_starttls_auto => true) + end + + it 'gives precedence to attributes from the url' do + config = {:address => 'ignored', :domain => 'ignored'} + expect(smtp_delivery_settings_for_url('smtp://user:pw@real-host?domain=real-domain.org', config)).not_to \ + include(config) + end + + it 'accepts additional attributes' do + config = { password: "secr@t" } + expect(smtp_delivery_settings_for_url('smtps://user@host', config)).to include(config) + end + + it 'unescapes credentials' do + expect(smtp_delivery_settings_for_url('smtps://user%40host:pw-with-%2F@mailer.org')).to \ + include(:user_name => 'user@host', + :password => 'pw-with-/') + end + + it 'accepts an URI' do + expect(smtp_delivery_settings_for_url(URI.parse('smtp://host?enable_starttls=auto'))).to \ + include(:address => 'host', :enable_starttls => :auto) + end + + it 'should raise an error for url without smtp(s) scheme' do + expect { + smtp_delivery_settings_for_url('foo://host') + }.to raise_error(/is not a valid SMTP-url/) + end + + it 'allows for authencation to be provided as part of the scheme' do + expect(smtp_delivery_settings_for_url(URI.parse('smtp+plain://host'))).to \ + include(authentication: "plain") + end + end + describe "general usage" do it "should send emails from given settings" do