Skip to content
This repository was archived by the owner on Apr 17, 2023. It is now read-only.

Commit 7823be5

Browse files
authored
Merge pull request #1967 from mssola/smtp-full
Expanded configuration for the mailer
2 parents 2cabc4c + 688cb50 commit 7823be5

File tree

7 files changed

+304
-38
lines changed

7 files changed

+304
-38
lines changed

app/controllers/passwords_controller.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ def create
1919
else
2020
redirect_to new_user_password_path, alert: resource.errors.full_messages, float: true
2121
end
22-
rescue *::Portus::Errors::NET => e
23-
msg = "#{e}: #{::Portus::Errors.message_from_exception(e)}"
22+
rescue *::Portus::Errors::NET, ::Net::SMTPAuthenticationError => e
23+
from = ::Portus::Errors.message_from_exception(e)
24+
msg = "#{e}: #{from if from}"
2425
Rails.logger.tagged("Mailer") { Rails.logger.info msg }
2526
redirect_to new_user_password_path,
2627
alert: "Something went wrong. Check the configuration of Portus",

config/config.yml

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
# (it will be ignored by git). For more info, you can read the dedicated page
44
# here: http://port.us.org/docs/Configuring-Portus.html.
55

6-
# Settings for the Portus mailer.
6+
# Settings for the Portus mailer. It's strongly recommended to read the
7+
# following documentation link before configuring the mailer:
8+
# http://port.us.org/docs/Configuring-Portus.html#email-configuration
79
email:
810
911
name: "Portus"
@@ -16,10 +18,24 @@ email:
1618
smtp:
1719
enabled: false
1820
address: "smtp.example.com"
19-
port: 587,
20-
user_name: "[email protected]"
21-
password: "password"
22-
domain: "example.com"
21+
port: 587,
22+
domain: "example.com"
23+
24+
##
25+
# SSL.
26+
27+
ssl_tls: ""
28+
enable_starttls_auto: false
29+
openssl_verify_mode: "none"
30+
ca_path: ""
31+
ca_file: ""
32+
33+
##
34+
# Authentication
35+
36+
user_name: ""
37+
password: ""
38+
authentication: "login"
2339

2440
# If enabled, then the profile picture will be picked from the Gravatar
2541
# associated with each user. See: https://en.gravatar.com/

config/environments/development.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@
4141
# config.action_view.raise_on_missing_translations = true
4242

4343
# Set this to true when debugging a mailer.
44-
config.action_mailer.raise_delivery_errors = false
44+
config.action_mailer.raise_delivery_errors = true
45+
# Uncomment the following two lines to test the mailer in the real world.
46+
# config.action_mailer.perform_deliveries = true
47+
# config.action_mailer.raise_delivery_errors = true
4548

4649
# Control which IP's have access to the console. In Dev mode we can allow all private networks
4750
config.web_console.whitelisted_ips = %w[127.0.0.1/1 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16]

config/initializers/mail.rb

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,25 @@
11
# frozen_string_literal: true
22

3-
def check_email!(key)
4-
value = APP_CONFIG["email"][key]
5-
return if value.match?(Devise.email_regexp)
6-
raise "Mail: bad config value for '#{key}'. '#{value}' is not a proper email..."
7-
end
3+
require "portus/mail"
84

95
unless Rails.env.test?
10-
check_email!("from")
11-
check_email!("reply_to") if APP_CONFIG["email"]["reply_to"].present?
12-
13-
# If SMTP was set, then use it as the delivery method and configure it with the
14-
# given config.
6+
# In some weird cases APP_CONFIG is not even there. In these cases, just go
7+
# back to sendmail.
8+
if defined?(APP_CONFIG)
9+
# Check that emails have the proper format.
10+
mail = ::Portus::Mail::Utils.new(APP_CONFIG["email"])
11+
mail.check_email_configuration!
1512

16-
if defined?(APP_CONFIG) && APP_CONFIG["email"]["smtp"]["enabled"]
17-
Portus::Application.config.action_mailer.delivery_method = :smtp
18-
smtp = APP_CONFIG["email"]["smtp"]
19-
smtp_settings = {
20-
address: smtp["address"],
21-
port: smtp["port"],
22-
domain: smtp["domain"],
23-
enable_starttls_auto: false
24-
}
25-
if smtp["user_name"].blank?
26-
Rails.logger.info "No smtp username supplied, not using smtp authentication"
13+
# Fetch SMTP settings. On success, it will set SMTP as the delivery method,
14+
# otherwise we fall back to sendmail.
15+
settings = mail.smtp_settings
16+
if settings
17+
Portus::Application.config.action_mailer.delivery_method = :smtp
18+
ActionMailer::Base.smtp_settings = settings
2719
else
28-
auth_settings = {
29-
user_name: smtp["user_name"],
30-
password: smtp["password"],
31-
authentication: :login,
32-
enable_starttls_auto: true
33-
}
34-
smtp_settings = smtp_settings.merge(auth_settings)
20+
Portus::Application.config.action_mailer.delivery_method = :sendmail
3521
end
36-
ActionMailer::Base.smtp_settings = smtp_settings
3722
else
38-
# If SMTP is not enabled, then go for sendmail.
3923
Portus::Application.config.action_mailer.delivery_method = :sendmail
4024
end
4125
end

