From e3056c880364f5cb79eb9280b4d34b34c3eb4f93 Mon Sep 17 00:00:00 2001 From: aamine Date: Fri, 2 May 2003 14:35:01 +0000 Subject: * lib/net/protocol.rb: remove Protocol class. * lib/net/smtp.rb (SMTP): ditto. * lib/net/pop.rb (POP3): ditto. * lib/net/http.rb (HTTP): ditto. * lib/net/protocol.rb: remove Command class. * lib/net/smtp.rb (SMTPCommand): ditto. * lib/net/pop.rb (POP3Command): ditto. * lib/net/pop.rb: remove APOPCommand class. * lib/net/protocol.rb: remove Code class and its all subclasses. * lib/net/protocol.rb: remove Response class and its all subclasses. * lib/net/pop.rb (POPMail): new method unique_id (alias uidl). git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@3747 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/net/http.rb | 158 +++++++++++++---- lib/net/pop.rb | 497 +++++++++++++++++++++++++++++++++++----------------- lib/net/protocol.rb | 343 +----------------------------------- lib/net/smtp.rb | 266 ++++++++++++++++++---------- 4 files changed, 626 insertions(+), 638 deletions(-) (limited to 'lib') diff --git a/lib/net/http.rb b/lib/net/http.rb index 7d425b1870..a88e69c0bb 100644 --- a/lib/net/http.rb +++ b/lib/net/http.rb @@ -562,10 +562,11 @@ module Net class HTTPHeaderSyntaxError < StandardError; end - class HTTP < Protocol + class HTTP - HTTPVersion = '1.1' + Revision = %q$Revision$.split[1] + HTTPVersion = '1.1' # # for backward compatibility @@ -591,7 +592,6 @@ module Net end private_class_method :setimplversion - # # short cut methods # @@ -644,13 +644,17 @@ module Net end private_class_method :get_by_uri - # - # connection + # HTTP session management # - protocol_param :default_port, '80' - protocol_param :socket_type, '::Net::InternetMessageIO' + def HTTP.default_port + 80 + end + + def HTTP.socket_type + InternetMessageIO + end class << HTTP def start( address, port = nil, p_addr = nil, p_port = nil, p_user = nil, p_pass = nil, &block ) @@ -666,25 +670,94 @@ module Net end end - def initialize( addr, port = nil ) - super + def initialize( address, port = nil ) + @address = address + @port = port || HTTP.default_port + @curr_http_version = HTTPVersion @seems_1_0_server = false @close_on_empty_response = false + @socket = nil + @started = false + + @open_timeout = 30 + @read_timeout = 60 + + @debug_output = nil + end + + def inspect + "#<#{self.class} #{@address}:#{@port} open=#{active?}>" + end + + def set_debug_output( arg ) # :nodoc: + @debug_output = arg end + attr_reader :address + attr_reader :port + + attr_accessor :open_timeout + + attr_reader :read_timeout + + def read_timeout=( sec ) + @socket.read_timeout = sec if @socket + @read_timeout = sec + end + + def started? + @started + end + + alias active? started? + attr_accessor :close_on_empty_response - private + def start + raise IOError, 'HTTP session already opened' if @started + if block_given? + begin + do_start + return yield(self) + ensure + finish + end + end + do_start + self + end def do_start - conn_socket + @socket = self.class.socket_type.open(conn_address(), conn_port(), + @open_timeout, @read_timeout, + @debug_output) + on_connect + @started = true end + private :do_start - def do_finish - disconn_socket + def conn_address + address() end + private :conn_address + def conn_port + port() + end + private :conn_port + + def on_connect + end + private :on_connect + + def finish + raise IOError, 'closing already closed HTTP session' unless @started + @socket.close if @socket and not @socket.closed? + @socket = nil + @started = false + nil + end # # proxy @@ -784,9 +857,8 @@ module Net end end - # - # http operations + # HTTP operations # public @@ -888,7 +960,8 @@ module Net def begin_transport( req ) if @socket.closed? - reconn_socket + @socket.reopen @open_timeout + on_connect end if @seems_1_0_server req['connection'] = 'close' @@ -930,7 +1003,6 @@ module Net false end - # # utils # @@ -954,7 +1026,7 @@ module Net ### - ### header + ### Header ### module HTTPHeader @@ -1012,6 +1084,7 @@ module Net def canonical( k ) k.split(/-/).map {|i| i.capitalize }.join('-') end + private :canonical def range s = @header['range'] or return nil @@ -1101,7 +1174,7 @@ module Net ### - ### request + ### Request ### class HTTPGenericRequest @@ -1188,14 +1261,12 @@ module Net class HTTPRequest < HTTPGenericRequest - def initialize( path, initheader = nil ) super self.class::METHOD, self.class::REQUEST_HAS_BODY, self.class::RESPONSE_HAS_BODY, path, initheader end - end @@ -1228,11 +1299,33 @@ module Net end - ### - ### response + ### Response ### + module HTTPExceptions + def initialize( msg, res ) + super msg + @response = res + end + attr_reader :response + alias data response + end + class HTTPError < ProtocolError + include HTTPExceptions + end + class HTTPRetriableError < ProtoRetriableError + include HTTPExceptions + end + # We cannot use the name "HTTPServerError", it is the name of the response. + class HTTPServerException < ProtoServerError + include HTTPExceptions + end + class HTTPFatalError < ProtoFatalError + include HTTPExceptions + end + + class HTTPResponse # predefine HTTPResponse class to allow inheritance @@ -1245,30 +1338,29 @@ module Net end end - class HTTPUnknownResponse < HTTPResponse HAS_BODY = true - EXCEPTION_TYPE = ProtocolError + EXCEPTION_TYPE = HTTPError end class HTTPInformation < HTTPResponse # 1xx HAS_BODY = false - EXCEPTION_TYPE = ProtocolError + EXCEPTION_TYPE = HTTPError end class HTTPSuccess < HTTPResponse # 2xx HAS_BODY = true - EXCEPTION_TYPE = ProtocolError + EXCEPTION_TYPE = HTTPError end class HTTPRedirection < HTTPResponse # 3xx HAS_BODY = true - EXCEPTION_TYPE = ProtoRetriableError + EXCEPTION_TYPE = HTTPRetriableError end class HTTPClientError < HTTPResponse # 4xx HAS_BODY = true - EXCEPTION_TYPE = ProtoServerError # for backward compatibility + EXCEPTION_TYPE = HTTPServerException # for backward compatibility end class HTTPServerError < HTTPResponse # 5xx HAS_BODY = true - EXCEPTION_TYPE = ProtoFatalError # for backward compatibility + EXCEPTION_TYPE = HTTPFatalError # for backward compatibility end class HTTPContinue < HTTPInformation # 100 @@ -1649,12 +1741,6 @@ module Net # for backward compatibility - module NetPrivate - HTTPResponse = ::Net::HTTPResponse - HTTPGenericRequest = ::Net::HTTPGenericRequest - HTTPRequest = ::Net::HTTPRequest - HTTPHeader = ::Net::HTTPHeader - end HTTPInformationCode = HTTPInformation HTTPSuccessCode = HTTPSuccess HTTPRedirectionCode = HTTPRedirection diff --git a/lib/net/pop.rb b/lib/net/pop.rb index 38fb7b6f76..9259750bb0 100644 --- a/lib/net/pop.rb +++ b/lib/net/pop.rb @@ -54,6 +54,7 @@ Replace 'pop3.server.address' your POP3 server address. (3) close POP session by calling POP3#finish or use block form #start. This example is using block form #start to close the session. + === Enshort Code The example above is very verbose. You can enshort code by using @@ -132,26 +133,48 @@ You can use utility method, Net::POP3.APOP(). Example: require 'net/pop' - # use APOP authentication if $isapop == true + # Use APOP authentication if $isapop == true pop = Net::POP3.APOP($isapop).new('apop.server.address', 110) pop.start(YourAccount', 'YourPassword') {|pop| # Rest code is same. } +=== Fetch Only Selected Mail Using POP UIDL Function + +If your POP server provides UIDL function, +you can pop only selected mails from POP server. +e.g. + + def need_pop?( id ) + # determine if we need pop this mail... + end + + Net::POP3.start('pop.server', 110, + 'Your account', 'Your password') {|pop| + pop.mails.select {|m| need_pop?(m.unique_id) }.each do |m| + do_something(m.pop) + end + } + +POPMail#unique_id method returns the unique-id of the message (String). +Normally unique-id is a hash of the message. + -== Net::POP3 class +== class Net::POP3 === Class Methods -: new( address, port = 110, apop = false ) +: new( address, port = 110, isapop = false ) creates a new Net::POP3 object. - This method does not open TCP connection yet. + This method does NOT open TCP connection yet. -: start( address, port = 110, account, password ) -: start( address, port = 110, account, password ) {|pop| .... } - equals to Net::POP3.new( address, port ).start( account, password ) +: start( address, port = 110, account, password, isapop = false ) +: start( address, port = 110, account, password, isapop = false ) {|pop| .... } + equals to Net::POP3.new(address, port, isapop).start(account, password). + This method raises POPAuthenticationError if authentication is failed. - Net::POP3.start( addr, port, account, password ) {|pop| + # Typical usage + Net::POP3.start(addr, port, account, password) {|pop| pop.each_mail do |m| file.write m.pop m.delete @@ -163,17 +186,17 @@ You can use utility method, Net::POP3.APOP(). Example: returns Net::POP3 class object if false. Use this method like: - # example 1 + # Example 1 pop = Net::POP3::APOP($isapop).new( addr, port ) - # example 2 + # Example 2 Net::POP3::APOP($isapop).start( addr, port ) {|pop| .... } -: foreach( address, port = 110, account, password ) {|mail| .... } +: foreach( address, port = 110, account, password, isapop = false ) {|mail| .... } starts POP3 protocol and iterates for each POPMail object. - This method equals to + This method equals to: Net::POP3.start( address, port, account, password ) {|pop| pop.each_mail do |m| @@ -181,29 +204,33 @@ You can use utility method, Net::POP3.APOP(). Example: end } - # example + This method raises POPAuthenticationError if authentication is failed. + + # Typical usage Net::POP3.foreach( 'your.pop.server', 110, 'YourAccount', 'YourPassword' ) do |m| file.write m.pop m.delete if $DELETE end -: delete_all( address, port = 110, account, password ) -: delete_all( address, port = 110, account, password ) {|mail| .... } +: delete_all( address, port = 110, account, password, isapop = false ) +: delete_all( address, port = 110, account, password, isapop = false ) {|mail| .... } starts POP3 session and delete all mails. If block is given, iterates for each POPMail object before delete. + This method raises POPAuthenticationError if authentication is failed. - # example + # Example Net::POP3.delete_all( addr, nil, 'YourAccount', 'YourPassword' ) do |m| m.pop file end -: auth_only( address, port = 110, account, password ) +: auth_only( address, port = 110, account, password, isapop = false ) (just for POP-before-SMTP) opens POP3 session and does autholize and quit. This method must not be called while POP3 session is opened. + This method raises POPAuthenticationError if authentication is failed. - # example + # Example Net::POP3.auth_only( 'your.pop3.server', nil, # using default (110) 'YourAccount', @@ -218,7 +245,9 @@ You can use utility method, Net::POP3.APOP(). Example: When called with block, gives a POP3 object to block and closes the session after block call finish. -: active? + This method raises POPAuthenticationError if authentication is failed. + +: started? true if POP3 session is started. : address @@ -245,44 +274,62 @@ You can use utility method, Net::POP3.APOP(). Example: : mails an array of Net::POPMail objects. - This array is renewed when session started. + This array is renewed when session restarts. + + This method raises POPError if any problem happend. : each_mail {|popmail| .... } : each {|popmail| .... } is equals to "pop3.mails.each" + This method raises POPError if any problem happend. + : delete_all : delete_all {|popmail| .... } deletes all mails on server. If called with block, gives mails to the block before deleting. - # example + # Example n = 1 pop.delete_all do |m| File.open("inbox/#{n}") {|f| f.write m.pop } n += 1 end + This method raises POPError if any problem happend. + : auth_only( account, password ) (just for POP-before-SMTP) - opens POP3 session and does autholize and quit. - This method must not be called while POP3 session is opened. - # example - pop = Net::POP3.new( 'your.pop3.server' ) - pop.auth_only 'YourAccount', 'YourPassword' + + opens POP3 session, does authorization, then quit. + You must not call this method after POP3 session is opened. + + This method raises POPAuthenticationError if authentication is failed. + + # Typical usage + pop = Net::POP3.new('your.pop3.server') + pop.auth_only('Your account', 'Your password') + Net::SMTP.start(....) {|smtp| + .... + } : reset reset the session. All "deleted mark" are removed. -== Net::APOP + This method raises POPError if any problem happend. + + +== class Net::APOP This class defines no new methods. Only difference from POP3 is using APOP authentification. === Super Class + Net::POP3 -== Net::POPMail + +== class Net::POPMail A class of mail which exists on POP server. @@ -291,7 +338,9 @@ A class of mail which exists on POP server. : pop( dest = '' ) This method fetches a mail and write to 'dest' using '<<' method. - # example + This method raises POPError if any problem happend. + + # Typical usage allmails = nil POP3.start( 'your.pop3.server', 110, 'YourAccount, 'YourPassword' ) {|pop| @@ -301,7 +350,9 @@ A class of mail which exists on POP server. : pop {|str| .... } gives the block part strings of a mail. - # example + This method raises POPError if any problem happend. + + # Typical usage POP3.start( 'localhost', 110 ) {|pop3| pop3.each_mail do |m| m.pop do |str| @@ -311,20 +362,32 @@ A class of mail which exists on POP server. } : header - This method fetches only mail header. + fetches only mail header. + + This method raises POPError if any problem happend. : top( lines ) - This method fetches mail header and LINES lines of body. + fetches mail header and LINES lines of body. + + This method raises POPError if any problem happend. : delete deletes mail on server. + This method raises POPError if any problem happend. + : size mail size (bytes) : deleted? true if mail was deleted +: unique_id + returns an unique-id of the message. + Normally unique-id is a hash of the message. + + This method raises POPError if any problem happend. + =end require 'net/protocol' @@ -333,38 +396,55 @@ require 'digest/md5' module Net - class BadResponseError < StandardError; end + class POPError < ProtocolError; end + class POPAuthenticationError < ProtoAuthError; end + class POPBadResponse < StandardError; end + + + class POP3 + + Revision = %q$Revision$.split[1] + # + # Class Parameters + # - class POP3 < Protocol + def POP3.default_port + 110 + end - protocol_param :default_port, '110' - protocol_param :command_type, '::Net::POP3Command' - protocol_param :apop_command_type, '::Net::APOPCommand' - protocol_param :mail_type, '::Net::POPMail' - protocol_param :socket_type, '::Net::InternetMessageIO' + def POP3.socket_type + Net::InternetMessageIO + end + + # + # Utilities + # def POP3.APOP( isapop ) isapop ? APOP : POP3 end def POP3.foreach( address, port = nil, - account = nil, password = nil, &block ) - start(address, port, account, password) {|pop| + account = nil, password = nil, + isapop = false, &block ) + start(address, port, account, password, isapop) {|pop| pop.each_mail(&block) } end def POP3.delete_all( address, port = nil, - account = nil, password = nil, &block ) - start(address, port, account, password) {|pop| + account = nil, password = nil, + isapop = false, &block ) + start(address, port, account, password, isapop) {|pop| pop.delete_all(&block) } end def POP3.auth_only( address, port = nil, - account = nil, password = nil ) - new(address, port).auth_only account, password + account = nil, password = nil, + isapop = false ) + new(address, port, isapop).auth_only account, password end def auth_only( account, password ) @@ -375,40 +455,116 @@ module Net end # - # connection + # Session management # - def initialize( addr, port = nil, apop = false ) - super addr, port + def POP3.start( address, port = nil, + account = nil, password = nil, + isapop = false, &block ) + new(address, port, isapop).start(account, password, &block) + end + + def initialize( addr, port = nil, isapop = false ) + @address = addr + @port = port || self.class.default_port + @apop = isapop + + @command = nil + @socket = nil + @started = false + @open_timeout = 30 + @read_timeout = 60 + @debug_output = nil + @mails = nil - @apop = false + @nmails = nil + @bytes = nil end - private + def apop? + @apop + end + + def inspect + "#<#{self.class} #{@address}:#{@port} open=#{@started}>" + end + + def set_debug_output( arg ) # :nodoc: + @debug_output = arg + end + + attr_reader :address + attr_reader :port + + attr_accessor :open_timeout + attr_reader :read_timeout + + def read_timeout=( sec ) + @command.socket.read_timeout = sec if @command + @read_timeout = sec + end + + def started? + @started + end + + alias active? started? # backward compatibility + + def start( account, password ) + raise IOError, 'already closed POP session' if @started + + if block_given? + begin + do_start account, password + return yield(self) + ensure + finish unless @started + end + else + do_start acount, password + return self + end + end def do_start( account, password ) - conn_socket - conn_command - @command.auth account, password + @socket = self.class.socket_type.open(@address, @port, + @open_timeout, @read_timeout, @debug_output) + on_connect + @command = POP3Command.new(@socket) + if apop? + @command.apop account, password + else + @command.auth account, password + end + @started = true end + private :do_start - def conn_command - @command = (@apop ? self.class.apop_command_type : - self.class.command_type ).new(socket()) + def on_connect end + private :on_connect - def do_finish + def finish + raise IOError, 'already closed POP session' unless @started @mails = nil - disconn_command - disconn_socket + @command.quit if @command + @command = nil + @socket.close if @socket and not @socket.closed? + @socket = nil + @started = false end + def command + raise IOError, 'POP session not opened yet' \ + if not @socket or @socket.closed? + @command + end + private :command + # - # POP operations + # POP protocol wrapper # - public - def mail_size return @nmails if @nmails @nmails, @bytes = command().stat @@ -422,21 +578,17 @@ module Net end def mails - return @mails if @mails + return @mails.dup if @mails if mail_size() == 0 # some popd raises error for LIST on the empty mailbox. @mails = [] - return @mails + return [] end - mails = [] - mailclass = self.class.mail_type - command().list.each_with_index do |size,idx| - mails.push mailclass.new(idx, size, command()) if size - end - @mails = mails.freeze - - @mails + @mails = command().list.map {|num, size| + POPMail.new(num, size, self, command()) + } + @mails.dup end def each_mail( &block ) @@ -461,25 +613,23 @@ module Net end end - def command - io_check - super - end - - def io_check - raise IOError, 'POP session is not opened yet'\ - if not socket() or socket().closed? + # internal use only (called from POPMail#uidl). + def set_all_uids + command().uidl.each do |num, uid| + @mails.find {|m| m.number == num }.uid = uid + end end end + # aliases POP = POP3 POPSession = POP3 POP3Session = POP3 class APOP < POP3 - def APOP.command_type - APOPCommand + def apop? + true end end @@ -488,171 +638,188 @@ module Net class POPMail - def initialize( n, s, cmd ) - @num = n - @size = s + def initialize( num, size, pop, cmd ) + @number = num + @size = size + @pop = pop @command = cmd - @deleted = false + @uid = nil end + attr_reader :number attr_reader :size def inspect - "#<#{self.class} #{@num}#{@deleted ? ' deleted' : ''}>" + "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>" end def pop( dest = '', &block ) - dest = ReadAdapter.new(block) if block - @command.retr @num, dest + @command.retr(@number, (block ? ReadAdapter.new(block) : dest)) end - alias all pop - alias mail pop + alias all pop # backward compatibility + alias mail pop # backward compatibility def top( lines, dest = '' ) - @command.top @num, lines, dest + @command.top(@number, lines, dest) end def header( dest = '' ) - top 0, dest + top(0, dest) end def delete - @command.dele @num + @command.dele @number @deleted = true end - alias delete! delete + alias delete! delete # backward compatibility def deleted? @deleted end - def uidl - @command.uidl @num + def unique_id + return @uid if @uid + @pop.set_all_uids + @uid + end + + alias uidl unique_id + + # internal use only (used from POP3#set_all_uids). + def uid=( uid ) + @uid = uid end end - class POP3Command < Command + class POP3Command def initialize( sock ) - super - atomic { - check_reply SuccessCode - } + @socket = sock + @in_critical_block = false + res = check_response(critical { recv_response() }) + @apop_stamp = res.slice(/<.+>/) end - def auth( account, pass ) - atomic { - @socket.writeline 'USER ' + account - check_reply_auth + def inspect + "#<#{self.class} socket=#{@socket}>" + end - @socket.writeline 'PASS ' + pass - check_reply_auth - } + def auth( account, password ) + check_response_auth(critical { get_response('USER ' + account) }) + check_response_auth(critical { get_response('PASS ' + password) }) + end + + def apop( account, password ) + raise POPAuthenticationError.new('not APOP server; cannot login', nil)\ + unless @apop_stamp + check_response_auth(critical { + get_reply('APOP %s %s', + account, + Digest::MD5.hexdigest(@apop_stamp + password)) + }) end def list - atomic { + critical { getok 'LIST' list = [] @socket.each_list_item do |line| m = /\A(\d+)[ \t]+(\d+)/.match(line) or - raise BadResponse, "bad response: #{line}" - list[m[1].to_i] = m[2].to_i + raise POPBadResponse, "bad response: #{line}" + list.push [m[1].to_i, m[2].to_i] end - return list + list } end def stat - atomic { - @socket.writeline 'STAT' - line = @socket.readline - m = /\A\+OK (\d+)[ \t]+(\d+)/.match(line) or - raise BadResponseError, "illegal response: #{line}" - return [m[1].to_i, m[2].to_i] - } + res = check_response(critical { get_response('STAT') }) + m = /\A\+OK\s+(\d+)\s+(\d+)/.match(res) or + raise POPBadResponse, "wrong response format: #{res}" + [m[1].to_i, m[2].to_i] end def rset - atomic { - getok 'RSET' - } + check_reply(critical { get_response 'RSET' }) end - def top( num, lines = 0, dest = '' ) - atomic { - getok sprintf('TOP %d %d', num, lines) - @socket.read_message_to dest + critical { + getok('TOP %d %d', num, lines) + @socket.read_message_to(dest) } end def retr( num, dest = '' ) - atomic { - getok sprintf('RETR %d', num) + critical { + getok('RETR %d', num) @socket.read_message_to dest } end def dele( num ) - atomic { - getok sprintf('DELE %d', num) - } + check_response(critical { get_response('DELE %d', num) }) end - def uidl( num ) - atomic { - getok(sprintf('UIDL %d', num)).message.split(/ /)[1] - } + def uidl( num = nil ) + if num + res = check_response(critical { get_response('UIDL %d', num) }) + 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 + } + end end def quit - atomic { - getok 'QUIT' - } + check_response(critical { get_response('QUIT') }) end private - def check_reply_auth - begin - return check_reply(SuccessCode) - rescue ProtocolError => err - raise ProtoAuthError.new('Fail to POP authentication', err.response) - end + def getok( *reqs ) + @socket.writeline sprintf(*reqs) + check_response(recv_response()) end - def get_reply - str = @socket.readline - if /\A\+/ === str - Response.new(SuccessCode, str[0,3], str[3, str.size - 3].strip) - else - Response.new(ErrorCode, str[0,4], str[4, str.size - 4].strip) - end + def get_response( *reqs ) + @socket.writeline sprintf(*reqs) + recv_response() end - end - + def recv_response + @socket.readline + end - class APOPCommand < POP3Command + def check_response( res ) + raise POPError, res unless /\A\+OK/i === res + res + end - def initialize( sock ) - @stamp = super(sock).message.slice(/<.+>/) or - raise ProtoAuthError.new("not APOP server: cannot login", nil) + def check_response_auth( res ) + raise POPAuthenticationError, res unless /\A\+OK/i === res + res end - def auth( account, pass ) - atomic { - @socket.writeline sprintf('APOP %s %s', - account, - Digest::MD5.hexdigest(@stamp + pass)) - check_reply_auth - } + def critical + return if @in_critical_block + # Do not use ensure-block. + @in_critical_block = true + result = yield + @in_critical_block = false + result end end diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb index 9da1ad8909..fd85771dd5 100644 --- a/lib/net/protocol.rb +++ b/lib/net/protocol.rb @@ -2,7 +2,8 @@ = net/protocol.rb -Copyright (c) 1999-2002 Yukihiro Matsumoto +Copyright (c) 1999-2003 Yukihiro Matsumoto +Copyright (c) 1999-2003 Minero Aoki written & maintained by Minero Aoki @@ -23,211 +24,6 @@ require 'timeout' module Net - class Protocol - - Version = '1.2.3' - Revision = '$Revision$'.slice(/[\d\.]+/) - - - class << self - - def port - default_port - end - - private - - def protocol_param( name, val ) - module_eval <<-EOS, __FILE__, __LINE__ + 1 - def self.#{name.id2name} - #{val} - end - EOS - end - - end - - - # - # --- Configuration Staffs for Sub Classes --- - # - # class method default_port - # class method command_type - # class method socket_type - # - # private method do_start - # private method do_finish - # - # private method conn_address - # private method conn_port - # - - - def Protocol.start( address, port = nil, *args ) - instance = new(address, port) - if block_given? - instance.start(*args) { - return yield(instance) - } - else - instance.start(*args) - instance - end - end - - def initialize( addr, port = nil ) - @address = addr - @port = port || self.class.default_port - - @command = nil - @socket = nil - - @started = false - - @open_timeout = 30 - @read_timeout = 60 - - @debug_output = nil - end - - attr_reader :address - attr_reader :port - - attr_reader :command - attr_reader :socket - - attr_accessor :open_timeout - - attr_reader :read_timeout - - def read_timeout=( sec ) - @socket.read_timeout = sec if @socket - @read_timeout = sec - end - - def started? - @started - end - - alias active? started? - - def set_debug_output( arg ) # un-documented - @debug_output = arg - end - - def inspect - "#<#{self.class} #{@address}:#{@port} open=#{active?}>" - end - - # - # open - # - - def start( *args ) - @started and raise IOError, 'protocol has been opened already' - - if block_given? - begin - do_start(*args) - @started = true - return yield(self) - ensure - finish if @started - end - end - - do_start(*args) - @started = true - self - end - - private - - # abstract do_start() - - def conn_socket - @socket = self.class.socket_type.open( - conn_address(), conn_port(), - @open_timeout, @read_timeout, @debug_output ) - on_connect - end - - alias conn_address address - alias conn_port port - - def reconn_socket - @socket.reopen @open_timeout - on_connect - end - - def conn_command - @command = self.class.command_type.new(@socket) - end - - def on_connect - end - - # - # close - # - - public - - def finish - raise IOError, 'closing already closed protocol' unless @started - do_finish - @started = false - nil - end - - private - - # abstract do_finish() - - def disconn_command - @command.quit if @command and not @command.critical? - @command = nil - end - - def disconn_socket - @socket.close if @socket and not @socket.closed? - @socket = nil - end - - end - - Session = Protocol - - - class Response - - def initialize( ctype, code, msg ) - @code_type = ctype - @code = code - @message = msg - super() - end - - attr_reader :code_type - attr_reader :code - attr_reader :message - alias msg message - - def inspect - "#<#{self.class} #{@code}>" - end - - def error! - raise error_type().new(code + ' ' + @message.dump, self) - end - - def error_type - @code_type.error_type - end - - end - - class ProtocolError < StandardError; end class ProtoSyntaxError < ProtocolError; end class ProtoFatalError < ProtocolError; end @@ -238,129 +34,6 @@ module Net class ProtoRetriableError < ProtocolError; end ProtocRetryError = ProtoRetriableError - class ProtocolError - - def initialize( msg, resp ) - super msg - @response = resp - end - - attr_reader :response - alias data response - - def inspect - "#<#{self.class} #{self.message}>" - end - - end - - - class Code - - def initialize( paren, err ) - @parents = [self] + paren - @error_type = err - end - - def parents - @parents.dup - end - - attr_reader :error_type - - def inspect - "#<#{self.class} #{sprintf '0x%x', __id__}>" - end - - def ===( response ) - response.code_type.parents.each do |c| - return true if c == self - end - false - end - - def mkchild( err = nil ) - self.class.new(@parents, err || @error_type) - end - - end - - ReplyCode = Code.new([], ProtoUnknownError) - InformationCode = ReplyCode.mkchild(ProtoUnknownError) - SuccessCode = ReplyCode.mkchild(ProtoUnknownError) - ContinueCode = ReplyCode.mkchild(ProtoUnknownError) - ErrorCode = ReplyCode.mkchild(ProtocolError) - SyntaxErrorCode = ErrorCode.mkchild(ProtoSyntaxError) - FatalErrorCode = ErrorCode.mkchild(ProtoFatalError) - ServerErrorCode = ErrorCode.mkchild(ProtoServerError) - AuthErrorCode = ErrorCode.mkchild(ProtoAuthError) - RetriableCode = ReplyCode.mkchild(ProtoRetriableError) - UnknownCode = ReplyCode.mkchild(ProtoUnknownError) - - - class Command - - def initialize( sock ) - @socket = sock - @last_reply = nil - @atomic = false - end - - attr_accessor :socket - attr_reader :last_reply - - def inspect - "#<#{self.class} socket=#{@socket.inspect} critical=#{@atomic}>" - end - - # abstract quit() - - private - - def check_reply( *oks ) - @last_reply = get_reply() - reply_must @last_reply, *oks - end - - # abstract get_reply() - - def reply_must( rep, *oks ) - oks.each do |i| - return rep if i === rep - end - rep.error! - end - - def getok( line, expect = SuccessCode ) - @socket.writeline line - check_reply expect - end - - # - # critical session - # - - public - - def critical? - @atomic - end - - def error_ok - @atomic = false - end - - private - - def atomic - @atomic = true - ret = yield - @atomic = false - ret - end - - end - class InternetMessageIO @@ -765,16 +438,4 @@ module Net end - - # for backward compatibility - module NetPrivate - Response = ::Net::Response - Command = ::Net::Command - Socket = ::Net::InternetMessageIO - BufferedSocket = ::Net::InternetMessageIO - WriteAdapter = ::Net::WriteAdapter - ReadAdapter = ::Net::ReadAdapter - end - BufferedSocket = ::Net::InternetMessageIO - end # module Net diff --git a/lib/net/smtp.rb b/lib/net/smtp.rb index 89740411bd..b963204160 100644 --- a/lib/net/smtp.rb +++ b/lib/net/smtp.rb @@ -44,7 +44,7 @@ executed. require 'net/smtp' Net::SMTP.start('your.smtp.server', 25) {|smtp| - # use smtp object only in this block + # use SMTP object only in this block } Replace 'your.smtp.server' by your SMTP server. Normally @@ -144,9 +144,15 @@ the SMTP session by inspecting HELO domain. authentication by using AUTH command. :plain or :cram_md5 is allowed for AUTHTYPE. -: active? +: started? true if SMTP session is started. +: esmtp? + true if the SMTP object uses ESMTP. + +: esmtp=(b) + set wheather SMTP should use ESMTP. + : address the address to connect @@ -209,14 +215,15 @@ the SMTP session by inspecting HELO domain. == Exceptions SMTP objects raise these exceptions: + : Net::ProtoSyntaxError - syntax error (errno.500) + Syntax error (errno.500) : Net::ProtoFatalError - fatal error (errno.550) + Fatal error (errno.550) : Net::ProtoUnknownError - unknown error. (is probably bug) + Unknown error. (is probably bug) : Net::ProtoServerBusy - temporary error (errno.420/450) + Temporal error (errno.420/450) =end @@ -226,16 +233,31 @@ require 'digest/md5' module Net - class SMTP < Protocol + class SMTP - protocol_param :default_port, '25' - protocol_param :command_type, '::Net::SMTPCommand' - protocol_param :socket_type, '::Net::InternetMessageIO' - + Revision = %q$Revision$.split[1] + + def SMTP.default_port + 25 + end + + def initialize( address, port = nil ) + @address = address + @port = port || SMTP.default_port - def initialize( addr, port = nil ) - super @esmtp = true + + @command = nil + @socket = nil + @started = false + @open_timeout = 30 + @read_timeout = 60 + + @debug_output = nil + end + + def inspect + "#<#{self.class} #{address}:#{@port} open=#{@started}>" end def esmtp? @@ -248,27 +270,70 @@ module Net alias esmtp esmtp? - private + attr_reader :address + attr_reader :port - def do_start( helo = 'localhost.localdomain', - user = nil, secret = nil, authtype = nil ) - conn_socket - conn_command + attr_accessor :open_timeout + attr_reader :read_timeout + def read_timeout=( sec ) + @socket.read_timeout = sec if @socket + @read_timeout = sec + end + + def set_debug_output( arg ) + @debug_output = arg + end + + # + # SMTP session control + # + + def SMTP.start( address, port = nil, + helo = 'localhost.localdomain', + user = nil, secret = nil, authtype = nil, + &block) + new(address, port).start(helo, user, secret, authtype, &block) + end + + def started? + @started + end + + def start( helo = 'localhost.localdomain', + user = nil, secret = nil, authtype = nil ) + raise IOError, 'SMTP session opened already' if @started + if block_given? + begin + do_start(helo, user, secret, authtype) + return yield(self) + ensure + finish if @started + end + else + do_start(helo, user, secret, authtype) + return self + end + end + + def do_start( helo, user, secret, authtype ) + @socket = InternetMessageIO.open(@address, @port, + @open_timeout, @read_timeout, + @debug_output) + @command = SMTPCommand.new(@socket) begin if @esmtp - command().ehlo helo + @command.ehlo helo else - command().helo helo + @command.helo helo end rescue ProtocolError if @esmtp @esmtp = false - command().error_ok + @command.error_ok retry - else - raise end + raise end if user or secret @@ -277,28 +342,30 @@ module Net mid = 'auth_' + (authtype || 'cram_md5').to_s raise ArgumentError, "wrong auth type #{authtype}"\ unless command().respond_to?(mid) - command().__send__ mid, user, secret + @command.__send__ mid, user, secret end end - - def do_finish - disconn_command - disconn_socket + private :do_start + + def finish + raise IOError, 'closing already closed SMTP session' unless @started + @command.quit if @command + @command = nil + @socket.close if @socket and not @socket.closed? + @socket = nil + @started = false end - # - # SMTP operations + # SMTP wrapper # - public - def send_mail( mailsrc, from_addr, *to_addrs ) do_ready from_addr, to_addrs.flatten command().write_mail mailsrc end - alias sendmail send_mail + alias sendmail send_mail # backward compatibility def ready( from_addr, *to_addrs, &block ) do_ready from_addr, to_addrs.flatten @@ -313,45 +380,49 @@ module Net command().rcpt to_addrs end + def command + raise IOError, "closed session" unless @command + @command + end + end SMTPSession = SMTP - class SMTPCommand < Command + class SMTPCommand def initialize( sock ) - super - atomic { - check_reply SuccessCode - } + @socket = sock + @in_critical_block = false + check_response(critical { recv_response() }) + end + + def inspect + "#<#{self.class} socket=#{@socket.inspect}>" end def helo( domain ) - atomic { - getok sprintf('HELO %s', domain) - } + getok('HELO %s', domain) end def ehlo( domain ) - atomic { - getok sprintf('EHLO %s', domain) - } + getok('EHLO %s', domain) end # "PLAIN" authentication [RFC2554] def auth_plain( user, secret ) - atomic { - getok sprintf('AUTH PLAIN %s', - ["\0#{user}\0#{secret}"].pack('m').chomp) - } + res = critical { get_response('AUTH PLAIN %s', + ["\0#{user}\0#{secret}"].pack('m').chomp) } + raise SMTPAuthenticationError, res unless /\A2../ === res end # "CRAM-MD5" authentication [RFC2195] def auth_cram_md5( user, secret ) - atomic { - rep = getok('AUTH CRAM-MD5', ContinueCode) - challenge = rep.msg.split(/ /)[1].unpack('m')[0] + 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) @@ -363,86 +434,89 @@ module Net tmp = Digest::MD5.digest(isecret + challenge) tmp = Digest::MD5.hexdigest(osecret + tmp) - getok [user + ' ' + tmp].pack('m').gsub(/\s+/, '') + res = get_response([user + ' ' + tmp].pack('m').gsub(/\s+/, '')) } + raise SMTPAuthenticationError, res unless /\A2../ === res end def mailfrom( fromaddr ) - atomic { - getok sprintf('MAIL FROM:<%s>', fromaddr) - } + getok('MAIL FROM:<%s>', fromaddr) end def rcpt( toaddrs ) toaddrs.each do |i| - atomic { - getok sprintf('RCPT TO:<%s>', i) - } + getok('RCPT TO:<%s>', i) end end def write_mail( src ) - atomic { - getok 'DATA', ContinueCode + res = critical { + check_response(get_response('DATA'), true) @socket.write_message src - check_reply SuccessCode + recv_response() } + check_response(res) end def through_mail( &block ) - atomic { - getok 'DATA', ContinueCode + res = critical { + check_response(get_response('DATA'), true) @socket.through_message(&block) - check_reply SuccessCode + recv_response() } + check_response(res) end def quit - atomic { - getok 'QUIT' - } + getok('QUIT') end private - def get_reply - arr = read_reply - stat = arr[0][0,3] - - klass = case stat[0] - when ?2 then SuccessCode - when ?3 then ContinueCode - when ?4 then ServerErrorCode - when ?5 then - case stat[1] - when ?0 then SyntaxErrorCode - when ?3 then AuthErrorCode - when ?5 then FatalErrorCode - end - end - klass ||= UnknownCode + def getok( fmt, *args ) + @socket.writeline sprintf(fmt, *args) + check_response(critical { recv_response() }) + end - Response.new(klass, stat, arr.join('')) + def get_response( fmt, *args ) + @socket.writeline sprintf(fmt, *args) + recv_response() end - def read_reply - arr = [] + def recv_response + res = '' while true - str = @socket.readline - break unless str[3] == ?- # "210-PIPELINING" - arr.push str + line = @socket.readline + res << line << "\n" + break unless line[3] == ?- # "210-PIPELINING" end - arr.push str - - arr + res end - end + def check_response( res, cont = false ) + etype = case res[0] + when ?2 then nil + when ?3 then cont ? 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 + end + def critical + return if @in_critical_block + @in_critical_block = true + result = yield() + @in_critical_block = false + result + end - # for backward compatibility - module NetPrivate - SMTPCommand = ::Net::SMTPCommand end end # module Net -- cgit v1.2.3