Thin and maintainable Rails mailers: How we refactored Rails mailers at mkdev
Tags: ruby, ror, rails, mailers
Published: 2019-10-22 11:53:41

---
title: Thin and maintainable Rails mailers: How we refactored Rails mailers at mkdev
published: true
description: How to make mailers in Rails simpler, smaller and more flexible using parameterized mailers
tags: Ruby, RoR, Rails, mailers
cover_image: https://assets.mkdev.me/posts/covers/000/000/160/original/thin-and-maintainable-rails-mailers-how-we-refactored-rails-mailers-at-mkdev.png
canonical_url: https://mkdev.me/en/posts/thin-and-maintainable-rails-mailers-how-we-refactored-rails-mailers-at-mkdev
---

*Important: this article was written some time ago, so don't be surprised  at parts that might look a bit out of date. It is however still totally appliable to the real world.*

A new feature in Rails 5.1, parameterized mailers, has just been introduced lately. With them you can you use default filters and parameters in mailers. These two highly useful features allowed us to effectively and quickly refactor our Rails mailers. And now were going to talk about it.

### Mailers before refactoring

There are lots of mailers that do not use parameterization. One of them is `NotificationsMailer` which notifies the students and the mentors about several important things, such as the subscription status, payments and stuff like that.

```ruby
# app/mailers/notifications_mailer.rb

class NotificationsMailer < ApplicationMailer

  def student_inactive_notice(user)
    @user = user
    I18n.locale = @user.locale
    mail subject: default_i18n_subject,
         to: @user.email
  end
 
  def new_payment_for_mentor(user, amount)
    @user = user
    @amount = amount
    I18n.locale = @user.mentor.locale
    mail subject: t('.subject'),
         to: @user.mentor.email
  end

  # ...

end

```

There are two localizations (Russian and English) for the template of each method. You set the language in the body of any method with `I18n.locale` and also call the method `mail(headers = {}, &block)`. We pass different arguments to this method, and set the default values for the most of them. In the aforementioned example we use `:subject` for an email subject, `:to` for a recipient mailing address, `:from` for a sender mailing address. The method `default_i18n_subject(interpolations = {})` is used to set an email subject in most mailers. The same thing happens if you call `t('.subject')`.

All mailer classes are inherited from `ApplicationMailer`, which will be a key pillar in our article. Before being refactored it looked that way:

```ruby
# app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  default from: "\"mkdev\" <#{ENV['SUPPORT_EMAIL']}>"
end

```


There is also a model where we call an instance method of `NotificationsMailer`. All the emails are sent in the background with `deliver_later(options = {})`, hence using Active Job. If you used to use `delay(options = {})` for this, now its time to change it, as its not compatible with parameterized mailers.

```ruby
# app/models/user.rb

class User < ApplicationRecord

  ...
  class << self
    def notify_inactive
      students.where("last_active_at < ?", 2.days.ago).find_each do |user|
        NotificationsMailer.student_inactive_notice(user).deliver_later
        # ...
      end
    end
  end
  ...

end
```

It goes without saying that all mailers are tested. They are rather homogeneous so I'm not going to show the code here.

So lets proceed with our crazy (believe me:)) refactoring to behold the elegance of parameterized mailers!

### Refactoring

Lets take headers `from:`, `subject:` and `to:` from `NotificationsMailer` and put them into the hash `default(value = nil)` inside `ApplicationMailer`, its options will be available for all the mailers by default.
 
Then lets define the filter `before_action(names, block)` to set the locale and a mail recipient. As the result well get a body positive `ApplicationMailer`:

```ruby
# app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base

  default from: "\"mkdev\" <#{ENV['SUPPORT_EMAIL']}>",
          subject: -> { default_i18n_subject },
          to: -> { @recipient.email }

  before_action :load_recipient

  private

  def load_recipient
    @recipient = params[:recipient]

    I18n.locale = if @recipient.is_a?(User)
                    @recipient.locale
                  elsif @recipient.user
                    @recipient.user.locale
                  else
                    :en
                  end
  end

end
```

That's it, now we have the locale for all the mailers and in one place. Convenient? Well, its also DRY.

But the best thing is the way `NotificationsMailer` is now going to look. And the rest are not bad-looking too, believe me!

I think it wouldn't hurt to note that instance variables, defined from the hash `params` entry, are needed for the templates.

```ruby
# app/mailers/notifications_mailer.rb

class NotificationsMailer < ApplicationMailer

  def student_inactive_notice
    mail
  end

  def new_student_for_mentor
    @student, @memo, @amount = params[:student], params[:memo], params[:amount]
    mail
  end

  # ...

end
```

You cant see it, but the code `NotificationsMailer` got 35% smaller and more legible and clear. There are loads of different mailers on mkdev, so you can imagine how the situation has been improved on the whole.

Now lets get back to the code, its time to correct mailer delivery in the models.

We just need to separate the key: value pairs with a comma in the `with(params)` method, which passes the parameters to the mailers.

```ruby
NotificationsMailer.with(recipient: user).student_inactive_notice # create an ActionMailer::MessageDelivery object
```

As the result we have this kind of model:

```ruby
# app/models/user.rb

class User < ApplicationRecord

  ...
  class << self
    def notify_inactive
      students.where("last_active_at < ?", 2.days.ago).find_each do |user|
        NotificationsMailer.with(recipient: user).student_inactive_notice.deliver_later
        # ...
      end
    end
  end
  ...

end
```

Since now we're done with refactoring, lets sum it all up.

Earlier all the parameters passed to Action Mailer were available only in the method body of the mailer. This problem is solved thanks to parameterized mailers. Now mailers can be called with, for instance, default behavior parameters, as was shown earlier. And this is amazing! Mailers become thinner and more legible, but remain as useful as before.

### Notifications settings

I would also like to mention that this refactoring allowed us to implement the settings (enable/disable) email notifications with ease. Now we can just call [policy](https://github.com/varvet/pundit) in one place, which was impossible without parameterized mailers:) By the way, we use the same settings not just for one mailer.

Let me show you how it was put into action with the code.

Lets add a common mailer method `mail_if_subscribed(*args)`.

```ruby
# app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base

  ...
  private
 
  def mail_if_subscribed(*args)
    mail *args if NotificationsPolicy.new(@recipient, self.class, self.action_name).notify?
  end
  ...

end
```

We need the `self.class` parameter to point out on which mailer we can call policy and `self.action_name` to show on which method exactly. And `*args` is used to rewrite the default headers passed to `mail`.

After rewriting `NotificationsMailer` we have:

```ruby
# app/mailers/notifications_mailer.rb

class NotificationsMailer < ApplicationMailer

  ...
  def student_subscription_expires
    mail_if_subscribed
  end

  def mentor_subscription_expires
    @student = params[:student]
    mail_if_subscribed
  end
  ...

end
```

Well, that's it for now, that's definitely it!

***

*This is [an mkdev artile](https://assets.mkdev.me/posts/covers/000/000/160/original/thin-and-maintainable-rails-mailers-how-we-refactored-rails-mailers-at-mkdev.png?1531856057) written by Yauheni Dakuka. You can hire [our RoR mentors](https://mkdev.me/en/specialization/developing-web-applications-with-ruby-on-rails) any time you need them.*