lib/portus/mail.rb

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# frozen_string_literal: true
2+
3+
module Portus
4+
# Mail implements a set of utilities for mailing purposes.
5+
module Mail
6+
# ConfigurationError is raised when the given configuration has semantic
7+
# problems (e.g. malformed emails).
8+
class ConfigurationError < StandardError; end
9+
10+
# Utils is a set of utility methods for mails.
11+
class Utils
12+
# config contains only the email configuration (i.e. APP_CONFIG["email"]
13+
# instead of APP_CONFIG directly).
14+
def initialize(config)
15+
@config = config
16+
end
17+
18+
# check_email_configuration! raises a ::Portus::Mail::ConfigurationError
19+
# when any of the relevant emails is badly formatted.
20+
def check_email_configuration!
21+
check_email!("from")
22+
check_email!("reply_to") if @config["reply_to"].present?
23+
end
24+
25+
# Returns a hash with the SMTP settings to be used by the mailer.
26+
def smtp_settings
27+
smtp = @config["smtp"]
28+
return unless smtp["enabled"]
29+
30+
{
31+
address: smtp["address"],
32+
port: smtp["port"],
33+
domain: smtp["domain"]
34+
}.merge(ssl_settings).merge(authentication_settings)
35+
end
36+
37+
protected
38+
39+
# Returns the SMTP settings around SSL.
40+
def ssl_settings
41+
{
42+
enable_starttls_auto: @config["smtp"]["enable_starttls_auto"],
43+
openssl_verify_mode: @config["smtp"]["openssl_verify_mode"]
44+
}.merge(ssl_tls).merge(ca)
45+
end
46+
47+
# Returns a hash with either SSL or TLS enabled if the configuration
48+
# specifies it. It returns an empty hash when no SSL/TLS has been
49+
# configured.
50+
def ssl_tls
51+
if @config["smtp"]["ssl_tls"] == "ssl"
52+
{ ssl: true }
53+
elsif @config["smtp"]["ssl_tls"] == "tls"
54+
{ tls: true }
55+
else
56+
{}
57+
end
58+
end
59+
60+
# Returns a hash with the `ca_path` and the `ca_file` options as specified
61+
# in the configuration.
62+
def ca
63+
{}.tap do |hsh|
64+
hsh[:ca_path] = @config["smtp"]["ca_path"] if @config["smtp"]["ca_path"]
65+
hsh[:ca_file] = @config["smtp"]["ca_file"] if @config["smtp"]["ca_file"]
66+
end
67+
end
68+
69+
# Returns a hash with the authentication settings as specified in the
70+
# configuration. It returns an empty hash if the `user_name` field has
71+
# been left blank.
72+
def authentication_settings
73+
return {} if @config["smtp"]["user_name"].blank?
74+
{
75+
user_name: @config["smtp"]["user_name"],
76+
password: @config["smtp"]["password"],
77+
authentication: @config["smtp"]["authentication"]
78+
}
79+
end
80+
81+
# check_email! raises an error when the given configuration key has a
82+
# badly formatted value.
83+
def check_email!(key)
84+
value = @config[key]
85+
return if value.match?(Devise.email_regexp)
86+
raise ConfigurationError,
87+
"Mail: bad config value for '#{key}'. '#{value}' is not a proper email..."
88+
end
89+
end
90+
end
91+
end

spec/controllers/passwords_controller_spec.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@
4545
expect(response.status).to eq 302
4646
end
4747

48+
it "redirects on ::Net::SMTPAuthenticationError" do
49+
allow(User).to receive(:send_reset_password_instructions) do
50+
raise ::Net::SMTPAuthenticationError, "error"
51+
end
52+
post :create, "user" => { "email" => @user.email }
53+
expect(response.status).to eq 302
54+
end
55+
4856
describe "LDAP support is enabled" do
4957
before do
5058
APP_CONFIG["ldap"]["enabled"] = true

