/ sendgrid

Integrating Sendgrid with your Application to receive Email related Events.

We've one Blogging application and whenever User post a new Article we wanted to show that to whom his/her post is delivered, who has opened a post or clicked.. events like that. So that User will know how many people are reading his/her post and also how often.

We were already using services of Sendgrid. Now Sendgrid provides different APIs for different purposes. Some of them I'm listing below.

  1. SMTP API,
  2. WEB API,
  3. EVENT WEBHOOK,
  4. PARSE API.

Now, We needed EVENT WEBHOOK API for what we wanted to achieve. Here is a link to EVENT WEBHOOK API.

The API says that

SendGrid’s Event Webhook will notify a URL of your choice via HTTP POST with information about events that occur as SendGrid processes your E-mail. Common uses of this data are to remove unsubscribes, react to spam reports, determine unengaged recipients, identify bounced E-mail addresses, or create advanced analytics of your E-mail program. With Unique Arguments and Category parameters, you can insert dynamic data that will help build a sharp, clear image of your mailings.

Now, we need to specify an endpoint to which Sendgrid will send us the events. Which we did in mail settings.

There is an Event Notifications tab which we turned on and we specified a post url to which we wanted to receive events. Below that HTTP POST URL Sendgrid has given INTEGRATION TESTING TOOL. After setting up your URL you can use this tool to check whether you've successfully configured your URL or not. What actually Sendgrid does is, it sends sample event notification to your specified url which allows you to check your integration. Below that testing tool there is SELECT ACTIONS options. Which allows you to select types of event you want to receive.

Since we've set up POST URL in Sendgrid, we also need to add that route in(routes.rb) our application.

post "/sendgrid_events", to: "sendgrid_post_events#sendgrid_events"

Later we'll set up a Controller to receive these events.

Now after setting up this configuration we were receiving events related to all E-mails. We only wanted to store events related to User's Article. So to differentiate between other events and "Article" related events we used unique arguments.

In our UserMailer we were sending an E-mail to all other Users when any new Article is posted. So we added unique arguments to that mail.

  def send_notification_on_new_post(users_list, post)
    @post = Post.find(post["id"])

    post_user = @post.user
    user_name = post_user.name.titleize || post_user.email

    headers "X-SMTPAPI" => {
      "unique_args": {
        "post_id": post.id
      }
    }.to_json

    @subject = user_name + " posted a new article on KnowBuddy"
    mail(bcc: users_list, subject: @subject)
  end

So what happens is whenever you send an unique arguments with an E-mail, Sendgrid returns that unique arguments in event. Now we can check whether that unique argument is present in an event or not and accordingly we can store it in our database.

Here is a sample event sent by Sendgrid.

{"_json"=>[{"email"=>"abc@xyz.com", "timestamp"=>1463495370, "ip"=>"64.233.173.163", "sg_event_id"=>"MTI3ZTY3YjYtMjQ4Ny00NzJlLWJlMGUtMDNiYjZiMzM3Njcx", "post_id"=>5, "sg_message_id"=>"L9e9SA7-S9akzUQPX0-qUg.filter0534p1mdw1.8867.573B2A7116.1", "useragent"=>"Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0 (via ggpht.com GoogleImageProxy)", "event"=>"open"}]}

You can see the unique argument "post_id" here. Now, to receive these events we made one controller SendgridPostEventsController and in that controller we declared one method "sendgrid_events". We defined one route to this method in our routes.rb file earlier.

class SendgridPostEventsController < ApplicationController    
  def sendgrid_events
    params["_json"].each do |value|
      SendgridEmailEvent.create(
        post_id: value["post_id"], event: value["event"], user: User.find_by(email: value["email"]),
        occurence_time: value["timestamp"]) unless value["post_id"].nil?
    end
    render nothing: true, status: 200
  end
end

The problem we faced was, whenever Sendgrid sent us an event is was showing two errors.

  1. cannot verify csrf token authenticity.
  2. unauthenticated user

So, what we did is we added

"protect_from_forgery with: :null_session" and
"skip_before_action :authenticate_user!, only: [:sendgrid_events]". So finally our controller looked like this.

class SendgridPostEventsController < ApplicationController
  skip_before_action :authenticate_user!, only: [:sendgrid_events]
  protect_from_forgery with: :null_session

  def sendgrid_events
    params["_json"].each do |value|
      SendgridEmailEvent.create(
        post_id: value["post_id"], event: value["event"], user: User.find_by(email: value["email"]),
        occurence_time: value["timestamp"]) unless value["post_id"].nil?
    end
    render nothing: true, status: 200
  end
end

Some extra thing that you might have noticed here are we're rendering nothing here and responding with status 200.
Now whenever we call a method rails expect to have a view named as that method. Since we didn't had any we added "render nothing: true".

As per status: 200, Sendgrid expects 200 status in return whenever it sends an event. So that Sendgrid can know you've received your event otherwise Sendgrid keeps sending you the same events for 24 hours in case you've misconfigured your URL.

With an event Sendgrid also sends us timestamp(UNIX) which is the exact time at which event has occured. You can convert that timestamp into time according to your zone. For that we specified our time-zone in application.rb .

config.time_zone = 'Mumbai'

And to convert that timestamp we added one helper method.

def sendgrid_event_occurence_time(timestamp)
  Time.zone.at(timestamp).strftime("Your Format") unless timestamp.nil?
end

kudos! That's it! Now you can access your table from any method and show the events in your view.