Skip to content

Instantly share code, notes, and snippets.

What would you like to do?

Building a data-driven approach to keeping user contacts up-to-date

This blog was written by Roy Young. Roy is a student from Tealeaf Academy.

Do you remember every email address you use with every one of your online accounts? You know, the accounts you use with your spam email address, versus your "real" email, versus your work email? Probably not. And neither do your users. That is why it makes sense to periodically confirm that you have the best contact information on file for your customers. You could do this every month, or once a year, or take a data driven approach that lets you focus only on the customers that will most likely find your notices to update their contact information helpful, rather than annoying.

Application logic for prompting user to update their email address

When you send email to an email address that no longer exists, the receiving party (e.g. Gmail or Yahoo) will bounce the email back to you. If you listen to these bounces, your app can react appropriately, creating a great user experience while keeping your customer database up-to-date. This blog outlines a sample application built on Heroku that uses Mailgun to listen to these bounced emails so that users with bad email addresses can continue to receive monthly invoice emails. The same logic could be applied to any of your emails, however, making sure that your users stay engaged and up-to-date with your company via email.

This post is built around the following workflow:

  1. Some user signs up for the app with some email address.
  2. App sends out automatic monthly billing emails to all users using Heroku Scheduler.
  3. User changes job, leaving their email address in system no longer valid.
  4. Emails sent to the old address bounce, and Mailgun sends a POST via the bounce webhook to the app.
  5. The app marks the user with a special status and stops sending the user monthly billing emails.
  6. When the user signs in to your app the next time, a flash message displays about email no longer being valid and takes them to the account page and prompts them to update their email.
  7. User updates their email address and they continue to receive the monthly billing emails.

I've created a demo site for this article and made the source code available on Github. This demo walks a novice through the entire process of setting up this functionality. Click here if you want to skip directly to integrating bounce web hooks into your a app.

####Sending monthly invoice emails via Mailgun using Heroku Scheduler

To send monthly invoices to your users, you'll need to write code to:

  • Deploy customized invoice emails to your hundreds, or thousands or millions of customers.
  • Actually Schedule the deployment of these emails.

Let's look at each of these in turn.

Deploying customized invoce emails using Mailgun

Sending emails via Mailgun simply requires an account. For the purposes of this tutorial, I'll you've already done that. From there, you'll want to send your customer list to Mailgun, along with any variables that you want dynamically inserted into the emails as a JSON file and the HTML or text body of the email. Here is the code.

class MailgunGateway
  include Rails.application.routes.url_helpers

  def send_batch_message(users),
    from: "Mailgun Demo <billing@#{ENV["mailgun_domain_name"]}>",
    to:", "),
    subject: "Monthly Billing Info",
    html: billing_info_text,
    :"h:Reply-To" =>  "billing@#{ENV["mailgun_domain_name"]}",
    :"recipient-variables" => recipient_variables(billing_recipients)


  def api_key
    @api_key ||= ENV["mailgun_api_key"]

  def messaging_api_end_point
    @messaging_api_end_piont ||= "https://api:#{api_key}{ENV["mailgun_domain_name"]}/messages"

  def billing_recipients
    @users ||= User.where(locked: false)

  def recipient_variables(recipients)
    vars = do |recipient|
      "\"#{}\": {\"name\":\"#{recipient.fullname}\"}"
    "{#{vars.join(', ')}}"

  def billing_info_text


Your bill for the current month is now available, please click on
to see details.
<p>Reply to this email directly</p>


In the send_batch_message method, we also use Recipient Variables so that we can define variables to customize each email. You'll notice that we are inserting the user's first name in the email. You could compose an invoice email that includes details like account number, invoice account, or anything else you like, but I'm keeping things simple.

####Sending out automatic monthly billing emails with Heroku Scheduler

Once we have our method for creating and deploying emails via Mailgun, we need to write some code that automatically triggers this method. I've chosen to deploy this app on Heroku and am using Heroku Scheduler because it does the job, is a free add-on, and is easy to integrate.

Heroku Scheduler will run a specified Rake task at a specified time. So here we need to build a Rake task to send billing emails using Mailgun in lib/tasks/scheduer.rake.

desc "This task is called by the Heroku scheduler add-on"

task :send_billing_info => :environment do
  if == 1
    User.where(locked: false).find_in_batches(batch_size: 1000) do |group|

Beacuse Heroku Scheduler "FREQUENCY" setting has only three selectors-- "daily, hourly, every 10 mins"-- we need to check whether the current day is the first day of the month using if == 1. If it is, the email is deployed. If not, no email.

When we send billing invoices to users, we need to find the email addresses of the customer we need to invoice.

In this sample app's very simple database shcema, we have a :locked attribute that is set to True when an email has bounced previously (more on that below). So our query simply pulls all our users where this field is False.

To actually send the email, I'm using Maigun Batch Sending. Beacuse the maximum number of recipients allowed per API call is 1,000, we use find_in_batches to send billing emails to Mailgun in batches of 1000 recipients.

This is what the Scheduler setting look like in Heroku:


####Receiving notification of bounced emails from Mailgun

When an email is bounced, Mailgun will send a POST via the bounce webhook to our app. You need to set the callback url under "Bounces" in the Mailgun Control Panel. In this demo, my callback url is ""


When the app receives the notification from Mailgun, the app will find the email address in the customer database, and set the user status to locked, so the app will not send billing email to the user until the user update his email address.

class Api::BouncedMailsController < ApplicationController
  skip_before_filter :verify_authenticity_token
  def create
    if verify(ENV["mailgun_api_key"], params[:token], params[:timestamp], params[:signature])
      user = User.find_by_email(params[:recipient])
      if user && params[:event] == "bounced"
  def verify(api_key, token, timestamp, signature)
    signature == OpenSSL::HMAC.hexdigest('sha256'),
      '%s%s' % [timestamp, token])
  • The user.lock! is to set user status to locked, so our Rake Task will never send emails to this user. This method is set in User model.
class User < ActiveRecord::Base
  def lock!
    self.locked = true
    save(validate: false)
  • The verity method is to verify the webhook is originating from Mailgun, otherwise any others could send a fake POST to lock the users in your app.

For more details on configuring web hooks you can read the Mailgun documentation - Events/Webhooks.

In the end, we return a 200 header to tell Mailgun that this interaction is successful, otherwise Mailgun will think our server is down and will keep trying to call our webhook.

####Displaying the flash screen for a user to update their email

Once the app knows which users have a bad email address, we need to display a flash screen to the user upon their next login with a prompt to verify their contact information on file.

class SessionsController < ApplicationController
  def new
    redirect_to home_path if current_user

  def create
    user = User.find_by_email(params[:email])
    if user && user.authenticate(params[:password])
      session[:user_id] =
      if user.locked
        flash[:error] = "Your Email Address is invalid, please update it."
        redirect_to edit_user_path(current_user)
        redirect_to home_path
      flash[:error] = "Incorrect email or password. Please try again."
      redirect_to sign_in_path

  def destroy
    session[:user_id] = nil
    redirect_to root_path


Once they've updated their email address, we set the locked attribute back to false so that they will continue to receive emails.  

That's it. Now, when your users email's bounce, you can prompt them to update their information, providing a great user experience with the added benefit of keeping your customer data fresh.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment