Working with ActionMailer is a bit different from working with ActiveRecord. While it makes sense to me that you would have a model for the data of the email message you send, the flow of control seems inverted from the rest of Rails. With ActionMailer call the model from the controller, then the model triggers the view code, rather than rendering the view from the controller with the model data as is typical with Rails. From the ActionMailer docs: “Emails are defined by creating methods within the model which are then used to set variables to be used in the mail template, to change options on the mail, or to add attachments. ” What follows is the steps that I followed to create a webform that sends an email in Rails, which turned out to be fairly simple, but it did take some digging to put the pieces together, so I figured other folks might find them useful.
Set up
This exercise requires Ruby 1.8.7 and Rails 2.2.1 or later to use TLS (required for Gmail SMTP). For older versions of either Ruby or Rails, you can try a plugin like action_mailer_optional_tls. There is a nice screencast introductory tutorial that runs through a subset of the following steps and is definitely worth watching.
To set up the context for this example, imagine we’re creating comment forms for a news website, where on each article page there is a comment form. To simulate this, we’ll just scaffold up some article pages:
$ rails mailer_example $ ./script/generate scaffold article title:string author:string body:text $ rake db:migrate
Test-Driven
In test-driven style (thanks to rubytutorials.net), here’s a spec for we’ll make work (at least the sending mail part of it). Note that “from” doesn’t meet expectations in 2.3.3 and 2.3.4 it is an array, rather than a string, but it has been fixed for a future release.
spec/models/notifier_spec.rb
equire File.dirname(__FILE__) + '/../spec_helper.rb' describe Notifier do before(:each) do ActionMailer::Base.delivery_method = :test ActionMailer::Base.perform_deliveries = true ActionMailer::Base.deliveries = [] end it "should send feedback email" do comment = {:name => "Name", :email => "[email protected]", :body => "nice article!"} mail = Notifier.deliver_feedback(comment) ActionMailer::Base.deliveries.size.should == 1 mail.body.should =~ Regexp.new(comment[:body]) mail.subject.should =~ Regexp.new(comment[:name]) #mail.from.should =~ Regexp.new(comment[:email]) end end
Mailer Configuraton
Typically the settings would be different per environment and would be specified in config/environment/development.rb, etc. For this exercise, I just modified the main environment.rb file.
in config/environment.rb:
Rails::Initializer.run do |config| : : config.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { :address => "smtp.gmail.com", :port => 587, :domain => "gmail.com", :user_name => "[email protected]", :password => "yourpassword", :authentication => :plain, :enable_starttls_auto => true end
Generator
ActionMailer has a generator which creates files for the model and a view file, along with a unit test which is a lot less blank that the ActiveRecord unit tests.
$ ./script/generate mailer Notifier feedback create app/models/ create app/views/notifier create test/unit/ create test/fixtures/notifier create app/models/notifier.rb create test/unit/notifier_test.rb create app/views/notifier/feedback.erb create test/fixtures/notifier/feedback
Model
Edit the model, so that it takes a hash as it’s first parameter (we’ll end up getting this from the form).
class Notifier < ActionMailer::Base def feedback(comment, sent_at = Time.now) subject "feedback from #{comment[:name]}" recipients "[email protected]" from "\"#{comment[:name]}\"<#{comment[:email]}>" sent_on sent_at body comment end end
Controller and Views
Rename the file “feedback.erb” to “feedback.html.text.erb” This will be the template used for the email, formatted in HTML. Had we wanted to send text-only emails, the file would have been called feedback.text.plain.erb. Rails sets the content type of the email to be the one in the filename. Or we could supply both and Rails will generate a “multipart/alternative” message with both template rendered for the html and plain mime parts.
Next we’ll create a form in the article show page and a controller action that will send the email.
in /views/articles/show.html.erb
<% form_tag :action => "feedback", :id => @article.id do -%> <table> <tr> <td><label for="comment_name">Name:</label></td> <td><%= text_field "comment", "name", :size => 30 %></td> </tr> <tr> <td><label for="comment_email">Email Address:</label></td> <td><%= text_field "comment", "email", :size => 30 %></td> </tr> <tr> <td><label for="comment_body">Body:</label></td> <td><%= text_area "comment", "body", :rows => 8, :cols => 30 %></td> </tr> </table> <input type="submit" value="Send Feedback" class="primary" /> <% end -%>
in /controllers/articles_controller.rb
def feedback article = Article.find(params[:id]) article_info = {:title => article.title, :author => article.author, :id => article.id} comment = params[:comment].merge(article_info) Notifier.deliver_feedback(comment) render :text => "Thank you for your feedback" end
Now you should have a form that sends email. Yay!
3 Comments
Great tips! I will try it definitely
thanks for sharing this!
Respect to post author, some good selective information .
Excellent write-up, especially the portion on testing. I followed the Railscasts mailer recipe, which did not include tests and was struggling with testing mailer. Thanks!