Decoding Rails Magic: How Does Calling Class Methods On Mailers Work
Have you ever wondered how it is possible that calling class methods on mailers work in Rails, even though you only define some instance methods in those classes? It seems like it’s quite common question, especially when you see the mailers in action for the first time. Apparently, there is some Ruby "magic" involved here, so let’s try to decode it and check what happens under the hood.
Anatomy of ActionMailer mailers
Let’s start with adding a very simple WelcomeMailer
as an example:
# app/mailers/welcome_mailer.rb
class WelcomeMailer < ApplicationMailer
def welcome(user)
@user = user
mail to: user.email
end
end
If we wanted to send welcome
email to some user
, we would write the following code:
WelcomeMailer.welcome(user).deliver_now
It’s quite interesting that it works just like that, even though we have never defined any class method in WelcomeMailer
. Most likely it’s handled with method_missing
magic in ActionMailer::Base
. To verify that, let’s dive into Rails source code. In ActionMailer::Base class we indeed have method_missing
defined for class methods:
def method_missing(method_name, *args) # :nodoc:
if action_methods.include?(method_name.to_s)
MessageDelivery.new(self, method_name, *args)
else
super
end
end
Basically, any action method defined in mailer class will be intercepted by method_missing
and will return an instance of MessageDelivery
, otherwise it runs the default implementation. And where do action methods
come from? ActionMailer::Base
inherits from AbstractController::Base
, so it works exactly the same as for controllers - it returns a set of public instance methods of a given class.
We now have a better idea what actually happens when we call class methods on mailers, but it still doesn’t answer the questions: how is the mailer instantiated and how is the instance method called on it? To investigate it further, we need to check MessageDelivery class. We are particularly interested in deliver_now method (could be any other delivery method, but let’s stick to this single one) with the following body:
def deliver_now
processed_mailer.handle_exceptions do
message.deliver
end
end
Looks like processed_mailer is the key method that we were looking for:
def processed_mailer
@processed_mailer ||= @mailer_class.new.tap do |mailer|
mailer.process @action, *@args
end
end
This method creates the instance of the mailer, calls process
method with @action
argument (which is the name of the instance method) and with @args
, which are the arguments passed to the class method and in the end it returns the created instance of the mailer. Inside handle_exceptions
the deliver
method is called. And where does this one come from? MessageDelivery
inherits from Delegator
class and delegates all the method calls for methods not implemented by MessageDelivery
to processed_mailer.message
, which is the attribute defined in our mailer instance itself.
And that’s it! It took a bit switching between different methods and classes to understand the entire flow and what happens under the hood, but it’s clear that such interface hiding all the complexity is quite convenient.
Wrapping up
Some parts of Rails may contain a lot of "magic" which makes understanding the details more difficult. However, thanks to that magic, the usage of these parts is greatly simplified by the nice abstraction and easy to use interface, which is exactly the case with Rails mailers.