Chapter 16. ActionMailer

 

It’s a cool way to send emails without tons of code

 
 --Jake Scruggs[1]

Integration with e-mail is a crucial part of most modern web application projects. Whether it’s support for retrieving lost passwords or letting users control their accounts via e-mail, you’ll be happy to hear that Rails offers great support for both sending and receiving e-mail, thanks to its ActionMailer framework.

In this chapter, we’ll cover what’s needed to set up your deployment to be able to send and receive mail with the ActionMailer framework and by writing mailer models, the entities in Rails that encapsulate code having to do with e-mail handling.

Setup

By default, Rails will try to send e-mail via SMTP (port 25) of localhost. If you are running Rails on a host that has an SMTP daemon running and it accepts SMTP e-mail locally, you don’t have to do anything else in order to send mail. If you don’t have SMTP available on localhost, you have to decide how your system will send outbound e-mail.

When not using SMTP directly, the main options are to use sendmail or to give Rails information on how to connect to an external mail server. Most organizations have SMTP servers available for this type of use, although it’s worth noting that due to abuse many hosting providers have stopped offering shared SMTP service.

Mailer Models

Now that we have the mail system configured, we can go ahead and create a mailer model that will contain code pertaining to sending and receiving a class of e-mail. Rails provides a generator to get us started rapidly.

To demonstrate, let’s create a mailer for sending late notices to users of our time-and-reporting sample application:

$ script/generate mailer LateNotice
      exists  app/models/
      create  app/views/late_notice
      exists  test/unit/
      create  test/fixtures/late_notice
      create  app/models/late_notice.rb
      create  test/unit/late_notice_test.rb

A view folder for the mailer is created at app/views/late_notice and the mailer itself is stubbed out at app/models/late_notice.rb:

class LateNotice < ActionMailer::Base
end

Kind of like a default ActiveRecord subclass, there’s not much there at the start. What about the test? See Listing 16.1.

Example 16.1. An ActionMailer Test

require File.dirname(__FILE__) + '/../test_helper'

class LateNoticeTest < Test::Unit::TestCase

  FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
  CHARSET = "utf-8"

  include ActionMailer::Quoting

  def setup
    ActionMailer::Base.delivery_method = :test
    ActionMailer::Base.perform_deliveries = true
    ActionMailer::Base.deliveries = []

    @expected = TMail: :Mail.new
    @expected.set_content_type "text", "plain", { "charset" => CHARSET }
    @expected.mime_version = '1.0'
  end

  private
    def read_fixture(action)
      IO.readlines("#{FIXTURES_PATH}/late_notice/#{action}")
    end

    def encode(subject)
      quoted_printable(subject, CHARSET)
    end
end

Whoa! There’s quite a lot more setup involved for this test than what we’re used to seeing, which reflects the greater underlying complexity of working with a mail subsystem.

Preparing Outbound Email Messages

You work with ActionMailer classes by defining public mailer methods that correspond to types of e-mails that you want to send. Inside the public method, you set the options for the message and assign any variables that will be needed by the mail message template.

Continuing with our example, let’s write a late_timesheet mailer method that takes user and week_of parameters. Notice that it sets the basic information needed to send our notice e-mail (see Listing 16.2).

Example 16.2. A mailer method

def late_timesheet(user, week_of)
  recipients user.email
  subject "[Time and Expenses] Late timesheet notice"
  from "[email protected]"
  body :recipient => user.name, :week => week_of
end

Here is a list of all the mail-related options that you can set inside of mailer methods.

attachment

Specify a file attachment. Can be invoked multiple times to make multiple file attachments.

bcc

Specifies blind recipient (Bcc:) addresses for the message, either as a string (for a single address) or an array for multiple addresses.

body

Defines the body of the message. Takes a hash (in which case it specifies the variables to pass to the template when it is rendered), or a string, in which case it specifies the actual text of the message.

ActionMailer automatically normalizes lines for plain-text body content, that is, it ensures that lines end with instead of a platform-specific character.

cc

Specifies carbon-copy recipient (Cc:) addresses for the message, either as a string (for a single address) or an array for multiple addresses.

charset

The character set to use for the message. Defaults to the value of the default_charset setting specified for ActionMailer::Base.

content_type

Specifies the content type for the message. Defaults to text/plain.

from

Specifies the from address for the message as a string (required).

headers

Specifies additional headers to be added to the message as a hash.

implicit_parts_order

An array specifying the order in which the parts of a multipart e-mail should be sorted, based on their MIME content-type. Defaults to the value of the default_implicit_parts_order setting specified on ActionMailer::Base and defaults to [ "text/html", "text/enriched", "text/plain" ].

mailer_name

Overrides the mailer name, which defaults to an inflected version of the mailer’s class name and governs the location of this mailer’s templates. If you want to use a template in a nonstandard location, you can use this to specify that location.

mime_version

Defaults to "1.0", but may be explicitly given if needed.

part

Enables sending of multipart email messages by letting you define sets of content-type, template, and body variables. Note that you don’t usually need to use this method, because ActionMailer will automatically detect and use multipart templates, where each template is named after the name of the action, followed by the content type.

On the other hand, this method is needed if you are trying to send HTML messages with inline attachments (usually image files). See the section “MultiPart Messages” a little further along in the chapter for more information, including the part method’s special little API.

recipients

The recipient addresses for the message, either as a string (for a single address) or an array (for multiple addresses). Remember that this method expects actual address strings not your application’s user objects.

recipients users.map(&:email)

sent_on

An optional explicit sent on date for the message, usually passed Time.now. Will be automatically set by the delivery mechanism if you don’t supply a value.

subject

The subject line for the message.

template

Specifies the template name to use for the current message. Since the template defaults to the name of the mailer method, this option may be used to have multiple mailer methods share the same template.

The body of the e-mail is created by using an ActionView template (regular ERb) that has the content of the body hash parameter available as instance variables. So the corresponding body template for the mailer method in Listing 16.2 could look like this:

Dear <%= @recipient %>,

Your timesheet for the week of <%= @week %> is late.

And if the recipient was David, the e-mail generated would look like this:

  Date: Sun, 12 Dec 2004 00:00:00 +0100
  From: [email protected]
  To: [email protected]
  Subject: [Time and Expenses] Late timesheet notice


Dear David Hansson,

  Your timesheet for the week of Aug 15th is late.

HTML Email Messages

To send mail as HTML, make sure your view template generates HTML and set the content type to html in your mailer method, as shown in Listing 16.3.

Example 16.3. An HTML Mailer Method

class MyMailer < ActionMailer::Base

  def signup_notification(recipient)
    recipients   recipient.email_address_with_name
    subject      "New account information"
    body         "account" => recipient
    from         "[email protected]"
    content_type "text/html"
  end

end

Other than the different content_type value, the process is exactly the same as sending plaintext email. Want to embed images in the HTML that will go along with the email (as inline attachments) and display to the end user? At the time of this writing there is an outstanding issue with ActionMailer that makes it difficult to do so. See http://dev.rubyonrails.org/ticket/2179 for more information and a patch that provides a workaround.[2]

Multipart Messages

The part method is a small API in and of itself for creating multipart messages. Using the part method, you can compose email messages made up of distinct kinds of content. A popular technique (as demonstrated in Listing 16.4) uses multiparts to send a plaintext part along with an HTML email message, so that recipients who can only read plaintext are not left in the dark.

Example 16.4. A Multipart Signup Notification Mailer Method

class ApplicationMailer < ActionMailer::Base

  def signup_notification(recipient)
    recipients      recipient.email_address_with_name
    subject         "New account information"
    from            "[email protected]"

    part :content_type => "text/html",
         :body => render_message("signup_as_html", :account =>
recipient)

    part "text/plain" do |p|
      p.body = render_message("signup_as_plain", :account =>
recipient)
      p.transfer_encoding = "base64"
    end
  end

end

Part Options

The part method accepts a variety of options, either as a hash or via block initialization. (Both types of initialization are demonstrated in Listing 16.4.)

  • :body Represents the body of the part, as a string! This should not be a hash (like ActionMailer::Base.) If you want a template to be rendered into the body of a subpart you can do it using the mailer’s render or render_template methods and assign the result to this option (like in Listing 16.4).

  • :charset Specify the charset for this subpart. By default, it will be the charset of the containing part or mailer (e.g. UTF8).

  • :content_type The MIME content type of the part.

  • :disposition The content disposition of this part, typically either “inline” or “attachment.”

  • :filename The filename to use for this subpart, usually attachments. The value of this option is the filename that users will see when they try to save the attachment and has nothing to do with the name of files on your server.

  • :headers Specifying additional headers to include with this part as a hash.

  • :transfer_encoding The transfer encoding to use for this subpart, like "base64" or "quoted-printable".

Implicit Multipart Messages

As mentioned earlier in the chapter, multipart messages can also be used implicitly, without invoking the part method, because ActionMailer will automatically detect and use multipart templates, where each template is named after the name of the action, followed by the content type. Each such detected template will be added as separate part to the message.

For example, if the following templates existed, each would be rendered and added as a separate part to the message, with the corresponding content type. The same body hash is passed to each template.

  • signup_notification.text.plain.erb

  • signup_notification.text.html.erb

  • signup_notification.text.xml.builder

  • signup_notification.text.x-yaml.erb

File Attachments

Attachments can be added by using the attachment method in conjunction with the File.read method of Ruby, or application code that generates file content. See Listing 16.5.

Example 16.5. Adding Attachments to an Email

class ApplicationMailer < ActionMailer::Base
  def signup_notification(recipient)
    recipients      recipient.email_address_with_name
    subject         "New account information"
    from            "[email protected]"

    attachment :content_type => "image/jpeg",
      :body => File.read("an-image.jpg")

    attachment "application/pdf" do |a|
      a.body = generate_your_pdf_here()
    end
  end
end

The attachment method is really just a convenience wrapper around the part API. The first attachment of Listing 16.5 could have been done (just a little less elegantly) with the following code:

part :content_type => "image/jpeg",
     :disposition => "inline",
     :filename => "an-image.jpg",
     :transfer_encoding => "base64" do |attachment|
  attachment.body = File.read("an-image.jpg")
end

We’ve now talked extensively about preparing email messages for sending, but what about actually sending them to the recipients?

Actually Sending an Email

Don’t ever try to actually call the instance methods like signed_up directly. Instead, call one of the two class methods that are generated for you based on the instance methods defined in your mailer class. Those class methods are prefixed with deliver_ and create_, respectively. Really, the main one that you care about is deliver.

For example, if you wrote a signed_up_notification instance method on a class named ApplicationMailer, using it would look like the following example:

# create a tmail object for testing
ApplicationMailer.create_signed_up_notification("[email protected]")

# send the signed_up_notification email
ApplicationMailer.deliver_signed_up("[email protected]")

# wrong!
ApplicationMailer.new.signed_up("[email protected]")

Receiving E-Mails

TMail is a Ruby library for email processing that dates back to 2003. It comes bundled in Rails as an included dependency of ActionMailer. There’s really only one TMail class that you care about as a Rails developer, and that is the TMail::Mail class.

To receive e-mails, you need to write a public method named receive on one of your application’s ActionMailer::Base subclasses. It will take a Tmail object instance as its single parameter. When there is incoming email to handle, you call a class method named receive on your Mailer class. The raw email string is converted into a Tmail object automatically and your receive method is invoked for further processing. You don’t have to implement the receive class method yourself, it is inherited from ActionMailer::Base.

That’s all pretty confusing to explain, but simple in practice. Listing 16.6 shows an example.

Example 16.6. The Simple MessageArchiver Mailer Class with a Receive Method

class MessageArchiver < ActionMailer::Base

  def receive(email)
    person = Person.find_by_email(email.to.first)
    person.emails.create(:subject => email.subject, :body =>
email.body)
  end

end

