--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.
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.
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.
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.
Specify a file attachment. Can be invoked multiple times to make multiple file attachments.
Specifies blind recipient (Bcc:) addresses for the message, either as a string (for a single address) or an array for multiple addresses.
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.
Specifies carbon-copy recipient (Cc:) addresses for the message, either as a string (for a single address) or an array for multiple addresses.
The character set to use for the message. Defaults to the value of the default_charset
setting specified for ActionMailer::Base
.
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" ]
.
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.
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.
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)
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.
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.
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]
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
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"
.
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
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?
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]")
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.
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
.
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.
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).
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.
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.
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 }
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.
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. |
18.191.165.62