Mail interceptor for different Rails environments
ArticleIf you are not interested in this more complex solution and just googling for quick way how to do Rails mailer interceptor then in the “Copy Paste solution” at the bottom of this article you will find what you are looking for.
There is already good examples out there how you can create mail interceptors (e.g.: RailsCasts no 206.)
But in this article I will show you how to configure mail interceptor while inheriting from production environment as proposed in Beyond the default Rails environments article.
The core of the article is that all your Rails environments on servers should be as close to production as they can be, therefore you should inherit all server configurations from production and just configure minor differences.
Imagine you are configuring several Rails environments.
- production should send emails as usual
- staging should never ever send emails to real address, but to product-manager email address
- demo server should never ever send emails to real address, but to product-manager email address
- devel should never deliver email
- test should never deliver email
Production environment
# config/environments/production.rb
MyApp::Application.configure do
# ...
config.action_mailer.default_url_options = { :protocol => 'https', :host => 'my-app.com' }
config.delivery_method = :smtp
config.action_mailer.smtp_settings = { address: 'smtp.mandrillapp.com', .... } # production SMTP settings
# ...
end
Production is using real SMTP settings that deliver real emails:
Staging & Demo environment:
… or any other server based environment, like staging, UAT, QA, beta-server, demo-server, …
# config/environments/staging.rb
# Based on production defaults
require Rails.root.join('config/environments/production')
require Rails.root.join('lib/server_mail_interceptor') # unless you are autoloading lib folder
Validations::Application.configure do
config.action_mailer.default_url_options = { :protocol => 'https', :host => 'my-staging-app.com' }
ActionMailer::Base.register_interceptor(ServerMailInterceptor) # Intercepts emails
end
# config/environments/demo.rb
# Based on production defaults
require Rails.root.join('config/environments/production')
require Rails.root.join('lib/server_mail_interceptor') # unless you are autoloading lib folder
Validations::Application.configure do
config.action_mailer.default_url_options = { :protocol => 'https', :host => 'my-demo-app.com' }
ActionMailer::Base.register_interceptor(ServerMailInterceptor) # Intercepts emails
end
Now please pay attention how the staging and demo config is not setting
delivery_method
or smtp_settings
…those are already set in
production
config that we are loading with require
. We are only
overwriting configuration values that are relevant.
Development environment:
# config/environments/development.rb
require Rails.root.join('lib/development_mail_interceptor') # unless you are autoloading lib folder
Validations::Application.configure do
# ...
config.action_mailer.default_url_options = { :host => '0.0.0.0:3000' }
config.delivery_method = :smtp
config.action_mailer.smtp_settings = { :address => '127.0.0.1', :port => 1025 } # development smtp settings
# ...
ActionMailer::Base.register_interceptor(DevelopmentMailInterceptor) # Intercept emails
# ...
For development we don’t want to use production environment settings. We don’t want to pollute production delivery logs with every developer junk.
So for testing in our development we don’t load configuration from production, but we use configuration from scratch where we specify custom delivery SMTP settings.
I really like Mailcatcher gem. It’s local SMTP server where you can send and inspect your developer emails. But if you want to use something like Gmail, or custom cloud smtp solution like Sendgird, Mailchimp, … you are free to do it.
Test environment:
# config/environments/test.rb
Rails.application.configure do
# ...
config.action_mailer.delivery_method = :test
# ...
end
Config option delivery_method = :test
will stop emails being
delivered, therefore in test environment you don’t need to set up smtp
server or mail interceptor details
Same as in Development environment we don’t load production settings.
Interceptor files
# lib/server_mail_interceptor.rb
class ServerMailInterceptor
def self.delivering_email(message)
message.to = '[email protected]'
# ...
end
end
If you want to see more complex interceptor, in the “Copy Paste solution” section of this article you can find more options
# lib/development_mail_interceptor.rb
class ServerMailInterceptor
def self.delivering_email(message)
message.subject = "#{message.subject} | TO: #{message.to}"
end
end
As you can see the “interceptor” is not only about intercepting emails. The can be used to place debugging information in them too. More on that in section “Enhanced production interceptor” in this article
Enhanced production interceptor
Let say you want your emails delivered as usual but every copy should be bcc’d to “archive” email address.
Mail interceptors can do that for you too:
# config/environments/production.rb
require Rails.root.join('lib/archive_copy_mail_interceptor') # unless you are autoloading lib folder
Validations::Application.configure do
# ...
ActionMailer::Base.register_interceptor(ServerMailInterceptor)
# ...
end
# lib/archive_copy_mail_interceptor.rb
class ArchiveCopyMailInterceptor
def self.delivering_email(message)
message.bcc = '[email protected]'
end
end
We are not changing the message.to
just adding the bcc part to email
that will tell smtp server to send hidden copy to [email protected]
Check if interceptor is registered
Lunch rails console for each environment:
RAILS_ENV=test rails c
RAILS_ENV=staging rails c
RAILS_ENV=beta rails c
RAILS_ENV=production rails c
RAILS_ENV=development rails c
…and check your interceptors with this ruby code:
ActionMailer::Base::Mail.class_variable_get(:@@delivery_interceptors)
# => (irb):10: warning: toplevel constant Mail referenced by ActionMailer::Base::Mail
# => [DevelopmentMailInterceptor]
Warning message is due to fact that
@@delivery_interceptors
. It’s cool to use this to make sure if we set interceptors correctly, it’s not cool to directly access it in production code.
Copy Paste solution
This article is quite high when you google for term “Rails Mail interceptor”.
If you are just looking for quick easy copy-paste solution for Email Interceptor that just works and you are not interested in all that stuff I said previously:
# confix/environments/staging.rb
# ...
config.action_mailer.default_url_options = ... # whatever
config.action_mailer.delivery_method = :smtp # whatever
config.action_mailer.smtp_settings = { ... } # whatever
config.mail_interceptor = 'SandboxMailInterceptor' # <<< this line ! String value, not class !
# ...
Or you prefer to have
config/initializers
file configuration you can crate file in it with content:ActionMailer::Base.register_interceptor(SandboxMailInterceptor) if Rails.env.staging?
# lix/sandbox_mail_interceptor.rb
module SandboxMailInterceptor
def self.delivering_email(message)
test_email_destination = '[email protected]'
development_information = "TO: #{message.to.inspect}"
development_information << " CC: #{message.cc.inspect}" if message.cc.try(:any?)
development_information << " BCC: #{message.bcc.inspect}" if message.bcc.try(:any?)
if app_domain_email = message.to.to_a.select { |e| e.to_s.match(/my-app\.com/) }.first
message.to = [test_email_destination, app_domain_email]
else
message.to = [test_email_destination]
end
message.cc = nil
message.bcc = nil
message.subject = "#{message.subject} | #{development_information}"
end
end
# spec/lib/sandbox_mail_interceptor_spec.rb
require 'rails_helper'
RSpec.describe SandboxMailInterceptor do
def trigger
described_class.delivering_email(message)
end
let(:cc) { nil }
let(:bcc) { nil }
let(:message) do
OpenStruct.new(to: [email], cc: cc, bcc: bcc, subject: 'Bla bla')
end
context 'when real email' do
let(:email) { '[email protected]' }
it 'intecrept the email' do
trigger
expect(message.to).to eq ['[email protected]']
expect(message.cc).to eq nil
expect(message.bcc).to eq nil
expect(message.subject).to eq('Bla bla | TO: ["[email protected]"]')
end
context 'when bcc & cc' do
let(:cc) { ['foo@bar'] }
let(:bcc) { ['car@dar'] }
it do
trigger
expect(message.to).to eq ['[email protected]']
expect(message.cc).to eq nil
expect(message.bcc).to eq nil
expect(message.subject).to eq('Bla bla | TO: ["[email protected]"] CC: ["foo@bar"] BCC: ["car@dar"]')
end
end
end
context 'when my-app.com email' do
let(:email) { '[email protected]' }
it 'intecrept the email' do
trigger
expect(message.to).to eq ['[email protected]', '[email protected]']
expect(message.cc).to eq nil
expect(message.bcc).to eq nil
expect(message.subject).to eq('Bla bla | TO: ["[email protected]"]')
end
end
end
This interceptor ensures you don’t send emails outside the domain
*my-app.com
. All emails are also send to collection box
[email protected]
.
This is example of live code used in one application once, feel free to alter it any way you want
Meta
Source: http://guides.rubyonrails.org/action_mailer_basics.html
Keywords: Rails 4.0.2, Ruby 2.1.1, own environment configuration, mail interceptor, stop mails in staging
Article updated: 2017-12-19
Reddit discussion: https://www.reddit.com/r/ruby/comments/7kstz4/mail_interceptor_for_different_rails_environments/
Entire blog website and all the articles can be forked from this Github Repo