1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00

* 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
This commit is contained in:
aamine 2006-08-15 02:21:59 +00:00
parent e86b8053d2
commit a03909606e
2 changed files with 315 additions and 159 deletions

View file

@ -1,3 +1,48 @@
Tue Aug 15 11:21:08 2006 Minero Aoki <aamine@loveruby.net>
* 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.
Sat Aug 12 22:33:06 2006 Eric Hodel <drbrain@segment7.net> Sat Aug 12 22:33:06 2006 Eric Hodel <drbrain@segment7.net>
* string.c (String#split): Describe grouping behavior. Patch by Jan * string.c (String#split): Describe grouping behavior. Patch by Jan

View file

@ -1,8 +1,8 @@
# = net/smtp.rb # = 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 <aamine@loveruby.net>. # Written & maintained by Minero Aoki <aamine@loveruby.net>.
# #
@ -23,7 +23,7 @@ require 'net/protocol'
require 'digest/md5' require 'digest/md5'
require 'timeout' require 'timeout'
begin begin
require "openssl" require 'openssl'
rescue LoadError rescue LoadError
end end
@ -174,38 +174,68 @@ module Net
25 25
end end
@use_tls = false # The default SMTP/SSL port, port 465.
@verify = nil def SMTP.default_ssl_port
@certs = nil 465
# 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 end
# Disable SSL for all new instances. # 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
# true if new objects use SMTP/SSL.
def SMTP.use_ssl?
@ssl
end
# 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 def SMTP.disable_tls
@use_tls = nil @tls = false
@verify = nil @ssl_context = nil
@certs = nil
end end
# true if new objects use SMTP/TLS.
def SMTP.use_tls? def SMTP.use_tls?
@use_tls @tls
end end
def SMTP.verify def SMTP.ssl_context
@verify @ssl_context
end end
def SMTP.certs def SMTP.default_ssl_context
@certs ctx = OpenSSL::SSL::SSLContext.new
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
ctx
end end
# #
@ -229,10 +259,17 @@ module Net
@read_timeout = 60 @read_timeout = 60
@error_occured = false @error_occured = false
@debug_output = nil @debug_output = nil
@use_tls = SMTP.use_tls? 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 @certs = SMTP.certs
@verify = SMTP.verify @verify = SMTP.verify
end end
end
# Provide human-readable stringification of class state. # Provide human-readable stringification of class state.
def inspect def inspect
@ -257,26 +294,47 @@ module Net
alias esmtp esmtp? 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? def use_tls?
@use_tls @tls
end end
# Enables STARTTLS for this instance. # Enables SMTP/SSL for this object. Must be called before the
# +verify+ is the type of verification to do on the Server Cert; Defaults # connection is established to have any effect.
# to OpenSSL::SSL::VERIFY_PEER. # +context+ is a OpenSSL::SSL::SSLContext object.
# +certs+ is a file or directory holding CA certs to use to verify the def enable_ssl(context = SMTP.default_ssl_context)
# server cert; Defaults to nil. raise 'openssl library not installed' unless defined?(OpenSSL)
def enable_tls(verify = OpenSSL::SSL::VERIFY_PEER, certs = nil) raise ArgumentError, "SSL and TLS is exclusive" if @tls
@use_tls = true @ssl = true
@verify = verify @ssl_context = context
@certs = certs
end end
# 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 def disable_tls
@use_tls = false @ssl = false
@verify = nil @ssl_context = nil
@certs = nil
end end
# The address of the SMTP server to connect to. # The address of the SMTP server to connect to.
@ -316,10 +374,12 @@ module Net
# .... # ....
# end # end
# #
def set_debug_output(arg) def debug_output=(arg)
@debug_output = arg @debug_output = arg
end end
alias set_debug_output debug_output=
# #
# SMTP session control # SMTP session control
# #
@ -437,54 +497,51 @@ module Net
user = nil, secret = nil, authtype = nil) # :yield: smtp user = nil, secret = nil, authtype = nil) # :yield: smtp
if block_given? if block_given?
begin begin
do_start(helo, user, secret, authtype) do_start helo, user, secret, authtype
return yield(self) return yield(self)
ensure ensure
do_finish do_finish
end end
else else
do_start(helo, user, secret, authtype) do_start helo, user, secret, authtype
return self return self
end end
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 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) } s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
@socket = InternetMessageIO.new(s) logging "Connection opened: #{@address}:#{@port}"
if use_ssl?
logging "SMTP session opened: #{@address}:#{@port}" s = new_ssl_socket(s)
@socket.read_timeout = @read_timeout s.connect
@socket.debug_output = @debug_output logging "SMTP/SSL started"
end
@socket = new_internet_message_io(s)
check_response(critical { recv_response() }) check_response(critical { recv_response() })
do_helo(helodomain) do_helo helo_domain
if use_tls?
if @use_tls s = new_ssl_socket(s)
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
starttls starttls
s.connect s.connect
logging 'TLS started' logging "SMTP/TLS started"
@socket = InternetMessageIO.new(s) @socket = new_internet_message_io(s)
@socket.read_timeout = @read_timeout
@socket.debug_output = @debug_output
# helo response may be different after STARTTLS # helo response may be different after STARTTLS
do_helo(helodomain) do_helo helo_domain
end end
authenticate user, secret, authtype if user authenticate user, secret, authtype if user
@started = true @started = true
ensure ensure
@ -494,17 +551,26 @@ module Net
@socket = nil @socket = nil
end end
end end
private :do_start
# method to send helo or ehlo based on defaults and to def new_internet_message_io(s)
# retry with helo if server doesn't like ehlo. io = InternetMessageIO.new(s)
# io.read_timeout = @read_timeout
def do_helo(helodomain) 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 begin
if @esmtp if @esmtp
ehlo helodomain ehlo helo_domain
else else
helo helodomain helo helo_domain
end end
rescue ProtocolError rescue ProtocolError
if @esmtp if @esmtp
@ -516,14 +582,6 @@ module Net
end end
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 def do_finish
quit if @socket and not @socket.closed? and not @error_occured quit if @socket and not @socket.closed? and not @error_occured
ensure ensure
@ -532,10 +590,9 @@ module Net
@socket.close if @socket and not @socket.closed? @socket.close if @socket and not @socket.closed?
@socket = nil @socket = nil
end end
private :do_finish
# #
# message send # Message Sending
# #
public public
@ -571,9 +628,10 @@ module Net
# * TimeoutError # * TimeoutError
# #
def send_message(msgstr, from_addr, *to_addrs) def send_message(msgstr, from_addr, *to_addrs)
send0(from_addr, to_addrs.flatten) { raise IOError, 'closed session' unless @socket
@socket.write_message msgstr mailfrom from_addr
} rcptto_list to_addrs
data msgstr
end end
alias send_mail send_message alias send_mail send_message
@ -624,71 +682,45 @@ module Net
# * TimeoutError # * 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) { raise IOError, 'closed session' unless @socket
@socket.write_message_by_block(&block) mailfrom from_addr
} rcptto_list to_addrs
data(&block)
end end
alias ready open_message_stream # obsolete 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)
end
# #
# auth # Authentication
# #
private public
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
def authenticate(user, secret, authtype) 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 end
def auth_plain(user, secret) def auth_plain(user, secret)
check_auth_args user, secret
res = critical { get_response('AUTH PLAIN %s', res = critical { get_response('AUTH PLAIN %s',
base64_encode("\0#{user}\0#{secret}")) } base64_encode("\0#{user}\0#{secret}")) }
raise SMTPAuthenticationError, res unless /\A2../ === res raise SMTPAuthenticationError, res unless /\A2../ =~ res
end end
def auth_login(user, secret) def auth_login(user, secret)
check_auth_args user, secret
res = critical { res = critical {
check_response(get_response('AUTH LOGIN'), true) check_response(get_response('AUTH LOGIN'), true)
check_response(get_response(base64_encode(user)), true) check_response(get_response(base64_encode(user)), true)
get_response(base64_encode(secret)) get_response(base64_encode(secret))
} }
raise SMTPAuthenticationError, res unless /\A2../ === res raise SMTPAuthenticationError, res unless /\A2../ =~ res
end end
def auth_cram_md5(user, secret) def auth_cram_md5(user, secret)
check_auth_args user, secret
# CRAM-MD5: [RFC2195] # CRAM-MD5: [RFC2195]
res = nil res = nil
critical { critical {
@ -709,7 +741,25 @@ module Net
res = get_response(base64_encode(user + ' ' + tmp)) 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 end
def base64_encode(str) def base64_encode(str)
@ -721,7 +771,11 @@ module Net
# SMTP command dispatcher # SMTP command dispatcher
# #
private public
def starttls
getok('STARTTLS')
end
def helo(domain) def helo(domain)
getok('HELO %s', domain) getok('HELO %s', domain)
@ -731,25 +785,73 @@ module Net
getok('EHLO %s', domain) getok('EHLO %s', domain)
end end
def mailfrom(fromaddr) def mailfrom(from_addr)
getok('MAIL FROM:<%s>', fromaddr) if $SAFE > 0
raise SecurityError, 'tainted from_addr' if from_addr.tainted?
end
getok('MAIL FROM:<%s>', from_addr)
end end
def rcptto(to) def rcptto_list(to_addrs)
getok('RCPT TO:<%s>', to) raise ArgumentError, 'mail destination not given' if to_addrs.empty?
to_addrs.each do |addr|
rcptto addr
end
end
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(<<EndMessage)
# From: john@example.com
# To: betty@example.com
# Subject: I found a bug
#
# Check vm.c:58879.
# EndMessage
#
# # Example 2 (by block)
# smtp.data {|f|
# f.puts "From: john@example.com"
# f.puts "To: betty@example.com"
# f.puts "Subject: I found a bug"
# f.puts ""
# f.puts "Check vm.c:58879."
# }
#
def data(msgstr = nil, &block) #:yield: stream
if msgstr and block
raise ArgumentError, "message and block are exclusive"
end
unless msgstr or block
raise ArgumentError, "message or block is required"
end
res = critical {
check_response(get_response('DATA'), true)
if msgstr
@socket.write_message msgstr
else
@socket.write_message_by_block(&block)
end
recv_response()
}
check_response(res)
end end
def quit def quit
getok('QUIT') getok('QUIT')
end end
def starttls
getok('STARTTLS')
end
#
# row level library
#
private private
def getok(fmt, *args) def getok(fmt, *args)
@ -776,15 +878,24 @@ module Net
end end
def check_response(res, allow_continue = false) def check_response(res, allow_continue = false)
return res if /\A2/ === res case res
return res if allow_continue and /\A3/ === res when /\A2/
err = case res return res
when /\A4/ then SMTPServerBusy when /\A3/
when /\A50/ then SMTPSyntaxError unless allow_continue
when /\A55/ then SMTPFatalError raise SMTPUnknownError,
else SMTPUnknownError "got response 3xx but not DATA: #{res.inspect}"
end
return res
when /\A4/
raise SMTPServerBusy, res
when /\A50/
raise SMTPSyntaxError, res
when /\A55/
raise SMTPFatalError, res
else
raise SMTPUnknownError, res
end end
raise err, res
end end
def critical(&block) def critical(&block)
@ -805,4 +916,4 @@ module Net
SMTPSession = SMTP SMTPSession = SMTP
end # module Net end