* lib/net/smtp.rb: support automatic STARTTLS.
* lib/net/smtp.rb: check server advertisement. * lib/net/smtp.rb: introduce new class SMTP::Response. * lib/net/smtp.rb (getok): should not use sprintf. * lib/net/smtp.rb (get_response): ditto. * lib/net/protocol.rb: reduce syntax warning on 1.9. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@11994 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
parent
736f3c28b0
commit
6dfd5fe953
14
ChangeLog
14
ChangeLog
@ -1,3 +1,17 @@
|
|||||||
|
Mon Mar 5 09:16:40 2007 Minero Aoki <aamine@loveruby.net>
|
||||||
|
|
||||||
|
* lib/net/smtp.rb: support automatic STARTTLS.
|
||||||
|
|
||||||
|
* lib/net/smtp.rb: check server advertisement.
|
||||||
|
|
||||||
|
* lib/net/smtp.rb: introduce new class SMTP::Response.
|
||||||
|
|
||||||
|
* lib/net/smtp.rb (getok): should not use sprintf.
|
||||||
|
|
||||||
|
* lib/net/smtp.rb (get_response): ditto.
|
||||||
|
|
||||||
|
* lib/net/protocol.rb: reduce syntax warning on 1.9.
|
||||||
|
|
||||||
Mon Mar 5 07:13:28 2007 Minero Aoki <aamine@loveruby.net>
|
Mon Mar 5 07:13:28 2007 Minero Aoki <aamine@loveruby.net>
|
||||||
|
|
||||||
* lib/net/smtp.rb: reconstruct SMTPS/STARTTLS interface. New
|
* lib/net/smtp.rb: reconstruct SMTPS/STARTTLS interface. New
|
||||||
|
@ -305,8 +305,8 @@ module Net # :nodoc:
|
|||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
else # generic reader
|
else # generic reader
|
||||||
src.each do |s|
|
src.each do |str|
|
||||||
buf << s
|
buf << str
|
||||||
yield if buf.size > 1024
|
yield if buf.size > 1024
|
||||||
end
|
end
|
||||||
yield unless buf.empty?
|
yield unless buf.empty?
|
||||||
|
289
lib/net/smtp.rb
289
lib/net/smtp.rb
@ -60,6 +60,11 @@ module Net
|
|||||||
include SMTPError
|
include SMTPError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Command is not supported on server.
|
||||||
|
class SMTPUnsupportedCommand < ProtocolError
|
||||||
|
include SMTPError
|
||||||
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
# = Net::SMTP
|
# = Net::SMTP
|
||||||
#
|
#
|
||||||
@ -207,6 +212,7 @@ module Net
|
|||||||
@address = address
|
@address = address
|
||||||
@port = (port || SMTP.default_port)
|
@port = (port || SMTP.default_port)
|
||||||
@esmtp = true
|
@esmtp = true
|
||||||
|
@capabilities = nil
|
||||||
@socket = nil
|
@socket = nil
|
||||||
@started = false
|
@started = false
|
||||||
@open_timeout = 30
|
@open_timeout = 30
|
||||||
@ -241,7 +247,52 @@ module Net
|
|||||||
|
|
||||||
alias esmtp esmtp?
|
alias esmtp esmtp?
|
||||||
|
|
||||||
# true if this object uses SMTPS.
|
# true if server advertises STARTTLS.
|
||||||
|
# You cannot get valid value before opening SMTP session.
|
||||||
|
def capable_starttls?
|
||||||
|
capable?('STARTTLS')
|
||||||
|
end
|
||||||
|
|
||||||
|
def capable?(key)
|
||||||
|
return nil unless @capabilities
|
||||||
|
@capabilities[key] ? true : false
|
||||||
|
end
|
||||||
|
private :capable?
|
||||||
|
|
||||||
|
# true if server advertises AUTH PLAIN.
|
||||||
|
# You cannot get valid value before opening SMTP session.
|
||||||
|
def capable_plain_auth?
|
||||||
|
auth_capable?('PLAIN')
|
||||||
|
end
|
||||||
|
|
||||||
|
# true if server advertises AUTH LOGIN.
|
||||||
|
# You cannot get valid value before opening SMTP session.
|
||||||
|
def capable_login_auth?
|
||||||
|
auth_capable?('LOGIN')
|
||||||
|
end
|
||||||
|
|
||||||
|
# true if server advertises AUTH CRAM-MD5.
|
||||||
|
# You cannot get valid value before opening SMTP session.
|
||||||
|
def capable_cram_md5_auth?
|
||||||
|
auth_capable?('CRAM-MD5')
|
||||||
|
end
|
||||||
|
|
||||||
|
def auth_capable?(type)
|
||||||
|
return nil unless @capabilities
|
||||||
|
return false unless @capabilities['AUTH']
|
||||||
|
@capabilities['AUTH'].include?(type)
|
||||||
|
end
|
||||||
|
private :auth_capable?
|
||||||
|
|
||||||
|
# Returns supported authentication methods on this server.
|
||||||
|
# You cannot get valid value before opening SMTP session.
|
||||||
|
def capable_auth_types
|
||||||
|
return [] unless @capabilities
|
||||||
|
return [] unless @capabilities['AUTH']
|
||||||
|
@capabilities['AUTH']
|
||||||
|
end
|
||||||
|
|
||||||
|
# true if this object uses SMTP/TLS (SMTPS).
|
||||||
def tls?
|
def tls?
|
||||||
@tls
|
@tls
|
||||||
end
|
end
|
||||||
@ -276,6 +327,16 @@ module Net
|
|||||||
@starttls
|
@starttls
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# true if this object uses STARTTLS.
|
||||||
|
def starttls_always?
|
||||||
|
@starttls == :always
|
||||||
|
end
|
||||||
|
|
||||||
|
# true if this object uses STARTTLS when server advertises STARTTLS.
|
||||||
|
def starttls_auto?
|
||||||
|
@starttls == :auto
|
||||||
|
end
|
||||||
|
|
||||||
# Enables SMTP/TLS (STARTTLS) for this object.
|
# Enables SMTP/TLS (STARTTLS) for this object.
|
||||||
# +context+ is a OpenSSL::SSL::SSLContext object.
|
# +context+ is a OpenSSL::SSL::SSLContext object.
|
||||||
def enable_starttls(context = SMTP.default_ssl_context)
|
def enable_starttls(context = SMTP.default_ssl_context)
|
||||||
@ -484,21 +545,25 @@ module Net
|
|||||||
def do_start(helo_domain, user, secret, authtype)
|
def do_start(helo_domain, user, secret, authtype)
|
||||||
raise IOError, 'SMTP session already started' if @started
|
raise IOError, 'SMTP session already started' if @started
|
||||||
if user or secret
|
if user or secret
|
||||||
check_auth_method authtype
|
check_auth_method(authtype || DEFAULT_AUTH_TYPE)
|
||||||
check_auth_args user, secret
|
check_auth_args user, secret
|
||||||
end
|
end
|
||||||
s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
|
s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
|
||||||
logging "Connection opened: #{@address}:#{@port}"
|
logging "Connection opened: #{@address}:#{@port}"
|
||||||
@socket = new_internet_message_io(tls? ? tlsconnect(s) : s)
|
@socket = new_internet_message_io(tls? ? tlsconnect(s) : s)
|
||||||
check_response(critical { recv_response() })
|
check_response critical { recv_response() }
|
||||||
do_helo helo_domain
|
do_helo helo_domain
|
||||||
if starttls?
|
if starttls_always? or (capable_starttls? and starttls_auto?)
|
||||||
|
unless capable_starttls?
|
||||||
|
raise SMTPUnsupportedCommand,
|
||||||
|
"STARTTLS is not supported on this server"
|
||||||
|
end
|
||||||
starttls
|
starttls
|
||||||
@socket = new_internet_message_io(tlsconnect(s))
|
@socket = new_internet_message_io(tlsconnect(s))
|
||||||
# helo response may be different after STARTTLS
|
# helo response may be different after STARTTLS
|
||||||
do_helo helo_domain
|
do_helo helo_domain
|
||||||
end
|
end
|
||||||
authenticate user, secret, authtype if user
|
authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user
|
||||||
@started = true
|
@started = true
|
||||||
ensure
|
ensure
|
||||||
unless @started
|
unless @started
|
||||||
@ -524,13 +589,9 @@ module Net
|
|||||||
end
|
end
|
||||||
|
|
||||||
def do_helo(helo_domain)
|
def do_helo(helo_domain)
|
||||||
begin
|
res = @esmtp ? ehlo(helo_domain) : helo(helo_domain)
|
||||||
if @esmtp
|
@capabilities = res.capabilities
|
||||||
ehlo helo_domain
|
rescue SMTPError
|
||||||
else
|
|
||||||
helo helo_domain
|
|
||||||
end
|
|
||||||
rescue ProtocolError
|
|
||||||
if @esmtp
|
if @esmtp
|
||||||
@esmtp = false
|
@esmtp = false
|
||||||
@error_occured = false
|
@error_occured = false
|
||||||
@ -538,7 +599,6 @@ module Net
|
|||||||
end
|
end
|
||||||
raise
|
raise
|
||||||
end
|
end
|
||||||
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
|
||||||
@ -654,63 +714,57 @@ module Net
|
|||||||
|
|
||||||
public
|
public
|
||||||
|
|
||||||
def authenticate(user, secret, authtype)
|
DEFAULT_AUTH_TYPE = :plain
|
||||||
|
|
||||||
|
def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE)
|
||||||
check_auth_method authtype
|
check_auth_method authtype
|
||||||
check_auth_args user, secret
|
check_auth_args user, secret
|
||||||
funcall "auth_#{authtype || 'cram_md5'}", user, secret
|
funcall auth_method(authtype), user, secret
|
||||||
end
|
end
|
||||||
|
|
||||||
def auth_plain(user, secret)
|
def auth_plain(user, secret)
|
||||||
check_auth_args user, secret
|
check_auth_args user, secret
|
||||||
res = critical { get_response('AUTH PLAIN %s',
|
res = critical {
|
||||||
base64_encode("\0#{user}\0#{secret}")) }
|
get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}"))
|
||||||
raise SMTPAuthenticationError, res unless /\A2../ =~ res
|
}
|
||||||
|
check_auth_response res
|
||||||
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
def auth_login(user, secret)
|
def auth_login(user, secret)
|
||||||
check_auth_args user, secret
|
check_auth_args user, secret
|
||||||
res = critical {
|
res = critical {
|
||||||
check_response(get_response('AUTH LOGIN'), true)
|
check_auth_continue get_response('AUTH LOGIN')
|
||||||
check_response(get_response(base64_encode(user)), true)
|
check_auth_continue get_response(base64_encode(user))
|
||||||
get_response(base64_encode(secret))
|
get_response(base64_encode(secret))
|
||||||
}
|
}
|
||||||
raise SMTPAuthenticationError, res unless /\A2../ =~ res
|
check_auth_response res
|
||||||
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
def auth_cram_md5(user, secret)
|
def auth_cram_md5(user, secret)
|
||||||
check_auth_args user, secret
|
check_auth_args user, secret
|
||||||
# CRAM-MD5: [RFC2195]
|
res = critical {
|
||||||
res = nil
|
check_auth_continue get_response('AUTH CRAM-MD5')
|
||||||
critical {
|
crammed = cram_md5_response(secret, res.cram_md5_challenge)
|
||||||
res = check_response(get_response('AUTH CRAM-MD5'), true)
|
get_response(base64_encode("#{user} #{crammed}"))
|
||||||
challenge = res.split(/ /)[1].unpack('m')[0]
|
|
||||||
secret = Digest::MD5.digest(secret) if secret.size > 64
|
|
||||||
|
|
||||||
isecret = secret + "\0" * (64 - secret.size)
|
|
||||||
osecret = isecret.dup
|
|
||||||
0.upto(63) do |i|
|
|
||||||
c = isecret[i].ord ^ 0x36
|
|
||||||
isecret[i] = c.chr
|
|
||||||
c = osecret[i].ord ^ 0x5c
|
|
||||||
osecret[i] = c.chr
|
|
||||||
end
|
|
||||||
tmp = Digest::MD5.digest(isecret + challenge)
|
|
||||||
tmp = Digest::MD5.hexdigest(osecret + tmp)
|
|
||||||
|
|
||||||
res = get_response(base64_encode(user + ' ' + tmp))
|
|
||||||
}
|
}
|
||||||
raise SMTPAuthenticationError, res unless /\A2../ =~ res
|
check_auth_response res
|
||||||
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_auth_method(type)
|
def check_auth_method(type)
|
||||||
mid = "auth_#{type || 'cram_md5'}"
|
unless respond_to?(auth_method(type), true)
|
||||||
unless respond_to?(mid, true)
|
|
||||||
raise ArgumentError, "wrong authentication type #{type}"
|
raise ArgumentError, "wrong authentication type #{type}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auth_method(type)
|
||||||
|
"auth_#{type.to_s.downcase}".intern
|
||||||
|
end
|
||||||
|
|
||||||
def check_auth_args(user, secret)
|
def check_auth_args(user, secret)
|
||||||
unless user
|
unless user
|
||||||
raise ArgumentError, 'SMTP-AUTH requested but missing user name'
|
raise ArgumentError, 'SMTP-AUTH requested but missing user name'
|
||||||
@ -725,6 +779,26 @@ module Net
|
|||||||
[str].pack('m').gsub(/\s+/, '')
|
[str].pack('m').gsub(/\s+/, '')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
IMASK = 0x36
|
||||||
|
OMASK = 0x5c
|
||||||
|
|
||||||
|
# CRAM-MD5: [RFC2195]
|
||||||
|
def cram_md5_response(secret, challenge)
|
||||||
|
tmp = Digest::MD5.digest(cram_secret(secret, IMASK) + challenge)
|
||||||
|
Digest::MD5.hexdigest(cram_secret(secret, OMASK) + tmp)
|
||||||
|
end
|
||||||
|
|
||||||
|
CRAM_BUFSIZE = 64
|
||||||
|
|
||||||
|
def cram_secret(secret, mask)
|
||||||
|
secret = Digest::MD5.digest(secret) if secret.size > CRAM_BUFSIZE
|
||||||
|
buf = secret.ljust(CRAM_BUFSIZE, "\0")
|
||||||
|
0.upto(buf.size) do |i|
|
||||||
|
buf[i] = (buf[i].ord ^ mask).chr
|
||||||
|
end
|
||||||
|
buf
|
||||||
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
# SMTP command dispatcher
|
# SMTP command dispatcher
|
||||||
#
|
#
|
||||||
@ -736,18 +810,18 @@ module Net
|
|||||||
end
|
end
|
||||||
|
|
||||||
def helo(domain)
|
def helo(domain)
|
||||||
getok('HELO %s', domain)
|
getok("HELO #{domain}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def ehlo(domain)
|
def ehlo(domain)
|
||||||
getok('EHLO %s', domain)
|
getok("EHLO #{domain}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def mailfrom(from_addr)
|
def mailfrom(from_addr)
|
||||||
if $SAFE > 0
|
if $SAFE > 0
|
||||||
raise SecurityError, 'tainted from_addr' if from_addr.tainted?
|
raise SecurityError, 'tainted from_addr' if from_addr.tainted?
|
||||||
end
|
end
|
||||||
getok('MAIL FROM:<%s>', from_addr)
|
getok("MAIL FROM:<#{from_addr}>")
|
||||||
end
|
end
|
||||||
|
|
||||||
def rcptto_list(to_addrs)
|
def rcptto_list(to_addrs)
|
||||||
@ -761,7 +835,7 @@ module Net
|
|||||||
if $SAFE > 0
|
if $SAFE > 0
|
||||||
raise SecurityError, 'tainted to_addr' if to.tainted?
|
raise SecurityError, 'tainted to_addr' if to.tainted?
|
||||||
end
|
end
|
||||||
getok('RCPT TO:<%s>', to_addr)
|
getok("RCPT TO:<#{to_addr}>")
|
||||||
end
|
end
|
||||||
|
|
||||||
# This method sends a message.
|
# This method sends a message.
|
||||||
@ -795,7 +869,7 @@ module Net
|
|||||||
raise ArgumentError, "message or block is required"
|
raise ArgumentError, "message or block is required"
|
||||||
end
|
end
|
||||||
res = critical {
|
res = critical {
|
||||||
check_response(get_response('DATA'), true)
|
check_continue get_response('DATA')
|
||||||
if msgstr
|
if msgstr
|
||||||
@socket.write_message msgstr
|
@socket.write_message msgstr
|
||||||
else
|
else
|
||||||
@ -803,7 +877,8 @@ module Net
|
|||||||
end
|
end
|
||||||
recv_response()
|
recv_response()
|
||||||
}
|
}
|
||||||
check_response(res)
|
check_response res
|
||||||
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
def quit
|
def quit
|
||||||
@ -812,48 +887,28 @@ module Net
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def getok(fmt, *args)
|
def getok(reqline)
|
||||||
res = critical {
|
res = critical {
|
||||||
@socket.writeline sprintf(fmt, *args)
|
@socket.writeline reqline
|
||||||
recv_response()
|
recv_response()
|
||||||
}
|
}
|
||||||
return check_response(res)
|
check_response res
|
||||||
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_response(fmt, *args)
|
def get_response(reqline)
|
||||||
@socket.writeline sprintf(fmt, *args)
|
@socket.writeline reqline
|
||||||
recv_response()
|
recv_response()
|
||||||
end
|
end
|
||||||
|
|
||||||
def recv_response
|
def recv_response
|
||||||
res = ''
|
buf = ''
|
||||||
while true
|
while true
|
||||||
line = @socket.readline
|
line = @socket.readline
|
||||||
res << line << "\n"
|
buf << line << "\n"
|
||||||
break unless line[3,1] == '-' # "210-PIPELINING"
|
break unless line[3,1] == '-' # "210-PIPELINING"
|
||||||
end
|
end
|
||||||
res
|
Response.parse(buf)
|
||||||
end
|
|
||||||
|
|
||||||
def check_response(res, allow_continue = false)
|
|
||||||
case res
|
|
||||||
when /\A2/
|
|
||||||
return res
|
|
||||||
when /\A3/
|
|
||||||
unless allow_continue
|
|
||||||
raise 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
|
end
|
||||||
|
|
||||||
def critical(&block)
|
def critical(&block)
|
||||||
@ -866,6 +921,84 @@ module Net
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_response(res)
|
||||||
|
unless res.success?
|
||||||
|
raise res.exception_class, res.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_continue(res)
|
||||||
|
unless res.continue?
|
||||||
|
raise SMTPUnknownError, "could not get 3xx (#{res.status})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_auth_response(res)
|
||||||
|
unless res.success?
|
||||||
|
raise SMTPAuthenticationError, res.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_auth_continue(res)
|
||||||
|
unless res.continue?
|
||||||
|
raise res.exception_class, res.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Response
|
||||||
|
def Response.parse(str)
|
||||||
|
new(str[0,3], str)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(status, string)
|
||||||
|
@status = status
|
||||||
|
@string = string
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :status
|
||||||
|
attr_reader :string
|
||||||
|
|
||||||
|
def status_type_char
|
||||||
|
@status[0, 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def success?
|
||||||
|
status_type_char() == '2'
|
||||||
|
end
|
||||||
|
|
||||||
|
def continue?
|
||||||
|
status_type_char() == '3'
|
||||||
|
end
|
||||||
|
|
||||||
|
def message
|
||||||
|
@string.lines.first
|
||||||
|
end
|
||||||
|
|
||||||
|
def cram_md5_challenge
|
||||||
|
@string.split(/ /)[1].unpack('m')[0]
|
||||||
|
end
|
||||||
|
|
||||||
|
def capabilities
|
||||||
|
return {} unless @string[3, 1] == '-'
|
||||||
|
h = {}
|
||||||
|
@string.lines.to_a[1..-1].each do |line|
|
||||||
|
k, *v = line[4..-1].chomp.split(nil)
|
||||||
|
h[k] = v
|
||||||
|
end
|
||||||
|
h
|
||||||
|
end
|
||||||
|
|
||||||
|
def exception_class
|
||||||
|
case @status
|
||||||
|
when /\A4/ then SMTPServerBusy
|
||||||
|
when /\A50/ then SMTPSyntaxError
|
||||||
|
when /\A53/ then SMTPAuthenticationError
|
||||||
|
when /\A5/ then SMTPFatalError
|
||||||
|
else SMTPUnknownError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def logging(msg)
|
def logging(msg)
|
||||||
@debug_output << msg + "\n" if @debug_output
|
@debug_output << msg + "\n" if @debug_output
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user