STARTTLS Problems with Ruby

2011-03-25

If you want to send email from a Ruby program, one approach is to hit an SMTP server where you have an account, and ask it to send the email in your name. This is nice, because you ensure your From: and Sender: fields contain real addresses. There are lots of sites out there that describe how to do this with Gmail. The best library to use seems to be Pony. From a Ruby 1.8.7 script, you can say this:

#!/usr/bin/env ruby

require 'rubygems'
require 'pony'

from = 'example@gmail.com'
to = 'someone@example.com'
subject = 'testing'
msg = <<EOM
Hello, this is a test!
EOM

Pony.mail(:to => to,
          :from => from,
          :subject => subject,
          :body => msg,
          :via => :smtp,
          :via_options => {
            :address => 'smtp.gmail.com',
            :port => '587',
            :enable_starttls_auto => true,
            :user_name => from,
            :password => 'OMITTED',
            :authentication => :plain,
          }
         )

The :enable_starttls_auto line is actually optional, because it defaults to true. Unless you tell it otherwise, Ruby’s SMTP library will negotiate with the server for TLS. This is an SSL-like encryption scheme which you initiate from a regular SMTP session by sending the STARTTLS command. Without TLS, the whole SMTP conversation is in plaintext, including the email body and your account password. So in general, Ruby’s approach is a good thing. The trouble I encountered is that I didn’t want my emails to come from a Gmail account. I wanted them to come from illuminatedcomputing.com. And while my webhost does let me send emails via its SMTP server, its SSL certificate is a little wanting: the domain name on it is localhost.

Strictly speaking, this means a client should reject the certificate, because it doesn’t match the domain you think you’re connecting to. This is what Ruby does, which means Pony was failing with this error:

/usr/lib/ruby/1.8/openssl/ssl.rb:124:in `post_connection_check':
hostname was not match with the server certificate (OpenSSL::SSL::SSLError)

(I guess this is the kind of error message you get when you use a language from Japan. :-)

I couldn’t find any obvious way around this. You can tell Ruby’s SSLContext to ignore a mismatched domain name, but how can you tell Pony to pass this setting on to Mail, which should pass it on to SMTP, which should set it on the SSLContext? It seemed hopeless. My first attempt was rather heavy-handed, just overriding the default SSLContext:

class << Net::SMTP
  remove_method :default_ssl_context # if defined?(Net::SMTP.default_ssl_context)
end

module Net
    class SMTP
        def SMTP.default_ssl_context
            ctx = OpenSSL::SSL::SSLContext.new
            ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
            ctx
        end
    end
end

This works, but it affects too much: probably all the SSL connections you make throughout your application.

With some more digging, I noticed that Mail::SMTP accepted an option called :openssl_verify_mode, which could be set to the constant OpenSSL::SSL::VERIFY_NONE. If I passed this to Pony in the :via_options hash, would it pass it along? There was no documentation suggesting it would, but I tried it anyway, and guess what? It worked! So this code is a way to get around a no-domain SSL certificate without wrecking SSL for your whole application:

Pony.mail(:to => to,
          :from => from,
          :subject => subject,
          :body => msg,
          :via => :smtp,
          :via_options => {
            :address => 'smtp.mydomain.com',
            :port => '25',
            :enable_starttls_auto => true,
            :user_name => from,
            :password => 'OMITTED',
            :authentication => :plain,
            :openssl_verify_mode => OpenSSL::SSL::VERIFY_NONE,
          }
         )

I’m very impressed that Pony lets you get away with this.

Please note that this approach is not entirely secure! The reason an SSL certificate contains a domain name is to prove you’re talking to the real owner of the domain. So the code above is not ideal. Please use it at your own risk, and don’t blame me if anything goes wrong. In my case, I’m not too surprised that my hosting provider doesn’t pay for a separate SSL certificate for every single customer, and I’m willing to risk a DNS spoofing attack.

UPDATE: The same parameter works with ActionMailer, so you can say this (Rails 3):

config.action_mailer.smtp_settings = {
      :address              => 'smtp.mydomain.com',
      :port                 => '25',
      :domain               => 'mydomain.com',
      :user_name            => 'example@mydomain.com',
      :password             => 'OMITTED',
      :authentication       => 'plain',
      :enable_starttls_auto => true,
      :openssl_verify_mode  => OpenSSL::SSL::VERIFY_NONE,
  }
blog comments powered by Disqus Prev: RESTless Doubts Next: What git add does