July 6, 2017

Monkey Patching Griddler to Add Mailgun Properties

Monkey Patching Griddler to Add Mailgun Properties

Griddler is a great gem to consider if your Rails application needs to receive email messages. Griddler works by creating an endpoint to receive webhooks from email services like SendGrid and Postmark. Since each email service formats their webhook payloads differently, Griddler adapts each incoming message into a generic Email class instance.

While this generic class makes it really easy to switch email services without rewriting your code, it does require you to discard any data from incoming messages that Griddler doesn’t include in its Email class. For example, I prefer to use Mailgun due to its addition a stripped-text parameter, which automatically removes quoted text and signature blocks from emails.

When using Griddler out of the box, messages received from Mailgun don’t include the stripped-text parameter since it’s not a common email field. Luckily, we can use monkey patching to cleanly extend Griddler and add this functionality just for our app.

Install Griddler

Let’s first install Griddler in our Rails app by adding the griddler-mailgun gem to the end of the Gemfile:

gem 'griddler-mailgun'

Run bundle install to install the Griddler gem with the Mailgun adapter. Next, add the default Griddler endpoint to app/config/routes.rb:

Rails.application.routes.draw do
  mount_griddler
end

This will create an endpoint at /email_processor - Mailgun will send emails to this route, and they’ll be processed by Griddler.

We also need to add an initializer file to configure Griddler, so create a new file named app/config/initializers/griddler.rb with the following code:

Griddler.configure do |config|
  config.email_service = :mailgun
end

Finally, we’ll need to create a class named EmailProcessor - Griddler needs this to be defined in our app, and it’s where you’ll eventually process email messages after they arrive. You can place this file wherever you prefer in your Rails project, but let’s create it at lib/email_processor.rb:

class EmailProcessor
  def initialize(email)
    @email = email
  end

  def process
    # do something with @email here
  end
end

In the process method above, the @email object will contain an email message that was sent from Mailgun, arrived at your app’s /email_processor endpoint, and was processed by Griddler. You can easily access the contents of the message as attributes, such as @email.from and @email.body.

Now let’s extend Griddler to handle Mailgun’s stripped-text attribute, which is currently simply dropped when an email is processed.

Extending the Mailgun Adapter

Let’s first take a look at the source code of the Griddler Mailgun adapter class, which processes webhook payloads as they arrive from Mailgun:

module Griddler
  module Mailgun
    class Adapter
      attr_reader :params

      def initialize(params)
        @params = params
      end

      def self.normalize_params(params)
        adapter = new(params)
        adapter.normalize_params
      end

      def normalize_params
        {
          to: to_recipients,
          cc: cc_recipients,
          bcc: Array.wrap(param_or_header(:Bcc)),
          from: determine_sender,
          subject: params[:subject],
          text: params['body-plain'],
          html: params['body-html'],
          attachments: attachment_files,
          headers: serialized_headers
        }
      end

      # ...
    end
  end
end

We’re mainly interested in normalize_params - this method maps the params values to a new hash that can be parsed into a Griddler::Email instance. Since we know stripped-text is included in the params sent from Mailgun, we simply need to add a new key/value pair to the hash returned from normalize_params.

Let’s create a new module in our app to extend Griddler::Mailgun::Adapter - add the following code to app/config/initializers/griddler.rb:

# Add Mailgun stripped-text to normalized params
module GriddlerMailgunAdapterExtensions
  def normalize_params
    normalized_params = super
    normalized_params[:stripped_text] = params['stripped-text']
    normalized_params
  end
end

# Prepend custom extensions
Griddler::Mailgun::Adapter.class_eval do
  prepend GriddlerMailgunAdapterExtensions
end

Our module contains its own normalize_params method, which will be used to override the normalize_params method in Griddler::Mailgun::Adapter. The super method calls the original normalize_params method, which returns a hash object. A new :stripped_text key is added to the hash, with the value of params['stripped-text'] from the Mailgun payload. The modified hash is then returned.

Our extension module is then applied to Griddler::Mailgun::Adapter by using class_eval to “open” the original class. The prepend method is then used to “mix in” our extension module method.

Now our new normalize_params code will execute in place of the original method in Griddler::Mailgun::Adapter, and the resulting hash output will include our new key/value pair.

Extending Griddler’s Email Class

We also need to monkey patch Griddler::Email to account for the updates we made to the Adapter. Here’s the relevant source code:

require 'htmlentities'

module Griddler
  class Email
    include ActionView::Helpers::SanitizeHelper

    attr_reader :to,
                :from,
                :cc,
                :bcc,
                :original_recipient,
                :reply_to,
                :subject,
                :body,
                :raw_body,
                :raw_text,
                :raw_html,
                :headers,
                :raw_headers,
                :attachments,
                :vendor_specific,
                :spam_report

    def initialize(params)
      @params = params

      @to = recipients(:to)
      @from = extract_address(params[:from])
      @subject = extract_subject

      @body = extract_body
      @raw_text = params[:text]
      @raw_html = params[:html]
      @raw_body = @raw_text.presence || @raw_html

      @headers = extract_headers

      @cc = recipients(:cc)
      @bcc = recipients(:bcc)
      @original_recipient = extract_address(params[:original_recipient])
      @reply_to = extract_address(params[:reply_to])

      @raw_headers = params[:headers]

      @attachments = params[:attachments]

      @vendor_specific = params.fetch(:vendor_specific, {})

      @spam_report = params[:spam_report]
    end

    # ...
  end
end

We need to add :stripped_text to the attr_reader declaration, and make sure it’s parsed in the initialize method.

Add this code to app/config/initializers/griddler.rb:

# Assign Mailgun stripped-text to attribute
module GriddlerEmailExtensions
  def initialize(params)
    super
    @stripped_text = params[:stripped_text]
  end
end

# Add attribute for Mailgun stripped-text and prepend custom extensions
Griddler::Email.class_eval do
  attr_reader :stripped_text
  prepend GriddlerEmailExtensions
end

We’ve now defined a module with an initialize method, which uses super to call the original overridden method. An instance variable named @stripped_text is declared to store the value of params[:stripped_text] from our modified adapter (see previous section).

Finally, class_eval is used to modify the Griddler::Email class, where we add :stripped_text to the attr_reader declarations, then prepend our extension module method.

Wrapping it Up

Here’s the final version of app/config/initializers/griddler.rb:

Griddler.configure do |config|
  config.email_service = :mailgun
end

# Add Mailgun stripped-text to normalized params
module GriddlerMailgunAdapterExtensions
  def normalize_params
    normalized_params = super
    normalized_params[:stripped_text] = params['stripped-text']
    normalized_params
  end
end

# Prepend custom extensions
Griddler::Mailgun::Adapter.class_eval do
  prepend GriddlerMailgunAdapterExtensions
end

# Assign Mailgun stripped-text to attribute
module GriddlerEmailExtensions
  def initialize(params)
    super
    @stripped_text = params[:stripped_text]
  end
end

# Add attribute for Mailgun stripped-text and prepend custom extensions
Griddler::Email.class_eval do
  attr_reader :stripped_text
  prepend GriddlerEmailExtensions
end

To recap, we’ve added a small amount of functionality to Griddler. When an email arrives from Mailgun, the stripped-text parameter is now processed, and is included as an attribute of the resulting Griddler::Email object. We’re now free to use stripped-text anywhere in our application!

Source Code Available on GitHub

You can find the full source code for this example here: https://github.com/jwkratz/griddler-stripped-text