Organizing your rails mailers

By Jeremy Venezia, on September 07, 2015
3 min

Rails mailer structure

Your application is growing, and you are starting to have a complex mailing system: notification emails, retention emails, misc user emails, admin emails, etc...

It's time to clean up your mailers !

Existing mailer

You may already have a single mailer, responsible of every emails, like this one:

app/mailers/user_mailer.rb

class UserMailer < ActionMailer::Base
  SENDER_EMAIL = 'notification@elcurator.net'
  SENDER = "elCurator <#{SENDER_EMAIL}>"
  default from: SENDER

  ...

  def new_article_notification(user, article)
    @user = user
    @article = article

    I18n.with_locale(UserMailer.user_locale user) do
      mail( to: user.email,
            subject: I18n.t('user_mailer.new_article_notification.subject'))
    end
  end

  ...

  def one_month_inactive_user(user)
    @user = user

    I18n.with_locale(UserMailer.user_locale user) do
      mail to: user.email,
           subject: I18n.t('user_mailer.one_month_inactive_user.subject')
    end
  end

  ...

  private

  def self.user_locale(user)
    user.try(:locale) || ENV['DEFAULT_LOCALE'] || :fr
  end
end

Associated templates should be:

  • app/views/user_mailer/new_article_notification.html.haml
  • app/views/user_mailer/one_month_inactive_user.html.haml

And I18n localizaion files:

  • config/locales/mailers/user_mailer.en.yml
  • config/locales/mailers/user_mailer.fr.yml

Split the mailer

Let's split our single mailer to separate emails with different responsibilities. Here we'll have a NotificationMailer and a RetentionMailer.

app/mailers/notification_mailer.rb

class NotificationMailer < ActionMailer::Base
  SENDER_EMAIL = 'notification@elcurator.net'
  SENDER = "elCurator <#{SENDER_EMAIL}>"
  default from: SENDER

  ...

  def new_article_notification(user, article)
    @user = user
    @article = article

    I18n.with_locale(NotificationMailer.user_locale user) do
      mail( to: user.email,
            subject: I18n.t('notification_mailer.new_article_notification.subject'))
    end
  end

  ...

  private

  def self.user_locale(user)
    user.try(:locale) || ENV['DEFAULT_LOCALE'] || :fr
  end
end

app/mailers/retention_mailer.rb

class RetentionMailer < ActionMailer::Base
  SENDER_EMAIL = 'notification@elcurator.net'
  SENDER = "elCurator <#{SENDER_EMAIL}>"
  default from: SENDER

  ...

  def one_month_inactive_user(user)
    @user = user

    I18n.with_locale(RetentionMailer.user_locale user) do
      mail to: user.email,
           subject: I18n.t('retention_mailer.one_month_inactive_user.subject')
    end
  end

  ...

  private

  def self.user_locale(user)
    user.try(:locale) || ENV['DEFAULT_LOCALE'] || :fr
  end
end

Templates have to be moved this way:

  • app/views/notification_mailer/new_article_notification.html.haml
  • app/views/retention_mailer/one_month_inactive_user.html.haml

Note that the I18n paths have change and are now separated for each mailer. Split your localization files too:

  • config/locales/mailers/notification_mailer.en.yml
  • config/locales/mailers/notification_mailer.fr.yml
  • config/locales/mailers/retention_mailer.en.yml
  • config/locales/mailers/retention_mailer.fr.yml

Code duplicaton

We have duplicated code here. Our two mailers uses the same sender. One way to clean this is to created an ApplicationMailer who will be inherited by our other mailers. Like so, NotificationMailer < ActionMailer::Base becomes NotificationMailer < ApplicationMailer.

app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  SENDER_EMAIL = 'notification@elcurator.net'
  SENDER = "elCurator <#{SENDER_EMAIL}>"
  default from: SENDER

  private

  def self.user_locale(user)
    user.try(:locale) || ENV['DEFAULT_LOCALE'] || :fr
  end
end

app/mailers/notification_mailer.rb

class NotificationMailer < ApplicationMailer
  ...

  def new_article_notification(user, article)
    @user = user
    @article = article

    I18n.with_locale(NotificationMailer.user_locale user) do
      mail( to: user.email,
            subject: I18n.t('notification_mailer.new_article_notification.subject'))
    end
  end

  ...
end

app/mailers/retention_mailer.rb

class RetentionMailer < ApplicationMailer
  ...

  def one_month_inactive_user(user)
    @user = user

    I18n.with_locale(RetentionMailer.user_locale user) do
      mail to: user.email,
           subject: I18n.t('retention_mailer.one_month_inactive_user.subject')
    end
  end

  ...
end

Move your mailing templates in a different folder

You may want to move you templates in a mailers folder now, to have something like this:

  • app/views/mailers/notification_mailer/new_article_notification.html.haml
  • app/views/mailers/retention_mailer/one_month_inactive_user.html.haml

Rails defaults settings wont work if you try this, because it won't search your templates within the app/views/mailers folder. One way to do this is to change the default template path in the ApplicationMailer, to find templates in app/views/mailers/{mailer_name}.

app/mailers/application_mailer.rb

class ApplicationMailer < ActionMailer::Base
  SENDER_EMAIL = 'notification@elcurator.net'
  SENDER = "elCurator <#{SENDER_EMAIL}>"
  default from: SENDER
  default template_path: -> (mailer) { "mailers/#{mailer.class.name.underscore}" }

  private

  def self.user_locale(user)
    user.try(:locale) || ENV['DEFAULT_LOCALE'] || :fr
  end
end

Now that we moved our templates in another folder, make shure that your relative I18n paths are not broken. All you mailer locales have to be moved in the mailers yml property.

app/views/mailers/notification_mailer/new_article_notification.html.haml

-# This relavite I18n path will search 'mailers.notification_mailer.new_article_notification.message'
.message= i18n.t('.message')

config/locales/mailers/notification_mailer.en.yml

en:
  mailers:
    notification_mailer:
      new_article_notification:
        message: New article!

Liked it ? Share it

A4892109ccf45280bc92ce1731f686d7
About the author
Jeremy Venezia is the tech leader and co-founder of elCurator.