Deep in Rails: ActionMailer#deliver Part IV
So we just opened up CompiledTemplates and defined (or compiled) our unique method for this particular template. Now we continue with render:
# (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
and our params:
view => #"Send this message"},
@helpers=#,
@controller=#"Send this message"},
@mailer_name="message_mailer",
@recipients="someemail@somedomain.com",
@parts=[],
@template=#,
@charset="utf-8">,
@_current_render=nil,
@assigns_added=nil,
@_first_render=nil,
@view_paths=["/example_app/app/views"]>
local_assigns => {}
The first thing we do after we compile our unique method is call with_template on view which is a convenient method that allows us to temporarily swap out the current template for the view, set it to another one, do some processing and then return the original template back to the view. Let’s see with_template:
# (line 298 action_pack/action_view/base.rb)
def with_template(current_template)
last_template, self.template = template, current_template
yield
ensure
self.template = last_template
end
Quite simple indeed. Now let’s see exactly what we are doing to the template. The first thing we do is send the method _evaluate_assigns_and_ivars to the view. This is private method in ActionView::Base which creates instance variables for each key in the `assigns@ attribute hash. This hash is filled with ourbodycall in the mailer model you create. Here isevaluateassignsandivars`:
# (line 307 action_pack/action_view/base.rb)
def _evaluate_assigns_and_ivars #:nodoc:
unless @assigns_added
@assigns.each { |key, value| instance_variable_set("@#{key}", value) }
_copy_ivars_from_controller
@assigns_added = true
end
end
After the instance variables are set, we call _copy_ivars_from_controller which does exactly that, copies the instance variables from this view’s controller to this view. This is how instance variables that you create in normal controller methods are accessed in the view. Let’s look at that definition:
# (line 315 action_pack/action_view/base.rb)
def _copy_ivars_from_controller #:nodoc:
if @controller
variables = @controller.instance_variable_names
variables -= @controller.protected_instance_variables if @controller.respond_to?(:protected_instance_variables)
variables.each { |name| instance_variable_set(name, @controller.instance_variable_get(name)) }
end
end
This method finds all proper instance variables in the attached controller and sets the same instance variables in the current view object. Once we have copied the instance variables we move to the next line in render:
view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type)
This is another private method in ActionView::Base and it’s job is quite simple: set the content_type for the response attribute if this view’s controller has one. Let’s look at the code:
# (line 323 action_pack/action_view/base.rb)
def _set_controller_content_type(content_type) #:nodoc:
if controller.respond_to?(:response)
controller.response.content_type ||= content_type
end
end
In our case, the controller is the MessageMailer object and it doesnt respond to response so we dont set a content_type. Now we come back to render:
# (line 193 action_pack/action_view/renderable.rb)
def render(view, local_assigns = {})
...
view.with_template self do
...
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
The next thing we are going to do is call our dynamically generated (and now compiled) view method for this template. We call it with a block that is used to capture content pieces that this view may use (think content_for). AM does not use any of these so we end up skipping over this whole section and just executing our template method. In a future series on ActionController, we will dive more into what the block does and how Rails uses it’s information, but for now we will move on.
So what exactly did we do when we called that dynamically generated method? Let’s find out. First, we will look at our method again:
def _run_erb_app47views47message_mailer47contact_form46erb(local_assigns)
old_output_buffer = output_buffer;;@output_buffer = ''; __in_erb_template=true ; @output_buffer.concat "Hi,\n"
@output_buffer.concat "\n"
@output_buffer.concat "I am contacting you with this message:\n"
@output_buffer.concat "\n"
@output_buffer.concat(( @message ).to_s); @output_buffer.concat "\n"
@output_buffer.concat "\n"
@output_buffer.concat "\n"
@output_buffer.concat "\n"@output_buffer.concat "Thanks\n"
@output_buffer.concat "\n"
@output_buffer.concat "Jake"; @output_buffer
ensure
self.output_buffer = old_output_buffer
end
Remember that output_buffer is an attribute inside of ActionView::Base and that it holds the current output for the view that is being processed. Our dynamic method first stores the current output_buffer and then resets it so that it can insert the newly parsed message. When we execute our method, we concat our entire view (with any ruby processing) into the output_buffer. So after we execute _run_erb_app47views47message_mailer47contact_form46erb our output_buffer buffer looks like:
@output_buffer => "Hi,\n\nI am contacting you with this message:\n\nSend this message\n\n\n\nThanks\n\nJake"
which is the final email body that will be sent.
Alright, now we have rendered our message and we hit the bottom of this method chain. We now pop all the way back up create!. I know, I know. It was so long ago that you dont remember. Here is a refresher:
(line 458 action_mailer/base.rb)
def create!(method_name, *parameters) #:nodoc:
unless String === @body
...
@body = render_message(@template, @body) if template_exists
if !@parts.empty? && String === @body
@parts.unshift Part.new(:charset => charset, :body => @body)
@body = nil
end
end
@mime_version ||= "1.0" if !@parts.empty?
@mail = create_mail
end
We just got through with render_message and now we have our @body attribute filled with our message. The next line checks to see if we have parts and if we have a body that’s a String. If we do, we create a new Part with the current @body and add it to all of our parts. After that, we get rid of @body. We do this so that create_mail doesnt try and rerender the body again if we have other parts. Since we dont, we skip over this and go straight to:
@mail = create_mail
Now we’re gettin’ somewhere. This starts the actual email processing part of AM. Let’s see create_mail:
(line 638 action_mailer/base.rb)
def create_mail
m = TMail::Mail.new
m.subject, = quote_any_if_necessary(charset, subject)
m.to, m.from = quote_any_address_if_necessary(charset, recipients, from)
m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil?
m.cc = quote_address_if_necessary(cc, charset) unless cc.nil?
m.reply_to = quote_address_if_necessary(reply_to, charset) unless reply_to.nil?
m.mime_version = mime_version unless mime_version.nil?
m.date = sent_on.to_time rescue sent_on if sent_on
headers.each { |k, v| m[k] = v }
real_content_type, ctype_attrs = parse_content_type
if @parts.empty?
m.set_content_type(real_content_type, nil, ctype_attrs)
m.body = normalize_new_lines(body)
else
if String === body
part = TMail::Mail.new
part.body = normalize_new_lines(body)
part.set_content_type(real_content_type, nil, ctype_attrs)
part.set_content_disposition "inline"
m.parts << part
end
@parts.each do |p|
part = (TMail::Mail === p ? p : p.to_mail(self))
m.parts << part
end
if real_content_type =~ /multipart/
ctype_attrs.delete "charset"
m.set_content_type(real_content_type, nil, ctype_attrs)
end
end
@mail = m
end
The first thing we do is initialize a new TMail::Mail instance and then we go on to assign several attributes for it.
m.subject, = quote_any_if_necessary(charset, subject)
m.to, m.from = quote_any_address_if_necessary(charset, recipients, from)
m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil?
m.cc = quote_address_if_necessary(cc, charset) unless cc.nil?
m.reply_to = quote_address_if_necessary(reply_to, charset) unless reply_to.nil?
m.mime_version = mime_version unless mime_version.nil?
m.date = sent_on.to_time rescue sent_on if sent_on
The quote_any_if_necessary, quote_any_address_if_necessary and quote_address_if_necessary are helper methods included from the module ActionMailer::Quoting. Let’s take a gander at those methods:
(line 5 action_mailer/quoting.rb)
def quoted_printable(text, charset)
text = text.gsub( /[^a-z ]/i ) { quoted_printable_encode($&) }.gsub( / /, "_" )
"=?#{charset}?Q?#{text}?="
end
def quoted_printable_encode(character)
result = ""
character.each_byte { |b| result << "=%02X" % b }
result
end
if !defined?(CHARS_NEEDING_QUOTING)
CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
end
def quote_if_necessary(text, charset)
text = text.dup.force_encoding(Encoding::ASCII_8BIT) if text.respond_to?(:force_encoding)
(text =~ CHARS_NEEDING_QUOTING) ?
quoted_printable(text, charset) :
text
end
def quote_any_if_necessary(charset, *args)
args.map { |v| quote_if_necessary(v, charset) }
end
def quote_address_if_necessary(address, charset)
if Array === address
address.map { |a| quote_address_if_necessary(a, charset) }
elsif address =~ /^(\S.*)\s+(<.*>)$/
address = $2
phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
"\"#{phrase}\" #{address}"
else
address
end
end
def quote_any_address_if_necessary(charset, *args)
args.map { |v| quote_address_if_necessary(v, charset) }
end
Im not going to bore you with the inane details, needless to say, these methods take illegal characters and quote them if necessary. Once these are quoted and sanitized, they are inserted into the Mail object. After we initialize those attributes, we set the header attributes for the Mail object with the following line:
headers.each { |k, v| m[k] = v }
real_content_type, ctype_attrs = parse_content_type
We cycle through all the headers of our mailer object and stick them into the TMail::Mail object. Next we call parse_content_type which is a private method that we get from ActionMailer::PartContainer:
(line 43 action_mailer/part_container.rb)
def parse_content_type(defaults=nil)
if content_type.blank?
return defaults ?
[ defaults.content_type, { 'charset' => defaults.charset } ] :
[ nil, {} ]
end
ctype, *attrs = content_type.split(/;\s*/)
attrs = attrs.inject({}) { |h,s| k,v = s.split(/=/, 2); h[k] = v; h }
[ctype, {"charset" => charset || defaults && defaults.charset}.merge(attrs)]
end
All we are doing here is parsing the contenttype of the mailer (in our case “text/plain”) and returning an array with the contenttype and a hash. Let’s see what our example ends up returning:
parse_content_type => ["text/plain", {"charset"=>"utf-8"}]
real_content_type => "text/plain"
ctype_attrs => {"charset"=>"utf-8"}
Now we move on to:
if @parts.empty?
m.set_content_type(real_content_type, nil, ctype_attrs)
m.body = normalize_new_lines(body)
else
...
end
In our case, @parts is empty so we set the content type of our TMail object with the results from our parsing. After that, we set the body of the TMail object to the current AM object’s body, but we first do some sanitation duties with normalize_new_lines which is a method in ActionMailer::Utils. In fact, it is the only method in that module:
(action_mailer/utils.rb)
module ActionMailer
module Utils #:nodoc:
def normalize_new_lines(text)
text.to_s.gsub(/\r\n?/, "\n")
end
end
end
What we are doing here is cleansing Microsoft evil from the message. Windows likes to use “\r\n” for line breaks in text, the rest of computing thinks it’s enough to just use “\n”. I agree with the rest of computing. Anyway, this method changes all “\r\n“‘s to “\n“‘s so that our email message parses correctly on all email servers.
After we normalize, we are done with setting up the object and the final line of create_mail is:
@mail = m
Now we set the AM attribute @mail to the newly instantiated TMail object and we’re done… with the creation of the email message. Now we have to actually send it. We’ll get to that next. See ya then.