Categories
Technical

Sending emails using Mandrill/Mailchimp with Ruby on Rails

I recently started working on a ticket where I was asked to add a new mail template and the logic behind it. I have not really worked with either Mandrill or Mailchimp much before, so I started doing research into how to do this. Every blog post and Stack Overflow post that I could find is aimed at doing this for the first time, i.e. integrating a Ruby on Rails app to use Mandrill/Mailchimp. After looking at documentation and reading through our codebase I managed to work it out in the end, but I hope that this post will explain to any other developer how to do this off the bat.


This is a screenshot of the email that I was provided with (on Mandrill) and asked to implement. The Mandrill interface is very user friendly, and you can click onto any of the images/text and change them if needed.

The important thing to note here is the merge tags, which are ways to display information dynamically in emails, such as *|FIRST_NAME|* and *|COMPANY_NAME|* shown here. (I’ll come back to these later) but other than that it is essentially a self-explanatory email.

So essentially, we want to send an email to users who have closed their case file the previous day without leaving feedback already. In the email, it should contain the button ‘Leave feedback’ which when clicked, takes them to the page to leave feedback. Simple!

My first step was to create a new mailer which lives in app/mailers/user_mailer.rb as it will be sent to users.

 def feedback_email(user_id, case_file_id)
   case_file = CaseFile.find(case_file_id)
   user      = User.find(user_id)
 
   global_merge_vars = {
     'RES_NUMBER' => case_file.resolver_ref,
     'COMPANY_NAME' => case_file.company.name,
     'FIRST_NAME' => user.first_name,
     'FEEDBACK_URL' => new_dashboard_case_file_feedback_url(case_file.id, locale: user.link_locale)
   }.map { |k, v| { name: k, content: v } }
 end

The two parameters are user_id and case_file_id as we need both of those to locate the relevant user and case file. The global_merge_vars are related to the email template and the merge tags that we saw previously. For instance, here the ‘FIRST_NAME’ from the global_merge_vars gets used on the template to display the first name, in the example case, ‘Katie’. You can use these to dynamically display any required information. FEEDBACK_URL is used to provide the link in the button for ‘Leave feedback’ from the email.

My next step was to add a query to find relevant case files. In this case, I need to find case files that were closed yesterday (0:00-23:59) that don’t already have feedback.

In app/models/case_file.rb:

scope :prompt_for_feedback, lambda {
   case_files = CaseFile.where(closed_at: 1.days.ago.beginning_of_day..1.days.ago.end_of_day)
   case_files = case_files.all { |cf| cf.feedback.nil? }
 }

Next, we need to add a new service that deals with the sending of the email.

In app/services/feedback_reminder.rb:

class FeedbackReminder
 def run
   CaseFile.prompt_for_feedback.find_each(batch_size: 100) do |case_file|
     UserMailer.feedback_email(case_file.user_id, case_file.id).deliver_later(wait_until: 7.hours.from_now)
   rescue StandardError => e
     ErrorLogging.notify(e, case_file_id: case_file.id)
     case_file.add_event(
       description: "[Error] #{e.message}",
       source: 'web',
       user_id: case_file.user_id,
       category: 'email.feedback_email'
     )
   end
 end
end

I chose to do this using batches (batch_size: 100) due to the high number of case files that this method will be handling.

Using deliver_later enqueues the emails using Active Job to be sent later. When they do get sent, they get sent using deliver_now. I’ll come back to the 7.hours.from_now part!

Next, I added the text for the subject inside of a locale.

In config/locales/mailers/en-GB.yml:

  feedback_email:
       subject: "How did your case go?"

Next, to add the rake schedule. We use rake as our scheduler, it is what runs the job behind the scenes.

In config/schedule.rb:

every :day, at: '5.00am' do
 rake 'scheduler:send_feedback_reminder_email'
end

I chose to run this job at 5:00am as it is a time of low traffic for us. Because this job will handle thousands of emails each day, it is important to pick a time of low traffic so that the server can handle it efficiently. This is why in app/services/feedback_reminder.rb I use:

deliver_later(wait_until: 7.hours.from_now)

deliver_later(wait_until: 7.hours.from_now)

As I want the email to be delivered to inboxes around midday so that people will be on lunch break and (hopefully!) more likely to read the email and to engage with it. Doing it this way, there’s no need to worry about traffic to the site as the job will already be enqueued for sending.

Next I added the task to the scheduler which calls the process.

In lib/tasks/scheduler.rake:

 desc 'This task sends a prompt to leave feedback'
 task send_feedback_reminder_email: %i[environment logger] do
   FeedbackReminder.new.run
 end

Finally, my specs. In spec/mailer/user_mailer_spec.rb:

describe '#feedback_email' do
   let(:message) { UserMailer.feedback_email(user.id, case_file.id).message }
   subject { described_class.new }
   let(:case_file) { create(:case_file, :closed_yesterday, user: user) }
 
   it 'sets from' do
     expect(message.from).to eq(['no-reply@example.com'])
   end
 
   it 'sets to' do
     expect(message.to).to eq([user.email])
   end
 
   it 'sets subject' do
     expect(message.subject).to eq('How did your case go?')
   end
 
   it 'sets template' do
     expect(message[:template].value).to eq 'en-gb-uk-feedback-followup'
   end
 
   it 'sets global merge vars' do
     expect(message[:global_merge_vars].instance_variable_get(:@unparsed_value)).to include(
       { name: 'FIRST_NAME', content: user.first_name },
       { name: 'COMPANY_NAME', content: case_file.company.name },
       { name: 'RES_NUMBER', content: case_file.resolver_ref },
       name: 'FEEDBACK_URL', content: "http://complaint.example.com/dashboard/case_files/#{case_file.id}/feedbacks/new"
     )
   end
 end

And spec/services/feedback_reminder_spec.rb:

require 'rails_helper'
 
RSpec.describe FeedbackReminder do
 before { create(:case_file, :closed_yesterday) }
 
 describe 'run' do
   it 'sends an email' do
     expect { FeedbackReminder.new.run }.to change { enqueued_jobs.count }.by(1)
     expect(enqueued_jobs.last[:args]).to include('UserMailer', 'feedback_email')
   end
 end
end

One thing to take note of is that if you make any changes to the template on Mailchimp then once you are finished, make sure to select ‘Send to Mandrill’ from the dropdown menu:

By Poppy Rodgers

Developer at Resolver