Post hero cover

Sending multiple emails with ActionMailer

If you have ever built a Rails application, you probably have encountered ActionMailer along on your travels. If that is not the case, then it suffices to say that it is the Rails way of sending emails. Here’s a basic example:

class Mailer < ActionMailer::Base
  default from: "notifications@subvisual.co"

  def welcome_email(recipient)
    @recipient = recipient
    mail(
      to: recipient.email,
      subject: "Welcome to Subvisual"
    )
  end
end

class UsersController < ApplicationController
  def create
    ...
    Mailer.
      welcome_email(@user).
      deliver_later
    ...
  end
end

In this example we are sending one email to one user, when we are able to create that user’s record. Notice also that we are using the deliver_later method, instead of deliver_now, which will use ActiveJob to send the email as a background job.

But what if we wanted to send a notification to multiple users all at once?

Recipients list

In this next example, we have a list of recipients we want to notify that a new comment has been created for a post. For that, we will take advantage of the fact that the mail method can take a list of emails instead of only one.

class Mailer < ActionMailer::Base
  default from: "notifications@subvisual.co"

  def new_comment_email(recipients, comment)
    @recipients = recipients
    mail(
      to: recipients.map(&:email).uniq, 
      subject: "New comment on the Subvisual blog"
    )
  end
end

I have omitted the controller for brevity, but you can probably figure it out. As you can see, it is pretty simple, to send an email to a list of recipients. A possible problem with this approach is that each recipient of the email is now aware of all the other recipients of the email, which might not be a good idea.

Recipients list - 2nd try

Another simple thing we can do, is sending the emails to recipients in blind carbon copy (bcc), like this:

class Mailer < ActionMailer::Base
  default from: "notifications@subvisual.co"

  def new_comment_email(recipients, comment)
    @recipients = recipients
    mail(
      bcc: recipients.map(&:email).uniq, 
      subject: "New comment on the Subvisual blog"
    )
  end
end

This would be a pretty good place to stop, since we are now sending emails to a lot of users in one go.

But, what if we need the content of the email to be different for each recipient? Maybe they have different roles in the system and are allowed to see different things in the email.

Well, that leaves us with no option other than actually sending one email per recipient, so that we can control what goes out in each of them (or we could try to group the recipients together by role, but ultimately we would end up sending more than one email).

Sending multiple emails

A naive first approach could be to try and loop over the mail method, once for each recipient, like so:

class Mailer < ActionMailer::Base
  default from: "notifications@subvisual.co"

  def new_comment_email(recipients, comment)
    @recipients = recipients
    @recipients.each do |recipient|
      mail(
        to: recipient.email, 
        subject: "New comment on the Subvisual blog"
      )
    end
  end
end

If you have ever tried this, you soon figured out that this does not work. ActionMailer will actually only send an email to the very first recipient and ignore the others.

Sending multiple emails - loop in the controller

The “by the book” way of doing this would be to move the loop into the controller:

class Mailer < ActionMailer::Base
  default from: "notifications@subvisual.co"

  def new_comment_email(recipient, comment)
    @recipient = recipient
    mail(
      to: recipient.email, 
      subject: "New comment on the Subvisual blog"
    )
  end
end

class CommentsController < ApplicationController
  def create
    ...
    @recipients.each do |recipient|
      Mailer.
        new_comment_email(recipient, @comment).
        deliver_later
    end
    ...
  end
end

This is fine, and just works™, but if we are in the mood for a bit of over engineering and monkey patching, there is a way to avoid having to write the loop every time (because we all know how tiring that is).

Sending multiple emails - monkey patching

I am going to lay this all on you at once, try to digest it.

module ActionMailer
  class MessageDelivery
    def deliver_multiple_later(options = {})
      recipients = options.
        delete(:recipients).
        uniq.
        compact
      recipients.each do |recipient|
        @args.unshift recipient
        enqueue_delivery :deliver_now, options
        @args.shift
      end
    end
  end
end

This is reaching into the internals of ActionMailer, and adding a new way to deliver emails, that does the loop and sends the recipient as the first argument to the Mailer method. Here is how we can use it:

class Mailer < ActionMailer::Base
  default from: "notifications@subvisual.co"

  def new_comment_email(recipient, comment)
    @recipient = recipient
    mail(
      to: recipient.email, 
      subject: "New comment on the Subvisual blog"
    )
  end
end

class CommentsController < ApplicationController
  def create
    ...
    Mailer.
      new_comment_email(recipient, @comment).
      deliver_multiple_later(recipients: @recipients)
    ...
  end
end

There you have it, your very own way to deliver an email to multiple recipients. Do not trust it too much, because it is using Rails internals, which might change in the future and break your application, but hey, what is life without a thrill?

Luis Zamith

About Luis Zamith

Has worked on the web for a while now, mainly using Ruby and Ruby on Rails. Enjoys open source and giving back to the community, having taught Rails to hundreds of people.
Like what you've read so far? Let's work together.