The receive class method can be the target for a Postfix recipe or any other mail-handler process that can pipe the contents of the email to another process. The Rails runner script makes it easy to handle incoming mail:

./script/runner 'MessageArchiver.receive(STDIN.read)'

That way, when a message is received, the receive class method would be fed the raw string content of the incoming email via STDIN.

TMail::Mail API Reference

Since the object representation of the incoming email message is an instance of TMail::Message, I think it makes sense to have a reference to at least the basic attributes of that class that you will be using. The online documentation for all of TMail is at http://i.loveruby.net/en/projects/tmail/doc/, but the following list of methods gives you pretty much everything you need.

attachments

An array of TMail::Attachment objects associated with the message object. TMail::Attachment extends Ruby’s own StringIO class and adds original_filename and content_type attributes to it. Other than that, you use it exactly as you would use any other StringIO (See Listing 16.7 for example).

body

The body text of the email message, assuming it’s a plain text single-part message. Multipart messages will return the preamble when body is called.

date

A Time object corresponding to the value of the Date: header field.

has_attachments?

Returns true or false based on whether the message contains an attachment.

multipart?

Returns true if the message is a MIME-multipart email message.

parts

An array of TMail::Mail objects, one for each part of the MIME-multipart email message.

subject

The subject line of the email message.

to

An array of strings representing the To: addresses associated with the message. The cc, bcc, and from attributes function similarly for their respective address fields.

Handling Attachments

Processing files attached to incoming email messages is just a matter of using the attachments attribute of TMail, as in Listing 16.7. This example assumes that you have a Person class, with a has_many association to an attachment_fu object named photos.

class PhotoByEmail < ActionMailer::Base

  def self.receive(email)
    from = email.from.first
    person = Person.find_by_email(from)
    logger.warn("Person not found [#{from}]") and return unless person

    if email.has_attachments?
      email.attachments.each do |file|
        person.photos.create(:uploaded_data => file)
      end
    end
  end
end

There’s not much more to it than that, except of course to wrestle with the configuration of your mail-processor (outside of Rails) since they are notoriously difficult to configure.[3] After you have your mail-processor calling the Rails runner script correctly, add a crontab so that incoming mail is handled about every five minutes or so, depending on the needs of your application.

Configuration

Most of the time, you don’t have to configure anything specifically to get mail sending to work, because your production server will have sendmail installed and ActionMailer will happily use it to send emails.

If you don’t have sendmail installed on your server, you can try setting up Rails to send email directly via SMTP. The ActionMailer::Base class has a hash named smtp_settings (server_settings prior to Rails 2.0) that holds configuration information. The settings here will vary depending on the SMTP server that you use.

The sample (as shown in Listing 16.7) demonstrates the SMTP server settings that are available (and their default values). You’ll want to add similar code to your config/environment.rb file:

Example 16.7. SMTP Settings for ActionMailer

ActionMailer::Base.smtp_settings = {
  :address => 'smtp.yourserver.com',    # default: localhost
  :port => '25',                        # default: 25
  :domain => 'yourserver.com',          # default:
localhost.localdomain
  :user_name => 'user',                 # no default
  :password => 'password',              # no default
  :authentication => :plain             # :plain, :login or :cram_md5
}

Conclusion

In this chapter, we learned how Rails makes sending and receiving email easy. With relatively little code, you can set up your application to send out email, even HTML email with inline graphics attachments. Receiving email is even easier, except perhaps for setting up mail-processing scripts and cron jobs. We also briefly covered the configuration settings that go in your config/environment.rb file related to mail.

References

1.

http://jakescruggs.blogspot.com/2007/02/actionmailer-tips.html

2.

Note that a Google search on the topic of inline image attachments will usually lead you to http://blog.caboo.se/articles/2006/02/19/how-to-send-multipart-alternative-e-mail-with-inline-attachments, which purports to give you an easy solution to the problem, but doesn’t actually work.

3.

Rob Orsini, author of O’Reilly’s Rails Cookbook recommends getmail, which you can get from http://pyropus.ca/software/getmail.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.252.204