Deep in Rails: ActionMailer#deliver Part II

Posted by admin 22/11/2009 at 10h34

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.