renz45 / mandrill_mailer

A small gem for sending Mandrill template emails
260 stars 84 forks source link

Mandrill Mailer

Build Status Gem Version Inline Documentation

Inherit the MandrillMailer class in your existing Rails mailers to send transactional emails through Mandrill using their template-based emails.

Installation

Add this line to your application's Gemfile:

gem 'mandrill_mailer'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install mandrill_mailer

Usage

Add the following to your mail.rb in your Rails app's config/initializers directory:

ActionMailer::Base.smtp_settings = {
    :address   => "smtp.mandrillapp.com",
    :port      => 587,
    :user_name => ENV['MANDRILL_USERNAME'],
    :password  => ENV['MANDRILL_API_KEY'],
    :domain    => 'heroku.com'
  }
ActionMailer::Base.delivery_method = :smtp

MandrillMailer.configure do |config|
  config.api_key = ENV['MANDRILL_API_KEY']
  config.deliver_later_queue_name = :default
end

You don't need to add the ActionMailer stuff unless you're still using ActionMailer emails.

This uses the Mandrill SMTP servers. If you're using template-based emails through the Mandrill API you only need the MandrillMailer.configure portion.

Do not forget to setup the environment (ENV) variables on your server instead of hardcoding your Mandrill username and password in the mail.rb initializer.

You will also need to set default_url_options for the mailer, similar to ActionMailer in your environment config files in config/environments:

config.mandrill_mailer.default_url_options = { :host => 'localhost' }

Creating a new mailer

Creating a new Mandrill mailer is similar to a typical Rails one:

class InvitationMailer < MandrillMailer::TemplateMailer
  default from: 'support@example.com'

  def invite(invitation)
    # in this example `invitation.invitees` is an Array
    invitees = invitation.invitees.map { |invitee| { email: invitee.email, name: invitee.name } }

    mandrill_mail(
      template: 'group-invite',
      subject: I18n.t('invitation_mailer.invite.subject'),
      to: invitees,
        # to: invitation.email,
        # to: { email: invitation.email, name: 'Honored Guest' },
      vars: {
        'OWNER_NAME' => invitation.owner_name,
        'PROJECT_NAME' => invitation.project_name
      },
      important: true,
      inline_css: true,
      recipient_vars: invitation.invitees.map do |invitee|
        { invitee.email =>
          {
            'INVITEE_NAME' => invitee.name,
            'INVITATION_URL' => new_invitation_url(
              invitee.email,
              secret: invitee.secret_code
            )
          }
        }
      end
     )
  end
end

Sending a message without template

Sending a message without template is similar to sending a one with a template. The biggest change is that you have to inherit from MandrillMailer::MessageMailer instead of the MandrillMailer::TemplateMailer class:

class InvitationMailer < MandrillMailer::MessageMailer
  default from: 'support@example.com'

  def invite(invitation)
    # in this example `invitation.invitees` is an Array
    invitees = invitation.invitees.map { |invitee| { email: invitee.email, name: invitee.name } }

    # no need to set up template and template_content attributes, set up the html and text directly
    mandrill_mail subject: I18n.t('invitation_mailer.invite.subject'),
                  to: invitees,
                  # to: invitation.email,
                  # to: { email: invitation.email, name: 'Honored Guest' },
                  text: "Example text content",
                  html: "<p>Example HTML content</p>",
                  # when you need to see the content of individual emails sent to users
                  view_content_link: true,
                  vars: {
                    'OWNER_NAME' => invitation.owner_name,
                    'PROJECT_NAME' => invitation.project_name
                  },
                  important: true,
                  inline_css: true,
                  attachments: [
                    {
                      content: File.read(File.expand_path('assets/offer.pdf')),
                      name: 'offer.pdf',
                      type: 'application/pdf'
                    }
                  ],
                  recipient_vars: invitation.invitees.map do |invitee| # invitation.invitees is an Array
                    { invitee.email =>
                      {
                        'INVITEE_NAME' => invitee.name,
                        'INVITATION_URL' => new_invitation_url(invitee.email, secret: invitee.secret_code)
                      }
                    }
                  end
  end
end

Sending an email

You can send the email by using the familiar syntax:

InvitationMailer.invite(invitation).deliver_now InvitationMailer.invite(invitation).deliver_later(wait: 1.hour) For deliver_later, Active Job will need to be configured

