From a03909606ed96f97588879b8effb8585899356c0 Mon Sep 17 00:00:00 2001 From: aamine Date: Tue, 15 Aug 2006 02:21:59 +0000 Subject: * lib/net/smtp.rb: support SMTP/SSL. Thanks Kazuhiro NISHIYAMA. * lib/net/smtp.rb: new method SMTP.use_ssl? * lib/net/smtp.rb: new method SMTP.enable_ssl. * lib/net/smtp.rb: new method SMTP.disable_ssl. * lib/net/smtp.rb: new method SMTP.default_ssl_port. * lib/net/smtp.rb: new method SMTP.default_tls_port. * lib/net/smtp.rb: now SMTP#enable_tls accepts a SSLContext object, instead of a verity and cert. [FEATURE CHANGE] * lib/net/smtp.rb: new method SMTP.ssl_context. * lib/net/smtp.rb: new method SMTP.default_ssl_context. * lib/net/smtp.rb: export SMTP.authenticate. * lib/net/smtp.rb: export SMTP.auth_plain. * lib/net/smtp.rb: export SMTP.auth_login. * lib/net/smtp.rb: export SMTP.auth_cram_md5. * lib/net/smtp.rb: export SMTP.starttls. * lib/net/smtp.rb: export SMTP.helo. * lib/net/smtp.rb: export SMTP.ehlo. * lib/net/smtp.rb: export SMTP.mailfrom. * lib/net/smtp.rb: export SMTP.rcptto. * lib/net/smtp.rb: export SMTP.rcptto_list. * lib/net/smtp.rb: export SMTP.data. * lib/net/smtp.rb: export SMTP.quit. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@10726 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/net/smtp.rb | 425 +++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 268 insertions(+), 157 deletions(-) (limited to 'lib') diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb index f68c47a9a7..ab9c1afb88 100644 --- a/lib/net/smtp.rb +++ b/lib/net/smtp.rb @@ -1,8 +1,8 @@ # = net/smtp.rb # -# Copyright (c) 1999-2004 Yukihiro Matsumoto. +# Copyright (c) 1999-2006 Yukihiro Matsumoto. # -# Copyright (c) 1999-2004 Minero Aoki. +# Copyright (c) 1999-2006 Minero Aoki. # # Written & maintained by Minero Aoki . # @@ -23,7 +23,7 @@ require 'net/protocol' require 'digest/md5' require 'timeout' begin - require "openssl" + require 'openssl' rescue LoadError end @@ -174,38 +174,68 @@ module Net 25 end - @use_tls = false - @verify = nil - @certs = nil + # The default SMTP/SSL port, port 465. + def SMTP.default_ssl_port + 465 + end + + # The default SMTP/TLS (STARTTLS) port, port 587. + def SMTP.default_tls_port + 587 + end + + @ssl = false + @tls = false + @ssl_context = nil + + # Enables SMTP/SSL for all new objects. + # +context+ is a OpenSSL::SSL::SSLContext object. + def SMTP.enable_ssl(context = SMTP.default_ssl_context) + raise 'openssl library not installed' unless defined?(OpenSSL) + raise ArgumentError, "SSL and TLS is exclusive" if @tls + @ssl = true + @ssl_context = context + end + + # Disables SMTP/SSL for all new objects. + def SMTP.disable_ssl + @ssl = false + @ssl_context = nil + 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) - @use_tls = true - @verify = verify - @certs = certs + # true if new objects use SMTP/SSL. + def SMTP.use_ssl? + @ssl end - # Disable SSL for all new instances. + # Enables SMTP/SSL for all new objects. + # +context+ is a OpenSSL::SSL::Context object. + def SMTP.enable_tls(context = SMTP.default_ssl_context) + raise 'openssl library not installed' unless defined?(OpenSSL) + raise ArgumentError, "SSL and TLS is exclusive" if @ssl + @tls = false + @ssl_context = context + end + + # Disable SMTP/TLS for all new objects. def SMTP.disable_tls - @use_tls = nil - @verify = nil - @certs = nil + @tls = false + @ssl_context = nil end + # true if new objects use SMTP/TLS. def SMTP.use_tls? - @use_tls + @tls end - def SMTP.verify - @verify + def SMTP.ssl_context + @ssl_context end - def SMTP.certs - @certs + def SMTP.default_ssl_context + ctx = OpenSSL::SSL::SSLContext.new + ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER + ctx end # @@ -229,9 +259,16 @@ module Net @read_timeout = 60 @error_occured = false @debug_output = nil - @use_tls = SMTP.use_tls? - @certs = SMTP.certs - @verify = SMTP.verify + if SMTP.use_ssl? or SMTP.use_tls? + @ssl = true + if SMTP.use_ssl? + @ssl_mode = :ssl + else + @ssl_mode = :tls + end + @certs = SMTP.certs + @verify = SMTP.verify + end end # Provide human-readable stringification of class state. @@ -257,26 +294,47 @@ module Net alias esmtp esmtp? - # does this instance use SSL? + # true if this object uses SMTP/SSL. + def use_ssl? + @ssl + end + + # true if this object uses SMTP/TLS def use_tls? - @use_tls + @tls + end + + # Enables SMTP/SSL for this object. Must be called before the + # connection is established to have any effect. + # +context+ is a OpenSSL::SSL::SSLContext object. + def enable_ssl(context = SMTP.default_ssl_context) + raise 'openssl library not installed' unless defined?(OpenSSL) + raise ArgumentError, "SSL and TLS is exclusive" if @tls + @ssl = true + @ssl_context = context 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 + # Disables SMTP/SSL for this object. Must be called before the + # connection is established to have any effect. + def disable_ssl + @ssl = false + @ssl_context = nil end + # Enables SMTP/TLS (STARTTLS) for this object. + # +context+ is a OpenSSL::SSL::SSLContext object. + def enable_tls(context = SMTP.default_ssl_context) + raise 'openssl library not installed' unless defined?(OpenSSL) + raise ArgumentError, "SSL and TLS is exclusive" if @ssl + @tls = true + @ssl_context = context + end + + # Disables SMTP/TLS (STARTTLS) for this object. Must be called + # before the connection is established to have any effect. def disable_tls - @use_tls = false - @verify = nil - @certs = nil + @ssl = false + @ssl_context = nil end # The address of the SMTP server to connect to. @@ -316,10 +374,12 @@ module Net # .... # end # - def set_debug_output(arg) + def debug_output=(arg) @debug_output = arg end + alias set_debug_output debug_output= + # # SMTP session control # @@ -437,54 +497,51 @@ module Net user = nil, secret = nil, authtype = nil) # :yield: smtp if block_given? begin - do_start(helo, user, secret, authtype) + do_start helo, user, secret, authtype return yield(self) ensure do_finish end else - do_start(helo, user, secret, authtype) + do_start helo, user, secret, authtype return self end end - def do_start(helodomain, user, secret, authtype) + # Finishes the SMTP session and closes TCP connection. + # Raises IOError if not started. + def finish + raise IOError, 'not yet started' unless started? + do_finish + end + + private + + def do_start(helo_domain, user, secret, authtype) raise IOError, 'SMTP session already started' if @started - check_auth_args user, secret, authtype if user or secret + if user or secret + check_auth_method authtype + check_auth_args user, secret + end s = timeout(@open_timeout) { TCPSocket.open(@address, @port) } - @socket = InternetMessageIO.new(s) - - logging "SMTP session opened: #{@address}:#{@port}" - @socket.read_timeout = @read_timeout - @socket.debug_output = @debug_output + logging "Connection opened: #{@address}:#{@port}" + if use_ssl? + s = new_ssl_socket(s) + s.connect + logging "SMTP/SSL started" + end + @socket = new_internet_message_io(s) check_response(critical { recv_response() }) - 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 - s = OpenSSL::SSL::SSLSocket.new(s, context) - s.sync_close = true + do_helo helo_domain + if use_tls? + s = new_ssl_socket(s) starttls s.connect - logging 'TLS started' - @socket = InternetMessageIO.new(s) - @socket.read_timeout = @read_timeout - @socket.debug_output = @debug_output + logging "SMTP/TLS started" + @socket = new_internet_message_io(s) # helo response may be different after STARTTLS - do_helo(helodomain) + do_helo helo_domain end - authenticate user, secret, authtype if user @started = true ensure @@ -494,17 +551,26 @@ module Net @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) + def new_internet_message_io(s) + io = InternetMessageIO.new(s) + io.read_timeout = @read_timeout + io.debug_output = @debug_output + io + end + + def new_ssl_socket(s) + s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) + s.sync_close = true + s + end + + def do_helo(helo_domain) begin if @esmtp - ehlo helodomain + ehlo helo_domain else - helo helodomain + helo helo_domain end rescue ProtocolError if @esmtp @@ -515,14 +581,6 @@ module Net raise end end - - - # Finishes the SMTP session and closes TCP connection. - # Raises IOError if not started. - def finish - raise IOError, 'not yet started' unless started? - do_finish - end def do_finish quit if @socket and not @socket.closed? and not @error_occured @@ -532,10 +590,9 @@ module Net @socket.close if @socket and not @socket.closed? @socket = nil end - private :do_finish # - # message send + # Message Sending # public @@ -571,9 +628,10 @@ module Net # * TimeoutError # def send_message(msgstr, from_addr, *to_addrs) - send0(from_addr, to_addrs.flatten) { - @socket.write_message msgstr - } + raise IOError, 'closed session' unless @socket + mailfrom from_addr + rcptto_list to_addrs + data msgstr end alias send_mail send_message @@ -624,71 +682,45 @@ module Net # * TimeoutError # def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream - send0(from_addr, to_addrs.flatten) { - @socket.write_message_by_block(&block) - } - end - - alias ready open_message_stream # obsolete - - private - - 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 - raise SecurityError, 'tainted from_addr' if from_addr.tainted? - to_addrs.each do |to| - raise SecurityError, 'tainted to_addr' if to.tainted? - end - end - mailfrom from_addr - to_addrs.each do |to| - rcptto to - end - res = critical { - check_response(get_response('DATA'), true) - yield - recv_response() - } - check_response(res) + rcptto_list to_addrs + data(&block) end + alias ready open_message_stream # obsolete + # - # auth + # Authentication # - private - - 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'}" - raise ArgumentError, "wrong auth type #{authtype}"\ - unless respond_to?(auth_method, true) - end + public def authenticate(user, secret, authtype) - __send__("auth_#{authtype || 'cram_md5'}", user, secret) + check_auth_method authtype + check_auth_args user, secret + funcall "auth_#{authtype || 'cram_md5'}", user, secret end def auth_plain(user, secret) + check_auth_args user, secret res = critical { get_response('AUTH PLAIN %s', base64_encode("\0#{user}\0#{secret}")) } - raise SMTPAuthenticationError, res unless /\A2../ === res + raise SMTPAuthenticationError, res unless /\A2../ =~ res end def auth_login(user, secret) + check_auth_args user, secret res = critical { check_response(get_response('AUTH LOGIN'), true) check_response(get_response(base64_encode(user)), true) get_response(base64_encode(secret)) } - raise SMTPAuthenticationError, res unless /\A2../ === res + raise SMTPAuthenticationError, res unless /\A2../ =~ res end def auth_cram_md5(user, secret) + check_auth_args user, secret # CRAM-MD5: [RFC2195] res = nil critical { @@ -709,7 +741,25 @@ module Net res = get_response(base64_encode(user + ' ' + tmp)) } - raise SMTPAuthenticationError, res unless /\A2../ === res + raise SMTPAuthenticationError, res unless /\A2../ =~ res + end + + private + + def check_auth_method(type) + mid = "auth_#{type || 'cram_md5'}" + unless respond_to?(mid, true) + raise ArgumentError, "wrong authentication type #{type}" + end + end + + def check_auth_args(user, secret) + unless user + raise ArgumentError, 'SMTP-AUTH requested but missing user name' + end + unless secret + raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase' + end end def base64_encode(str) @@ -721,7 +771,11 @@ module Net # SMTP command dispatcher # - private + public + + def starttls + getok('STARTTLS') + end def helo(domain) getok('HELO %s', domain) @@ -731,24 +785,72 @@ module Net getok('EHLO %s', domain) end - def mailfrom(fromaddr) - getok('MAIL FROM:<%s>', fromaddr) + def mailfrom(from_addr) + if $SAFE > 0 + raise SecurityError, 'tainted from_addr' if from_addr.tainted? + end + getok('MAIL FROM:<%s>', from_addr) end - def rcptto(to) - getok('RCPT TO:<%s>', to) + def rcptto_list(to_addrs) + raise ArgumentError, 'mail destination not given' if to_addrs.empty? + to_addrs.each do |addr| + rcptto addr + end end - def quit - getok('QUIT') + def rcptto(to_addr) + if $SAFE > 0 + raise SecurityError, 'tainted to_addr' if to.tainted? + end + getok('RCPT TO:<%s>', to_addr) + end + + # This method sends a message. + # If +msgstr+ is given, sends it as a message. + # If block is given, yield a message writer stream. + # You must write message before the block is closed. + # + # # Example 1 (by string) + # smtp.data(<