mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
* 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
3 changed files with 232 additions and 85 deletions
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>
|
||||
|
||||
* lib/net/smtp.rb: reconstruct SMTPS/STARTTLS interface. New
|
||||
|
|
|
@ -305,8 +305,8 @@ module Net # :nodoc:
|
|||
yield
|
||||
end
|
||||
else # generic reader
|
||||
src.each do |s|
|
||||
buf << s
|
||||
src.each do |str|
|
||||
buf << str
|
||||
yield if buf.size > 1024
|
||||
end
|
||||
yield unless buf.empty?
|
||||
|
|
299
lib/net/smtp.rb
299
lib/net/smtp.rb
|
@ -60,6 +60,11 @@ module Net
|
|||
include SMTPError
|
||||
end
|
||||
|
||||
# Command is not supported on server.
|
||||
class SMTPUnsupportedCommand < ProtocolError
|
||||
include SMTPError
|
||||
end
|
||||
|
||||
#
|
||||
# = Net::SMTP
|
||||
#
|
||||
|
@ -207,6 +212,7 @@ module Net
|
|||
@address = address
|
||||
@port = (port || SMTP.default_port)
|
||||
@esmtp = true
|
||||
@capabilities = nil
|
||||
@socket = nil
|
||||
@started = false
|
||||
@open_timeout = 30
|
||||
|
@ -241,7 +247,52 @@ module Net
|
|||
|
||||
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?
|
||||
@tls
|
||||
end
|
||||
|
@ -275,6 +326,16 @@ module Net
|
|||
def starttls?
|
||||
@starttls
|
||||
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.
|
||||
# +context+ is a OpenSSL::SSL::SSLContext object.
|
||||
|
@ -484,21 +545,25 @@ module Net
|
|||
def do_start(helo_domain, user, secret, authtype)
|
||||
raise IOError, 'SMTP session already started' if @started
|
||||
if user or secret
|
||||
check_auth_method authtype
|
||||
check_auth_method(authtype || DEFAULT_AUTH_TYPE)
|
||||
check_auth_args user, secret
|
||||
end
|
||||
s = timeout(@open_timeout) { TCPSocket.open(@address, @port) }
|
||||
logging "Connection opened: #{@address}:#{@port}"
|
||||
@socket = new_internet_message_io(tls? ? tlsconnect(s) : s)
|
||||
check_response(critical { recv_response() })
|
||||
check_response critical { recv_response() }
|
||||
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
|
||||
@socket = new_internet_message_io(tlsconnect(s))
|
||||
# helo response may be different after STARTTLS
|
||||
do_helo helo_domain
|
||||
end
|
||||
authenticate user, secret, authtype if user
|
||||
authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user
|
||||
@started = true
|
||||
ensure
|
||||
unless @started
|
||||
|
@ -524,20 +589,15 @@ module Net
|
|||
end
|
||||
|
||||
def do_helo(helo_domain)
|
||||
begin
|
||||
if @esmtp
|
||||
ehlo helo_domain
|
||||
else
|
||||
helo helo_domain
|
||||
end
|
||||
rescue ProtocolError
|
||||
if @esmtp
|
||||
@esmtp = false
|
||||
@error_occured = false
|
||||
retry
|
||||
end
|
||||
raise
|
||||
res = @esmtp ? ehlo(helo_domain) : helo(helo_domain)
|
||||
@capabilities = res.capabilities
|
||||
rescue SMTPError
|
||||
if @esmtp
|
||||
@esmtp = false
|
||||
@error_occured = false
|
||||
retry
|
||||
end
|
||||
raise
|
||||
end
|
||||
|
||||
def do_finish
|
||||
|
@ -654,63 +714,57 @@ module Net
|
|||
|
||||
public
|
||||
|
||||
def authenticate(user, secret, authtype)
|
||||
DEFAULT_AUTH_TYPE = :plain
|
||||
|
||||
def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE)
|
||||
check_auth_method authtype
|
||||
check_auth_args user, secret
|
||||
funcall "auth_#{authtype || 'cram_md5'}", user, secret
|
||||
funcall auth_method(authtype), 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
|
||||
res = critical {
|
||||
get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}"))
|
||||
}
|
||||
check_auth_response res
|
||||
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)
|
||||
check_auth_continue get_response('AUTH LOGIN')
|
||||
check_auth_continue get_response(base64_encode(user))
|
||||
get_response(base64_encode(secret))
|
||||
}
|
||||
raise SMTPAuthenticationError, res unless /\A2../ =~ res
|
||||
check_auth_response res
|
||||
res
|
||||
end
|
||||
|
||||
def auth_cram_md5(user, secret)
|
||||
check_auth_args user, secret
|
||||
# CRAM-MD5: [RFC2195]
|
||||
res = nil
|
||||
critical {
|
||||
res = check_response(get_response('AUTH CRAM-MD5'), true)
|
||||
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))
|
||||
res = critical {
|
||||
check_auth_continue get_response('AUTH CRAM-MD5')
|
||||
crammed = cram_md5_response(secret, res.cram_md5_challenge)
|
||||
get_response(base64_encode("#{user} #{crammed}"))
|
||||
}
|
||||
raise SMTPAuthenticationError, res unless /\A2../ =~ res
|
||||
check_auth_response res
|
||||
res
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_auth_method(type)
|
||||
mid = "auth_#{type || 'cram_md5'}"
|
||||
unless respond_to?(mid, true)
|
||||
unless respond_to?(auth_method(type), true)
|
||||
raise ArgumentError, "wrong authentication type #{type}"
|
||||
end
|
||||
end
|
||||
|
||||
def auth_method(type)
|
||||
"auth_#{type.to_s.downcase}".intern
|
||||
end
|
||||
|
||||
def check_auth_args(user, secret)
|
||||
unless user
|
||||
raise ArgumentError, 'SMTP-AUTH requested but missing user name'
|
||||
|
@ -725,6 +779,26 @@ module Net
|
|||
[str].pack('m').gsub(/\s+/, '')
|
||||
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
|
||||
#
|
||||
|
@ -736,18 +810,18 @@ module Net
|
|||
end
|
||||
|
||||
def helo(domain)
|
||||
getok('HELO %s', domain)
|
||||
getok("HELO #{domain}")
|
||||
end
|
||||
|
||||
def ehlo(domain)
|
||||
getok('EHLO %s', domain)
|
||||
getok("EHLO #{domain}")
|
||||
end
|
||||
|
||||
def mailfrom(from_addr)
|
||||
if $SAFE > 0
|
||||
raise SecurityError, 'tainted from_addr' if from_addr.tainted?
|
||||
end
|
||||
getok('MAIL FROM:<%s>', from_addr)
|
||||
getok("MAIL FROM:<#{from_addr}>")
|
||||
end
|
||||
|
||||
def rcptto_list(to_addrs)
|
||||
|
@ -761,7 +835,7 @@ module Net
|
|||
if $SAFE > 0
|
||||
raise SecurityError, 'tainted to_addr' if to.tainted?
|
||||
end
|
||||
getok('RCPT TO:<%s>', to_addr)
|
||||
getok("RCPT TO:<#{to_addr}>")
|
||||
end
|
||||
|
||||
# This method sends a message.
|
||||
|
@ -795,7 +869,7 @@ module Net
|
|||
raise ArgumentError, "message or block is required"
|
||||
end
|
||||
res = critical {
|
||||
check_response(get_response('DATA'), true)
|
||||
check_continue get_response('DATA')
|
||||
if msgstr
|
||||
@socket.write_message msgstr
|
||||
else
|
||||
|
@ -803,7 +877,8 @@ module Net
|
|||
end
|
||||
recv_response()
|
||||
}
|
||||
check_response(res)
|
||||
check_response res
|
||||
res
|
||||
end
|
||||
|
||||
def quit
|
||||
|
@ -812,48 +887,28 @@ module Net
|
|||
|
||||
private
|
||||
|
||||
def getok(fmt, *args)
|
||||
def getok(reqline)
|
||||
res = critical {
|
||||
@socket.writeline sprintf(fmt, *args)
|
||||
@socket.writeline reqline
|
||||
recv_response()
|
||||
}
|
||||
return check_response(res)
|
||||
check_response res
|
||||
res
|
||||
end
|
||||
|
||||
def get_response(fmt, *args)
|
||||
@socket.writeline sprintf(fmt, *args)
|
||||
def get_response(reqline)
|
||||
@socket.writeline reqline
|
||||
recv_response()
|
||||
end
|
||||
|
||||
def recv_response
|
||||
res = ''
|
||||
buf = ''
|
||||
while true
|
||||
line = @socket.readline
|
||||
res << line << "\n"
|
||||
buf << line << "\n"
|
||||
break unless line[3,1] == '-' # "210-PIPELINING"
|
||||
end
|
||||
res
|
||||
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
|
||||
Response.parse(buf)
|
||||
end
|
||||
|
||||
def critical(&block)
|
||||
|
@ -866,6 +921,84 @@ module Net
|
|||
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)
|
||||
@debug_output << msg + "\n" if @debug_output
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue