STARTTLS support for net/smtp

From: "Daniel Hobe" <daniel@...>
Date: 2004-04-18 17:50:38 UTC
List: ruby-core #2789
This patch adds STARTTLS support to net/smtp.  The new methods are modeled
on my previous patch to add SSL support to pop.

Comments?

From the added docs:

# === STARTTLS support
#
# The Net::SMTP class supports STARTTLS.
#
#     # Per Instance STARTTLS
#     smtp = Net::SMTP.new('smtp.example.com',25)
#     smtp.enable_tls(verify, certs) if $use_tls            #(1)
#     smtp.start('your host','username','password') { |s|
#       s.send_message msgstr,
#                      'your@mail.address',
#                      'recipient@example.com'
#     }
#     smtp.finish
#
# 1. +verify+ tells the openssl library how to verify the server
#    certificate.  Defaults to OpenSSL::SSL::VERIFY_PEER
#    +certs+ is a file or directory holding CA certs to use to verify the
#    server cert; Defaults to nil.
#
#
#     # USE STARTTLS for all subsequent instances
#     Net::SMTP.enable_tls
#     # We will now use starttls for all connections.
#     Net::SMTP.start('your.smtp.server', 25, 'mail.from,domain',
#                     'Your Account', 'Your Password', :plain) {|smtp|
#       smtp.send_message msgstr,
#                         'your@mail.address',
#                         'his_addess@example.com'
#     }
#

-- 
Daniel Hobe <daniel@nightrunner.com>

Attachments (1)

smtp_tls.diff (7.12 KB, text/x-patch)
--- ../../../ruby.orig/lib/net/smtp.rb	2004-03-28 23:54:37.000000000 -0800
+++ smtp.rb	2004-04-18 10:27:32.000000000 -0700
@@ -104,7 +104,8 @@
 # The Net::SMTP class supports three authentication schemes;
 # PLAIN, LOGIN and CRAM MD5.  (SMTP Authentication: [RFC2554])
 # To use SMTP authentication, pass extra arguments to 
-# SMTP.start/SMTP#start.
+# SMTP.start/SMTP#start.  Use in conjunction with STARTTLS to
+# prevent authentication information passing in the clear.
 # 
 #     # PLAIN
 #     Net::SMTP.start('your.smtp.server', 25, 'mail.from,domain',
@@ -116,11 +117,46 @@
 #     # CRAM MD5
 #     Net::SMTP.start('your.smtp.server', 25, 'mail.from,domain',
 #                     'Your Account', 'Your Password', :cram_md5)
-
+# 
+# === STARTTLS support
+#
+# The Net::SMTP class supports STARTTLS.
+# 
+#     # Per Instance STARTTLS
+#     smtp = Net::SMTP.new('smtp.example.com',25)
+#     smtp.enable_tls(verify, certs) if $use_tls            #(1)
+#     smtp.start('your host','username','password') { |s|
+#       s.send_message msgstr,
+#                      'your@mail.address',
+#                      'recipient@example.com'
+#     }
+#     smtp.finish
+# 
+# 1. +verify+ tells the openssl library how to verify the server
+#    certificate.  Defaults to OpenSSL::SSL::VERIFY_PEER
+#    +certs+ is a file or directory holding CA certs to use to verify the 
+#    server cert; Defaults to nil.
+# 
+# 
+#     # USE STARTTLS for all subsequent instances
+#     Net::SMTP.enable_tls
+#     # We will now use starttls for all connections.
+#     Net::SMTP.start('your.smtp.server', 25, 'mail.from,domain',
+#                     'Your Account', 'Your Password', :plain) {|smtp|
+#       smtp.send_message msgstr,
+#                         'your@mail.address',
+#                         'his_addess@example.com'
+#     }
+# 
 require 'net/protocol'
 require 'digest/md5'
 require 'timeout'
 
+begin
+  require "openssl"
+rescue LoadError
+end
+
 module Net # :nodoc:
 
   # Module mixed in to all SMTP error classes
@@ -163,11 +199,33 @@
 
     Revision = %q$Revision: 1.71 $.split[1]
 
+    @@usetls = false
+    @@verify = nil
+    @@certs = nil
+    
     # The default SMTP port, port 25.
     def SMTP.default_port
       25
     end
 
