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: unify SMTP and SMTPCommand.

* lib/net/smtp.rb: new exception class SMTPError.
* lib/net/smtp.rb: new exception class SMTPAuthenticationError.
* lib/net/smtp.rb: new exception class SMTPServerBusy.
* lib/net/smtp.rb: new exception class SMTPSyntaxError.
* lib/net/smtp.rb: new exception class SMTPFatalError.
* lib/net/smtp.rb: new exception class SMTPUnknownError.
* lib/net/smtp.rb: change critical section protect algorithm.
* lib/net/smtp.rb (SMTP#do_start): check authentication args before all.
* lib/net/smtp.rb: new method send_message (alias send_mail).
* lib/net/smtp.rb: new method open_message_stream (alias ready).
* lib/net/pop.rb: POPBadResponse is a POPError.
* lib/net/pop.rb (POPMail#pop): ban ReadAdapter.
* lib/net/pop.rb (POPMail#top): ditto.
* lib/net/pop.rb (POP3Command): change critical section protect algorithm.
* lib/net/pop.rb (POP3Command#auth): USER and PASS should be one critical block.
* lib/net/pop.rb (POP3Command#retr): ban `dest' argument using iterator.
* lib/net/pop.rb (POP3Command#top): ditto.
* lib/net/protocol.rb: #read_message_to -> #each_message_chunk
* lib/net/protocol.rb: #D -> #LOG
* lib/net/protocol.rb: #D_off -> #LOG_off
* lib/net/protocol.rb: #D_on -> #LOG_on


git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@4026 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
aamine 2003-07-02 02:34:39 +00:00
parent f473140e62
commit c20ecb1ba4
4 changed files with 410 additions and 329 deletions

View file

@ -1,3 +1,53 @@
Wed Jul 2 11:39:50 2003 Minero Aoki <aamine@loveruby.net>
* lib/net/smtp.rb: unify SMTP and SMTPCommand.
* lib/net/smtp.rb: new exception class SMTPError.
* lib/net/smtp.rb: new exception class SMTPAuthenticationError.
* lib/net/smtp.rb: new exception class SMTPServerBusy.
* lib/net/smtp.rb: new exception class SMTPSyntaxError.
* lib/net/smtp.rb: new exception class SMTPFatalError.
* lib/net/smtp.rb: new exception class SMTPUnknownError.
* lib/net/smtp.rb: change critical section protect algorithm.
* lib/net/smtp.rb (SMTP#do_start): check authentication args
before all.
* lib/net/smtp.rb: new method send_message (alias send_mail).
* lib/net/smtp.rb: new method open_message_stream (alias ready).
* lib/net/pop.rb: POPBadResponse is a POPError.
* lib/net/pop.rb (POPMail#pop): ban ReadAdapter.
* lib/net/pop.rb (POPMail#top): ditto.
* lib/net/pop.rb (POP3Command): change critical section protect
algorithm.
* lib/net/pop.rb (POP3Command#auth): USER and PASS should be one
critical block.
* lib/net/pop.rb (POP3Command#retr): ban `dest' argument using
iterator.
* lib/net/pop.rb (POP3Command#top): ditto.
* lib/net/protocol.rb: #read_message_to -> #each_message_chunk
* lib/net/protocol.rb: #D -> #LOG
* lib/net/protocol.rb: #D_off -> #LOG_off
* lib/net/protocol.rb: #D_on -> #LOG_on
Wed Jul 2 11:10:47 2003 Minero Aoki <aamine@loveruby.net> Wed Jul 2 11:10:47 2003 Minero Aoki <aamine@loveruby.net>
* lib/net/http.rb: set old class aliases for backward * lib/net/http.rb: set old class aliases for backward

View file

@ -390,7 +390,7 @@ module Net
class POPError < ProtocolError; end class POPError < ProtocolError; end
class POPAuthenticationError < ProtoAuthError; end class POPAuthenticationError < ProtoAuthError; end
class POPBadResponse < StandardError; end class POPBadResponse < POPError; end
class POP3 < Protocol class POP3 < Protocol
@ -405,6 +405,7 @@ module Net
110 110
end end
# obsolete
def POP3.socket_type def POP3.socket_type
Net::InternetMessageIO Net::InternetMessageIO
end end
@ -421,7 +422,7 @@ module Net
account = nil, password = nil, account = nil, password = nil,
isapop = false, &block ) isapop = false, &block )
start(address, port, account, password, isapop) {|pop| start(address, port, account, password, isapop) {|pop|
pop.each_mail(&block) pop.each_mail(&block)
} }
end end
@ -429,7 +430,7 @@ module Net
account = nil, password = nil, account = nil, password = nil,
isapop = false, &block ) isapop = false, &block )
start(address, port, account, password, isapop) {|pop| start(address, port, account, password, isapop) {|pop|
pop.delete_all(&block) pop.delete_all(&block)
} }
end end
@ -442,7 +443,7 @@ module Net
def auth_only( account, password ) def auth_only( account, password )
raise IOError, 'opening already opened POP session' if started? raise IOError, 'opening already opened POP session' if started?
start(account, password) { start(account, password) {
; ;
} }
end end
@ -481,7 +482,7 @@ module Net
"#<#{self.class} #{@address}:#{@port} open=#{@started}>" "#<#{self.class} #{@address}:#{@port} open=#{@started}>"
end end
def set_debug_output( arg ) # :nodoc: def set_debug_output( arg )
@debug_output = arg @debug_output = arg
end end
@ -500,7 +501,7 @@ module Net
@started @started
end end
alias active? started? # backward compatibility alias active? started? # obsolete
def start( account, password ) def start( account, password )
raise IOError, 'POP session already started' if @started raise IOError, 'POP session already started' if @started
@ -578,7 +579,7 @@ module Net
end end
@mails = command().list.map {|num, size| @mails = command().list.map {|num, size|
POPMail.new(num, size, self, command()) POPMail.new(num, size, self, command())
} }
@mails.dup @mails.dup
end end
@ -600,7 +601,7 @@ module Net
command().rset command().rset
mails().each do |m| mails().each do |m|
m.instance_eval { m.instance_eval {
@deleted = false @deleted = false
} }
end end
end end
@ -612,9 +613,9 @@ module Net
end end
end end
end end # class POP3
# aliases # class aliases
POP = POP3 POP = POP3
POPSession = POP3 POPSession = POP3
POP3Session = POP3 POP3Session = POP3
@ -630,9 +631,9 @@ module Net
class POPMail class POPMail
def initialize( num, size, pop, cmd ) def initialize( num, len, pop, cmd )
@number = num @number = num
@size = size @length = len
@pop = pop @pop = pop
@command = cmd @command = cmd
@deleted = false @deleted = false
@ -640,23 +641,37 @@ module Net
end end
attr_reader :number attr_reader :number
attr_reader :size attr_reader :length
alias size length
def inspect def inspect
"#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>" "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>"
end end
def pop( dest = '', &block ) def pop( dest = '', &block )
@command.retr(@number, (block ? ReadAdapter.new(block) : dest)) if block_given?
@command.retr(@number, &block)
nil
else
@command.retr(@number) do |chunk|
dest << chunk
end
dest
end
end end
alias all pop # backward compatibility alias all pop # backward compatibility
alias mail pop # backward compatibility alias mail pop # backward compatibility
# `dest' argument is obsolete
def top( lines, dest = '' ) def top( lines, dest = '' )
@command.top(@number, lines, dest) @command.top(@number, lines) do |chunk|
dest << chunk
end
dest
end end
# `dest' argument is obsolete
def header( dest = '' ) def header( dest = '' )
top(0, dest) top(0, dest)
end end
@ -685,14 +700,14 @@ module Net
@uid = uid @uid = uid
end end
end end # class POPMail
class POP3Command class POP3Command
def initialize( sock ) def initialize( sock )
@socket = sock @socket = sock
@in_critical_block = false @error_occured = false
res = check_response(critical { recv_response() }) res = check_response(critical { recv_response() })
@apop_stamp = res.slice(/<.+>/) @apop_stamp = res.slice(/<.+>/)
end end
@ -702,30 +717,32 @@ module Net
end end
def auth( account, password ) def auth( account, password )
check_response_auth(critical { get_response('USER ' + account) }) check_response_auth(critical {
check_response_auth(critical { get_response('PASS ' + password) }) check_response_auth(get_response('USER ' + account))
get_response('PASS ' + password)
})
end end
def apop( account, password ) def apop( account, password )
raise POPAuthenticationError, 'not APOP server; cannot login' \ raise POPAuthenticationError, 'not APOP server; cannot login' \
unless @apop_stamp unless @apop_stamp
check_response_auth(critical { check_response_auth(critical {
get_response('APOP %s %s', get_response('APOP %s %s',
account, account,
Digest::MD5.hexdigest(@apop_stamp + password)) Digest::MD5.hexdigest(@apop_stamp + password))
}) })
end end
def list def list
critical { critical {
getok 'LIST' getok 'LIST'
list = [] list = []
@socket.each_list_item do |line| @socket.each_list_item do |line|
m = /\A(\d+)[ \t]+(\d+)/.match(line) or m = /\A(\d+)[ \t]+(\d+)/.match(line) or
raise POPBadResponse, "bad response: #{line}" raise POPBadResponse, "bad response: #{line}"
list.push [m[1].to_i, m[2].to_i] list.push [m[1].to_i, m[2].to_i]
end end
list return list
} }
end end
@ -740,17 +757,17 @@ module Net
check_response(critical { get_response 'RSET' }) check_response(critical { get_response 'RSET' })
end end
def top( num, lines = 0, dest = '' ) def top( num, lines = 0, &block )
critical { critical {
getok('TOP %d %d', num, lines) getok('TOP %d %d', num, lines)
@socket.read_message_to(dest) @socket.each_message_chunk(&block)
} }
end end
def retr( num, dest = '' ) def retr( num, &block )
critical { critical {
getok('RETR %d', num) getok('RETR %d', num)
@socket.read_message_to dest @socket.each_message_chunk(&block)
} }
end end
@ -761,16 +778,16 @@ module Net
def uidl( num = nil ) def uidl( num = nil )
if num if num
res = check_response(critical { get_response('UIDL %d', num) }) res = check_response(critical { get_response('UIDL %d', num) })
res.split(/ /)[1] return res.split(/ /)[1]
else else
critical { critical {
getok('UIDL') getok('UIDL')
table = {} table = {}
@socket.each_list_item do |line| @socket.each_list_item do |line|
num, uid = line.split num, uid = line.split
table[num.to_i] = uid table[num.to_i] = uid
end end
table return table
} }
end end
end end
@ -781,13 +798,13 @@ module Net
private private
def getok( *reqs ) def getok( fmt, *fargs )
@socket.writeline sprintf(*reqs) @socket.writeline sprintf(fmt, *fargs)
check_response(recv_response()) check_response(recv_response())
end end
def get_response( *reqs ) def get_response( fmt, *fargs )
@socket.writeline sprintf(*reqs) @socket.writeline sprintf(fmt, *fargs)
recv_response() recv_response()
end end
@ -806,14 +823,15 @@ module Net
end end
def critical def critical
return if @in_critical_block return '+OK dummy ok response' if @error_occured
# Do not use ensure-block. begin
@in_critical_block = true return yield()
result = yield rescue Exception
@in_critical_block = false @error_occured = true
result raise
end
end end
end end # class POP3Command
end # module Net end # module Net

View file

@ -52,24 +52,25 @@ module Net
alias open new alias open new
end end
def initialize( addr, port, otime = nil, rtime = nil, dout = nil ) def initialize( addr, port,
open_timeout = nil, read_timeout = nil,
debug_output = nil )
@address = addr @address = addr
@port = port @port = port
@read_timeout = rtime @read_timeout = read_timeout
@debug_output = dout @debug_output = debug_output
@socket = nil
@socket = nil @rbuf = nil # read buffer
@rbuf = nil @wbuf = nil # write buffer
connect open_timeout
connect otime LOG 'opened'
D 'opened'
end end
attr_reader :address attr_reader :address
attr_reader :port attr_reader :port
def ip_address def ip_address
@socket or return '' return '' unless @socket
@socket.addr[3] @socket.addr[3]
end end
@ -77,10 +78,10 @@ module Net
attr_reader :socket attr_reader :socket
def connect( otime ) def connect( open_timeout )
D "opening connection to #{@address}..." LOG "opening connection to #{@address}..."
timeout(otime) { timeout(open_timeout) {
@socket = TCPsocket.new(@address, @port) @socket = TCPsocket.new(@address, @port)
} }
@rbuf = '' @rbuf = ''
end end
@ -89,19 +90,19 @@ module Net
def close def close
if @socket if @socket
@socket.close @socket.close
D 'closed' LOG 'closed'
else else
D 'close call for already closed socket' LOG 'close call for already closed socket'
end end
@socket = nil @socket = nil
@rbuf = '' @rbuf = ''
end end
def reopen( otime = nil ) def reopen( open_timeout = nil )
D 'reopening...' LOG 'reopening...'
close close
connect otime connect open_timeout
D 'reopened' LOG 'reopened'
end end
def closed? def closed?
@ -109,7 +110,7 @@ module Net
end end
def inspect def inspect
"#<#{self.class} #{closed? ? 'closed' : 'opened'}>" "#<#{self.class} #{closed?() ? 'closed' : 'opened'}>"
end end
### ###
@ -118,74 +119,85 @@ module Net
public public
def read( len, dest = '', ignore = false ) def read( len, dest = '', ignore_eof = false )
D_off "reading #{len} bytes..." LOG "reading #{len} bytes..."
LOG_off()
rsize = 0 read_bytes = 0
begin begin
while rsize + @rbuf.size < len while read_bytes + @rbuf.size < len
rsize += rbuf_moveto(dest, @rbuf.size) read_bytes += rbuf_moveto(dest, @rbuf.size)
rbuf_fill rbuf_fill
end end
rbuf_moveto dest, len - rsize rbuf_moveto dest, len - read_bytes
rescue EOFError rescue EOFError
raise unless ignore raise unless ignore_eof
end end
LOG_on()
D_on "read #{len} bytes" LOG "read #{read_bytes} bytes"
dest dest
end end
def read_all( dest = '' ) def read_all( dest = '' )
D_off 'reading all...' LOG 'reading all...'
LOG_off()
rsize = 0 read_bytes = 0
begin begin
while true while true
rsize += rbuf_moveto(dest, @rbuf.size) read_bytes += rbuf_moveto(dest, @rbuf.size)
rbuf_fill rbuf_fill
end end
rescue EOFError rescue EOFError
; ;
end end
LOG_on()
D_on "read #{rsize} bytes" LOG "read #{read_bytes} bytes"
dest dest
end end
def readuntil( target, ignore = false ) def readuntil( terminator, ignore_eof = false )
dest = '' dest = ''
begin begin
until idx = @rbuf.index(target) until idx = @rbuf.index(terminator)
rbuf_fill rbuf_fill
end end
rbuf_moveto dest, idx + target.size rbuf_moveto dest, idx + terminator.size
rescue EOFError rescue EOFError
raise unless ignore raise unless ignore_eof
rbuf_moveto dest, @rbuf.size rbuf_moveto dest, @rbuf.size
end end
dest dest
end end
def readline def readline
ret = readuntil("\n") readuntil("\n").chop
ret.chop! end
ret
def each_message_chunk
LOG 'reading message...'
LOG_off()
read_bytes = 0
while (line = readuntil("\r\n")) != ".\r\n"
read_bytes += line.size
yield line.sub(/\A\./, '')
end
LOG_on()
LOG "read message (#{read_bytes} bytes)"
end
# *library private* (cannot handle 'break')
def each_list_item
while (str = readuntil("\r\n")) != ".\r\n"
yield str.chop
end
end end
private private
BLOCK_SIZE = 1024
def rbuf_fill def rbuf_fill
until IO.select [@socket], nil, nil, @read_timeout until IO.select([@socket], nil, nil, @read_timeout)
on_read_timeout raise TimeoutError, "socket read timeout (#{@read_timeout} sec)"
end end
@rbuf << @socket.sysread(BLOCK_SIZE) @rbuf << @socket.sysread(1024)
end
def on_read_timeout
raise TimeoutError, "socket read timeout (#{@read_timeout} sec)"
end end
def rbuf_moveto( dest, len ) def rbuf_moveto( dest, len )
@ -194,173 +206,147 @@ module Net
len len
end end
#
# message read
#
public
def read_message_to( dest )
D_off 'reading text...'
rsize = 0
while (str = readuntil("\r\n")) != ".\r\n"
rsize += str.size
dest << str.sub(/\A\./, '')
end
D_on "read #{rsize} bytes"
dest
end
# private use only (cannot handle 'break')
def each_list_item
while (str = readuntil("\r\n")) != ".\r\n"
yield str.chop
end
end
### ###
### WRITE ### WRITE
### ###
#
# basic write
#
public public
def write( str ) def write( str )
writing { writing {
do_write str write0 str
} }
end end
def writeline( str ) def writeline( str )
writing { writing {
do_write str + "\r\n" write0 str + "\r\n"
} }
end end
def write_message( src )
LOG "writing message from #{src.class}"
LOG_off()
len = using_each_crlf_line {
write_message_0 src
}
LOG_on()
LOG "wrote #{len} bytes"
len
end
def write_message_by_block( &block )
LOG 'writing message from block'
LOG_off()
len = using_each_crlf_line {
begin
block.call(WriteAdapter.new(self, :write_message_0))
rescue LocalJumpError
# allow `break' from writer block
end
}
LOG_on()
LOG "wrote #{len} bytes"
len
end
private private
def writing def writing
@writtensize = 0 @written_bytes = 0
@debug_output << '<- ' if @debug_output @debug_output << '<- ' if @debug_output
yield yield
@socket.flush @socket.flush
@debug_output << "\n" if @debug_output @debug_output << "\n" if @debug_output
@writtensize bytes = @written_bytes
@written_bytes = nil
bytes
end end
def do_write( str ) def write0( str )
@debug_output << str.dump if @debug_output @debug_output << str.dump if @debug_output
@writtensize += (n = @socket.write(str)) len = @socket.write(str)
n @written_bytes += len
len
end end
# #
# message write # Reads string from src calling :each, and write to @socket.
# Escapes '.' on the each line head.
# #
def write_message_0( src )
public prev = @written_bytes
def write_message( src )
D_off "writing text from #{src.class}"
wsize = using_each_crlf_line {
wpend_in src
}
D_on "wrote #{wsize} bytes text"
wsize
end
def through_message
D_off 'writing text from block'
wsize = using_each_crlf_line {
yield WriteAdapter.new(self, :wpend_in)
}
D_on "wrote #{wsize} bytes text"
wsize
end
private
def wpend_in( src )
line = nil
pre = @writtensize
each_crlf_line(src) do |line| each_crlf_line(src) do |line|
do_write '.' if line[0] == ?. if line[0] == ?.
do_write line then write0 '.' + line
else write0 line
end
end end
@written_bytes - prev
@writtensize - pre
end end
#
# setup @wbuf for each_crlf_line.
#
def using_each_crlf_line def using_each_crlf_line
writing { writing {
@wbuf = '' @wbuf = ''
yield yield
if not @wbuf.empty? # unterminated last line if not @wbuf.empty? # unterminated last line
if @wbuf[-1] == ?\r if @wbuf[-1] == ?\r
@wbuf.chop! @wbuf.chop!
end end
@wbuf.concat "\r\n" @wbuf.concat "\r\n"
do_write @wbuf write0 @wbuf
elsif @writtensize == 0 # empty src elsif @written_bytes == 0 # empty src
do_write "\r\n" write0 "\r\n"
end end
do_write ".\r\n" write0 ".\r\n"
@wbuf = nil @wbuf = nil
} }
end end
#
# extract a CR-LF-terminating-line from @wbuf and yield it.
#
def each_crlf_line( src ) def each_crlf_line( src )
str = m = beg = nil
adding(src) do adding(src) do
beg = 0 beg = 0
buf = @wbuf buf = @wbuf
while buf.index(/\n|\r\n|\r/, beg) while buf.index(/\n|\r\n|\r/, beg)
m = Regexp.last_match m = Regexp.last_match
if m.begin(0) == buf.size - 1 and buf[-1] == ?\r if (m.begin(0) == buf.length - 1) and buf[-1] == ?\r
# "...\r" : can follow "\n..." # "...\r" : can follow "\n..."
break break
end end
str = buf[ beg ... m.begin(0) ] str = buf[beg ... m.begin(0)]
str.concat "\r\n" str.concat "\r\n"
yield str yield str
beg = m.end(0) beg = m.end(0)
end end
@wbuf = buf[ beg ... buf.size ] @wbuf = buf[beg ... buf.length]
end end
end end
#
# Reads strings from SRC and add to @wbuf, then yield.
#
def adding( src ) def adding( src )
i = s = nil
case src case src
when String when String # for speeding up.
0.step(src.size - 1, 2048) do |i| 0.step(src.size - 1, 2048) do |i|
@wbuf << src[i,2048] @wbuf << src[i,2048]
yield yield
end end
when File when File # for speeding up.
while s = src.read(2048) while s = src.read(2048)
s[0,0] = @wbuf s[0,0] = @wbuf
@wbuf = s @wbuf = s
yield yield
end end
else else # generic reader
src.each do |s| src.each do |s|
@wbuf << s @wbuf << s
yield if @wbuf.size > 2048 yield if @wbuf.size > 2048
@ -375,18 +361,17 @@ module Net
private private
def D_off( msg ) def LOG_off
D msg @save_debug_out = @debug_output
@savedo, @debug_output = @debug_output, nil @debug_output = nil
end end
def D_on( msg ) def LOG_on
@debug_output = @savedo @debug_output = @save_debug_out
D msg
end end
def D( msg ) def LOG( msg )
@debug_output or return return unless @debug_output
@debug_output << msg @debug_output << msg
@debug_output << "\n" @debug_output << "\n"
end end
@ -394,11 +379,14 @@ module Net
end end
#
# The writer adapter class
#
class WriteAdapter class WriteAdapter
def initialize( sock, mid ) def initialize( sock, mid )
@socket = sock @socket = sock
@mid = mid @method_id = mid
end end
def inspect def inspect
@ -406,7 +394,7 @@ module Net
end end
def write( str ) def write( str )
@socket.__send__ @mid, str @socket.__send__(@method_id, str)
end end
alias print write alias print write
@ -427,6 +415,9 @@ module Net
end end
#
# The reader adapter class for internal use only.
#
class ReadAdapter class ReadAdapter
def initialize( block ) def initialize( block )
@ -443,6 +434,11 @@ module Net
private private
#
# This method is needed because @block must be called by yield,
# not Proc#call. You can see difference when using `break' in
# the block.
#
def call_block( str ) def call_block( str )
yield str yield str
end end

View file

@ -249,7 +249,28 @@ require 'digest/md5'
module Net module Net
class SMTP < Protocol module SMTPError
# This *class* is module for some reason.
# In ruby 1.9.x, this module becomes a class.
end
class SMTPAuthenticationError < ProtoAuthError
include SMTPError
end
class SMTPServerBusy < ProtoServerError
include SMTPError
end
class SMTPSyntaxError < ProtoSyntaxError
include SMTPError
end
class SMTPFatalError < ProtoFatalError
include SMTPError
end
class SMTPUnknownError < ProtoUnknownError
include SMTPError
end
class SMTP
Revision = %q$Revision$.split[1] Revision = %q$Revision$.split[1]
@ -259,21 +280,18 @@ module Net
def initialize( address, port = nil ) def initialize( address, port = nil )
@address = address @address = address
@port = port || SMTP.default_port @port = (port || SMTP.default_port)
@esmtp = true @esmtp = true
@command = nil
@socket = nil @socket = nil
@started = false @started = false
@open_timeout = 30 @open_timeout = 30
@read_timeout = 60 @read_timeout = 60
@error_occured = false
@debug_output = nil @debug_output = nil
end end
def inspect def inspect
"#<#{self.class} #{address}:#{@port} open=#{@started}>" "#<#{self.class} #{@address}:#{@port} started=#{@started}>"
end end
def esmtp? def esmtp?
@ -318,7 +336,6 @@ module Net
def start( helo = 'localhost.localdomain', def start( helo = 'localhost.localdomain',
user = nil, secret = nil, authtype = nil ) user = nil, secret = nil, authtype = nil )
raise IOError, 'SMTP session already started' if @started
if block_given? if block_given?
begin begin
do_start(helo, user, secret, authtype) do_start(helo, user, secret, authtype)
@ -332,98 +349,98 @@ module Net
end end
end end
def do_start( helo, 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, @socket = InternetMessageIO.open(@address, @port,
@open_timeout, @read_timeout, @open_timeout, @read_timeout,
@debug_output) @debug_output)
@command = SMTPCommand.new(@socket) check_response(critical { recv_response() })
begin begin
if @esmtp if @esmtp
@command.ehlo helo ehlo helodomain
else else
@command.helo helo helo helodomain
end end
rescue ProtocolError rescue ProtocolError
if @esmtp if @esmtp
@esmtp = false @esmtp = false
@command = SMTPCommand.new(@socket) @error_occured = false
retry retry
end end
raise raise
end end
authenticate user, secret, authtype if user
if user or secret
raise ArgumentError, 'both of account and password are required'\
unless user and secret
mid = 'auth_' + (authtype || 'cram_md5').to_s
raise ArgumentError, "wrong auth type #{authtype}"\
unless command().respond_to?(mid)
@command.__send__ mid, user, secret
end
end end
private :do_start private :do_start
def finish def finish
raise IOError, 'closing already closed SMTP session' unless @started raise IOError, 'closing already closed SMTP session' unless @started
@command.quit if @command quit if @socket and not @socket.closed? and not @error_occured
@command = nil
@socket.close if @socket and not @socket.closed? @socket.close if @socket and not @socket.closed?
@socket = nil @socket = nil
@error_occured = false
@started = false @started = false
end end
# #
# SMTP wrapper # message send
# #
def send_mail( mailsrc, from_addr, *to_addrs ) public
do_ready from_addr, to_addrs.flatten
command().write_mail mailsrc def send_message( msgstr, from_addr, *to_addrs )
send0(from_addr, to_addrs.flatten) {
@socket.write_message msgstr
}
end end
alias sendmail send_mail # backward compatibility alias send_mail send_message
alias sendmail send_message # obsolete
def ready( from_addr, *to_addrs, &block ) def open_message_stream( from_addr, *to_addrs, &block )
do_ready from_addr, to_addrs.flatten send0(from_addr, to_addrs.flatten) {
command().through_mail(&block) @socket.write_message_by_block(&block)
}
end end
alias ready open_message_stream # obsolete
private private
def do_ready( from_addr, to_addrs ) def send0( from_addr, to_addrs )
raise IOError, "closed session" unless @socket
raise ArgumentError, 'mail destination does not given' if to_addrs.empty? raise ArgumentError, 'mail destination does not given' if to_addrs.empty?
command().mailfrom from_addr
command().rcpt to_addrs 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 end
def command #
raise IOError, "closed session" unless @command # auth
@command #
private
def check_auth_args( user, secret, authtype )
raise ArgumentError, 'both of 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)
end end
end def authenticate( user, secret, authtype )
__send__("auth_#{authtype || 'cram_md5'}", user, secret)
SMTPSession = SMTP
class SMTPCommand
def initialize( sock )
@socket = sock
@in_critical_block = false
check_response(critical { recv_response() })
end
def inspect
"#<#{self.class} socket=#{@socket.inspect}>"
end
def helo( domain )
getok('HELO %s', domain)
end
def ehlo( domain )
getok('EHLO %s', domain)
end end
def auth_plain( user, secret ) def auth_plain( user, secret )
@ -434,9 +451,9 @@ module Net
def auth_login( user, secret ) def auth_login( 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
@ -445,20 +462,20 @@ module Net
# CRAM-MD5: [RFC2195] # CRAM-MD5: [RFC2195]
res = nil res = nil
critical { critical {
res = check_response(get_response('AUTH CRAM-MD5'), true) res = check_response(get_response('AUTH CRAM-MD5'), true)
challenge = res.split(/ /)[1].unpack('m')[0] challenge = res.split(/ /)[1].unpack('m')[0]
secret = Digest::MD5.digest(secret) if secret.size > 64 secret = Digest::MD5.digest(secret) if secret.size > 64
isecret = secret + "\0" * (64 - secret.size) isecret = secret + "\0" * (64 - secret.size)
osecret = isecret.dup osecret = isecret.dup
0.upto(63) do |i| 0.upto(63) do |i|
isecret[i] ^= 0x36 isecret[i] ^= 0x36
osecret[i] ^= 0x5c osecret[i] ^= 0x5c
end end
tmp = Digest::MD5.digest(isecret + challenge) tmp = Digest::MD5.digest(isecret + challenge)
tmp = Digest::MD5.hexdigest(osecret + tmp) tmp = Digest::MD5.hexdigest(osecret + tmp)
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 end
@ -467,45 +484,45 @@ module Net
# expects "str" may not become too long # expects "str" may not become too long
[str].pack('m').gsub(/\s+/, '') [str].pack('m').gsub(/\s+/, '')
end end
private :base64_encode
#
# SMTP command dispatcher
#
private
def helo( domain )
getok('HELO %s', domain)
end
def ehlo( domain )
getok('EHLO %s', domain)
end
def mailfrom( fromaddr ) def mailfrom( fromaddr )
getok('MAIL FROM:<%s>', fromaddr) getok('MAIL FROM:<%s>', fromaddr)
end end
def rcpt( toaddrs ) def rcptto( to )
toaddrs.each do |i| getok('RCPT TO:<%s>', to)
getok('RCPT TO:<%s>', i)
end
end
def write_mail( src )
res = critical {
check_response(get_response('DATA'), true)
@socket.write_message src
recv_response()
}
check_response(res)
end
def through_mail( &block )
res = critical {
check_response(get_response('DATA'), true)
@socket.through_message(&block)
recv_response()
}
check_response(res)
end end
def quit def quit
getok('QUIT') getok('QUIT')
end end
#
# row level library
#
private private
def getok( fmt, *args ) def getok( fmt, *args )
@socket.writeline sprintf(fmt, *args) res = critical {
check_response(critical { recv_response() }) @socket.writeline sprintf(fmt, *args)
recv_response()
}
return check_response(res)
end end
def get_response( fmt, *args ) def get_response( fmt, *args )
@ -524,29 +541,29 @@ module Net
end end
def check_response( res, allow_continue = false ) def check_response( res, allow_continue = false )
etype = case res[0] return res if /\A2/ === res
when ?2 then nil return res if allow_continue and /\A354/ === res
when ?3 then allow_continue ? nil : ProtoUnknownError err = case res
when ?4 then ProtoServerError when /\A4/ then SMTPServerBusy
when ?5 then when /\A50/ then SMTPSyntaxError
case res[1] when /\A55/ then SMTPFatalError
when ?0 then ProtoSyntaxError else SMTPUnknownError
when ?3 then ProtoAuthError end
when ?5 then ProtoFatalError raise err, res
end
end
raise etype, res if etype
res
end end
def critical def critical( &block )
return if @in_critical_block return '200 dummy reply code' if @error_occured
@in_critical_block = true begin
result = yield() return yield()
@in_critical_block = false rescue Exception
result @error_occured = true
raise
end
end end
end end # class SMTP
SMTPSession = SMTP
end # module Net end # module Net