Monday, June 2, 2008

Rails: Getting the best out of ActionMailer and smtp.gmail.com

A typical Rails developer casually needs to answer an important question when it comes to using ActionMailer to send mails; shall I use a local smtp server (like exim or sendmail) or shall I use a trustful, more reliable smtp server like smtp.gmail.com ?

Let's list the pros and cons of each choice. Using a local smtp server residing on the same machine with the application reduces communication overhead and consequently improves performance. However, mails sent using such smtp servers will mostly be considered as spam by most famous mail providers, because the sender is not a trustful email account. Using smtp.gmail.com eliminates this hazard, as the mail messages are sent from a trustful account, but dramatically decreases performance due to blocking communication overhead. Also, the used gmail account could be blocked if gmail sensed multiple connections using it, which is the typical case when the application is serving multiple requests.

Now let's tweak the second choice to eliminate its cons, getting the best out of ActionMailer and smtp.gmail.com. The basic idea is to prepare the mail to be sent, until the only step left is calling ActionMailer::Base.deliver. Instead of delivering the mail at this point, we'll dump it to the database using a model for all emails in the application. Then we'll do a simple rake task that gets those mails from the database, sends them and deletes them. Following is a detailed example.

ActionMailer configuration, normally, will look like this (after installing the TLS plugin)
  config.action_mailer.smtp_settings = {
:address => 'smtp.gmail.com',
:port => 587,
:domain => "www.yourdomainname.com",
:authentication => :login,
:user_name => "account@gmail.com",
:password => "@cc0unt_p@$$w0rd",
:tls => true
}


Assume one of the mailers in the application is called 'InvitationMailer'. The typical three lines of code that send an email are:
  mail = InvitationMailer.create_invite(invitation)
mail.set_content_type("text/html")

#deliver mail
InvitationMailer.deliver mail


Instead of this, we'll add a new model, PendinMail, with two attributes: id (integer) and serialized_mail (text). We'll replace the last line to serialize the mail object and save it in the database:
  mail = InvitationMailer.create_invite(invitation)
mail.set_content_type("text/html")

#dumping the mail to DB instead of calling deliver
PendingMail.new(:serialized_mail => Marshal.dump(mail)).save


The only thing left is a rake task that runs periodically on the server to send those mails:
  desc "Send all pending emails stored in DB table pending_mails"
task :send_pending_mails do
mails = PendingMail.find :all
for mail in mails do
ActionMailer::Base.deliver(Marshal.load(mail.serialized_mail))
mail.destroy
end
end


Now, what did we gain from this scheme?
- first: We used smtp.gmail.com and avoided considering our emails as spam.
- second: we removed the blocking communication overhead with gmail's smtp server, during which the request being served is blocked.
- third: we avoided the hazard of using the same gmail account from multiple connections, because we're sure that only one process will be using this account all the time.


3 comments:

Anonymous said...

I am using Windows XP, RUBY 1.8.6 and trying to send email using smtp.tls

i am not able to send the email.Please help me, I am usng the following code

require 'action_mailer'
require 'smtp_tls'
require 'tmail'
require 'rubygems'
require 'net/smtp'

class AnnounceMailer < ActionMailer::Base

def hack_night_message(recipient)
from 'xxx@gmail.com'
recipients recipient
subject 'Reminder: Open Ruby Hack Night #6 - Monday 7pm - Whenever (9ish) @ Waves Coffee'
end
end

puts AnnounceMailer.create_hack_night_message( 'xxx@gmail.com' )

mail = TMail::Mail.new
mail.to = xxx@gmail.com'
mail.from = 'xxx@gmail.com'
mail.subject = 'Test message'
mail.body = 'This is a test message from Ruby'

Net::SMTP.start( 'smtp.gmail.com', 587 ) do|smtp|
smtp.send_message(
mail.to_s,
'xxx@gmail.com',
'xxx@gmail.com'
)
end
ActionMailer::Base.smtp_settings = {
:tls => true,
:enable_starttls_auto => true,
:address => 'smtp.gmail.com',
:port => 587,
:authentication => :plain,
:user_name => 'xxx@gmail.com',
:password => 'xxxx'
}

AnnounceMailer.deliver_hack_night_message( 'XXX@gmail.com' )

Haitham Mohammad said...

@Anonymous: I don't really know what's the problem here, but I can see too much redundant code. No need for TMail and other stuff.

I wrote the following lines on the fly and it works like a charm. you can use them if you want. there is an issue that you have to take care of; that is, in most TLS smtp servers, the 'from' header of the mail should contain the same mail account as the login of the smtp account. so, in the following lines, the :login ActionMailer option should be the same as the 'from' option of the email sent.

Just download the tls plugin and place its folder beside the file containing the following code, replace the :domain, :user_name, :password, 'from' with yours. tell me if it works:

require 'active_support'
require 'action_mailer'

require 'action_mailer_optional_tls/lib/action_mailer_tls'
require 'action_mailer_optional_tls/lib/smtp_tls'

ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
:address => "smtp.gmail.com",
:port => 587,
:domain => "www.yourdomain.com",
:user_name => "sender@gmail.com",
:password => "senderpass",
:authentication => :login,
:tls => true
}

class AnnounceMailer < ActionMailer::Base

def hack_night_message(recipient)
from 'sender@gmail.com'
recipients recipient
subject "Reminder: Open Ruby Hack Night #6 - Monday 7pm - Whenever (9ish) @ Waves Coffee"
content_type "text/html"
body "Your body here. It could be HTML if you want."
end

end

AnnounceMailer.deliver_hack_night_message("recipient@gmail.com")

Wael said...

Haitham, your fix solved the issues I've been having with Actionmailer all day.

Thanks! :)