Creating a test method

When switching over to Mandrill for transactional emails we found that it was hard to setup a mailer in the console to send test emails easily (those darn designers), but really, you don't want to have to setup test objects everytime you want to send a test email. You can set up a testing 'mock' once and then call the .test method to send the test email.

You can test the above email by typing: InvitationMailer.test(:invite, email:<your email>) into the Rails Console.

The test for this particular Mailer is setup like so:

test_setup_for :invite do |mailer, options|
    invitation = MandrillMailer::Mock.new({
      email: options[:email],
      owner_name: 'foobar',
      secret: rand(9000000..1000000).to_s
    })
    mailer.invite(invitation).deliver
end

Use MandrillMailer::Mock to mock out objects.

If in order to represent a url within a mock, make sure there is a url or path attribute, for example, if I had a course mock and I was using the course_url route helper within the mailer I would create the mock like so:

course = MandrillMailer::Mock.new({
  title: 'zombies',
  type: 'Ruby',
  url: 'http://funzone.com/zombies'
})

This would ensure that course_url(course) works as expected.

The mailer and options passed to the .test method are yielded to the block.

The :email option is the only required option, make sure to add at least this to your test object.

Offline Testing

You can turn on offline testing by requiring this file (say, in your spec_helper.rb):

require 'mandrill_mailer/offline'

And then if you wish you can look at the contents of MandrillMailer.deliveries to see whether an email was queued up by your test:

email = MandrillMailer.deliveries.detect { |mail|
  mail.template_name == 'my-template' &&
  mail.message['to'].any? { |to| to[:email] == 'my@email.com' }
}
expect(email).to_not be_nil

Don't forget to clear out deliveries:

before :each { MandrillMailer.deliveries.clear }

Using Delayed Job

The typical Delayed Job mailer syntax won't work with this as of now. Either create a custom job or queue the mailer as you would queue a method. Take a look at the following examples:

def send_hallpass_expired_mailer
  HallpassMailer.hallpass_expired(user).deliver
end
handle_asynchronously :send_hallpass_expired_mailer

or using a custom job

def update_email_on_newsletter_subscription(user)
  Delayed::Job.enqueue( UpdateEmailJob.new(user_id: user.id) )
end

The job looks like (Don't send full objects into jobs, send ids and requery inside the job. This prevents Delayed Job from having to serialize and deserialize whole ActiveRecord Objects and this way, your data is current when the job runs):

class UpdateEmailJob < Struct.new(:user_id)
  def perform
    user = User.find(user_id)
    HallpassMailer.hallpass_expired(user).deliver
  end
end

Using Sidekiq

Create a custom worker:

class UpdateEmailJob
  include Sidekiq::Worker
  def perform(user_id)
    user = User.find(user_id)
    HallpassMailer.hallpass_expired(user).deliver
  end
end

#called by
UpdateEmailJob.perform_async(<user_id>)

Or depending on how up to date things are, try adding the following to config/initializers/mandrill_mailer_sidekiq.rb

::MandrillMailer::TemplateMailer.extend(Sidekiq::Extensions::ActionMailer)

This should enable you to use this mailer the same way you use ActionMailer. More info: https://github.com/mperham/sidekiq/wiki/Delayed-Extensions#actionmailer

Using Resque

Create a job:

class SendUserMailJob
  def initialize(user_id)
    @user_id = user_id
  end

  def work
    user = User.find(@user_id)
    UserMailer.send_user_email(user).deliver
  end
end

Send your job to Resque:

resque = Resque.new
resque << SendUserMailJob.new(<user id>)

Using an interceptor

You can set a mailer interceptor to override any params used when you deliver an e-mail. The interceptor is a Proc object that gets called with the mail object being sent to the api.

Example that adds multiple bcc recipients:

MandrillMailer.configure do |config|
  config.interceptor = Proc.new {|params|

    params["to"] =  [
      params["to"],
      { "email" => "bccEmailThatWillBeUsedInAll@emailsSent1.com", "name" => "name", "type" => "bcc" },
      { "email" => "bccEmailThatWillBeUsedInAll@emailsSent2.com", "name" => "name", "type" => "bcc" },
      { "email" => "bccEmailThatWillBeUsedInAll@emailsSent3.com", "name" => "name", "type" => "bcc" }
    ].flatten
  }
end