New ActionMailer API in Rails 3.0

Tue Jan 26 12:13:00 -0800 2010

Action Mailer has long been the black sheep of the Rails family. Somehow, through many arguments, you get it doing exactly what you want. But it takes work! Well, we just fixed that.

Action Mailer now has a new API.

But why? Well, I had an itch to scratch, I am the maintainer for TMail, but found it very hard to use well, so I sat down and wrote a really Ruby Mail library, called, imaginatively enough, Mail

But Action Mailer was still using TMail, so then I replaced out TMail with Mail in Action Mailer

And now, with all the flexibility that Mail gives us, we all thought it would be a good idea to re-write the Action Mailer DSL. So with a lot of ideas thrown about between David, Yehuda and myself, we came up with a great DSL.

I then grabbed José Valim to pair program together (with him in Poland to me in Sydney!) on ripping out the guts of Action Mailer and replacing it with a lean, mean mailing machine.

This was merged today.

So what does this all mean? Well, code speaks louder than words, so:

Creating Email Messages:

Instead of this:

 class Notifier < ActionMailer::Base
   def signup_notification(recipient)
     recipients      recipient.email_address_with_name
     subject         "New account information"
     from            "system@example.com"
     content_type    "multipart/alternative"
     body            :account => recipient

     part :content_type => "text/html",
       :data => render_message("signup-as-html")

     part "text/plain" do |p|
       p.body = render_message("signup-as-plain")
       p.content_transfer_encoding = "base64"
     end
     
     attachment "application/pdf" do |a|
       a.body = generate_your_pdf_here()
     end

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

You can do this:

class Notifier < ActionMailer::Base
  default :from => "system@example.com"
  
  def signup_notification(recipient)
    @account = recipient

    attachments['an-image.jp'] = File.read("an-image.jpg")
    attachments['terms.pdf'] = {:content => generate_your_pdf_here() }

    mail(:to => recipient.email_address_with_name,
         :subject => "New account information")
  end
end

Which I like a lot more :)

Any instance variables you define in the method become available in the email templates, just like it does with Action Controller, so all of the templates will have access to the @account instance var which has the recipient in it.

The mail method above also accepts a block so that you can do something like this:

def hello_email
  mail(:to => recipient.email_address_with_name) do |format|
    format.text { render :text => "This is text!" }
    format.html { render :text => "<h1>This is HTML</h1>" }
  end
end

In the same style that a respond_to block works in Action Controller.

Sending Email Messages:

Additionally, sending messages has been simplified as well. A Mail::Message object knows how to deliver itself, so all of the delivery code in Action Mailer was simply removed and responsibility given to the Mail::Message.

Instead of having magic methods called deliver_* and create_* we just call the method which returns a Mail::Message object, and you just call deliver on that:

So this:

Notifier.deliver_signup_notification(recipient)

Becomes this:

Notifier.signup_notification(recipient).deliver

And this:

message = Notifier.create_signup_notification(recipient)
Notifier.deliver(message)

Becomes this:

message = Notifier.signup_notification(recipient)
message.deliver

You still have access to all the usual types of delivery agents though, :smtp, :sendmail, :file and :test, these all work as they did with the prior version of ActionMailer.

Receiving Emails

This has not changed, except now you get a Mail::Message object instead of a TMail object.

Mail::Message will be getting a :reply method soon which will automatically map the Reply related fields properly. Once this is done, we will re-vamp receiving emails as well to simplify.

Old API

And… of course, if you still “like the old way”, the new Action Mailer still supports the old API and all the old tests still pass. We have moved everything relating to the old API into deprecated_api.rb and this will be removed in a future release of Rails.

Summary

With Mail and this refactor, Action Mailer has now finally become just a DSL wrapper between Mail and Action Controller.

blogLater

