Read more

Rails: How to write custom email interceptors

Daniel Straßner
June 20, 2017Software engineer at makandra GmbH

Nowadays it is fairly easy to intercept and modify mails Show archive.org snapshot globally before they are sent. All you have to do is register an interceptor class which responds to .delivering_email(message). This card will show you two common use cases.

Subject prefix:

Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show archive.org snapshot

Usually you want to prefix the subject line of emails with the current environment (except production) so you can differentiate between production mails and mails from other environments. Of course a prefix can be set directly in the mailer, however, it is safer and cleaner to do this with an interceptor as you don't have to modify the subject line for each new mailer method.

class SubjectPrefixInterceptor

  def self.delivering_email(message)
    message.subject = "[#{Rails.env}] #{message.subject}"
  end

end

unless Rails.env.production?
  ActionMailer::Base.register_interceptor(SubjectPrefixInterceptor)
end

Define it in an initializer like config/initializers/mail_subject_prefixing.rb.

Whitelisted email addresses:

Sometimes you might want to try sending mails from staging but just to selected recipients. This example will show you how to define a email whitelist for staging using a mail interceptor.

class Whitelist

  def initialize(config)
    if config.empty?
      @active = false
    else
      @active = true
      @config = config
    end
  end

  def active?
    @active
  end

  def fallback_email
    @fallback_email ||= @config.fetch('fallback_email')
  end

  def allowed
    @allowed ||= @config.fetch('whitelist')
  end

end


class WhitelistConfig

  module ClassMethods

    def for(environment)
      @whitelists ||= {}
      @whitelists[environment] ||= Whitelist.new(config.fetch(environment, {}))
    end

    private

    def config
      @config ||= YAML.load_file(Rails.root.join('config/mail_whitelist.yml')).freeze
    end
  end

  extend ClassMethods
end


class WhitelistInterceptor

  def self.delivering_email(message)
    whitelist = WhitelistConfig.for(Rails.env)
    if whitelist.active?
      unless (message.to - whitelist.allowed).empty?
        message.subject = "#{message.subject} [#{message.to}]"
        message.to = whitelist.fallback_email
      end
    end
  end

end

ActionMailer::Base.register_interceptor(WhitelistInterceptor)

Your whitelist config should be defined in config/mail_whitelist/staging.yml like this:

staging:
  fallback_email: fallback@example.com
  whitelist:
    - personal1@example.com
    - personal2@example.com

A spec could look something like this:

describe WhitelistInterceptor do

  describe '.delivering_email' do

    def send_email(**options)
      ActionMailer::Base.mail(from: 'app@example.com', subject: 'My Subject', body: 'My Message', **options).deliver_now
    end

    context 'on staging' do
      before { allow(Rails).to receive(:env).and_return('staging'.inquiry) }

      it 'redirects e-mails that would be sent to public recipients' do
        email = send_email(to: 'not-whitelisted@example.com')
        expect(email.to).to contain_exactly('fallback@example.com')
        expect(email.subject).to eq('My Subject [not-whitelisted@example.com]')
      end

      it 'allows sending e-mails to whitelisted recipients' do
        email = send_email(to: 'personal1@example.com')
        expect(email.to).to contain_exactly('personal1@example.com')
        expect(email.subject).to eq('My Subject')
      end
    end

    context 'on production' do
      before { allow(Rails).to receive(:env).and_return('production'.inquiry) }

      it 'does not redirect e-mails' do
        email = send_email(to: 'not-whitelisted@example.com')
        expect(email.to).to contain_exactly('not-whitelisted@example.com')
        expect(email.subject).to eq('My Subject')
      end
    end

  end

end

Related:

You might also be interested in gems for mail interception:

Daniel Straßner
June 20, 2017Software engineer at makandra GmbH
Posted by Daniel Straßner to makandra dev (2017-06-20 09:14)