spec/lib/portus/mail_spec.rb

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
describe ::Portus::Mail::Utils do
6+
let(:no_smtp) do
7+
{
8+
"from" => "[email protected]",
9+
"name" => "test",
10+
"smtp" => { "enabled": false }
11+
}.freeze
12+
end
13+
14+
let(:basic) do
15+
{
16+
"from" => "[email protected]",
17+
"name" => "test",
18+
"smtp" => {
19+
"enabled" => true,
20+
"address" => "[email protected]",
21+
"port" => 567,
22+
"domain" => "example.com",
23+
"enable_starttls_auto" => false,
24+
"openssl_verify_mode" => "none"
25+
}
26+
}.freeze
27+
end
28+
29+
let(:authentication) do
30+
{
31+
"from" => "[email protected]",
32+
"name" => "test",
33+
"smtp" => {
34+
"enabled" => true,
35+
"address" => "[email protected]",
36+
"port" => 567,
37+
"domain" => "example.com",
38+
"enable_starttls_auto" => false,
39+
"openssl_verify_mode" => "none",
40+
"user_name" => "mssola",
41+
"password" => "password",
42+
"authentication" => "login"
43+
}
44+
}.freeze
45+
end
46+
47+
let(:tls_noca) do
48+
{
49+
"from" => "[email protected]",
50+
"name" => "test",
51+
"smtp" => {
52+
"enabled" => true,
53+
"address" => "[email protected]",
54+
"port" => 567,
55+
"domain" => "example.com",
56+
"enable_starttls_auto" => true,
57+
"openssl_verify_mode" => "peer",
58+
"ssl_tls" => "tls"
59+
}
60+
}.freeze
61+
end
62+
63+
let(:notls_ca) do
64+
{
65+
"from" => "[email protected]",
66+
"name" => "test",
67+
"smtp" => {
68+
"enabled" => true,
69+
"address" => "[email protected]",
70+
"port" => 567,
71+
"domain" => "example.com",
72+
"enable_starttls_auto" => true,
73+
"openssl_verify_mode" => "peer",
74+
"ca_path" => "/lala",
75+
"ca_file" => "/lala"
76+
}
77+
}.freeze
78+
end
79+
80+
let(:ssl_ca) do
81+
{
82+
"from" => "[email protected]",
83+
"name" => "test",
84+
"smtp" => {
85+
"enabled" => true,
86+
"address" => "[email protected]",
87+
"port" => 567,
88+
"domain" => "example.com",
89+
"enable_starttls_auto" => true,
90+
"openssl_verify_mode" => "peer",
91+
"ca_path" => "/lala",
92+
"ca_file" => "/lala",
93+
"ssl_tls" => "ssl"
94+
}
95+
}.freeze
96+
end
97+
98+
describe "#check_email_configuration!" do
99+
it "raises an exception on malformed 'from'" do
100+
expect do
101+
described_class.new("from" => "!").check_email_configuration!
102+
end.to raise_error(::Portus::Mail::ConfigurationError)
103+
end
104+
105+
it "raises an exception on malformed 'reply_to'" do
106+
expect do
107+
described_class.new("from" => "[email protected]", "reply_to" => "!").check_email_configuration!
108+
end.to raise_error(::Portus::Mail::ConfigurationError)
109+
end
110+
111+
it "does not raise an exception when everything is alright" do
112+
expect do
113+
hsh = { "from" => "[email protected]", "reply_to" => "[email protected]" }
114+
described_class.new(hsh).check_email_configuration!
115+
end.not_to raise_error
116+
end
117+
end
118+
119+
describe "#smtp_settings" do
120+
it "returns nil when disabled" do
121+
res = described_class.new(no_smtp).smtp_settings
122+
expect(res).to be_nil
123+
end
124+
125+
it "returns a basic smtp configuration" do
126+
res = described_class.new(basic).smtp_settings
127+
%i[address port domain enable_starttls_auto openssl_verify_mode].each do |key|
128+
expect(res[key]).not_to be_nil
129+
end
130+
end
131+
132+
it "returns a configuration with authentication" do
133+
res = described_class.new(authentication).smtp_settings
134+
%i[address port domain enable_starttls_auto openssl_verify_mode
135+
user_name password authentication].each do |key|
136+
expect(res[key]).not_to be_nil
137+
end
138+
end
139+
140+
it "returns a configuration with SSL (ssl/tls and no ca)" do
141+
res = described_class.new(tls_noca).smtp_settings
142+
%i[address port domain enable_starttls_auto openssl_verify_mode tls].each do |key|
143+
expect(res[key]).not_to be_nil
144+
end
145+
end
146+
147+
it "returns a configuration with SSL (ssl/tls and ca)" do
148+
res = described_class.new(ssl_ca).smtp_settings
149+
%i[address port domain enable_starttls_auto openssl_verify_mode ssl
150+
ca_file ca_path].each do |key|
151+
expect(res[key]).not_to be_nil
152+
end
153+
end
154+
155+
it "returns a configuration with SSL (no ssl/tls and ca)" do
156+
res = described_class.new(notls_ca).smtp_settings
157+
%i[address port domain enable_starttls_auto openssl_verify_mode
158+
ca_file ca_path].each do |key|
159+
expect(res[key]).not_to be_nil
160+
end
161+
end
162+
end
163+
end

0 commit comments

Comments
 (0)