Dynamic SMTP Settings in ActionMailer

#Rails, #Ruby, #How-To


Imagine a single instance Rails application serving two different domain names, let’s say foo.com and bar.com. Some things should be handled differently depending on the domain used to access the application. For example transactional mails (notifications, password reset instructions, …) should have different sender addresses (e.g. info@foo.com or info@bar.com).

Originally, using different sender domains was no problem: ActionMailer was configured to send the mails via a local MTA (Postfix), which gladly accepted any sender address. But with spam filters starting to rely more and more on technologies like SPF and DKIM the effort of configuring, maintaining and monitoring the mail server was increasing steadily. After all you want to make sure that your password recovery mails aren’t classified as spam.

Switching to Mailgun

So we decided to switch to Mailgun for increased deliverability and easier maintenance. In general, this is as simple as changing the SMTP credentials to the ones provided by Mailgun. However, in this case there was a gotcha:

Mailgun requires you to use different SMTP credentials for each sender domain. But ActionMailer settings, including SMTP credentials, are stored in environment-specific configuration files (eg. config/environments/production.rb) and loaded as part of the app’s initialization process. There is no obvious way of dynamically changing settings while the app is running.

Mail interceptors to the rescue

Enter mail interceptors: ActionMailer allows you to register interceptors that can modify messages before they are sent. An interceptor can be any class as long as it implements a #delivering_email class method:

1
2
3
4
5
6
# app/models/dynamic_smtp_settings_interceptor.rb
class DynamicSmtpSettingsInterceptor
  def self.delivering_email(message)
    # do something with `message`
  end
end

Of course an interceptor also needs to be registered somewhere. Standard way of doing this is to use a Rails initializer:

1
2
# config/initializers/dynamic_smtp_settings_interceptor.rb
ActionMailer::Base.register_interceptor "DynamicSmtpSettingsInterceptor"

Storing the credentials

Which credentials we need to use depends on the sender domain. To store these domain-dependent credentials we simply add a hash to the production environment’s configuration file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# config/environments/production.rb
FooBar::Application.configure do
  # … snip …
  config.dynamic_smtp_settings = {
    "foo.com" => {
      user_name: "postmaster@mg.foo.com",
      password: "(redacted)",
    },
    "bar.com" => {
      user_name: "postmaster@mg.bar.com",
      password: "(redacted)",
    },
  }
end

During runtime the settings can then be accessed with Rails.configuration.dynamic_smtp_settings.

Setting SMTP credentials dynamically

Now that the credentials are configured and our interceptor is in place, how can we actually change the SMTP credentials dynamically? Luckily it turns out that the delivery settings (including the credentials) for each message are stored in the message’s Mail object instance. These settings are of course originally set according to ActionMailer’s configuration, but we can access and change them in the interceptor using message.delivery_method.settings:

1
2
3
4
5
6
7
8
class DynamicSmtpSettingsInterceptor
  def self.delivering_email(message)
    message.delivery_method.settings # => {:address=>"smtp.mailgun.org",
                                     #     :port=>587,
                                     #     :authentication=>"login",
                                     #     :enable_starttls_auto=>true}
  end
end

Now we have everything in place to implement the interceptor (comment present in the original source code, too):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class DynamicSmtpSettingsInterceptor
  def self.delivering_email(message)
    # Dynamic settings are currently only used in production, but we want to check for a
    # valid sender domain in all environments to make sure we catch missing senders in the
    # development and test, too.
    /@(?<sender_domain>.+)/ =~ message.sender
      or raise "No valid sender for determining dynamic SMTP settings: `#{message.sender}`"

    if Rails.configuration.respond_to?(:dynamic_smtp_settings)
      dynamic_settings = Rails.configuration.dynamic_smtp_settings[sender_domain]
        or raise "No dynamic smtp settings configured for `#{sender_domain}`"
      message.delivery_method.settings.merge!(dynamic_settings)
    end
  end
end

Note that the regexp in line 6 contains a named capture: (?<sender_domain>.+). And because we’re using the =~ operator together with this regexp, we automatically get a local variable with the same name: sender_domain. The variable will contain the matched string or nil, if the regexp didn’t match. I personally find this way more expressive than using $1 and friends.

If you’re interested in why we decided to implement the specific runtime checks in lines 7 and 11 check out the article Save Your Future Self Some Debugging Time.


We offer Ruby on Rails consulting based on over 11 years of experience, going back to Rails 0.9.3 in 2005.

Want to know more?