Deep in Rails: ActionMailer#deliver Part II
When we left off last, we were just about to get into rendering the message. Let’s recap that code:
(line 553 action_mailer/base.rb)
def render_message(method_name, body)
if method_name.respond_to?(:content_type)
@current_template_content_type = method_name.content_type
end
render :file => method_name, :body => body
ensure
@current_template_content_type = nil
end
When we call render_message in create!, we call it with @template and @body, let’s see what those get set to inside of render_message for our example:
@template = method_name => "contact_form"
@body = body => {:message=>"Send this message"}
At the beginning of render_message @body is set to the hash of instance variables that we set in our contact_form method in our mailer. Once we are finished processing, that hash will be gone and @body will be replaced with the full rendered message.
Next, we see that we want to set the @current_template_content_type instance variable to the proper content type. This is mainly used for multipart so that if render is passed one of those fancy ActionView::ReloadableTemplate objects it renders it with the proper parser. Since we did not specify a content_type the method_name “contact_form” is passed and we get to skip over that part and go straight to the meat:
render :file => method_name, :body => body
This is not the same render from ActionController or ActionView, but it does use the AV template render methods. Let’s take a look at the definition:
(line 562 action_mailer/base.rb)
def render(opts)
body = opts.delete(:body)
if opts[:file] && (opts[:file] !~ /\// && !opts[:file].respond_to?(:render))
opts[:file] = "#{mailer_name}/#{opts[:file]}"
end
begin
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
@template.render(opts.merge(:layout => layout))
ensure
@template = old_template
end
end
and the params with our example:
opts => {
:file => "contact_form",
:body => {:message=>"Send this message"}
}
The first thing we do is set the variable body to opts[:body] and delete it from the options hash (this is all done in one stroke by the call delete which returns the value that was deleted). Next, we test to see if opts[:file] does not match “/” and does not respond to render then we set opts[:file] to the same thing but prepended with the mailer_name, in our case “message_mailer”.
Now we are getting to the real meat. First, we need to set some quick variables before we render. Here are the 2 lines we will be examining next:
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
In the first line we are swapping out the current @template variable with the result of initialize_template_class(body) and storing the old value in old_template. Here is the definition of initialize_template_class:
(line 603 action_mailer/base.rb)
def initialize_template_class(assigns)
template = ActionView::Base.new(self.class.view_paths, assigns, self)
template.template_format = default_template_format
template
end
and assigns gets set to:
body => {:message=>"Send this message"}
The first thing we see is that we create a var named template that is a new instantiation of ActionView::Base with the view_paths, assigns, and the model (MessageMailer in our example) passed to it. Let’s dig in and cross over to ActionView::Base and look at initialize:
(line 221 action_pack/action_view/base.rb)
def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc:
@assigns = assigns_for_first_render
@assigns_added = nil
@controller = controller
@helpers = ProxyModule.new(self)
self.view_paths = view_paths
@_first_render = nil
@_current_render = nil
end
and the params with our example:
view_paths => ["/example_app/app/views"]
assigns_for_first_render => {:message=>"Send this message"}
controller => #<MessageMailer:0x34a579c
@action_name="contact_form",
@subject="Im contacting you",
@content_type="text/plain",
@implicit_parts_order=["text/html", "text/enriched", "text/plain"],
@from="jake@somewhere.com",
@sent_on=Sun Nov 08 11:34:17 -0600 2009,
@headers={},
@default_template_name="contact_form",
@mime_version="1.0",
@body={:message=>"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template="contact_form",
@charset="utf-8">
This allows us to wrap the mailers view around the ActionView templating system and reuse the rendering module from ActionView in AM. This also stores our body variables for use inside of our view. Now that we have a template, we need to initialize the template_format with our mailer’s default template format.
template.template_format = default_template_format
Let’s open up the definition for default_template_format
(line 577 action_mailer/base.rb)
def default_template_format
if @current_template_content_type
Mime::Type.lookup(@current_template_content_type).to_sym
else
:html
end
end
This method checks the @current_template_content_type ivar and returns the proper mime-type. This is used as a switch by the renderer. If you dont explicitly set the content type AM defaults to HTML and your message is rendered in HTML mode. Since we didnt define a content_type in our example @current_template_content_type is nil and :html will be returned for the default_template_format.
After that we return the created template instance and that gets us through the initialize_template_class method.
So now that we have set the @template ivar in:
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
We move down to setting the layout for the mailer. In Rails you have the ability to use layouts just like in regular views. In our case, if we wanted to use a layout, we would have to add a file named message_mailer.html.erb in app/views/layouts. The other option is specifying a layout name in our mailer model (just like we would in a controller). So we could define MessageMailer as:
class MessageMailer < ActionMailer::Base
layout "my_email_layout"
def contact_form(msg_from, message)
...
end
end
In that case, we would need a file named my_email_layout.html.erb in our layouts directory. Since we include ActionController::Layout in AM::Base our model will respond to pick_layout. This is the method that will determine the layout needed for the renderer. Let’s dive into pick_layout, which is defined in ActionController::Layout
# (line 229 action_pack/action_controller/layout.rb)
def pick_layout(options)
if options.has_key?(:layout)
case layout = options.delete(:layout)
when FalseClass
nil
when NilClass, TrueClass
active_layout if action_has_layout? && candidate_for_layout?(:template => default_template_name)
else
active_layout(layout, :html_fallback => true)
end
else
active_layout if action_has_layout? && candidate_for_layout?(options)
end
end
and the params for our example:
options => {:file=>"message_mailer/contact_form"}
The first part of pick_layout checks for the key :layout in options and since we didnt define it in our example we will skip over it and straight to the else part of the conditional:
active_layout if action_has_layout? && candidate_for_layout?(options)
Here, we call the method active_layout if action_has_layout? returns true and candidate_for_layout?(options) returns true. action_has_layout? is a simple method to check if this action or mailer method has a layout and is basically making sure that your :except and :only actions render properly. Let’s take a look at action_has_layout? with our example
# (line 244 action_pack/action_controller/layout.rb)
def action_has_layout?
if conditions = self.class.layout_conditions
case
when only = conditions[:only]
only.include?(action_name)
when except = conditions[:except]
!except.include?(action_name)
else
true
end
else
true
end
end
and conditions would be:
conditions => nil
Since we are not setting a layout for our message the method self.class.layout_conditions returns nil. layout_conditions is a class method on AM that reads the attribute accessor hash layout_conditions. These conditions are written when you make the call:
layout "my_template" # or layout "my_template", :except => "some_message"
This writes all your conditions to a attribute hash for easy reading and subclassing. Since our example returns nil for layout_conditions we end up at the outer else statement and return true out of this method and on to candidate_for_layout?(options). This is a base method is written in layout.rb and overridden in AM::base.rb:
# (line 585 action_mailer/base.rb)
def candidate_for_layout?(options)
!self.view_paths.find_template(default_template_name, default_template_format).exempt_from_layout?
rescue ActionView::MissingTemplate
return true
end
and our paramaters and extra variables/methods :
options => {:file=>"message_mailer/contact_form"}
default_template_name => "contact_form"
default_template_format => :html
In this method, we are checking to see if there is a template with the above params. Since we dont have any layouts Rails throws an ActionView::MissingTemplate exception which we catch and just return true from it. This sets up the final part of pick_layout which is active_layout. Here is the definition for that:
# (line 201 action_pack/action_controller/layout.rb)
def active_layout(passed_layout = nil, options = {})
layout = passed_layout || default_layout
return layout if layout.respond_to?(:render)
active_layout = case layout
when Symbol then __send__(layout)
when Proc then layout.call(self)
else layout
end
find_layout(active_layout, default_template_format, options[:html_fallback]) if active_layout
end
In our case, we dont pass anything to active_layout so the first line will get set to default_layout, which is a private method that looks like:
# (line 220 action_pack/action_controller/layout.rb)
def default_layout #:nodoc:
layout = self.class.read_inheritable_attribute(:layout)
return layout unless self.class.read_inheritable_attribute(:auto_layout)
find_layout(layout, default_template_format)
rescue ActionView::MissingTemplate
nil
end
Since we didnt set a layout, the first line will return nil and the second line will return that value because auto_layout will also be nil. When we get back to active_layout, the variable layout is set to nil. The variable active_layout gets set to nil as well since we are layout-less. Now we get to find_layout which doesnt get called in our example because active_layout is nil. That’s it, we are now ready to do the main rendering. Let’s look at the code again:
(line 562 action_mailer/base.rb)
def render(opts)
...
begin
old_template, @template = @template, initialize_template_class(body)
layout = respond_to?(:pick_layout, true) ? pick_layout(opts) : false
@template.render(opts.merge(:layout => layout))
ensure
@template = old_template
end
end
and the params and current variables with our example:
opts => {
:file => "contact_form",
:body => {:message=>"Send this message"}
}
old_template => "contact_form"
@template => #<ActionView::Base:0x3496c10
@template_format=:html,
@assigns={:message=>"Send this message"},
@helpers=#<ActionView::Base::ProxyModule:0x3496b84>,
@controller=#<MessageMailer:0x34a5814
@action_name="contact_form",
@subject="Im contacting you",
@content_type="text/plain",
@implicit_parts_order=["text/html", "text/enriched", "text/plain"],
@from="jake@somewhere.com",
@sent_on=Sun Nov 08 18:15:42 -0600 2009,
@headers={},
@default_template_name="contact_form",
@mime_version="1.0",
@body={:message=>"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template=#<ActionView::Base:0x3496c10 ...>,
@charset="utf-8">,
@_current_render=nil,
@assigns_added=nil,
@_first_render=nil,
@view_paths=["/example_app/app/views"]>
So now we see that layout ends up being nil in our example which we then pass on to @template.render along with our original options. Now we’ll jump into render:
# (line 201 action_view/base.rb)
def render(options = {}, local_assigns = {}, &block) #:nodoc:
local_assigns ||= {}
case options
when Hash
options = options.reverse_merge(:locals => {})
if options[:layout]
_render_with_layout(options, local_assigns, &block)
elsif options[:file]
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
elsif options[:partial]
render_partial(options)
elsif options[:inline]
InlineTemplate.new(options[:inline], options[:type]).render(self, options[:locals])
elsif options[:text]
options[:text]
end
when :update
update_page(&block)
else
render_partial(:partial => options, :locals => local_assigns)
end
end
and our passed in params:
options => {:file=>"message_mailer/contact_form", :layout=>nil}
local_assigns => {}
Since options is a Hash, we jump right in and first merge in :locals => {} with options. options[:layout] returns nil so we skip that statement and go into the options[:file] conditional
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
Here, we search out the template from the AM models view_paths with our file and the temlpate_format, which in our case is :html. view_paths is an instance method in ActionView::Base that accesses the attribute @view_paths which holds an instance of ActionView::PathSet. This is a helper class that wraps an array with some pretty handy methods, one of which is find_template. This helper class stores each of the view_paths as a ActionView::ReloadableTemplate::ReloadablePath object. This allows us to do other cooler things which we go over later. In our case, view_paths returns:
self.view_paths => ["/example_app/app/views"]
Let’s dive into find_template:
# (line 201 action_pack/action_view/paths.rb)
def find_template(original_template_path, format = nil, html_fallback = true)
return original_template_path if original_template_path.respond_to?(:render)
template_path = original_template_path.sub(/^\//, '')
each do |load_path|
if format && (template = load_path["#{template_path}.#{I18n.locale}.#{format}"])
return template
elsif format && (template = load_path["#{template_path}.#{format}"])
return template
elsif template = load_path["#{template_path}.#{I18n.locale}"]
return template
elsif template = load_path[template_path]
return template
# Try to find html version if the format is javascript
elsif format == :js && html_fallback && template = load_path["#{template_path}.#{I18n.locale}.html"]
return template
elsif format == :js && html_fallback && template = load_path["#{template_path}.html"]
return template
end
end
return Template.new(original_template_path, original_template_path =~ /\A\// ? "" : ".") if File.file?(original_template_path)
raise MissingTemplate.new(self, original_template_path, format)
end
and our params:
original_template_path => "message_mailer/contact_form"
format => :html
The first line just returns back if we have a full fledged ActionView model that responds to render. Since we dont, we keep going and see that the next line makes sure that our original_template_path isnt an absolute path by removing the initial “/” if it is there and storing the final result in template_path. Next, we iterate over ourselves and do some checking (remember, PathSet is a subclass of Array, so we can call each and iterate over the contents internally). In our example, we end up executing the line:
elsif template = load_path[template_path]
return template
This is part of the fanciness of ActionView::ReloadableTemplate::ReloadablePath. One of the methods implemented is [] which works like a hash and pulls out the proper ReloadableTemplate from the paths array, in our case it’s
template => #<ActionView::ReloadableTemplate:0x34a2ad8
@base_path="message_mailer",
@template_path="message_mailer/contact_form.erb",
@_memoized_path=["message_mailer/contact_form.erb"],
@name="contact_form",
@previously_last_modified=Sat Nov 07 10:32:50 -0600 2009,
@_memoized_method_segment=["app47views47message_mailer47contact_form46erb"],
@format=nil,
@filename="/example_app/app/views/message_mailer/contact_form.erb",
@_memoized_relative_path=["app/views/message_mailer/contact_form.erb"],
@extension="erb",
@locale=nil,
@_memoized_path_without_extension=["message_mailer/contact_form"],
@_memoized_method_name_without_locals=["_run_erb_app47views47message_mailer47contact_form46erb"],
@load_path="/example_app/app/views">
So we found the proper view and we are now done with @find_template@. Back to @render@:
# (line 201 action_pack/action_view/base.rb)
def render(options = {}, local_assigns = {}, &block) #:nodoc:
local_assigns ||= {}
case options
when Hash
...
elsif options[:file]
template = self.view_paths.find_template(options[:file], template_format)
template.render_template(self, options[:locals])
elsif options[:partial]
...
end
when :update
...
end
end
The next line renders the template with any locals (in our case there are none). Before we dive into render_template ‘s definition, let’s take a look at the view we created for our example message:
(message_mailer/contact_form.erb)
Hi,
I am contacting you with this message:
<%= @message %>
Thanks
Jake
A ridiculously simple mailer view for our equally ridiculously simple mailer message. Back to render_template
# (line 193 action_pack/action_view/template.rb)
def render_template(view, local_assigns = {})
render(view, local_assigns)
rescue Exception => e
raise e unless filename
if TemplateError === e
e.sub_template_of(self)
raise e
else
raise TemplateError.new(self, view.assigns, e)
end
end
and our current params:
view => #<ActionView::Base:0x3496c10
@template_format=:html,
@assigns={:message=>"Send this message"},
@helpers=#<ActionView::Base::ProxyModule:0x3496b84>,
@controller=#<MessageMailer:0x34a5814
@action_name="contact_form",
@subject="Im contacting you",
@content_type="text/plain",
@implicit_parts_order=["text/html", "text/enriched", "text/plain"],
@from="jake@somewhere.com",
@sent_on=Mon Nov 09 20:09:58 -0600 2009,
@headers={},
@default_template_name="contact_form",
@mime_version="1.0",
@body={:message=>"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template=#<ActionView::Base:0x3496c10 ...>,
@charset="utf-8">,
@_current_render=nil,
@assigns_added=nil,
@_first_render=nil,
@view_paths=["/example_app/app/views"]>
local_assigns => {}
As we can see, render_template is a relatively simple method that delegates most of it’s work to Renderable#render. That’s where we are going next:
# (line 193 action_pack/action_view/renderable.rb)
def render(view, local_assigns = {})
compile(local_assigns)
view.with_template self do
view.send(:_evaluate_assigns_and_ivars)
view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type)
view.send(method_name(local_assigns), local_assigns) do |*names|
ivar = :@_proc_for_layout
if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
view.capture(*names, &proc)
elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}")
view.instance_variable_get(ivar)
end
end
end
end
Whew, that’s it for this week. Next week, we get super deep into rendering and see how Rails performs all of it’s rendering duties.