Index: smtp.rb =================================================================== RCS file: /src/ruby/lib/net/smtp.rb,v retrieving revision 1.69.2.2 diff -u -r1.69.2.2 smtp.rb --- smtp.rb 9 May 2004 13:42:04 -0000 1.69.2.2 +++ smtp.rb 2 Aug 2004 12:38:19 -0000 @@ -15,13 +15,17 @@ # NOTE: You can find Japanese version of this document in # the doc/net directory of the standard ruby interpreter package. # -# $Id: smtp.rb,v 1.69.2.2 2004/05/09 13:42:04 gsinclair Exp $ +# $Id: smtp.rb,v 1.2 2004/08/02 11:07:30 koma2 Exp $ # # See Net::SMTP for documentation. # -require 'net/protocol' +require 'net/protocols' require 'digest/md5' +begin + require "openssl" +rescue LoadError +end module Net @@ -163,13 +167,47 @@ # class SMTP - Revision = %q$Revision: 1.69.2.2 $.split[1] + Revision = %q$Revision: 1.2 $.split[1] # The default SMTP port, port 25. def SMTP.default_port 25 end + @use_tls = false + @verify = nil + @certs = nil + + # 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) + @use_tls = true + @verify = verify + @certs = certs + end + + # Disable SSL for all new instances. + def SMTP.disable_tls + @use_tls = nil + @verify = nil + @certs = nil + end + + def SMTP.use_tls? + @use_tls + end + + def SMTP.verify + @verify + end + + def SMTP.certs + @certs + end + # # Creates a new Net::SMTP object. # @@ -181,7 +219,7 @@ # SMTP.start instead of SMTP.new if you want to do everything # at once. Otherwise, follow SMTP.new with SMTP#start. # - def initialize( address, port = nil ) + def initialize(address, port = nil) @address = address @port = (port || SMTP.default_port) @esmtp = true @@ -191,8 +229,11 @@ @read_timeout = 60 @error_occured = false @debug_output = nil + @use_tls = SMTP.use_tls? + @certs = SMTP.certs + @verify = SMTP.verify end - + # Provide human-readable stringification of class state. def inspect "#<#{self.class} #{@address}:#{@port} started=#{@started}>" @@ -210,12 +251,34 @@ # object will automatically switch to plain SMTP mode and # retry (but not vice versa). # - def esmtp=( bool ) + def esmtp=(bool) @esmtp = bool end alias esmtp esmtp? + # does this instance use SSL? + def use_tls? + @use_tls + 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) + @use_tls = true + @verify = verify + @certs = certs + end + + def disable_tls + @use_tls = false + @verify = nil + @certs = nil + end + # The address of the SMTP server to connect to. attr_reader :address @@ -234,7 +297,7 @@ # Set the number of seconds to wait until timing-out a read(2) # call. - def read_timeout=( sec ) + def read_timeout=(sec) @socket.read_timeout = sec if @socket @read_timeout = sec end @@ -253,7 +316,7 @@ # .... # end # - def set_debug_output( arg ) + def set_debug_output(arg) @debug_output = arg end @@ -309,10 +372,9 @@ # * IOError # * TimeoutError # - def SMTP.start( address, port = nil, - helo = 'localhost.localdomain', - user = nil, secret = nil, authtype = nil, - &block) # :yield: smtp + def SMTP.start(address, port = nil, helo = 'localhost.localdomain', + user = nil, secret = nil, authtype = nil, + &block) # :yield: smtp new(address, port).start(helo, user, secret, authtype, &block) end @@ -371,8 +433,8 @@ # * IOError # * TimeoutError # - def start( helo = 'localhost.localdomain', - user = nil, secret = nil, authtype = nil ) # :yield: smtp + def start(helo = 'localhost.localdomain', + user = nil, secret = nil, authtype = nil) # :yield: smtp if block_given? begin do_start(helo, user, secret, authtype) @@ -386,15 +448,52 @@ end end - def do_start( helodomain, user, secret, authtype ) + 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 - @socket = InternetMessageIO.open(@address, @port, - @open_timeout, @read_timeout, - @debug_output) + @socket = SSLIO.open(@address, @port, + @open_timeout, @read_timeout, + @debug_output) + logging "SMTP session opened: #{@address}:#{@port}" check_response(critical { recv_response() }) - begin + do_helo(helodomain) + + if @use_tls + raise 'openssl library not installed' unless defined?(OpenSSL) + context = OpenSSL::SSL::SSLContext.new + context.verify_mode = @verify + if @certs + if File.file?(@certs) + context.ca_file = @certs + elsif File.directory?(@certs) + context.ca_path = @certs + else + raise ArgumentError, "certs given but is not file or directory: #{@certs}" + end + end + starttls + logging 'TLS started' + @socket.ssl_connect() + do_helo(helodomain) + end + + authenticate user, secret, authtype if user + @started = true + ensure + unless @started + # authentication failed, cancel connection. + @socket.close if not @started and @socket and not @socket.closed? + @socket = nil + end + 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 @@ -408,12 +507,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. @@ -468,7 +563,7 @@ # * IOError # * TimeoutError # - def send_message( msgstr, from_addr, *to_addrs ) + def send_message(msgstr, from_addr, *to_addrs) send0(from_addr, to_addrs.flatten) { @socket.write_message msgstr } @@ -521,7 +616,7 @@ # * IOError # * TimeoutError # - def open_message_stream( from_addr, *to_addrs, &block ) # :yield: stream + def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream send0(from_addr, to_addrs.flatten) { @socket.write_message_by_block(&block) } @@ -531,7 +626,7 @@ private - def send0( from_addr, to_addrs ) + def send0(from_addr, to_addrs) raise IOError, 'closed session' unless @socket raise ArgumentError, 'mail destination not given' if to_addrs.empty? if $SAFE > 0 @@ -559,7 +654,7 @@ private - def check_auth_args( user, secret, authtype ) + def check_auth_args(user, secret, authtype) raise ArgumentError, 'both user and secret are required'\ unless user and secret auth_method = "auth_#{authtype || 'cram_md5'}" @@ -567,17 +662,17 @@ unless respond_to?(auth_method, true) end - def authenticate( user, secret, authtype ) + def authenticate(user, secret, authtype) __send__("auth_#{authtype || 'cram_md5'}", user, secret) end - def auth_plain( user, secret ) + def auth_plain(user, secret) res = critical { get_response('AUTH PLAIN %s', base64_encode("\0#{user}\0#{secret}")) } raise SMTPAuthenticationError, res unless /\A2../ === res end - def auth_login( user, secret ) + def auth_login(user, secret) res = critical { check_response(get_response('AUTH LOGIN'), true) check_response(get_response(base64_encode(user)), true) @@ -586,7 +681,7 @@ raise SMTPAuthenticationError, res unless /\A2../ === res end - def auth_cram_md5( user, secret ) + def auth_cram_md5(user, secret) # CRAM-MD5: [RFC2195] res = nil critical { @@ -608,7 +703,7 @@ raise SMTPAuthenticationError, res unless /\A2../ === res end - def base64_encode( str ) + def base64_encode(str) # expects "str" may not become too long [str].pack('m').gsub(/\s+/, '') end @@ -619,19 +714,19 @@ private - def helo( domain ) + def helo(domain) getok('HELO %s', domain) end - def ehlo( domain ) + def ehlo(domain) getok('EHLO %s', domain) end - def mailfrom( fromaddr ) + def mailfrom(fromaddr) getok('MAIL FROM:<%s>', fromaddr) end - def rcptto( to ) + def rcptto(to) getok('RCPT TO:<%s>', to) end @@ -639,13 +734,16 @@ getok('QUIT') end + def starttls + getok('STARTTLS') + end # # row level library # private - def getok( fmt, *args ) + def getok(fmt, *args) res = critical { @socket.writeline sprintf(fmt, *args) recv_response() @@ -653,7 +751,7 @@ return check_response(res) end - def get_response( fmt, *args ) + def get_response(fmt, *args) @socket.writeline sprintf(fmt, *args) recv_response() end @@ -668,7 +766,7 @@ res end - def check_response( res, allow_continue = false ) + def check_response(res, allow_continue = false) return res if /\A2/ === res return res if allow_continue and /\A3/ === res err = case res @@ -680,7 +778,7 @@ raise err, res end - def critical( &block ) + def critical(&block) return '200 dummy reply code' if @error_occured begin return yield() @@ -688,6 +786,10 @@ @error_occured = true raise end + end + + def logging(msg) + @debug_output << msg + "\n" if @debug_output end end # class SMTP