Posted by: noorul | May 25, 2008

Ruby 1.8 with POP3 with SSL support.

Ruby 1.8 does’nt have code to access POP3 servers using SSL. But there is code for accessing HTTP servers using SSL. So I thought I can tweak the same code to use with Net::POP3 class. Since these days gmail is so popular that most of the people started using it. Gmail only provides POP3 over SSL. So this tweak may help those who want to access Gmail using Ruby.

The dynamic removal of methods from ruby classes helped me with this. I had to even remove the constructor to keep the changes minimal. But ruby is smart enough to throw warning message for removing the constructor. I ignored the warning because in our case it is not going to create any havoc.

Even though this code resides in a new file pops.rb, the changes are made to the class POP3 which is defined in pop.rb. Actually you will be using a modified version of POP3 class rather than a new version of it. The only additional thing that need to be taken care of is while creating an instance you have to pass the SSL port and also after that you need to set the value of use_ssl attribute of POP3 class to true. If use_ssl is false then the POP3 behaves normally. In the code itself there is an example.

Below is the code for net/pops.rb.

require 'net/pop'
require 'openssl'

module Net

  # == Examples
  # 
  # === Retrieving Messages 
  # 
  # This example retrieves messages from the server and deletes them 
  # on the server.
  #
  # Messages are written to files named 'inbox/1', 'inbox/2', ....
  # Replace 'pop.example.com' with your POP3 server address, and
  # 'YourAccount' and 'YourPassword' with the appropriate account
  # details.
  # 
  #     require 'net/pops'
  # 
  #     pop = Net::POP3.new('pop.example.com', pop3_ssl_port)
  #     pop.use_ssl = true
  #     pop.start('YourAccount', 'YourPassword')             # (1)
  #     if pop.mails.empty?
  #       puts 'No mail.'
  #     else
  #       i = 0
  #       pop.each_mail do |m|   # or "pop.mails.each ..."   # (2)
  #         File.open("inbox/#{i}", 'w') do |f|
  #           f.write m.pop
  #         end
  #         m.delete
  #         i += 1
  #       end
  #       puts "#{pop.mails.size} mails popped."
  #     end
  #     pop.finish                                           # (3)
  # 
  # 1. Call Net::POP3#start and start POP session.
  # 2. Access messages by using POP3#each_mail and/or POP3#mails.
  # 3. Close POP session by calling POP3#finish or use the block form of #start.

  class POP3

    remove_method :do_start
    remove_method :initialize

    def initialize( addr, port = nil, isapop = false )
      @address = addr
      @port = port || self.class.default_port
      @apop = isapop

      @command = nil
      @socket = nil
      @started = false
      @open_timeout = 30
      @read_timeout = 60
      @debug_output = nil

      @mails = nil
      @n_mails = nil
      @n_bytes = nil

      @use_ssl = false
      @ssl_context = nil

    end

    def do_start( account, password )
      s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
      if use_ssl?
        unless @ssl_context.verify_mode
          warn "warning: peer certificate won't be verified in this SSL session"
          @ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
        end
        s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
        s.sync_close = true
      end
      @socket = Net::InternetMessageIO.new(s)
      @socket.read_timeout = @read_timeout
      @socket.debug_output = @debug_output

      if use_ssl?
        s.connect
      end

      on_connect
      @command = POP3Command.new(@socket)
      if apop?
        @command.apop account, password
      else
        @command.auth account, password
      end
      @started = true
    ensure
      do_finish if not @started
    end
    private :do_start

    def use_ssl?
      @use_ssl
    end

    # For backward compatibility.
    alias use_ssl use_ssl?

    # Turn on/off SSL.
    # This flag must be set before starting session.
    # If you change use_ssl value after session started,
    # a Net::HTTP object raises IOError.
    def use_ssl=(flag)
      flag = (flag ? true : false)
      raise IOError, "use_ssl value changed, but session already started" \
          if started? and @use_ssl != flag
      if flag and not @ssl_context
        @ssl_context = OpenSSL::SSL::SSLContext.new
      end
      @use_ssl = flag
    end

    def self.ssl_context_accessor(name)
      module_eval(<<-End, __FILE__, __LINE__ + 1)
        def #{name}
          return nil unless @ssl_context
          @ssl_context.#{name}
        end

        def #{name}=(val)
          @ssl_context ||= OpenSSL::SSL::SSLContext.new
          @ssl_context.#{name} = val
        end
      End
    end

    ssl_context_accessor :key
    ssl_context_accessor :cert
    ssl_context_accessor :ca_file
    ssl_context_accessor :ca_path
    ssl_context_accessor :verify_mode
    ssl_context_accessor :verify_callback
    ssl_context_accessor :verify_depth
    ssl_context_accessor :cert_store

    def ssl_timeout
      return nil unless @ssl_context
      @ssl_context.timeout
    end

    def ssl_timeout=(sec)
      raise ArgumentError, 'Net::POP3#ssl_timeout= called but use_ssl=false' \
          unless use_ssl?
      @ssl_context ||= OpenSSL::SSL::SSLContext.new
      @ssl_context.timeout = sec
    end

    # For backward compatibility
    alias timeout= ssl_timeout=

    def peer_cert
      return nil if not use_ssl? or not @socket
      @socket.io.peer_cert
    end
  end

end

Another way is to use socat. You can download it from internet or can be installed it using your favourite package manager.

Here is how the command is to be used.

socat TCP4-L:3000 OPENSSL:pop.gmail.com:995,verify=0

Now insted of using ‘net/pops’ you can use ‘net/pop’ itself and communicate with POP3 servers using SSL. But the only change will be that, instead of using the target server in the constructor you need to use the localhost with the port that you mention for TCP4-L in socat.

pop = Net::POP3.new('localhost', 3000)

I don’t know whether this will help people. If it does, then drop me a mail. I think ruby 1.9 will have this built inside ‘net/pop’.

Have fun!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories

%d bloggers like this: