Deep in Rails: ActionMailer#deliver Part V

Posted by admin 14/12/2009 at 12h54

So now we have our mail object. Let’s send it.

For the sending of the message, we need to go back to the beginning, method_missing:

(line 62 action_mailer/base.rb)
def method_missing(method_symbol, *parameters) #:nodoc:
  if match = matches_dynamic_method?(method_symbol)

    case match[1]
      when 'create'  then new(match[2], *parameters).mail
      when 'deliver' then new(match[2], *parameters).deliver!
      when 'new'     then nil
      else super
    end
  else
    super
  end
end

You remember method_missing from a long time ago. We just finished the first part of the line:

when 'deliver' then new(match[2], *parameters).deliver!

We created the new AM object and now we call deliver! on it. That’s our next stop:

(line 520 action_mailer/base.rb)
def deliver!(mail = @mail)
  raise "no mail object available for delivery!" unless mail
  unless logger.nil?
    logger.info  "Sent mail to #{Array(recipients).join(', ')}"
    logger.debug "\n#{mail.encoded}"
  end

  begin
    __send__("perform_delivery_#{delivery_method}", mail) if perform_deliveries
  rescue Exception => e  # Net::SMTP errors or sendmail pipe errors
    raise e if raise_delivery_errors
  end

  return mail
end

The beginning of this method does some checking and logging. The main part of this method is:

__send__("perform_delivery_#{delivery_method}", mail) if perform_deliveries

We are again using __send__ to bypass any possible overridden method and calling perform_delivery_#{delivery_method} with the mail object. delivery_method is a configuration attribute on the AM class along with perform_deliveries. AM defaults to “smtp” for delivery_method and “true” for perform_deliveries so the method we end up calling in our example is perform_delivery_smtp. Let’s see that code:

(line 680 action_mailer/base.rb)
def perform_delivery_smtp(mail)
  destinations = mail.destinations
  mail.ready_to_send
  sender = (mail['return-path'] && mail['return-path'].spec) || mail.from

  smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port])
  smtp.enable_starttls_auto if smtp_settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto)
  smtp.start(smtp_settings[:domain], smtp_settings[:user_name], smtp_settings[:password],
             smtp_settings[:authentication]) do |smtp|
    smtp.sendmail(mail.encoded, sender, destinations)
  end
end

This is the finale. The climax. The whole reason we even engage in ActionMailer, to actualy send an email. Here we go.

The first line pulls the destinations (recipients) from the mail object and stores them in a variable. Next, we call ready_to_send on the mail object which is also the TMail object (remember that we assigned the final TMail object to the @mail attribute on our AM object). This method is defined in TMail::Net:

(line 62 action_mailer/tmail-1.2.3/tmail-1.2.3/net.rb)
def ready_to_send
  delete_no_send_fields
  add_message_id
  add_date
end

NOSEND_FIELDS = %w(
  received
  bcc
)

def delete_no_send_fields
  NOSEND_FIELDS.each do |nm|
    delete nm
  end
  delete_if {|n,v| v.empty? }
end

def add_message_id( fqdn = nil )
  self.message_id = ::TMail::new_message_id(fqdn)
end

def add_date
  self.date = Time.now
end

Now, if you scan through that code you may notice that we are calling delete and delete_if like we were in a standard Enumerable object, but we are not. TMail also defines those methods:

def delete( key )
  @header.delete key.downcase
end

def delete_if
  @header.delete_if do |key,val|
    if Array === val
      val.delete_if {|v| yield key, v }
      val.empty?
    else
      yield key, val
    end
  end
end

What we are doing with all this is stripping and setting some defaults. delete_no_send_fields get’s rid of the “received” and “bcc” keys from our header and we are getting rid of any headers that have empty values. After that we set a nice new hex type id for this email with TMail.new_message_id which is defined in TMail::Utils which gives us a string with a hex code attached to your machine’s hostname. The last line adds the date to the mail message and that’s it.

The next line sets the sender attribute.

sender = (mail['return-path'] && mail['return-path'].spec) || mail.from

Now we get into the actual sending of the email. We aren’t going to go into it because it is part of Ruby’s core lib, but we will quickly skim over it to see what’s going on:

smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port])
smtp.enable_starttls_auto if smtp_settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto)
smtp.start(smtp_settings[:domain], smtp_settings[:user_name], smtp_settings[:password],
           smtp_settings[:authentication]) do |smtp|
  smtp.sendmail(mail.encoded, sender, destinations)
end

First, we create the new SMTP object and then we enable StartTLS if necessary (this allows you to send email through SMTP hosts that require SSL connections). Next we login to the SMTP server (with the start command) and call sendmail inside the block to do the actual delivery.

We’re done. That’s how an email gets sent in Rails. Next week we will create a quick monkey patch for ActionMailer based on the information that we learned from the trek.

This entry was posted on 14/12/2009 at 12h54 and Posted in . You can follow any response to this entry through the Atom feed. You can leave a comment .

Tags , , , ,


Comments

Leave a comment