+    # Enable SSL for all new instances.
+    # +verify+ is the type of verification to do on the Server Cert; Defaults
+    # to OpenSSL::SSL::VERIFY_PEER.
+    # +certs+ is a file or directory holding CA certs to use to verify the 
+    # server cert; Defaults to nil.
+    def SMTP.enable_tls(verify = OpenSSL::SSL::VERIFY_PEER, certs = nil)
+      @@usetls = true
+      @@verify = verify
+      @@certs = certs  
+    end
+
+    # Disable SSL for all new instances.
+    def SMTP.disable_tls
+      @@usetls = nil
+      @@verify = nil
+      @@certs = nil
+    end
+    
     # Creates a new Net::SMTP object. +address+ is the hostname
     # or ip address of your SMTP server.  +port+ is the port to
     # connect to; it defaults to port 25.
@@ -182,8 +240,33 @@
       @read_timeout = 60
       @error_occured = false
       @debug_output = nil
+      @usetls = @@usetls
+      @certs = @@certs
+      @verify = @@verify      
+    end
+
+    # does this instance use SSL?
+    def use_tls?
+      @usetls
+    end
+    
+    # Enables STARTTLS for this instance.
+    # +verify+ is the type of verification to do on the Server Cert; Defaults
+    # to OpenSSL::SSL::VERIFY_PEER.
+    # +certs+ is a file or directory holding CA certs to use to verify the 
+    # server cert; Defaults to nil.
+    def enable_tls(verify = OpenSSL::SSL::VERIFY_PEER, certs = nil)
+      @usetls = true
+      @verify = verify
+      @certs = certs
+    end
+    
+    def disable_tls
+      @usetls = nil
+      @verify = nil
+      @certs = nil
     end
-
+    
     # Provide human-readable stringification of class state.
     def inspect
       "#<#{self.class} #{@address}:#{@port} started=#{@started}>"
@@ -252,12 +335,12 @@
     #
     # This method is equivalent to:
     # 
-    #     Net::SMTP.new(address,port).start(helo_domain,account,password,authtype)
+    #   Net::SMTP.new(address,port).start(helo_domain,account,password,authtype)
     #
-    #     # example
-    #     Net::SMTP.start('your.smtp.server') {
-    #       smtp.send_message msgstr, 'from@example.com', ['dest@example.com']
-    #     }
+    #   # example
+    #   Net::SMTP.start('your.smtp.server') {
+    #     smtp.send_message msgstr, 'from@example.com', ['dest@example.com']
+    #   }
     #
     # If called with a block, the newly-opened Net::SMTP object is yielded
     # to the block, and automatically closed when the block finishes.  If called
@@ -341,15 +424,47 @@
     def do_start( helodomain, user, secret, authtype )
       raise IOError, 'SMTP session already started' if @started
       check_auth_args user, secret, authtype if user or secret
+      s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }   
+      @socket = InternetMessageIO.new(s)
 
-      @socket = InternetMessageIO.new(timeout(@open_timeout) {
-                  TCPSocket.open(@address, @port)
-                })
       logging "SMTP session opened: #{@address}:#{@port}"
       @socket.read_timeout = @read_timeout
       @socket.debug_output = @debug_output
       check_response(critical { recv_response() })
-      begin
+      do_helo(helodomain)
+      
+      if @usetls
+        unless defined?(OpenSSL)
+          raise "SSL extension not installed"
+        end
+        sslctx = OpenSSL::SSL::SSLContext.new
+        sslctx.verify_mode = @verify
+        sslctx.ca_file = @certs if @certs && FileTest::file?(@certs)
+        sslctx.ca_path = @certs if @certs && FileTest::directory?(@certs)
+        s = OpenSSL::SSL::SSLSocket.new(s, sslctx)
+        s.sync_close = true
+        starttls
+        s.connect
+        logging "STARTTLS Enabled.  Connection now encrypted.\n"
+        @socket = InternetMessageIO.new(s)
+        @socket.read_timeout = @read_timeout
+        @socket.debug_output = @debug_output
+        # helo response may be different after STARTTLS
+        do_helo(helodomain)
+      end
+
+      authenticate user, secret, authtype if user
+      @started = true
+    ensure
+      @socket.close if not @started and @socket and not @socket.closed?
+    end
+    private :do_start
+
+    # method to send helo or ehlo based on defaults and to
+    # retry with helo if server doesn't like ehlo.
+    # 
+    def do_helo(helodomain)
+       begin
         if @esmtp
           ehlo helodomain
         else
@@ -363,12 +478,8 @@
         end
         raise
       end
-      authenticate user, secret, authtype if user
-      @started = true
-    ensure
-      @socket.close if not @started and @socket and not @socket.closed?
     end
-    private :do_start
+      
 
     # Finishes the SMTP session and closes TCP connection.
     # Raises IOError if not started.
@@ -580,6 +691,9 @@
       getok('QUIT')
     end
 
+    def starttls
+      getok('STARTTLS')
+    end
     #
     # row level library
     #

In This Thread

Prev Next