Mikel

  1. Marc Says:

    Absolutely fantastic news Mikel! Thanks for this!

  2. Kieran P Says:

    Well done. Some top notch work from both yourself and José. Keep it up!

  3. mikhailov Says:

    Method #deliver is pretty good instead of #…_deliver

  4. Zing Says:

    It’s not really an accurate model of the real world that a message knows how to deliver itself, although the postal system might be better off if zero-intelligence envelopes all delivered themselves (just a little smarter than the average postal worker!)

  5. Chris Says:

    Looks amazing! Does the new API also support inline attachments? Right now I have to use an aweful hack.

  6. Mikel Lindsaar Says:

    @Chris it should, just pass the :content_disposition => ‘inline’ to the attachments[‘filename’] method…

    Try it and let me know :)

    Mikel

  7. Olek Says:

    I like getting rid of #deliver_* and #create_* magic methods, but replacing methods with hash keys (to, subject, etc.) seems a bit unnatural and against common sense, especially considering the opposite direction of ActiveRecord 3 (:conditions, :limit, etc. becomes #where, #limit, etc.).

    Also, the #mail method doesn’t feel too Ruby’ish semantically – of course, it’s a #mail since we’re already in ActionMailer :) – but I imagine that’s a problem with calling the method directly and not wrapping it in #deliver_*.

  8. chris finne Says:

    I love it! Following the conventions from controller with instance variables and format blocks feels so natural.

    Can’t wait to use the new way of inline attachments. My old hacks were ugly.

  9. gustin Says:

    Nice! looks like they reside in /app/mailers now — certainly a needed organizational touch.

  10. gustin Says:

    Nice! looks like they reside in /app/mailers now — certainly a needed organizational touch.

  11. Brandon Wright Says:

    Excellent work. Your Mail library has a pretty straightforward feel to it.

    In the line:

    attachments[‘an-image.jp’] = File.read(“an-image.jpg”)

    you missed the “g”. Would the attachment then be delivered to the recipient without the “g” in the filename?

  12. Richard Says:

    Good work, I really like the .deliver methods! Is there an easy way to forward a message including attachments?

    As of now I have to parse through the message pull out the html body, the plain body, the attachments, and then re-assemble into a coherent message…all so i can just change the “to” field. (hopefully i’m doing this wrong and someone can school me)

  13. Scott Bronson Says:

    Nice work! I love getting rid of the deliver_blah magic, obj.deliver makes so much more intuitive sense.

    I’m very curious, how did you manage to pair program over that distance? Skype screen sharing?

  14. Eduardo Sampaio Says:

    That’s awesome great work!

    BTW, isn’t this wrong?
    format.text { render :text => “

    This is HTML” }
    format.html { render :text => “This is text!” }

    Should be:
    format.text { render :text => “This is text!” }
    format.html { render :text => “

    This is HTML” }

    Shouldn’t it?

  15. Mikel Lindsaar Says:

    @Eduardo, you are right! :) I guess I could say it is there to make sure people read the post, but I will fix it none the less

  16. adam Says:

    awesome…i always thought the deliver_* syntax was weird and much prefer the *.deliver – Well done and thanks guys!!

  17. Jack Says:

    Hi Mikel,

    Thanks for the great code and update on how to use it. I confess to being stumped by one thing: I have both an html and text version of a template I’d like to use depending on a user’s preference. I thought I could pass a block to the mail call with some sort of logic but no matter what I do, it seems to always create a multipart email.

    So, with two files, a.html.haml and a.text.haml in my app/views/notifier folder, what is the correct way to send an email with only one of those templates being used?

    Thanks!

  18. Mikel Lindsaar Says:

    @Jack, though I haven’t tried it, Action Mailer uses the same code that ActionController does, ie, Abstract Controller. So in your mail action, instead of using the default rendering, render a specific template and return, this should do it.

  19. Jack Says:

    Thanks Mikel. Just to make sure I’m clear. Should this code only send one type of email (regardless of whether multiple types exist in the directory:

    mail(:to => user.email, :subject => “hello”) do |format|
    format.html
    end

    Or are you saying I need to explicitly call render in a block passed to format.html.

    Will try this some more tonight but just wanted to make sure I was understanding you before going down this road.

    Thanks!

  20. Jack Says:

    @Mikel,

    I realized what I was doing wrong. Some code was returning before I could call mail in the method. It was then autorendering everything it could find.

    This wasn’t obvious to me. I guess it makes sense given that Rails actions automatically render for you. Might be worth noting in the documentation some where that a call to mail isn’t actually needed to have the templates rendered.

    I’m now trying to figure out how to return out of the message without going through the rendering. Is this possible? I’ve tried returning nil, etc. If I can’t then it sounds like any logic on whether to actually build the mail object should go outside/before the method?

  21. Tony Says:

    Is there support for TLS ie gmail?

  22. Anime Yourself Says:

    Very nice work. This should make things a lot easier and save a lot of time. Where can I get some documentation on this?

  23. Magellan GPS Systems Says:

    Very nice work Mike, I’d also like to know where to get a few pieces of documentation.

    Thanks for all the help.

Leave a Reply