Sendmail & Spam Filter Tricks in Rails

Earlier in the week I ran into an unfortunate feature on Google Apps: a daily limit of 500 outbound emails from your account. Up to that point Anatol Pomozov's TLS mail patch was working fine and dandy, and I still recommend the GMail route if you know that you will be below the limit, but I had to make the switch and after considering my options, I settled on sendmail. Hardly known for its security record, sendmail is nonetheless one of the most ubiquitous and easy to setup services out there, especially if all you're looking for is outbound email.

Making Sendmail play with ActionMailer

The setup process takes all of thirty seconds: install the binary distributable for your OS using yum, synaptic or any such tool and then start the service. Next, a one line change to our environment.rb file is due to tell ActionMailer to look for sendmail:

ActionMailer::Base.delivery_method = :sendmail

That's all you need to get started. However, after a few test runs, I started noticing intermittent failures where some of the emails would never reach their destinations. Sure enough, after plowing through the mail daemon's logs, I realized that spam filters were flagging and rejecting my sendmail messages! The ubiquity of sendmail has also made it the number one culprit for spam, hence many filters were unforgiving.

Getting the headers right

The first rule with spam filters is to get the originating from and reply-to addresses to be the same. Most script-kiddies don't get this right, and this in turn becomes a great heuristic for separating the 'real mail' from the spam. Said, done:

class EmailNotifier < ActionMailer::Base

  def activation(user)
    setup_email(user)
    @body[:url]  = "#{user.activation_url}"
  end
  
protected
  def setup_email(user)
    @recipients   = "#{user.email}"
    @from         = "Support <support@yourapp.com>"
    headers         "Reply-to" => "support@yourapp.com"
    @subject      = "Welcome!"
    @sent_on      = Time.now
    @content_type = "text/html"
    @body[:user]  = user
  end
end

After a few quick tests, a few more emails got through, but some were still getting stuck in the pipeline. A deeper look at the headers revealed the problem: sendmail was appending return path headers and a signature based on its runtime. Hence, running sendmail under a 'daemon' user resulted in 'daemon@hostname.com' header, which was being flagged down and rejected. Half expecting to give up on sendmail, a brief visit to the man page and the Rails RDoc resulted in a simple fix:

ActionMailer::Base.delivery_method = :sendmail
ActionMailer::Base.sendmail_settings = { 
  :location       => '/usr/sbin/sendmail', 
  :arguments      => '-i -t -f support@yourapp.com'
} 

Passing the from (-f) flag to sendmail overwrote its default signature and allowed me to match it to my from/reply addresses, finally fixing the problem. Additionally, adding the ignore dots (-i), and scan for recipients (-t) flags made sure that Rails played nicely with sendmail.


Ilya Grigorik

Ilya Grigorik is a web performance engineer and developer advocate at Google, where his focus is on making the web fast and driving adoption of performance best practices at Google and beyond.