From c20ecb1ba48fde8e81701f1365b738625c25582d Mon Sep 17 00:00:00 2001 From: aamine Date: Wed, 2 Jul 2003 02:34:39 +0000 Subject: * 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 --- lib/net/pop.rb | 128 ++++++++++++---------- lib/net/protocol.rb | 302 ++++++++++++++++++++++++++-------------------------- lib/net/smtp.rb | 255 +++++++++++++++++++++++--------------------- 3 files changed, 358 insertions(+), 327 deletions(-) (limited to 'lib') diff --git a/lib/net/pop.rb b/lib/net/pop.rb index 4fe33337cf..67542d794e 100644 --- a/lib/net/pop.rb +++ b/lib/net/pop.rb @@ -390,7 +390,7 @@ module Net class POPError < ProtocolError; end class POPAuthenticationError < ProtoAuthError; end - class POPBadResponse < StandardError; end + class POPBadResponse < POPError; end class POP3 < Protocol @@ -405,6 +405,7 @@ module Net 110 end + # obsolete def POP3.socket_type Net::InternetMessageIO end @@ -421,7 +422,7 @@ module Net account = nil, password = nil, isapop = false, &block ) start(address, port, account, password, isapop) {|pop| - pop.each_mail(&block) + pop.each_mail(&block) } end @@ -429,7 +430,7 @@ module Net account = nil, password = nil, isapop = false, &block ) start(address, port, account, password, isapop) {|pop| - pop.delete_all(&block) + pop.delete_all(&block) } end @@ -442,7 +443,7 @@ module Net def auth_only( account, password ) raise IOError, 'opening already opened POP session' if started? start(account, password) { - ; + ; } end @@ -481,7 +482,7 @@ module Net "#<#{self.class} #{@address}:#{@port} open=#{@started}>" end - def set_debug_output( arg ) # :nodoc: + def set_debug_output( arg ) @debug_output = arg end @@ -500,7 +501,7 @@ module Net @started end - alias active? started? # backward compatibility + alias active? started? # obsolete def start( account, password ) raise IOError, 'POP session already started' if @started @@ -578,7 +579,7 @@ module Net end @mails = command().list.map {|num, size| - POPMail.new(num, size, self, command()) + POPMail.new(num, size, self, command()) } @mails.dup end @@ -600,7 +601,7 @@ module Net command().rset mails().each do |m| m.instance_eval { - @deleted = false + @deleted = false } end end @@ -612,9 +613,9 @@ module Net end end - end + end # class POP3 - # aliases + # class aliases POP = POP3 POPSession = POP3 POP3Session = POP3 @@ -630,9 +631,9 @@ module Net class POPMail - def initialize( num, size, pop, cmd ) + def initialize( num, len, pop, cmd ) @number = num - @size = size + @length = len @pop = pop @command = cmd @deleted = false @@ -640,23 +641,37 @@ module Net end attr_reader :number - attr_reader :size + attr_reader :length + alias size length def inspect "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>" end 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 alias all pop # backward compatibility alias mail pop # backward compatibility + # `dest' argument is obsolete def top( lines, dest = '' ) - @command.top(@number, lines, dest) + @command.top(@number, lines) do |chunk| + dest << chunk + end + dest end + # `dest' argument is obsolete def header( dest = '' ) top(0, dest) end @@ -685,14 +700,14 @@ module Net @uid = uid end - end + end # class POPMail class POP3Command def initialize( sock ) @socket = sock - @in_critical_block = false + @error_occured = false res = check_response(critical { recv_response() }) @apop_stamp = res.slice(/<.+>/) end @@ -702,30 +717,32 @@ module Net end def auth( account, password ) - check_response_auth(critical { get_response('USER ' + account) }) - check_response_auth(critical { get_response('PASS ' + password) }) + check_response_auth(critical { + check_response_auth(get_response('USER ' + account)) + get_response('PASS ' + password) + }) end def apop( account, password ) raise POPAuthenticationError, 'not APOP server; cannot login' \ unless @apop_stamp check_response_auth(critical { - get_response('APOP %s %s', - account, - Digest::MD5.hexdigest(@apop_stamp + password)) + get_response('APOP %s %s', + account, + Digest::MD5.hexdigest(@apop_stamp + password)) }) end def list critical { - getok 'LIST' - list = [] - @socket.each_list_item do |line| - m = /\A(\d+)[ \t]+(\d+)/.match(line) or - raise POPBadResponse, "bad response: #{line}" - list.push [m[1].to_i, m[2].to_i] - end - list + getok 'LIST' + list = [] + @socket.each_list_item do |line| + m = /\A(\d+)[ \t]+(\d+)/.match(line) or + raise POPBadResponse, "bad response: #{line}" + list.push [m[1].to_i, m[2].to_i] + end + return list } end @@ -740,17 +757,17 @@ module Net check_response(critical { get_response 'RSET' }) end - def top( num, lines = 0, dest = '' ) + def top( num, lines = 0, &block ) critical { - getok('TOP %d %d', num, lines) - @socket.read_message_to(dest) + getok('TOP %d %d', num, lines) + @socket.each_message_chunk(&block) } end - def retr( num, dest = '' ) + def retr( num, &block ) critical { - getok('RETR %d', num) - @socket.read_message_to dest + getok('RETR %d', num) + @socket.each_message_chunk(&block) } end @@ -761,16 +778,16 @@ module Net def uidl( num = nil ) if num res = check_response(critical { get_response('UIDL %d', num) }) - res.split(/ /)[1] + return res.split(/ /)[1] else critical { - getok('UIDL') - table = {} - @socket.each_list_item do |line| - num, uid = line.split - table[num.to_i] = uid - end - table + getok('UIDL') + table = {} + @socket.each_list_item do |line| + num, uid = line.split + table[num.to_i] = uid + end + return table } end end @@ -781,13 +798,13 @@ module Net private - def getok( *reqs ) - @socket.writeline sprintf(*reqs) + def getok( fmt, *fargs ) + @socket.writeline sprintf(fmt, *fargs) check_response(recv_response()) end - def get_response( *reqs ) - @socket.writeline sprintf(*reqs) + def get_response( fmt, *fargs ) + @socket.writeline sprintf(fmt, *fargs) recv_response() end @@ -806,14 +823,15 @@ module Net end def critical - return if @in_critical_block - # Do not use ensure-block. - @in_critical_block = true - result = yield - @in_critical_block = false - result + return '+OK dummy ok response' if @error_occured + begin + return yield() + rescue Exception + @error_occured = true + raise + end end - end + end # class POP3Command end # module Net diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb index b3ac5e5093..d33e93b266 100644 --- a/lib/net/protocol.rb +++ b/lib/net/protocol.rb @@ -52,24 +52,25 @@ module Net alias open new 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 @port = port - @read_timeout = rtime - @debug_output = dout - - @socket = nil - @rbuf = nil - - connect otime - D 'opened' + @read_timeout = read_timeout + @debug_output = debug_output + @socket = nil + @rbuf = nil # read buffer + @wbuf = nil # write buffer + connect open_timeout + LOG 'opened' end attr_reader :address attr_reader :port def ip_address - @socket or return '' + return '' unless @socket @socket.addr[3] end @@ -77,10 +78,10 @@ module Net attr_reader :socket - def connect( otime ) - D "opening connection to #{@address}..." - timeout(otime) { - @socket = TCPsocket.new(@address, @port) + def connect( open_timeout ) + LOG "opening connection to #{@address}..." + timeout(open_timeout) { + @socket = TCPsocket.new(@address, @port) } @rbuf = '' end @@ -89,19 +90,19 @@ module Net def close if @socket @socket.close - D 'closed' + LOG 'closed' else - D 'close call for already closed socket' + LOG 'close call for already closed socket' end @socket = nil @rbuf = '' end - def reopen( otime = nil ) - D 'reopening...' + def reopen( open_timeout = nil ) + LOG 'reopening...' close - connect otime - D 'reopened' + connect open_timeout + LOG 'reopened' end def closed? @@ -109,7 +110,7 @@ module Net end def inspect - "#<#{self.class} #{closed? ? 'closed' : 'opened'}>" + "#<#{self.class} #{closed?() ? 'closed' : 'opened'}>" end ### @@ -118,74 +119,85 @@ module Net public - def read( len, dest = '', ignore = false ) - D_off "reading #{len} bytes..." - - rsize = 0 + def read( len, dest = '', ignore_eof = false ) + LOG "reading #{len} bytes..." + LOG_off() + read_bytes = 0 begin - while rsize + @rbuf.size < len - rsize += rbuf_moveto(dest, @rbuf.size) + while read_bytes + @rbuf.size < len + read_bytes += rbuf_moveto(dest, @rbuf.size) rbuf_fill end - rbuf_moveto dest, len - rsize + rbuf_moveto dest, len - read_bytes rescue EOFError - raise unless ignore + raise unless ignore_eof end - - D_on "read #{len} bytes" + LOG_on() + LOG "read #{read_bytes} bytes" dest end def read_all( dest = '' ) - D_off 'reading all...' - - rsize = 0 + LOG 'reading all...' + LOG_off() + read_bytes = 0 begin while true - rsize += rbuf_moveto(dest, @rbuf.size) + read_bytes += rbuf_moveto(dest, @rbuf.size) rbuf_fill end rescue EOFError ; end - - D_on "read #{rsize} bytes" + LOG_on() + LOG "read #{read_bytes} bytes" dest end - def readuntil( target, ignore = false ) + def readuntil( terminator, ignore_eof = false ) dest = '' begin - until idx = @rbuf.index(target) + until idx = @rbuf.index(terminator) rbuf_fill end - rbuf_moveto dest, idx + target.size + rbuf_moveto dest, idx + terminator.size rescue EOFError - raise unless ignore + raise unless ignore_eof rbuf_moveto dest, @rbuf.size end dest end def readline - ret = readuntil("\n") - ret.chop! - ret + readuntil("\n").chop end - private + 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 - BLOCK_SIZE = 1024 + private def rbuf_fill - until IO.select [@socket], nil, nil, @read_timeout - on_read_timeout + until IO.select([@socket], nil, nil, @read_timeout) + raise TimeoutError, "socket read timeout (#{@read_timeout} sec)" end - @rbuf << @socket.sysread(BLOCK_SIZE) - end - - def on_read_timeout - raise TimeoutError, "socket read timeout (#{@read_timeout} sec)" + @rbuf << @socket.sysread(1024) end def rbuf_moveto( dest, len ) @@ -194,173 +206,147 @@ module Net len 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 ### - # - # basic write - # - public def write( str ) writing { - do_write str + write0 str } end def writeline( str ) writing { - do_write str + "\r\n" + write0 str + "\r\n" } 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 def writing - @writtensize = 0 + @written_bytes = 0 @debug_output << '<- ' if @debug_output yield @socket.flush @debug_output << "\n" if @debug_output - @writtensize + bytes = @written_bytes + @written_bytes = nil + bytes end - def do_write( str ) + def write0( str ) @debug_output << str.dump if @debug_output - @writtensize += (n = @socket.write(str)) - n + len = @socket.write(str) + @written_bytes += len + len end # - # message write + # Reads string from src calling :each, and write to @socket. + # Escapes '.' on the each line head. # - - public - - 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 + def write_message_0( src ) + prev = @written_bytes each_crlf_line(src) do |line| - do_write '.' if line[0] == ?. - do_write line + if line[0] == ?. + then write0 '.' + line + else write0 line + end end - - @writtensize - pre + @written_bytes - prev end + # + # setup @wbuf for each_crlf_line. + # def using_each_crlf_line writing { @wbuf = '' - yield - if not @wbuf.empty? # unterminated last line if @wbuf[-1] == ?\r @wbuf.chop! end @wbuf.concat "\r\n" - do_write @wbuf - elsif @writtensize == 0 # empty src - do_write "\r\n" + write0 @wbuf + elsif @written_bytes == 0 # empty src + write0 "\r\n" end - do_write ".\r\n" - + write0 ".\r\n" @wbuf = nil } end + # + # extract a CR-LF-terminating-line from @wbuf and yield it. + # def each_crlf_line( src ) - str = m = beg = nil - adding(src) do beg = 0 buf = @wbuf while buf.index(/\n|\r\n|\r/, beg) 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..." break end - str = buf[ beg ... m.begin(0) ] + str = buf[beg ... m.begin(0)] str.concat "\r\n" yield str beg = m.end(0) end - @wbuf = buf[ beg ... buf.size ] + @wbuf = buf[beg ... buf.length] end end + # + # Reads strings from SRC and add to @wbuf, then yield. + # def adding( src ) - i = s = nil - case src - when String + when String # for speeding up. 0.step(src.size - 1, 2048) do |i| @wbuf << src[i,2048] yield end - when File + when File # for speeding up. while s = src.read(2048) s[0,0] = @wbuf @wbuf = s yield end - else + else # generic reader src.each do |s| @wbuf << s yield if @wbuf.size > 2048 @@ -375,18 +361,17 @@ module Net private - def D_off( msg ) - D msg - @savedo, @debug_output = @debug_output, nil + def LOG_off + @save_debug_out = @debug_output + @debug_output = nil end - def D_on( msg ) - @debug_output = @savedo - D msg + def LOG_on + @debug_output = @save_debug_out end - def D( msg ) - @debug_output or return + def LOG( msg ) + return unless @debug_output @debug_output << msg @debug_output << "\n" end @@ -394,11 +379,14 @@ module Net end + # + # The writer adapter class + # class WriteAdapter def initialize( sock, mid ) @socket = sock - @mid = mid + @method_id = mid end def inspect @@ -406,7 +394,7 @@ module Net end def write( str ) - @socket.__send__ @mid, str + @socket.__send__(@method_id, str) end alias print write @@ -427,6 +415,9 @@ module Net end + # + # The reader adapter class for internal use only. + # class ReadAdapter def initialize( block ) @@ -443,6 +434,11 @@ module Net 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 ) yield str end diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb index d944f4710e..6329d0731d 100644 --- a/lib/net/smtp.rb +++ b/lib/net/smtp.rb @@ -249,7 +249,28 @@ require 'digest/md5' 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] @@ -259,21 +280,18 @@ module Net def initialize( address, port = nil ) @address = address - @port = port || SMTP.default_port - + @port = (port || SMTP.default_port) @esmtp = true - - @command = nil @socket = nil @started = false @open_timeout = 30 @read_timeout = 60 - + @error_occured = false @debug_output = nil end def inspect - "#<#{self.class} #{address}:#{@port} open=#{@started}>" + "#<#{self.class} #{@address}:#{@port} started=#{@started}>" end def esmtp? @@ -318,7 +336,6 @@ module Net def start( helo = 'localhost.localdomain', user = nil, secret = nil, authtype = nil ) - raise IOError, 'SMTP session already started' if @started if block_given? begin do_start(helo, user, secret, authtype) @@ -332,98 +349,98 @@ module Net 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, @open_timeout, @read_timeout, @debug_output) - @command = SMTPCommand.new(@socket) + check_response(critical { recv_response() }) begin if @esmtp - @command.ehlo helo + ehlo helodomain else - @command.helo helo + helo helodomain end rescue ProtocolError if @esmtp @esmtp = false - @command = SMTPCommand.new(@socket) + @error_occured = false retry end raise end - - 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 + authenticate user, secret, authtype if user end private :do_start def finish raise IOError, 'closing already closed SMTP session' unless @started - @command.quit if @command - @command = nil + quit if @socket and not @socket.closed? and not @error_occured @socket.close if @socket and not @socket.closed? @socket = nil + @error_occured = false @started = false end # - # SMTP wrapper + # message send # - def send_mail( mailsrc, from_addr, *to_addrs ) - do_ready from_addr, to_addrs.flatten - command().write_mail mailsrc + public + + def send_message( msgstr, from_addr, *to_addrs ) + send0(from_addr, to_addrs.flatten) { + @socket.write_message msgstr + } end - alias sendmail send_mail # backward compatibility + alias send_mail send_message + alias sendmail send_message # obsolete - def ready( from_addr, *to_addrs, &block ) - do_ready from_addr, to_addrs.flatten - command().through_mail(&block) + def open_message_stream( from_addr, *to_addrs, &block ) + send0(from_addr, to_addrs.flatten) { + @socket.write_message_by_block(&block) + } end + alias ready open_message_stream # obsolete + 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? - command().mailfrom from_addr - command().rcpt to_addrs - end - def command - raise IOError, "closed session" unless @command - @command + 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 - - SMTPSession = SMTP - - - class SMTPCommand + # + # auth + # - def initialize( sock ) - @socket = sock - @in_critical_block = false - check_response(critical { recv_response() }) - end + private - def inspect - "#<#{self.class} socket=#{@socket.inspect}>" + 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 - def helo( domain ) - getok('HELO %s', domain) - end - - def ehlo( domain ) - getok('EHLO %s', domain) + def authenticate( user, secret, authtype ) + __send__("auth_#{authtype || 'cram_md5'}", user, secret) end def auth_plain( user, secret ) @@ -434,9 +451,9 @@ module Net def auth_login( user, secret ) res = critical { - check_response(get_response('AUTH LOGIN'), true) - check_response(get_response(base64_encode(user)), true) - get_response(base64_encode(secret)) + 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 end @@ -445,20 +462,20 @@ module Net # 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| - isecret[i] ^= 0x36 - osecret[i] ^= 0x5c - end - tmp = Digest::MD5.digest(isecret + challenge) - tmp = Digest::MD5.hexdigest(osecret + tmp) - - res = get_response(base64_encode(user + ' ' + tmp)) + 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| + isecret[i] ^= 0x36 + osecret[i] ^= 0x5c + 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 end @@ -467,45 +484,45 @@ module Net # expects "str" may not become too long [str].pack('m').gsub(/\s+/, '') end - private :base64_encode - def mailfrom( fromaddr ) - getok('MAIL FROM:<%s>', fromaddr) + # + # SMTP command dispatcher + # + + private + + def helo( domain ) + getok('HELO %s', domain) end - def rcpt( toaddrs ) - toaddrs.each do |i| - getok('RCPT TO:<%s>', i) - end + def ehlo( domain ) + getok('EHLO %s', domain) end - def write_mail( src ) - res = critical { - check_response(get_response('DATA'), true) - @socket.write_message src - recv_response() - } - check_response(res) + def mailfrom( fromaddr ) + getok('MAIL FROM:<%s>', fromaddr) end - def through_mail( &block ) - res = critical { - check_response(get_response('DATA'), true) - @socket.through_message(&block) - recv_response() - } - check_response(res) + def rcptto( to ) + getok('RCPT TO:<%s>', to) end def quit getok('QUIT') end + # + # row level library + # + private def getok( fmt, *args ) - @socket.writeline sprintf(fmt, *args) - check_response(critical { recv_response() }) + res = critical { + @socket.writeline sprintf(fmt, *args) + recv_response() + } + return check_response(res) end def get_response( fmt, *args ) @@ -524,29 +541,29 @@ module Net end def check_response( res, allow_continue = false ) - etype = case res[0] - when ?2 then nil - when ?3 then allow_continue ? nil : ProtoUnknownError - when ?4 then ProtoServerError - when ?5 then - case res[1] - when ?0 then ProtoSyntaxError - when ?3 then ProtoAuthError - when ?5 then ProtoFatalError - end - end - raise etype, res if etype - res + return res if /\A2/ === res + return res if allow_continue and /\A354/ === res + err = case res + when /\A4/ then SMTPServerBusy + when /\A50/ then SMTPSyntaxError + when /\A55/ then SMTPFatalError + else SMTPUnknownError + end + raise err, res + end + + def critical( &block ) + return '200 dummy reply code' if @error_occured + begin + return yield() + rescue Exception + @error_occured = true + raise + end end - def critical - return if @in_critical_block - @in_critical_block = true - result = yield() - @in_critical_block = false - result - end + end # class SMTP - end + SMTPSession = SMTP end # module Net -- cgit v1.2.3