Xiaohong Deng I am super awesome
Published

六 01 七月 2017

←Home

Rails ActionMailer: Explain Why Calling Class Methods That Do Not Exist on Mailers Works

Here is an article from Karol Galanciak you can refer to as a start. I'm gonna rewrite his article in a way that conveys pretty much the same information with addditional detail generated by me reading the Rails source code. Hopefully it would crystalize your understanding on the subject further. Most of the code snippets are snatched from Michael Hartl's Rails Tutorial

In a Rails app, you generate a Mailer by typing the following in the terminal

rails g mailer UserMailer account_activation

Inside user_mailer.rb you have

class UserMailer < ApplicationMailer
  def account_activation(user)
    @user = user
    mail to: user.email, subject: "Account activation"
  end
end

After that you can send email to user by

UserMailer.account_activation(user).deliver_now

You can preview the mail in user_mailer_preview by the following

class UserMailerPreview < ActionMailer::Preview

  # Preview this email at http://localhost:3000/rails/mailers/user_mailer/account_activation
  def account_activation
    user = User.first
    user.activation_token = User.new_token
    # account_activation is an instance method
    # of UserMailer. What's going on?
    UserMailer.account_activation(user)
  end

You can create a user_mailer_test.rb and do the following

require 'test_helper'

class UserMailerTest < ActionMailer::TestCase

  test "account_activation" do
    mail = UserMailer.account_activation
    assert_equal "Account activation", mail.subject
    assert_equal ["to@example.org"], mail.to
    assert_equal ["from@example.com"], mail.from
    assert_match "Hi", mail.body.encoded
  end
end

Why account_activation Is Defined as An Instance Method But Called as A Class Method?

You'd probably have guessed method_missing is behind the curtain. That's correct.

class Base
  class << self
  ...
    def method_missing(method_name, *args)
      if action_methods.include?(method_name.to_s)
        MessageDelivery.new(self, method_name, *args)
      else
        super
      end
    end
  ...
  end
end

Calling the instance method as a class method results in a MessageDelivery object. MessageDelivery is a subclass of Delegator. It's a wrapper of the related object. In this case Mail::Message. You can retrieve the wrapped object by md.message given md is a MessageDelivery object. The Mailer class, in this case UserMailer, instance method account_activation and arg user are passed to the MessageDelivery object.

Note that action_methods is inherited from AbstractController::Base. method_missing here is defined in singleton class. The scope in singleton class is the class itself. So method_missing handles class methods.

Returning a MessageDelivery object doesn't quite answer the question, does it? What about instance method account_activation? Does it get called somewhere?

What Does MessageDelivery#deliver_now Do

First, we have

def deliver_now
  processed_mailer.handle_exceptions do
    message.deliver
  end
end

Note message in the code is called on an instance of MessageDelivery. Though internally it is a delegator of processed_mailer. That is, deliver is actually called on processed_mailer.message. message is an attr_internal.

Let's look into processed_mailer method

def processed_mailer
  @processed_mailer ||= @mailer_class.new.tap do |mailer|
    mailer.process @action, *@args
  end
end

Here @mailer_class is UserMailer in our case. tap is defiend in Object as follows

class Object
  def tap
    yield self
    self
  end
end

So processed_mailer is equivalent to

mailer = @mailer_class.new
mailer.process @action, *args
@processed_mailer ||= mailer

I won't dig into process deeper because it is quite involved. I assume process executes the instance method on the UserMailer object. Almost all instance methods of MessageDelivery call processed_mailer internally. That more or less answers the question that if account_activation gets called somewhere?

Why Preview Can Show Mail Content by Returning a MessageDelivery Object

I can only guess that by visiting the URL specified above the method, some instance methods get triggered on the returned MessageDelivery object.

Why Methods to, from and subject Can Be Called on MessageDelivery Object

You guessed it again, method_missing! This time it's the method_missing inherited from Delegator. It calls __getobj__ which calls message on MessageDelivery object and returns the wrapped object, Mail::Message. It has methods to, from and subject.

Wrap Up

That's it. That is our little tour of visiting some Mailer related Rails source code. Hope that resolves your confusion to some extent.

Go Top
comments powered by Disqus