diff options
Diffstat (limited to 'jni/ruby/lib/net')
| -rw-r--r-- | jni/ruby/lib/net/ftp.rb | 1121 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http.rb | 1559 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http/backward.rb | 25 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http/exceptions.rb | 25 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http/generic_request.rb | 332 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http/header.rb | 452 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http/proxy_delta.rb | 16 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http/request.rb | 20 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http/requests.rb | 122 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http/response.rb | 416 | ||||
| -rw-r--r-- | jni/ruby/lib/net/http/responses.rb | 273 | ||||
| -rw-r--r-- | jni/ruby/lib/net/https.rb | 22 | ||||
| -rw-r--r-- | jni/ruby/lib/net/imap.rb | 3622 | ||||
| -rw-r--r-- | jni/ruby/lib/net/pop.rb | 1021 | ||||
| -rw-r--r-- | jni/ruby/lib/net/protocol.rb | 420 | ||||
| -rw-r--r-- | jni/ruby/lib/net/smtp.rb | 1073 | ||||
| -rw-r--r-- | jni/ruby/lib/net/telnet.rb | 763 | 
17 files changed, 11282 insertions, 0 deletions
| diff --git a/jni/ruby/lib/net/ftp.rb b/jni/ruby/lib/net/ftp.rb new file mode 100644 index 0000000..f513ca6 --- /dev/null +++ b/jni/ruby/lib/net/ftp.rb @@ -0,0 +1,1121 @@ +# +# = net/ftp.rb - FTP Client Library +# +# Written by Shugo Maeda <shugo@ruby-lang.org>. +# +# Documentation by Gavin Sinclair, sourced from "Programming Ruby" (Hunt/Thomas) +# and "Ruby In a Nutshell" (Matsumoto), used with permission. +# +# This library is distributed under the terms of the Ruby license. +# You can freely distribute/modify this library. +# +# It is included in the Ruby standard library. +# +# See the Net::FTP class for an overview. +# + +require "socket" +require "monitor" +require "net/protocol" + +module Net + +  # :stopdoc: +  class FTPError < StandardError; end +  class FTPReplyError < FTPError; end +  class FTPTempError < FTPError; end +  class FTPPermError < FTPError; end +  class FTPProtoError < FTPError; end +  class FTPConnectionError < FTPError; end +  # :startdoc: + +  # +  # This class implements the File Transfer Protocol.  If you have used a +  # command-line FTP program, and are familiar with the commands, you will be +  # able to use this class easily.  Some extra features are included to take +  # advantage of Ruby's style and strengths. +  # +  # == Example +  # +  #   require 'net/ftp' +  # +  # === Example 1 +  # +  #   ftp = Net::FTP.new('example.com') +  #   ftp.login +  #   files = ftp.chdir('pub/lang/ruby/contrib') +  #   files = ftp.list('n*') +  #   ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) +  #   ftp.close +  # +  # === Example 2 +  # +  #   Net::FTP.open('example.com') do |ftp| +  #     ftp.login +  #     files = ftp.chdir('pub/lang/ruby/contrib') +  #     files = ftp.list('n*') +  #     ftp.getbinaryfile('nif.rb-0.91.gz', 'nif.gz', 1024) +  #   end +  # +  # == Major Methods +  # +  # The following are the methods most likely to be useful to users: +  # - FTP.open +  # - #getbinaryfile +  # - #gettextfile +  # - #putbinaryfile +  # - #puttextfile +  # - #chdir +  # - #nlst +  # - #size +  # - #rename +  # - #delete +  # +  class FTP +    include MonitorMixin + +    # :stopdoc: +    FTP_PORT = 21 +    CRLF = "\r\n" +    DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE +    # :startdoc: + +    # When +true+, transfers are performed in binary mode.  Default: +true+. +    attr_reader :binary + +    # When +true+, the connection is in passive mode.  Default: +false+. +    attr_accessor :passive + +    # When +true+, all traffic to and from the server is written +    # to +$stdout+.  Default: +false+. +    attr_accessor :debug_mode + +    # Sets or retrieves the +resume+ status, which decides whether incomplete +    # transfers are resumed or restarted.  Default: +false+. +    attr_accessor :resume + +    # Number of seconds to wait for the connection to open. Any number +    # may be used, including Floats for fractional seconds. If the FTP +    # object cannot open a connection in this many seconds, it raises a +    # Net::OpenTimeout exception. The default value is +nil+. +    attr_accessor :open_timeout + +    # Number of seconds to wait for one block to be read (via one read(2) +    # call). Any number may be used, including Floats for fractional +    # seconds. If the FTP object cannot read data in this many seconds, +    # it raises a Timeout::Error exception. The default value is 60 seconds. +    attr_reader :read_timeout + +    # Setter for the read_timeout attribute. +    def read_timeout=(sec) +      @sock.read_timeout = sec +      @read_timeout = sec +    end + +    # The server's welcome message. +    attr_reader :welcome + +    # The server's last response code. +    attr_reader :last_response_code +    alias lastresp last_response_code + +    # The server's last response. +    attr_reader :last_response + +    # +    # A synonym for <tt>FTP.new</tt>, but with a mandatory host parameter. +    # +    # If a block is given, it is passed the +FTP+ object, which will be closed +    # when the block finishes, or when an exception is raised. +    # +    def FTP.open(host, user = nil, passwd = nil, acct = nil) +      if block_given? +        ftp = new(host, user, passwd, acct) +        begin +          yield ftp +        ensure +          ftp.close +        end +      else +        new(host, user, passwd, acct) +      end +    end + +    # +    # Creates and returns a new +FTP+ object. If a +host+ is given, a connection +    # is made. Additionally, if the +user+ is given, the given user name, +    # password, and (optionally) account are used to log in.  See #login. +    # +    def initialize(host = nil, user = nil, passwd = nil, acct = nil) +      super() +      @binary = true +      @passive = false +      @debug_mode = false +      @resume = false +      @sock = NullSocket.new +      @logged_in = false +      @open_timeout = nil +      @read_timeout = 60 +      if host +        connect(host) +        if user +          login(user, passwd, acct) +        end +      end +    end + +    # A setter to toggle transfers in binary mode. +    # +newmode+ is either +true+ or +false+ +    def binary=(newmode) +      if newmode != @binary +        @binary = newmode +        send_type_command if @logged_in +      end +    end + +    # Sends a command to destination host, with the current binary sendmode +    # type. +    # +    # If binary mode is +true+, then "TYPE I" (image) is sent, otherwise "TYPE +    # A" (ascii) is sent. +    def send_type_command # :nodoc: +      if @binary +        voidcmd("TYPE I") +      else +        voidcmd("TYPE A") +      end +    end +    private :send_type_command + +    # Toggles transfers in binary mode and yields to a block. +    # This preserves your current binary send mode, but allows a temporary +    # transaction with binary sendmode of +newmode+. +    # +    # +newmode+ is either +true+ or +false+ +    def with_binary(newmode) # :nodoc: +      oldmode = binary +      self.binary = newmode +      begin +        yield +      ensure +        self.binary = oldmode +      end +    end +    private :with_binary + +    # Obsolete +    def return_code # :nodoc: +      $stderr.puts("warning: Net::FTP#return_code is obsolete and do nothing") +      return "\n" +    end + +    # Obsolete +    def return_code=(s) # :nodoc: +      $stderr.puts("warning: Net::FTP#return_code= is obsolete and do nothing") +    end + +    # Constructs a socket with +host+ and +port+. +    # +    # If SOCKSSocket is defined and the environment (ENV) defines +    # SOCKS_SERVER, then a SOCKSSocket is returned, else a TCPSocket is +    # returned. +    def open_socket(host, port) # :nodoc: +      return Timeout.timeout(@open_timeout, Net::OpenTimeout) { +        if defined? SOCKSSocket and ENV["SOCKS_SERVER"] +          @passive = true +          sock = SOCKSSocket.open(host, port) +        else +          sock = TCPSocket.open(host, port) +        end +        io = BufferedSocket.new(sock) +        io.read_timeout = @read_timeout +        io +      } +    end +    private :open_socket + +    # +    # Establishes an FTP connection to host, optionally overriding the default +    # port. If the environment variable +SOCKS_SERVER+ is set, sets up the +    # connection through a SOCKS proxy. Raises an exception (typically +    # <tt>Errno::ECONNREFUSED</tt>) if the connection cannot be established. +    # +    def connect(host, port = FTP_PORT) +      if @debug_mode +        print "connect: ", host, ", ", port, "\n" +      end +      synchronize do +        @sock = open_socket(host, port) +        voidresp +      end +    end + +    # +    # Set the socket used to connect to the FTP server. +    # +    # May raise FTPReplyError if +get_greeting+ is false. +    def set_socket(sock, get_greeting = true) +      synchronize do +        @sock = sock +        if get_greeting +          voidresp +        end +      end +    end + +    # If string +s+ includes the PASS command (password), then the contents of +    # the password are cleaned from the string using "*" +    def sanitize(s) # :nodoc: +      if s =~ /^PASS /i +        return s[0, 5] + "*" * (s.length - 5) +      else +        return s +      end +    end +    private :sanitize + +    # Ensures that +line+ has a control return / line feed (CRLF) and writes +    # it to the socket. +    def putline(line) # :nodoc: +      if @debug_mode +        print "put: ", sanitize(line), "\n" +      end +      line = line + CRLF +      @sock.write(line) +    end +    private :putline + +    # Reads a line from the sock.  If EOF, then it will raise EOFError +    def getline # :nodoc: +      line = @sock.readline # if get EOF, raise EOFError +      line.sub!(/(\r\n|\n|\r)\z/n, "") +      if @debug_mode +        print "get: ", sanitize(line), "\n" +      end +      return line +    end +    private :getline + +    # Receive a section of lines until the response code's match. +    def getmultiline # :nodoc: +      line = getline +      buff = line +      if line[3] == ?- +          code = line[0, 3] +        begin +          line = getline +          buff << "\n" << line +        end until line[0, 3] == code and line[3] != ?- +      end +      return buff << "\n" +    end +    private :getmultiline + +    # Receives a response from the destination host. +    # +    # Returns the response code or raises FTPTempError, FTPPermError, or +    # FTPProtoError +    def getresp # :nodoc: +      @last_response = getmultiline +      @last_response_code = @last_response[0, 3] +      case @last_response_code +      when /\A[123]/ +        return @last_response +      when /\A4/ +        raise FTPTempError, @last_response +      when /\A5/ +        raise FTPPermError, @last_response +      else +        raise FTPProtoError, @last_response +      end +    end +    private :getresp + +    # Receives a response. +    # +    # Raises FTPReplyError if the first position of the response code is not +    # equal 2. +    def voidresp # :nodoc: +      resp = getresp +      if resp[0] != ?2 +        raise FTPReplyError, resp +      end +    end +    private :voidresp + +    # +    # Sends a command and returns the response. +    # +    def sendcmd(cmd) +      synchronize do +        putline(cmd) +        return getresp +      end +    end + +    # +    # Sends a command and expect a response beginning with '2'. +    # +    def voidcmd(cmd) +      synchronize do +        putline(cmd) +        voidresp +      end +    end + +    # Constructs and send the appropriate PORT (or EPRT) command +    def sendport(host, port) # :nodoc: +      af = (@sock.peeraddr)[0] +      if af == "AF_INET" +        cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",") +      elsif af == "AF_INET6" +        cmd = sprintf("EPRT |2|%s|%d|", host, port) +      else +        raise FTPProtoError, host +      end +      voidcmd(cmd) +    end +    private :sendport + +    # Constructs a TCPServer socket +    def makeport # :nodoc: +      TCPServer.open(@sock.addr[3], 0) +    end +    private :makeport + +    # sends the appropriate command to enable a passive connection +    def makepasv # :nodoc: +      if @sock.peeraddr[0] == "AF_INET" +        host, port = parse227(sendcmd("PASV")) +      else +        host, port = parse229(sendcmd("EPSV")) +        #     host, port = parse228(sendcmd("LPSV")) +      end +      return host, port +    end +    private :makepasv + +    # Constructs a connection for transferring data +    def transfercmd(cmd, rest_offset = nil) # :nodoc: +      if @passive +        host, port = makepasv +        conn = open_socket(host, port) +        if @resume and rest_offset +          resp = sendcmd("REST " + rest_offset.to_s) +          if resp[0] != ?3 +            raise FTPReplyError, resp +          end +        end +        resp = sendcmd(cmd) +        # skip 2XX for some ftp servers +        resp = getresp if resp[0] == ?2 +        if resp[0] != ?1 +          raise FTPReplyError, resp +        end +      else +        sock = makeport +        begin +          sendport(sock.addr[3], sock.addr[1]) +          if @resume and rest_offset +            resp = sendcmd("REST " + rest_offset.to_s) +            if resp[0] != ?3 +              raise FTPReplyError, resp +            end +          end +          resp = sendcmd(cmd) +          # skip 2XX for some ftp servers +          resp = getresp if resp[0] == ?2 +          if resp[0] != ?1 +            raise FTPReplyError, resp +          end +          conn = BufferedSocket.new(sock.accept) +          conn.read_timeout = @read_timeout +          sock.shutdown(Socket::SHUT_WR) rescue nil +          sock.read rescue nil +        ensure +          sock.close +        end +      end +      return conn +    end +    private :transfercmd + +    # +    # Logs in to the remote host.  The session must have been +    # previously connected.  If +user+ is the string "anonymous" and +    # the +password+ is +nil+, "anonymous@" is used as a password.  If +    # the +acct+ parameter is not +nil+, an FTP ACCT command is sent +    # following the successful login.  Raises an exception on error +    # (typically <tt>Net::FTPPermError</tt>). +    # +    def login(user = "anonymous", passwd = nil, acct = nil) +      if user == "anonymous" and passwd == nil +        passwd = "anonymous@" +      end + +      resp = "" +      synchronize do +        resp = sendcmd('USER ' + user) +        if resp[0] == ?3 +          raise FTPReplyError, resp if passwd.nil? +          resp = sendcmd('PASS ' + passwd) +        end +        if resp[0] == ?3 +          raise FTPReplyError, resp if acct.nil? +          resp = sendcmd('ACCT ' + acct) +        end +      end +      if resp[0] != ?2 +        raise FTPReplyError, resp +      end +      @welcome = resp +      send_type_command +      @logged_in = true +    end + +    # +    # Puts the connection into binary (image) mode, issues the given command, +    # and fetches the data returned, passing it to the associated block in +    # chunks of +blocksize+ characters. Note that +cmd+ is a server command +    # (such as "RETR myfile"). +    # +    def retrbinary(cmd, blocksize, rest_offset = nil) # :yield: data +      synchronize do +        with_binary(true) do +          begin +            conn = transfercmd(cmd, rest_offset) +            loop do +              data = conn.read(blocksize) +              break if data == nil +              yield(data) +            end +            conn.shutdown(Socket::SHUT_WR) +            conn.read_timeout = 1 +            conn.read +          ensure +            conn.close if conn +          end +          voidresp +        end +      end +    end + +    # +    # Puts the connection into ASCII (text) mode, issues the given command, and +    # passes the resulting data, one line at a time, to the associated block. If +    # no block is given, prints the lines. Note that +cmd+ is a server command +    # (such as "RETR myfile"). +    # +    def retrlines(cmd) # :yield: line +      synchronize do +        with_binary(false) do +          begin +            conn = transfercmd(cmd) +            loop do +              line = conn.gets +              break if line == nil +              yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?) +            end +            conn.shutdown(Socket::SHUT_WR) +            conn.read_timeout = 1 +            conn.read +          ensure +            conn.close if conn +          end +          voidresp +        end +      end +    end + +    # +    # Puts the connection into binary (image) mode, issues the given server-side +    # command (such as "STOR myfile"), and sends the contents of the file named +    # +file+ to the server. If the optional block is given, it also passes it +    # the data, in chunks of +blocksize+ characters. +    # +    def storbinary(cmd, file, blocksize, rest_offset = nil) # :yield: data +      if rest_offset +        file.seek(rest_offset, IO::SEEK_SET) +      end +      synchronize do +        with_binary(true) do +          conn = transfercmd(cmd) +          loop do +            buf = file.read(blocksize) +            break if buf == nil +            conn.write(buf) +            yield(buf) if block_given? +          end +          conn.close +          voidresp +        end +      end +    rescue Errno::EPIPE +      # EPIPE, in this case, means that the data connection was unexpectedly +      # terminated.  Rather than just raising EPIPE to the caller, check the +      # response on the control connection.  If getresp doesn't raise a more +      # appropriate exception, re-raise the original exception. +      getresp +      raise +    end + +    # +    # Puts the connection into ASCII (text) mode, issues the given server-side +    # command (such as "STOR myfile"), and sends the contents of the file +    # named +file+ to the server, one line at a time. If the optional block is +    # given, it also passes it the lines. +    # +    def storlines(cmd, file) # :yield: line +      synchronize do +        with_binary(false) do +          conn = transfercmd(cmd) +          loop do +            buf = file.gets +            break if buf == nil +            if buf[-2, 2] != CRLF +              buf = buf.chomp + CRLF +            end +            conn.write(buf) +            yield(buf) if block_given? +          end +          conn.close +          voidresp +        end +      end +    rescue Errno::EPIPE +      # EPIPE, in this case, means that the data connection was unexpectedly +      # terminated.  Rather than just raising EPIPE to the caller, check the +      # response on the control connection.  If getresp doesn't raise a more +      # appropriate exception, re-raise the original exception. +      getresp +      raise +    end + +    # +    # Retrieves +remotefile+ in binary mode, storing the result in +localfile+. +    # If +localfile+ is nil, returns retrieved data. +    # If a block is supplied, it is passed the retrieved data in +blocksize+ +    # chunks. +    # +    def getbinaryfile(remotefile, localfile = File.basename(remotefile), +                      blocksize = DEFAULT_BLOCKSIZE) # :yield: data +      result = nil +      if localfile +        if @resume +          rest_offset = File.size?(localfile) +          f = open(localfile, "a") +        else +          rest_offset = nil +          f = open(localfile, "w") +        end +      elsif !block_given? +        result = "" +      end +      begin +        f.binmode if localfile +        retrbinary("RETR " + remotefile.to_s, blocksize, rest_offset) do |data| +          f.write(data) if localfile +          yield(data) if block_given? +          result.concat(data) if result +        end +        return result +      ensure +        f.close if localfile +      end +    end + +    # +    # Retrieves +remotefile+ in ASCII (text) mode, storing the result in +    # +localfile+. +    # If +localfile+ is nil, returns retrieved data. +    # If a block is supplied, it is passed the retrieved data one +    # line at a time. +    # +    def gettextfile(remotefile, localfile = File.basename(remotefile)) # :yield: line +      result = nil +      if localfile +        f = open(localfile, "w") +      elsif !block_given? +        result = "" +      end +      begin +        retrlines("RETR " + remotefile) do |line, newline| +          l = newline ? line + "\n" : line +          f.print(l) if localfile +          yield(line, newline) if block_given? +          result.concat(l) if result +        end +        return result +      ensure +        f.close if localfile +      end +    end + +    # +    # Retrieves +remotefile+ in whatever mode the session is set (text or +    # binary).  See #gettextfile and #getbinaryfile. +    # +    def get(remotefile, localfile = File.basename(remotefile), +            blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data +      if @binary +        getbinaryfile(remotefile, localfile, blocksize, &block) +      else +        gettextfile(remotefile, localfile, &block) +      end +    end + +    # +    # Transfers +localfile+ to the server in binary mode, storing the result in +    # +remotefile+. If a block is supplied, calls it, passing in the transmitted +    # data in +blocksize+ chunks. +    # +    def putbinaryfile(localfile, remotefile = File.basename(localfile), +                      blocksize = DEFAULT_BLOCKSIZE, &block) # :yield: data +      if @resume +        begin +          rest_offset = size(remotefile) +        rescue Net::FTPPermError +          rest_offset = nil +        end +      else +        rest_offset = nil +      end +      f = open(localfile) +      begin +        f.binmode +        if rest_offset +          storbinary("APPE " + remotefile, f, blocksize, rest_offset, &block) +        else +          storbinary("STOR " + remotefile, f, blocksize, rest_offset, &block) +        end +      ensure +        f.close +      end +    end + +    # +    # Transfers +localfile+ to the server in ASCII (text) mode, storing the result +    # in +remotefile+. If callback or an associated block is supplied, calls it, +    # passing in the transmitted data one line at a time. +    # +    def puttextfile(localfile, remotefile = File.basename(localfile), &block) # :yield: line +      f = open(localfile) +      begin +        storlines("STOR " + remotefile, f, &block) +      ensure +        f.close +      end +    end + +    # +    # Transfers +localfile+ to the server in whatever mode the session is set +    # (text or binary).  See #puttextfile and #putbinaryfile. +    # +    def put(localfile, remotefile = File.basename(localfile), +            blocksize = DEFAULT_BLOCKSIZE, &block) +      if @binary +        putbinaryfile(localfile, remotefile, blocksize, &block) +      else +        puttextfile(localfile, remotefile, &block) +      end +    end + +    # +    # Sends the ACCT command. +    # +    # This is a less common FTP command, to send account +    # information if the destination host requires it. +    # +    def acct(account) +      cmd = "ACCT " + account +      voidcmd(cmd) +    end + +    # +    # Returns an array of filenames in the remote directory. +    # +    def nlst(dir = nil) +      cmd = "NLST" +      if dir +        cmd = cmd + " " + dir +      end +      files = [] +      retrlines(cmd) do |line| +        files.push(line) +      end +      return files +    end + +    # +    # Returns an array of file information in the directory (the output is like +    # `ls -l`).  If a block is given, it iterates through the listing. +    # +    def list(*args, &block) # :yield: line +      cmd = "LIST" +      args.each do |arg| +        cmd = cmd + " " + arg.to_s +      end +      if block +        retrlines(cmd, &block) +      else +        lines = [] +        retrlines(cmd) do |line| +          lines << line +        end +        return lines +      end +    end +    alias ls list +    alias dir list + +    # +    # Renames a file on the server. +    # +    def rename(fromname, toname) +      resp = sendcmd("RNFR " + fromname) +      if resp[0] != ?3 +        raise FTPReplyError, resp +      end +      voidcmd("RNTO " + toname) +    end + +    # +    # Deletes a file on the server. +    # +    def delete(filename) +      resp = sendcmd("DELE " + filename) +      if resp[0, 3] == "250" +        return +      elsif resp[0] == ?5 +        raise FTPPermError, resp +      else +        raise FTPReplyError, resp +      end +    end + +    # +    # Changes the (remote) directory. +    # +    def chdir(dirname) +      if dirname == ".." +        begin +          voidcmd("CDUP") +          return +        rescue FTPPermError => e +          if e.message[0, 3] != "500" +            raise e +          end +        end +      end +      cmd = "CWD " + dirname +      voidcmd(cmd) +    end + +    # +    # Returns the size of the given (remote) filename. +    # +    def size(filename) +      with_binary(true) do +        resp = sendcmd("SIZE " + filename) +        if resp[0, 3] != "213" +          raise FTPReplyError, resp +        end +        return resp[3..-1].strip.to_i +      end +    end + +    MDTM_REGEXP = /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/  # :nodoc: + +    # +    # Returns the last modification time of the (remote) file.  If +local+ is +    # +true+, it is returned as a local time, otherwise it's a UTC time. +    # +    def mtime(filename, local = false) +      str = mdtm(filename) +      ary = str.scan(MDTM_REGEXP)[0].collect {|i| i.to_i} +      return local ? Time.local(*ary) : Time.gm(*ary) +    end + +    # +    # Creates a remote directory. +    # +    def mkdir(dirname) +      resp = sendcmd("MKD " + dirname) +      return parse257(resp) +    end + +    # +    # Removes a remote directory. +    # +    def rmdir(dirname) +      voidcmd("RMD " + dirname) +    end + +    # +    # Returns the current remote directory. +    # +    def pwd +      resp = sendcmd("PWD") +      return parse257(resp) +    end +    alias getdir pwd + +    # +    # Returns system information. +    # +    def system +      resp = sendcmd("SYST") +      if resp[0, 3] != "215" +        raise FTPReplyError, resp +      end +      return resp[4 .. -1] +    end + +    # +    # Aborts the previous command (ABOR command). +    # +    def abort +      line = "ABOR" + CRLF +      print "put: ABOR\n" if @debug_mode +      @sock.send(line, Socket::MSG_OOB) +      resp = getmultiline +      unless ["426", "226", "225"].include?(resp[0, 3]) +        raise FTPProtoError, resp +      end +      return resp +    end + +    # +    # Returns the status (STAT command). +    # +    def status +      line = "STAT" + CRLF +      print "put: STAT\n" if @debug_mode +      @sock.send(line, Socket::MSG_OOB) +      return getresp +    end + +    # +    # Returns the raw last modification time of the (remote) file in the format +    # "YYYYMMDDhhmmss" (MDTM command). +    # +    # Use +mtime+ if you want a parsed Time instance. +    # +    def mdtm(filename) +      resp = sendcmd("MDTM " + filename) +      if resp[0, 3] == "213" +        return resp[3 .. -1].strip +      end +    end + +    # +    # Issues the HELP command. +    # +    def help(arg = nil) +      cmd = "HELP" +      if arg +        cmd = cmd + " " + arg +      end +      sendcmd(cmd) +    end + +    # +    # Exits the FTP session. +    # +    def quit +      voidcmd("QUIT") +    end + +    # +    # Issues a NOOP command. +    # +    # Does nothing except return a response. +    # +    def noop +      voidcmd("NOOP") +    end + +    # +    # Issues a SITE command. +    # +    def site(arg) +      cmd = "SITE " + arg +      voidcmd(cmd) +    end + +    # +    # Closes the connection.  Further operations are impossible until you open +    # a new connection with #connect. +    # +    def close +      if @sock and not @sock.closed? +        begin +          @sock.shutdown(Socket::SHUT_WR) rescue nil +          orig, self.read_timeout = self.read_timeout, 3 +          @sock.read rescue nil +        ensure +          @sock.close +          self.read_timeout = orig +        end +      end +    end + +    # +    # Returns +true+ iff the connection is closed. +    # +    def closed? +      @sock == nil or @sock.closed? +    end + +    # handler for response code 227 +    # (Entering Passive Mode (h1,h2,h3,h4,p1,p2)) +    # +    # Returns host and port. +    def parse227(resp) # :nodoc: +      if resp[0, 3] != "227" +        raise FTPReplyError, resp +      end +      if m = /\((?<host>\d+(,\d+){3}),(?<port>\d+,\d+)\)/.match(resp) +        return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"]) +      else +        raise FTPProtoError, resp +      end +    end +    private :parse227 + +    # handler for response code 228 +    # (Entering Long Passive Mode) +    # +    # Returns host and port. +    def parse228(resp) # :nodoc: +      if resp[0, 3] != "228" +        raise FTPReplyError, resp +      end +      if m = /\(4,4,(?<host>\d+(,\d+){3}),2,(?<port>\d+,\d+)\)/.match(resp) +        return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"]) +      elsif m = /\(6,16,(?<host>\d+(,(\d+)){15}),2,(?<port>\d+,\d+)\)/.match(resp) +        return parse_pasv_ipv6_host(m["host"]), parse_pasv_port(m["port"]) +      else +        raise FTPProtoError, resp +      end +    end +    private :parse228 + +    def parse_pasv_ipv4_host(s) +      return s.tr(",", ".") +    end +    private :parse_pasv_ipv4_host + +    def parse_pasv_ipv6_host(s) +      return s.split(/,/).map { |i| +        "%02x" % i.to_i +      }.each_slice(2).map(&:join).join(":") +    end +    private :parse_pasv_ipv6_host + +    def parse_pasv_port(s) +      return s.split(/,/).map(&:to_i).inject { |x, y| +        (x << 8) + y +      } +    end +    private :parse_pasv_port + +    # handler for response code 229 +    # (Extended Passive Mode Entered) +    # +    # Returns host and port. +    def parse229(resp) # :nodoc: +      if resp[0, 3] != "229" +        raise FTPReplyError, resp +      end +      if m = /\((?<d>[!-~])\k<d>\k<d>(?<port>\d+)\k<d>\)/.match(resp) +        return @sock.peeraddr[3], m["port"].to_i +      else +        raise FTPProtoError, resp +      end +    end +    private :parse229 + +    # handler for response code 257 +    # ("PATHNAME" created) +    # +    # Returns host and port. +    def parse257(resp) # :nodoc: +      if resp[0, 3] != "257" +        raise FTPReplyError, resp +      end +      if resp[3, 2] != ' "' +        return "" +      end +      dirname = "" +      i = 5 +      n = resp.length +      while i < n +        c = resp[i, 1] +        i = i + 1 +        if c == '"' +          if i > n or resp[i, 1] != '"' +            break +          end +          i = i + 1 +        end +        dirname = dirname + c +      end +      return dirname +    end +    private :parse257 + +    # :stopdoc: +    class NullSocket +      def read_timeout=(sec) +      end + +      def close +      end + +      def method_missing(mid, *args) +        raise FTPConnectionError, "not connected" +      end +    end + +    class BufferedSocket < BufferedIO +      [:addr, :peeraddr, :send, :shutdown].each do |method| +        define_method(method) { |*args| +          @io.__send__(method, *args) +        } +      end + +      def read(len = nil) +        if len +          s = super(len, "", true) +          return s.empty? ? nil : s +        else +          result = "" +          while s = super(DEFAULT_BLOCKSIZE, "", true) +            break if s.empty? +            result << s +          end +          return result +        end +      end + +      def gets +        line = readuntil("\n", true) +        return line.empty? ? nil : line +      end + +      def readline +        line = gets +        if line.nil? +          raise EOFError, "end of file reached" +        end +        return line +      end +    end +    # :startdoc: +  end +end + + +# Documentation comments: +#  - sourced from pickaxe and nutshell, with improvements (hopefully) diff --git a/jni/ruby/lib/net/http.rb b/jni/ruby/lib/net/http.rb new file mode 100644 index 0000000..55a67ac --- /dev/null +++ b/jni/ruby/lib/net/http.rb @@ -0,0 +1,1559 @@ +# +# = net/http.rb +# +# Copyright (c) 1999-2007 Yukihiro Matsumoto +# Copyright (c) 1999-2007 Minero Aoki +# Copyright (c) 2001 GOTOU Yuuzou +# +# Written and maintained by Minero Aoki <aamine@loveruby.net>. +# HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>. +# +# This file is derived from "http-access.rb". +# +# Documented by Minero Aoki; converted to RDoc by William Webber. +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms of ruby itself --- +# Ruby Distribution License or GNU General Public License. +# +# See Net::HTTP for an overview and examples. +# + +require 'net/protocol' +require 'uri' + +module Net   #:nodoc: +  autoload :OpenSSL, 'openssl' + +  # :stopdoc: +  class HTTPBadResponse < StandardError; end +  class HTTPHeaderSyntaxError < StandardError; end +  # :startdoc: + +  # == An HTTP client API for Ruby. +  # +  # Net::HTTP provides a rich library which can be used to build HTTP +  # user-agents.  For more details about HTTP see +  # [RFC2616](http://www.ietf.org/rfc/rfc2616.txt) +  # +  # Net::HTTP is designed to work closely with URI.  URI::HTTP#host, +  # URI::HTTP#port and URI::HTTP#request_uri are designed to work with +  # Net::HTTP. +  # +  # If you are only performing a few GET requests you should try OpenURI. +  # +  # == Simple Examples +  # +  # All examples assume you have loaded Net::HTTP with: +  # +  #   require 'net/http' +  # +  # This will also require 'uri' so you don't need to require it separately. +  # +  # The Net::HTTP methods in the following section do not persist +  # connections.  They are not recommended if you are performing many HTTP +  # requests. +  # +  # === GET +  # +  #   Net::HTTP.get('example.com', '/index.html') # => String +  # +  # === GET by URI +  # +  #   uri = URI('http://example.com/index.html?count=10') +  #   Net::HTTP.get(uri) # => String +  # +  # === GET with Dynamic Parameters +  # +  #   uri = URI('http://example.com/index.html') +  #   params = { :limit => 10, :page => 3 } +  #   uri.query = URI.encode_www_form(params) +  # +  #   res = Net::HTTP.get_response(uri) +  #   puts res.body if res.is_a?(Net::HTTPSuccess) +  # +  # === POST +  # +  #   uri = URI('http://www.example.com/search.cgi') +  #   res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50') +  #   puts res.body +  # +  # === POST with Multiple Values +  # +  #   uri = URI('http://www.example.com/search.cgi') +  #   res = Net::HTTP.post_form(uri, 'q' => ['ruby', 'perl'], 'max' => '50') +  #   puts res.body +  # +  # == How to use Net::HTTP +  # +  # The following example code can be used as the basis of a HTTP user-agent +  # which can perform a variety of request types using persistent +  # connections. +  # +  #   uri = URI('http://example.com/some_path?query=string') +  # +  #   Net::HTTP.start(uri.host, uri.port) do |http| +  #     request = Net::HTTP::Get.new uri +  # +  #     response = http.request request # Net::HTTPResponse object +  #   end +  # +  # Net::HTTP::start immediately creates a connection to an HTTP server which +  # is kept open for the duration of the block.  The connection will remain +  # open for multiple requests in the block if the server indicates it +  # supports persistent connections. +  # +  # The request types Net::HTTP supports are listed below in the section "HTTP +  # Request Classes". +  # +  # If you wish to re-use a connection across multiple HTTP requests without +  # automatically closing it you can use ::new instead of ::start.  #request +  # will automatically open a connection to the server if one is not currently +  # open.  You can manually close the connection with #finish. +  # +  # For all the Net::HTTP request objects and shortcut request methods you may +  # supply either a String for the request path or a URI from which Net::HTTP +  # will extract the request path. +  # +  # === Response Data +  # +  #   uri = URI('http://example.com/index.html') +  #   res = Net::HTTP.get_response(uri) +  # +  #   # Headers +  #   res['Set-Cookie']            # => String +  #   res.get_fields('set-cookie') # => Array +  #   res.to_hash['set-cookie']    # => Array +  #   puts "Headers: #{res.to_hash.inspect}" +  # +  #   # Status +  #   puts res.code       # => '200' +  #   puts res.message    # => 'OK' +  #   puts res.class.name # => 'HTTPOK' +  # +  #   # Body +  #   puts res.body if res.response_body_permitted? +  # +  # === Following Redirection +  # +  # Each Net::HTTPResponse object belongs to a class for its response code. +  # +  # For example, all 2XX responses are instances of a Net::HTTPSuccess +  # subclass, a 3XX response is an instance of a Net::HTTPRedirection +  # subclass and a 200 response is an instance of the Net::HTTPOK class.  For +  # details of response classes, see the section "HTTP Response Classes" +  # below. +  # +  # Using a case statement you can handle various types of responses properly: +  # +  #   def fetch(uri_str, limit = 10) +  #     # You should choose a better exception. +  #     raise ArgumentError, 'too many HTTP redirects' if limit == 0 +  # +  #     response = Net::HTTP.get_response(URI(uri_str)) +  # +  #     case response +  #     when Net::HTTPSuccess then +  #       response +  #     when Net::HTTPRedirection then +  #       location = response['location'] +  #       warn "redirected to #{location}" +  #       fetch(location, limit - 1) +  #     else +  #       response.value +  #     end +  #   end +  # +  #   print fetch('http://www.ruby-lang.org') +  # +  # === POST +  # +  # A POST can be made using the Net::HTTP::Post request class.  This example +  # creates a urlencoded POST body: +  # +  #   uri = URI('http://www.example.com/todo.cgi') +  #   req = Net::HTTP::Post.new(uri) +  #   req.set_form_data('from' => '2005-01-01', 'to' => '2005-03-31') +  # +  #   res = Net::HTTP.start(uri.hostname, uri.port) do |http| +  #     http.request(req) +  #   end +  # +  #   case res +  #   when Net::HTTPSuccess, Net::HTTPRedirection +  #     # OK +  #   else +  #     res.value +  #   end +  # +  # At this time Net::HTTP does not support multipart/form-data.  To send +  # multipart/form-data use Net::HTTPRequest#body= and +  # Net::HTTPRequest#content_type=: +  # +  #   req = Net::HTTP::Post.new(uri) +  #   req.body = multipart_data +  #   req.content_type = 'multipart/form-data' +  # +  # Other requests that can contain a body such as PUT can be created in the +  # same way using the corresponding request class (Net::HTTP::Put). +  # +  # === Setting Headers +  # +  # The following example performs a conditional GET using the +  # If-Modified-Since header.  If the files has not been modified since the +  # time in the header a Not Modified response will be returned.  See RFC 2616 +  # section 9.3 for further details. +  # +  #   uri = URI('http://example.com/cached_response') +  #   file = File.stat 'cached_response' +  # +  #   req = Net::HTTP::Get.new(uri) +  #   req['If-Modified-Since'] = file.mtime.rfc2822 +  # +  #   res = Net::HTTP.start(uri.hostname, uri.port) {|http| +  #     http.request(req) +  #   } +  # +  #   open 'cached_response', 'w' do |io| +  #     io.write res.body +  #   end if res.is_a?(Net::HTTPSuccess) +  # +  # === Basic Authentication +  # +  # Basic authentication is performed according to +  # [RFC2617](http://www.ietf.org/rfc/rfc2617.txt) +  # +  #   uri = URI('http://example.com/index.html?key=value') +  # +  #   req = Net::HTTP::Get.new(uri) +  #   req.basic_auth 'user', 'pass' +  # +  #   res = Net::HTTP.start(uri.hostname, uri.port) {|http| +  #     http.request(req) +  #   } +  #   puts res.body +  # +  # === Streaming Response Bodies +  # +  # By default Net::HTTP reads an entire response into memory.  If you are +  # handling large files or wish to implement a progress bar you can instead +  # stream the body directly to an IO. +  # +  #   uri = URI('http://example.com/large_file') +  # +  #   Net::HTTP.start(uri.host, uri.port) do |http| +  #     request = Net::HTTP::Get.new uri +  # +  #     http.request request do |response| +  #       open 'large_file', 'w' do |io| +  #         response.read_body do |chunk| +  #           io.write chunk +  #         end +  #       end +  #     end +  #   end +  # +  # === HTTPS +  # +  # HTTPS is enabled for an HTTP connection by Net::HTTP#use_ssl=. +  # +  #   uri = URI('https://secure.example.com/some_path?query=string') +  # +  #   Net::HTTP.start(uri.host, uri.port, +  #     :use_ssl => uri.scheme == 'https') do |http| +  #     request = Net::HTTP::Get.new uri +  # +  #     response = http.request request # Net::HTTPResponse object +  #   end +  # +  # In previous versions of Ruby you would need to require 'net/https' to use +  # HTTPS.  This is no longer true. +  # +  # === Proxies +  # +  # Net::HTTP will automatically create a proxy from the +http_proxy+ +  # environment variable if it is present.  To disable use of +http_proxy+, +  # pass +nil+ for the proxy address. +  # +  # You may also create a custom proxy: +  # +  #   proxy_addr = 'your.proxy.host' +  #   proxy_port = 8080 +  # +  #   Net::HTTP.new('example.com', nil, proxy_addr, proxy_port).start { |http| +  #     # always proxy via your.proxy.addr:8080 +  #   } +  # +  # See Net::HTTP.new for further details and examples such as proxies that +  # require a username and password. +  # +  # === Compression +  # +  # Net::HTTP automatically adds Accept-Encoding for compression of response +  # bodies and automatically decompresses gzip and deflate responses unless a +  # Range header was sent. +  # +  # Compression can be disabled through the Accept-Encoding: identity header. +  # +  # == HTTP Request Classes +  # +  # Here is the HTTP request class hierarchy. +  # +  # * Net::HTTPRequest +  #   * Net::HTTP::Get +  #   * Net::HTTP::Head +  #   * Net::HTTP::Post +  #   * Net::HTTP::Patch +  #   * Net::HTTP::Put +  #   * Net::HTTP::Proppatch +  #   * Net::HTTP::Lock +  #   * Net::HTTP::Unlock +  #   * Net::HTTP::Options +  #   * Net::HTTP::Propfind +  #   * Net::HTTP::Delete +  #   * Net::HTTP::Move +  #   * Net::HTTP::Copy +  #   * Net::HTTP::Mkcol +  #   * Net::HTTP::Trace +  # +  # == HTTP Response Classes +  # +  # Here is HTTP response class hierarchy.  All classes are defined in Net +  # module and are subclasses of Net::HTTPResponse. +  # +  # HTTPUnknownResponse:: For unhandled HTTP extensions +  # HTTPInformation::                    1xx +  #   HTTPContinue::                        100 +  #   HTTPSwitchProtocol::                  101 +  # HTTPSuccess::                        2xx +  #   HTTPOK::                              200 +  #   HTTPCreated::                         201 +  #   HTTPAccepted::                        202 +  #   HTTPNonAuthoritativeInformation::     203 +  #   HTTPNoContent::                       204 +  #   HTTPResetContent::                    205 +  #   HTTPPartialContent::                  206 +  #   HTTPMultiStatus::                     207 +  #   HTTPIMUsed::                          226 +  # HTTPRedirection::                    3xx +  #   HTTPMultipleChoices::                 300 +  #   HTTPMovedPermanently::                301 +  #   HTTPFound::                           302 +  #   HTTPSeeOther::                        303 +  #   HTTPNotModified::                     304 +  #   HTTPUseProxy::                        305 +  #   HTTPTemporaryRedirect::               307 +  # HTTPClientError::                    4xx +  #   HTTPBadRequest::                      400 +  #   HTTPUnauthorized::                    401 +  #   HTTPPaymentRequired::                 402 +  #   HTTPForbidden::                       403 +  #   HTTPNotFound::                        404 +  #   HTTPMethodNotAllowed::                405 +  #   HTTPNotAcceptable::                   406 +  #   HTTPProxyAuthenticationRequired::     407 +  #   HTTPRequestTimeOut::                  408 +  #   HTTPConflict::                        409 +  #   HTTPGone::                            410 +  #   HTTPLengthRequired::                  411 +  #   HTTPPreconditionFailed::              412 +  #   HTTPRequestEntityTooLarge::           413 +  #   HTTPRequestURITooLong::               414 +  #   HTTPUnsupportedMediaType::            415 +  #   HTTPRequestedRangeNotSatisfiable::    416 +  #   HTTPExpectationFailed::               417 +  #   HTTPUnprocessableEntity::             422 +  #   HTTPLocked::                          423 +  #   HTTPFailedDependency::                424 +  #   HTTPUpgradeRequired::                 426 +  #   HTTPPreconditionRequired::            428 +  #   HTTPTooManyRequests::                 429 +  #   HTTPRequestHeaderFieldsTooLarge::     431 +  # HTTPServerError::                    5xx +  #   HTTPInternalServerError::             500 +  #   HTTPNotImplemented::                  501 +  #   HTTPBadGateway::                      502 +  #   HTTPServiceUnavailable::              503 +  #   HTTPGatewayTimeOut::                  504 +  #   HTTPVersionNotSupported::             505 +  #   HTTPInsufficientStorage::             507 +  #   HTTPNetworkAuthenticationRequired::   511 +  # +  # There is also the Net::HTTPBadResponse exception which is raised when +  # there is a protocol error. +  # +  class HTTP < Protocol + +    # :stopdoc: +    Revision = %q$Revision: 49278 $.split[1] +    HTTPVersion = '1.1' +    begin +      require 'zlib' +      require 'stringio'  #for our purposes (unpacking gzip) lump these together +      HAVE_ZLIB=true +    rescue LoadError +      HAVE_ZLIB=false +    end +    # :startdoc: + +    # Turns on net/http 1.2 (Ruby 1.8) features. +    # Defaults to ON in Ruby 1.8 or later. +    def HTTP.version_1_2 +      true +    end + +    # Returns true if net/http is in version 1.2 mode. +    # Defaults to true. +    def HTTP.version_1_2? +      true +    end + +    def HTTP.version_1_1?  #:nodoc: +      false +    end + +    class << HTTP +      alias is_version_1_1? version_1_1?   #:nodoc: +      alias is_version_1_2? version_1_2?   #:nodoc: +    end + +    # +    # short cut methods +    # + +    # +    # Gets the body text from the target and outputs it to $stdout.  The +    # target can either be specified as +    # (+uri+), or as (+host+, +path+, +port+ = 80); so: +    # +    #    Net::HTTP.get_print URI('http://www.example.com/index.html') +    # +    # or: +    # +    #    Net::HTTP.get_print 'www.example.com', '/index.html' +    # +    def HTTP.get_print(uri_or_host, path = nil, port = nil) +      get_response(uri_or_host, path, port) {|res| +        res.read_body do |chunk| +          $stdout.print chunk +        end +      } +      nil +    end + +    # Sends a GET request to the target and returns the HTTP response +    # as a string.  The target can either be specified as +    # (+uri+), or as (+host+, +path+, +port+ = 80); so: +    # +    #    print Net::HTTP.get(URI('http://www.example.com/index.html')) +    # +    # or: +    # +    #    print Net::HTTP.get('www.example.com', '/index.html') +    # +    def HTTP.get(uri_or_host, path = nil, port = nil) +      get_response(uri_or_host, path, port).body +    end + +    # Sends a GET request to the target and returns the HTTP response +    # as a Net::HTTPResponse object.  The target can either be specified as +    # (+uri+), or as (+host+, +path+, +port+ = 80); so: +    # +    #    res = Net::HTTP.get_response(URI('http://www.example.com/index.html')) +    #    print res.body +    # +    # or: +    # +    #    res = Net::HTTP.get_response('www.example.com', '/index.html') +    #    print res.body +    # +    def HTTP.get_response(uri_or_host, path = nil, port = nil, &block) +      if path +        host = uri_or_host +        new(host, port || HTTP.default_port).start {|http| +          return http.request_get(path, &block) +        } +      else +        uri = uri_or_host +        start(uri.hostname, uri.port, +              :use_ssl => uri.scheme == 'https') {|http| +          return http.request_get(uri, &block) +        } +      end +    end + +    # Posts HTML form data to the specified URI object. +    # The form data must be provided as a Hash mapping from String to String. +    # Example: +    # +    #   { "cmd" => "search", "q" => "ruby", "max" => "50" } +    # +    # This method also does Basic Authentication iff +url+.user exists. +    # But userinfo for authentication is deprecated (RFC3986). +    # So this feature will be removed. +    # +    # Example: +    # +    #   require 'net/http' +    #   require 'uri' +    # +    #   Net::HTTP.post_form URI('http://www.example.com/search.cgi'), +    #                       { "q" => "ruby", "max" => "50" } +    # +    def HTTP.post_form(url, params) +      req = Post.new(url) +      req.form_data = params +      req.basic_auth url.user, url.password if url.user +      start(url.hostname, url.port, +            :use_ssl => url.scheme == 'https' ) {|http| +        http.request(req) +      } +    end + +    # +    # HTTP session management +    # + +    # The default port to use for HTTP requests; defaults to 80. +    def HTTP.default_port +      http_default_port() +    end + +    # The default port to use for HTTP requests; defaults to 80. +    def HTTP.http_default_port +      80 +    end + +    # The default port to use for HTTPS requests; defaults to 443. +    def HTTP.https_default_port +      443 +    end + +    def HTTP.socket_type   #:nodoc: obsolete +      BufferedIO +    end + +    # :call-seq: +    #   HTTP.start(address, port, p_addr, p_port, p_user, p_pass, &block) +    #   HTTP.start(address, port=nil, p_addr=nil, p_port=nil, p_user=nil, p_pass=nil, opt, &block) +    # +    # Creates a new Net::HTTP object, then additionally opens the TCP +    # connection and HTTP session. +    # +    # Arguments are the following: +    # _address_ :: hostname or IP address of the server +    # _port_    :: port of the server +    # _p_addr_  :: address of proxy +    # _p_port_  :: port of proxy +    # _p_user_  :: user of proxy +    # _p_pass_  :: pass of proxy +    # _opt_     :: optional hash +    # +    # _opt_ sets following values by its accessor. +    # The keys are ca_file, ca_path, cert, cert_store, ciphers, +    # close_on_empty_response, key, open_timeout, read_timeout, ssl_timeout, +    # ssl_version, use_ssl, verify_callback, verify_depth and verify_mode. +    # If you set :use_ssl as true, you can use https and default value of +    # verify_mode is set as OpenSSL::SSL::VERIFY_PEER. +    # +    # If the optional block is given, the newly +    # created Net::HTTP object is passed to it and closed when the +    # block finishes.  In this case, the return value of this method +    # is the return value of the block.  If no block is given, the +    # return value of this method is the newly created Net::HTTP object +    # itself, and the caller is responsible for closing it upon completion +    # using the finish() method. +    def HTTP.start(address, *arg, &block) # :yield: +http+ +      arg.pop if opt = Hash.try_convert(arg[-1]) +      port, p_addr, p_port, p_user, p_pass = *arg +      port = https_default_port if !port && opt && opt[:use_ssl] +      http = new(address, port, p_addr, p_port, p_user, p_pass) + +      if opt +        if opt[:use_ssl] +          opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt) +        end +        http.methods.grep(/\A(\w+)=\z/) do |meth| +          key = $1.to_sym +          opt.key?(key) or next +          http.__send__(meth, opt[key]) +        end +      end + +      http.start(&block) +    end + +    class << HTTP +      alias newobj new # :nodoc: +    end + +    # Creates a new Net::HTTP object without opening a TCP connection or +    # HTTP session. +    # +    # The +address+ should be a DNS hostname or IP address, the +port+ is the +    # port the server operates on.  If no +port+ is given the default port for +    # HTTP or HTTPS is used. +    # +    # If none of the +p_+ arguments are given, the proxy host and port are +    # taken from the +http_proxy+ environment variable (or its uppercase +    # equivalent) if present.  If the proxy requires authentication you must +    # supply it by hand.  See URI::Generic#find_proxy for details of proxy +    # detection from the environment.  To disable proxy detection set +p_addr+ +    # to nil. +    # +    # If you are connecting to a custom proxy, +p_addr+ the DNS name or IP +    # address of the proxy host, +p_port+ the port to use to access the proxy, +    # and +p_user+ and +p_pass+ the username and password if authorization is +    # required to use the proxy. +    # +    def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil) +      http = super address, port + +      if proxy_class? then # from Net::HTTP::Proxy() +        http.proxy_from_env = @proxy_from_env +        http.proxy_address  = @proxy_address +        http.proxy_port     = @proxy_port +        http.proxy_user     = @proxy_user +        http.proxy_pass     = @proxy_pass +      elsif p_addr == :ENV then +        http.proxy_from_env = true +      else +        http.proxy_address = p_addr +        http.proxy_port    = p_port || default_port +        http.proxy_user    = p_user +        http.proxy_pass    = p_pass +      end + +      http +    end + +    # Creates a new Net::HTTP object for the specified server address, +    # without opening the TCP connection or initializing the HTTP session. +    # The +address+ should be a DNS hostname or IP address. +    def initialize(address, port = nil) +      @address = address +      @port    = (port || HTTP.default_port) +      @local_host = nil +      @local_port = nil +      @curr_http_version = HTTPVersion +      @keep_alive_timeout = 2 +      @last_communicated = nil +      @close_on_empty_response = false +      @socket  = nil +      @started = false +      @open_timeout = nil +      @read_timeout = 60 +      @continue_timeout = nil +      @debug_output = nil + +      @proxy_from_env = false +      @proxy_uri      = nil +      @proxy_address  = nil +      @proxy_port     = nil +      @proxy_user     = nil +      @proxy_pass     = nil + +      @use_ssl = false +      @ssl_context = nil +      @ssl_session = nil +      @enable_post_connection_check = true +      @sspi_enabled = false +      SSL_IVNAMES.each do |ivname| +        instance_variable_set ivname, nil +      end +    end + +    def inspect +      "#<#{self.class} #{@address}:#{@port} open=#{started?}>" +    end + +    # *WARNING* This method opens a serious security hole. +    # Never use this method in production code. +    # +    # Sets an output stream for debugging. +    # +    #   http = Net::HTTP.new(hostname) +    #   http.set_debug_output $stderr +    #   http.start { .... } +    # +    def set_debug_output(output) +      warn 'Net::HTTP#set_debug_output called after HTTP started' if started? +      @debug_output = output +    end + +    # The DNS host name or IP address to connect to. +    attr_reader :address + +    # The port number to connect to. +    attr_reader :port + +    # The local host used to establish the connection. +    attr_accessor :local_host + +    # The local port used to establish the connection. +    attr_accessor :local_port + +    attr_writer :proxy_from_env +    attr_writer :proxy_address +    attr_writer :proxy_port +    attr_writer :proxy_user +    attr_writer :proxy_pass + +    # Number of seconds to wait for the connection to open. Any number +    # may be used, including Floats for fractional seconds. If the HTTP +    # object cannot open a connection in this many seconds, it raises a +    # Net::OpenTimeout exception. The default value is +nil+. +    attr_accessor :open_timeout + +    # Number of seconds to wait for one block to be read (via one read(2) +    # call). Any number may be used, including Floats for fractional +    # seconds. If the HTTP object cannot read data in this many seconds, +    # it raises a Net::ReadTimeout exception. The default value is 60 seconds. +    attr_reader :read_timeout + +    # Setter for the read_timeout attribute. +    def read_timeout=(sec) +      @socket.read_timeout = sec if @socket +      @read_timeout = sec +    end + +    # Seconds to wait for 100 Continue response. If the HTTP object does not +    # receive a response in this many seconds it sends the request body. The +    # default value is +nil+. +    attr_reader :continue_timeout + +    # Setter for the continue_timeout attribute. +    def continue_timeout=(sec) +      @socket.continue_timeout = sec if @socket +      @continue_timeout = sec +    end + +    # Seconds to reuse the connection of the previous request. +    # If the idle time is less than this Keep-Alive Timeout, +    # Net::HTTP reuses the TCP/IP socket used by the previous communication. +    # The default value is 2 seconds. +    attr_accessor :keep_alive_timeout + +    # Returns true if the HTTP session has been started. +    def started? +      @started +    end + +    alias active? started?   #:nodoc: obsolete + +    attr_accessor :close_on_empty_response + +    # Returns true if SSL/TLS is being used with HTTP. +    def use_ssl? +      @use_ssl +    end + +    # Turn on/off SSL. +    # This flag must be set before starting session. +    # If you change use_ssl value after session started, +    # a Net::HTTP object raises IOError. +    def use_ssl=(flag) +      flag = flag ? true : false +      if started? and @use_ssl != flag +        raise IOError, "use_ssl value changed, but session already started" +      end +      @use_ssl = flag +    end + +    SSL_IVNAMES = [ +      :@ca_file, +      :@ca_path, +      :@cert, +      :@cert_store, +      :@ciphers, +      :@key, +      :@ssl_timeout, +      :@ssl_version, +      :@verify_callback, +      :@verify_depth, +      :@verify_mode, +    ] +    SSL_ATTRIBUTES = [ +      :ca_file, +      :ca_path, +      :cert, +      :cert_store, +      :ciphers, +      :key, +      :ssl_timeout, +      :ssl_version, +      :verify_callback, +      :verify_depth, +      :verify_mode, +    ] + +    # Sets path of a CA certification file in PEM format. +    # +    # The file can contain several CA certificates. +    attr_accessor :ca_file + +    # Sets path of a CA certification directory containing certifications in +    # PEM format. +    attr_accessor :ca_path + +    # Sets an OpenSSL::X509::Certificate object as client certificate. +    # (This method is appeared in Michal Rokos's OpenSSL extension). +    attr_accessor :cert + +    # Sets the X509::Store to verify peer certificate. +    attr_accessor :cert_store + +    # Sets the available ciphers.  See OpenSSL::SSL::SSLContext#ciphers= +    attr_accessor :ciphers + +    # Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. +    # (This method is appeared in Michal Rokos's OpenSSL extension.) +    attr_accessor :key + +    # Sets the SSL timeout seconds. +    attr_accessor :ssl_timeout + +    # Sets the SSL version.  See OpenSSL::SSL::SSLContext#ssl_version= +    attr_accessor :ssl_version + +    # Sets the verify callback for the server certification verification. +    attr_accessor :verify_callback + +    # Sets the maximum depth for the certificate chain verification. +    attr_accessor :verify_depth + +    # Sets the flags for server the certification verification at beginning of +    # SSL/TLS session. +    # +    # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable. +    attr_accessor :verify_mode + +    # Returns the X.509 certificates the server presented. +    def peer_cert +      if not use_ssl? or not @socket +        return nil +      end +      @socket.io.peer_cert +    end + +    # Opens a TCP connection and HTTP session. +    # +    # When this method is called with a block, it passes the Net::HTTP +    # object to the block, and closes the TCP connection and HTTP session +    # after the block has been executed. +    # +    # When called with a block, it returns the return value of the +    # block; otherwise, it returns self. +    # +    def start  # :yield: http +      raise IOError, 'HTTP session already opened' if @started +      if block_given? +        begin +          do_start +          return yield(self) +        ensure +          do_finish +        end +      end +      do_start +      self +    end + +    def do_start +      connect +      @started = true +    end +    private :do_start + +    def connect +      if proxy? then +        conn_address = proxy_address +        conn_port    = proxy_port +      else +        conn_address = address +        conn_port    = port +      end + +      D "opening connection to #{conn_address}:#{conn_port}..." +      s = Timeout.timeout(@open_timeout, Net::OpenTimeout) { +        TCPSocket.open(conn_address, conn_port, @local_host, @local_port) +      } +      s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) +      D "opened" +      if use_ssl? +        ssl_parameters = Hash.new +        iv_list = instance_variables +        SSL_IVNAMES.each_with_index do |ivname, i| +          if iv_list.include?(ivname) and +            value = instance_variable_get(ivname) +            ssl_parameters[SSL_ATTRIBUTES[i]] = value if value +          end +        end +        @ssl_context = OpenSSL::SSL::SSLContext.new +        @ssl_context.set_params(ssl_parameters) +        D "starting SSL for #{conn_address}:#{conn_port}..." +        s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) +        s.sync_close = true +        D "SSL established" +      end +      @socket = BufferedIO.new(s) +      @socket.read_timeout = @read_timeout +      @socket.continue_timeout = @continue_timeout +      @socket.debug_output = @debug_output +      if use_ssl? +        begin +          if proxy? +            buf = "CONNECT #{@address}:#{@port} HTTP/#{HTTPVersion}\r\n" +            buf << "Host: #{@address}:#{@port}\r\n" +            if proxy_user +              credential = ["#{proxy_user}:#{proxy_pass}"].pack('m') +              credential.delete!("\r\n") +              buf << "Proxy-Authorization: Basic #{credential}\r\n" +            end +            buf << "\r\n" +            @socket.write(buf) +            HTTPResponse.read_new(@socket).value +          end +          if @ssl_session and +             Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout +            s.session = @ssl_session if @ssl_session +          end +          # Server Name Indication (SNI) RFC 3546 +          s.hostname = @address if s.respond_to? :hostname= +          Timeout.timeout(@open_timeout, Net::OpenTimeout) { s.connect } +          if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE +            s.post_connection_check(@address) +          end +          @ssl_session = s.session +        rescue => exception +          D "Conn close because of connect error #{exception}" +          @socket.close if @socket and not @socket.closed? +          raise exception +        end +      end +      on_connect +    end +    private :connect + +    def on_connect +    end +    private :on_connect + +    # Finishes the HTTP session and closes the TCP connection. +    # Raises IOError if the session has not been started. +    def finish +      raise IOError, 'HTTP session not yet started' unless started? +      do_finish +    end + +    def do_finish +      @started = false +      @socket.close if @socket and not @socket.closed? +      @socket = nil +    end +    private :do_finish + +    # +    # proxy +    # + +    public + +    # no proxy +    @is_proxy_class = false +    @proxy_from_env = false +    @proxy_addr = nil +    @proxy_port = nil +    @proxy_user = nil +    @proxy_pass = nil + +    # Creates an HTTP proxy class which behaves like Net::HTTP, but +    # performs all access via the specified proxy. +    # +    # This class is obsolete.  You may pass these same parameters directly to +    # Net::HTTP.new.  See Net::HTTP.new for details of the arguments. +    def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil) +      return self unless p_addr + +      Class.new(self) { +        @is_proxy_class = true + +        if p_addr == :ENV then +          @proxy_from_env = true +          @proxy_address = nil +          @proxy_port    = nil +        else +          @proxy_from_env = false +          @proxy_address = p_addr +          @proxy_port    = p_port || default_port +        end + +        @proxy_user = p_user +        @proxy_pass = p_pass +      } +    end + +    class << HTTP +      # returns true if self is a class which was created by HTTP::Proxy. +      def proxy_class? +        defined?(@is_proxy_class) ? @is_proxy_class : false +      end + +      # Address of proxy host. If Net::HTTP does not use a proxy, nil. +      attr_reader :proxy_address + +      # Port number of proxy host. If Net::HTTP does not use a proxy, nil. +      attr_reader :proxy_port + +      # User name for accessing proxy. If Net::HTTP does not use a proxy, nil. +      attr_reader :proxy_user + +      # User password for accessing proxy. If Net::HTTP does not use a proxy, +      # nil. +      attr_reader :proxy_pass +    end + +    # True if requests for this connection will be proxied +    def proxy? +      !!if @proxy_from_env then +        proxy_uri +      else +        @proxy_address +      end +    end + +    # True if the proxy for this connection is determined from the environment +    def proxy_from_env? +      @proxy_from_env +    end + +    # The proxy URI determined from the environment for this connection. +    def proxy_uri # :nodoc: +      @proxy_uri ||= URI::HTTP.new( +        "http".freeze, nil, address, port, nil, nil, nil, nil, nil +      ).find_proxy +    end + +    # The address of the proxy server, if one is configured. +    def proxy_address +      if @proxy_from_env then +        proxy_uri && proxy_uri.hostname +      else +        @proxy_address +      end +    end + +    # The port of the proxy server, if one is configured. +    def proxy_port +      if @proxy_from_env then +        proxy_uri && proxy_uri.port +      else +        @proxy_port +      end +    end + +    # The proxy username, if one is configured +    def proxy_user +      @proxy_user +    end + +    # The proxy password, if one is configured +    def proxy_pass +      @proxy_pass +    end + +    alias proxyaddr proxy_address   #:nodoc: obsolete +    alias proxyport proxy_port      #:nodoc: obsolete + +    private + +    # without proxy, obsolete + +    def conn_address # :nodoc: +      address() +    end + +    def conn_port # :nodoc: +      port() +    end + +    def edit_path(path) +      if proxy? and not use_ssl? then +        "http://#{addr_port}#{path}" +      else +        path +      end +    end + +    # +    # HTTP operations +    # + +    public + +    # Retrieves data from +path+ on the connected-to host which may be an +    # absolute path String or a URI to extract the path from. +    # +    # +initheader+ must be a Hash like { 'Accept' => '*/*', ... }, +    # and it defaults to an empty hash. +    # If +initheader+ doesn't have the key 'accept-encoding', then +    # a value of "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" is used, +    # so that gzip compression is used in preference to deflate +    # compression, which is used in preference to no compression. +    # Ruby doesn't have libraries to support the compress (Lempel-Ziv) +    # compression, so that is not supported.  The intent of this is +    # to reduce bandwidth by default.   If this routine sets up +    # compression, then it does the decompression also, removing +    # the header as well to prevent confusion.  Otherwise +    # it leaves the body as it found it. +    # +    # This method returns a Net::HTTPResponse object. +    # +    # If called with a block, yields each fragment of the +    # entity body in turn as a string as it is read from +    # the socket.  Note that in this case, the returned response +    # object will *not* contain a (meaningful) body. +    # +    # +dest+ argument is obsolete. +    # It still works but you must not use it. +    # +    # This method never raises an exception. +    # +    #     response = http.get('/index.html') +    # +    #     # using block +    #     File.open('result.txt', 'w') {|f| +    #       http.get('/~foo/') do |str| +    #         f.write str +    #       end +    #     } +    # +    def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+ +      res = nil +      request(Get.new(path, initheader)) {|r| +        r.read_body dest, &block +        res = r +      } +      res +    end + +    # Gets only the header from +path+ on the connected-to host. +    # +header+ is a Hash like { 'Accept' => '*/*', ... }. +    # +    # This method returns a Net::HTTPResponse object. +    # +    # This method never raises an exception. +    # +    #     response = nil +    #     Net::HTTP.start('some.www.server', 80) {|http| +    #       response = http.head('/index.html') +    #     } +    #     p response['content-type'] +    # +    def head(path, initheader = nil) +      request(Head.new(path, initheader)) +    end + +    # Posts +data+ (must be a String) to +path+. +header+ must be a Hash +    # like { 'Accept' => '*/*', ... }. +    # +    # This method returns a Net::HTTPResponse object. +    # +    # If called with a block, yields each fragment of the +    # entity body in turn as a string as it is read from +    # the socket.  Note that in this case, the returned response +    # object will *not* contain a (meaningful) body. +    # +    # +dest+ argument is obsolete. +    # It still works but you must not use it. +    # +    # This method never raises exception. +    # +    #     response = http.post('/cgi-bin/search.rb', 'query=foo') +    # +    #     # using block +    #     File.open('result.txt', 'w') {|f| +    #       http.post('/cgi-bin/search.rb', 'query=foo') do |str| +    #         f.write str +    #       end +    #     } +    # +    # You should set Content-Type: header field for POST. +    # If no Content-Type: field given, this method uses +    # "application/x-www-form-urlencoded" by default. +    # +    def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ +      send_entity(path, data, initheader, dest, Post, &block) +    end + +    # Sends a PATCH request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ +      send_entity(path, data, initheader, dest, Patch, &block) +    end + +    def put(path, data, initheader = nil)   #:nodoc: +      request(Put.new(path, initheader), data) +    end + +    # Sends a PROPPATCH request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def proppatch(path, body, initheader = nil) +      request(Proppatch.new(path, initheader), body) +    end + +    # Sends a LOCK request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def lock(path, body, initheader = nil) +      request(Lock.new(path, initheader), body) +    end + +    # Sends a UNLOCK request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def unlock(path, body, initheader = nil) +      request(Unlock.new(path, initheader), body) +    end + +    # Sends a OPTIONS request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def options(path, initheader = nil) +      request(Options.new(path, initheader)) +    end + +    # Sends a PROPFIND request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def propfind(path, body = nil, initheader = {'Depth' => '0'}) +      request(Propfind.new(path, initheader), body) +    end + +    # Sends a DELETE request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def delete(path, initheader = {'Depth' => 'Infinity'}) +      request(Delete.new(path, initheader)) +    end + +    # Sends a MOVE request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def move(path, initheader = nil) +      request(Move.new(path, initheader)) +    end + +    # Sends a COPY request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def copy(path, initheader = nil) +      request(Copy.new(path, initheader)) +    end + +    # Sends a MKCOL request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def mkcol(path, body = nil, initheader = nil) +      request(Mkcol.new(path, initheader), body) +    end + +    # Sends a TRACE request to the +path+ and gets a response, +    # as an HTTPResponse object. +    def trace(path, initheader = nil) +      request(Trace.new(path, initheader)) +    end + +    # Sends a GET request to the +path+. +    # Returns the response as a Net::HTTPResponse object. +    # +    # When called with a block, passes an HTTPResponse object to the block. +    # The body of the response will not have been read yet; +    # the block can process it using HTTPResponse#read_body, +    # if desired. +    # +    # Returns the response. +    # +    # This method never raises Net::* exceptions. +    # +    #     response = http.request_get('/index.html') +    #     # The entity body is already read in this case. +    #     p response['content-type'] +    #     puts response.body +    # +    #     # Using a block +    #     http.request_get('/index.html') {|response| +    #       p response['content-type'] +    #       response.read_body do |str|   # read body now +    #         print str +    #       end +    #     } +    # +    def request_get(path, initheader = nil, &block) # :yield: +response+ +      request(Get.new(path, initheader), &block) +    end + +    # Sends a HEAD request to the +path+ and returns the response +    # as a Net::HTTPResponse object. +    # +    # Returns the response. +    # +    # This method never raises Net::* exceptions. +    # +    #     response = http.request_head('/index.html') +    #     p response['content-type'] +    # +    def request_head(path, initheader = nil, &block) +      request(Head.new(path, initheader), &block) +    end + +    # Sends a POST request to the +path+. +    # +    # Returns the response as a Net::HTTPResponse object. +    # +    # When called with a block, the block is passed an HTTPResponse +    # object.  The body of that response will not have been read yet; +    # the block can process it using HTTPResponse#read_body, if desired. +    # +    # Returns the response. +    # +    # This method never raises Net::* exceptions. +    # +    #     # example +    #     response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...') +    #     p response.status +    #     puts response.body          # body is already read in this case +    # +    #     # using block +    #     http.request_post('/cgi-bin/nice.rb', 'datadatadata...') {|response| +    #       p response.status +    #       p response['content-type'] +    #       response.read_body do |str|   # read body now +    #         print str +    #       end +    #     } +    # +    def request_post(path, data, initheader = nil, &block) # :yield: +response+ +      request Post.new(path, initheader), data, &block +    end + +    def request_put(path, data, initheader = nil, &block)   #:nodoc: +      request Put.new(path, initheader), data, &block +    end + +    alias get2   request_get    #:nodoc: obsolete +    alias head2  request_head   #:nodoc: obsolete +    alias post2  request_post   #:nodoc: obsolete +    alias put2   request_put    #:nodoc: obsolete + + +    # Sends an HTTP request to the HTTP server. +    # Also sends a DATA string if +data+ is given. +    # +    # Returns a Net::HTTPResponse object. +    # +    # This method never raises Net::* exceptions. +    # +    #    response = http.send_request('GET', '/index.html') +    #    puts response.body +    # +    def send_request(name, path, data = nil, header = nil) +      has_response_body = name != 'HEAD' +      r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header) +      request r, data +    end + +    # Sends an HTTPRequest object +req+ to the HTTP server. +    # +    # If +req+ is a Net::HTTP::Post or Net::HTTP::Put request containing +    # data, the data is also sent. Providing data for a Net::HTTP::Head or +    # Net::HTTP::Get request results in an ArgumentError. +    # +    # Returns an HTTPResponse object. +    # +    # When called with a block, passes an HTTPResponse object to the block. +    # The body of the response will not have been read yet; +    # the block can process it using HTTPResponse#read_body, +    # if desired. +    # +    # This method never raises Net::* exceptions. +    # +    def request(req, body = nil, &block)  # :yield: +response+ +      unless started? +        start { +          req['connection'] ||= 'close' +          return request(req, body, &block) +        } +      end +      if proxy_user() +        req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl? +      end +      req.set_body_internal body +      res = transport_request(req, &block) +      if sspi_auth?(res) +        sspi_auth(req) +        res = transport_request(req, &block) +      end +      res +    end + +    private + +    # Executes a request which uses a representation +    # and returns its body. +    def send_entity(path, data, initheader, dest, type, &block) +      res = nil +      request(type.new(path, initheader), data) {|r| +        r.read_body dest, &block +        res = r +      } +      res +    end + +    IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc: + +    def transport_request(req) +      count = 0 +      begin +        begin_transport req +        res = catch(:response) { +          req.exec @socket, @curr_http_version, edit_path(req.path) +          begin +            res = HTTPResponse.read_new(@socket) +            res.decode_content = req.decode_content +          end while res.kind_of?(HTTPContinue) + +          res.uri = req.uri + +          res.reading_body(@socket, req.response_body_permitted?) { +            yield res if block_given? +          } +          res +        } +      rescue Net::OpenTimeout +        raise +      rescue Net::ReadTimeout, IOError, EOFError, +             Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, +             # avoid a dependency on OpenSSL +             defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError, +             Timeout::Error => exception +        if count == 0 && IDEMPOTENT_METHODS_.include?(req.method) +          count += 1 +          @socket.close if @socket and not @socket.closed? +          D "Conn close because of error #{exception}, and retry" +          retry +        end +        D "Conn close because of error #{exception}" +        @socket.close if @socket and not @socket.closed? +        raise +      end + +      end_transport req, res +      res +    rescue => exception +      D "Conn close because of error #{exception}" +      @socket.close if @socket and not @socket.closed? +      raise exception +    end + +    def begin_transport(req) +      if @socket.closed? +        connect +      elsif @last_communicated && @last_communicated + @keep_alive_timeout < Time.now +        D 'Conn close because of keep_alive_timeout' +        @socket.close +        connect +      end + +      if not req.response_body_permitted? and @close_on_empty_response +        req['connection'] ||= 'close' +      end + +      req.update_uri address, port, use_ssl? +      req['host'] ||= addr_port() +    end + +    def end_transport(req, res) +      @curr_http_version = res.http_version +      @last_communicated = nil +      if @socket.closed? +        D 'Conn socket closed' +      elsif not res.body and @close_on_empty_response +        D 'Conn close' +        @socket.close +      elsif keep_alive?(req, res) +        D 'Conn keep-alive' +        @last_communicated = Time.now +      else +        D 'Conn close' +        @socket.close +      end +    end + +    def keep_alive?(req, res) +      return false if req.connection_close? +      if @curr_http_version <= '1.0' +        res.connection_keep_alive? +      else   # HTTP/1.1 or later +        not res.connection_close? +      end +    end + +    def sspi_auth?(res) +      return false unless @sspi_enabled +      if res.kind_of?(HTTPProxyAuthenticationRequired) and +          proxy? and res["Proxy-Authenticate"].include?("Negotiate") +        begin +          require 'win32/sspi' +          true +        rescue LoadError +          false +        end +      else +        false +      end +    end + +    def sspi_auth(req) +      n = Win32::SSPI::NegotiateAuth.new +      req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}" +      # Some versions of ISA will close the connection if this isn't present. +      req["Connection"] = "Keep-Alive" +      req["Proxy-Connection"] = "Keep-Alive" +      res = transport_request(req) +      authphrase = res["Proxy-Authenticate"]  or return res +      req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}" +    rescue => err +      raise HTTPAuthenticationError.new('HTTP authentication failed', err) +    end + +    # +    # utils +    # + +    private + +    def addr_port +      if use_ssl? +        address() + (port == HTTP.https_default_port ? '' : ":#{port()}") +      else +        address() + (port == HTTP.http_default_port ? '' : ":#{port()}") +      end +    end + +    def D(msg) +      return unless @debug_output +      @debug_output << msg +      @debug_output << "\n" +    end +  end + +end + +require 'net/http/exceptions' + +require 'net/http/header' + +require 'net/http/generic_request' +require 'net/http/request' +require 'net/http/requests' + +require 'net/http/response' +require 'net/http/responses' + +require 'net/http/proxy_delta' + +require 'net/http/backward' + diff --git a/jni/ruby/lib/net/http/backward.rb b/jni/ruby/lib/net/http/backward.rb new file mode 100644 index 0000000..faf47b8 --- /dev/null +++ b/jni/ruby/lib/net/http/backward.rb @@ -0,0 +1,25 @@ +# for backward compatibility + +# :enddoc: + +class Net::HTTP +  ProxyMod = ProxyDelta +end + +module Net +  HTTPSession = Net::HTTP +end + +module Net::NetPrivate +  HTTPRequest = ::Net::HTTPRequest +end + +Net::HTTPInformationCode  = Net::HTTPInformation +Net::HTTPSuccessCode      = Net::HTTPSuccess +Net::HTTPRedirectionCode  = Net::HTTPRedirection +Net::HTTPRetriableCode    = Net::HTTPRedirection +Net::HTTPClientErrorCode  = Net::HTTPClientError +Net::HTTPFatalErrorCode   = Net::HTTPClientError +Net::HTTPServerErrorCode  = Net::HTTPServerError +Net::HTTPResponceReceiver = Net::HTTPResponse + diff --git a/jni/ruby/lib/net/http/exceptions.rb b/jni/ruby/lib/net/http/exceptions.rb new file mode 100644 index 0000000..6c5d81c --- /dev/null +++ b/jni/ruby/lib/net/http/exceptions.rb @@ -0,0 +1,25 @@ +# Net::HTTP exception class. +# You cannot use Net::HTTPExceptions directly; instead, you must use +# its subclasses. +module Net::HTTPExceptions +  def initialize(msg, res)   #:nodoc: +    super msg +    @response = res +  end +  attr_reader :response +  alias data response    #:nodoc: obsolete +end +class Net::HTTPError < Net::ProtocolError +  include Net::HTTPExceptions +end +class Net::HTTPRetriableError < Net::ProtoRetriableError +  include Net::HTTPExceptions +end +class Net::HTTPServerException < Net::ProtoServerError +  # We cannot use the name "HTTPServerError", it is the name of the response. +  include Net::HTTPExceptions +end +class Net::HTTPFatalError < Net::ProtoFatalError +  include Net::HTTPExceptions +end + diff --git a/jni/ruby/lib/net/http/generic_request.rb b/jni/ruby/lib/net/http/generic_request.rb new file mode 100644 index 0000000..00ff434 --- /dev/null +++ b/jni/ruby/lib/net/http/generic_request.rb @@ -0,0 +1,332 @@ +# HTTPGenericRequest is the parent of the HTTPRequest class. +# Do not use this directly; use a subclass of HTTPRequest. +# +# Mixes in the HTTPHeader module to provide easier access to HTTP headers. +# +class Net::HTTPGenericRequest + +  include Net::HTTPHeader + +  def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) +    @method = m +    @request_has_body = reqbody +    @response_has_body = resbody + +    if URI === uri_or_path then +      @uri = uri_or_path.dup +      host = @uri.hostname.dup +      host << ":".freeze << @uri.port.to_s if @uri.port != @uri.default_port +      @path = uri_or_path.request_uri +      raise ArgumentError, "no HTTP request path given" unless @path +    else +      @uri = nil +      host = nil +      raise ArgumentError, "no HTTP request path given" unless uri_or_path +      raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty? +      @path = uri_or_path.dup +    end + +    @decode_content = false + +    if @response_has_body and Net::HTTP::HAVE_ZLIB then +      if !initheader || +         !initheader.keys.any? { |k| +           %w[accept-encoding range].include? k.downcase +         } then +        @decode_content = true +        initheader = initheader ? initheader.dup : {} +        initheader["accept-encoding"] = +          "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" +      end +    end + +    initialize_http_header initheader +    self['Accept'] ||= '*/*' +    self['User-Agent'] ||= 'Ruby' +    self['Host'] ||= host if host +    @body = nil +    @body_stream = nil +    @body_data = nil +  end + +  attr_reader :method +  attr_reader :path +  attr_reader :uri + +  # Automatically set to false if the user sets the Accept-Encoding header. +  # This indicates they wish to handle Content-encoding in responses +  # themselves. +  attr_reader :decode_content + +  def inspect +    "\#<#{self.class} #{@method}>" +  end + +  ## +  # Don't automatically decode response content-encoding if the user indicates +  # they want to handle it. + +  def []=(key, val) # :nodoc: +    @decode_content = false if key.downcase == 'accept-encoding' + +    super key, val +  end + +  def request_body_permitted? +    @request_has_body +  end + +  def response_body_permitted? +    @response_has_body +  end + +  def body_exist? +    warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?" if $VERBOSE +    response_body_permitted? +  end + +  attr_reader :body + +  def body=(str) +    @body = str +    @body_stream = nil +    @body_data = nil +    str +  end + +  attr_reader :body_stream + +  def body_stream=(input) +    @body = nil +    @body_stream = input +    @body_data = nil +    input +  end + +  def set_body_internal(str)   #:nodoc: internal use only +    raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream) +    self.body = str if str +    if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted? +      self.body = '' +    end +  end + +  # +  # write +  # + +  def exec(sock, ver, path)   #:nodoc: internal use only +    if @body +      send_request_with_body sock, ver, path, @body +    elsif @body_stream +      send_request_with_body_stream sock, ver, path, @body_stream +    elsif @body_data +      send_request_with_body_data sock, ver, path, @body_data +    else +      write_header sock, ver, path +    end +  end + +  def update_uri(addr, port, ssl) # :nodoc: internal use only +    # reflect the connection and @path to @uri +    return unless @uri + +    if ssl +      scheme = 'https'.freeze +      klass = URI::HTTPS +    else +      scheme = 'http'.freeze +      klass = URI::HTTP +    end + +    if host = self['host'] +      host.sub!(/:.*/s, ''.freeze) +    elsif host = @uri.host +    else +     host = addr +    end +    # convert the class of the URI +    if @uri.is_a?(klass) +      @uri.host = host +      @uri.port = port +    else +      @uri = klass.new( +        scheme, @uri.userinfo, +        host, port, nil, +        @uri.path, nil, @uri.query, nil) +    end +  end + +  private + +  class Chunker #:nodoc: +    def initialize(sock) +      @sock = sock +      @prev = nil +    end + +    def write(buf) +      # avoid memcpy() of buf, buf can huge and eat memory bandwidth +      @sock.write("#{buf.bytesize.to_s(16)}\r\n") +      rv = @sock.write(buf) +      @sock.write("\r\n") +      rv +    end + +    def finish +      @sock.write("0\r\n\r\n") +    end +  end + +  def send_request_with_body(sock, ver, path, body) +    self.content_length = body.bytesize +    delete 'Transfer-Encoding' +    supply_default_content_type +    write_header sock, ver, path +    wait_for_continue sock, ver if sock.continue_timeout +    sock.write body +  end + +  def send_request_with_body_stream(sock, ver, path, f) +    unless content_length() or chunked? +      raise ArgumentError, +          "Content-Length not given and Transfer-Encoding is not `chunked'" +    end +    supply_default_content_type +    write_header sock, ver, path +    wait_for_continue sock, ver if sock.continue_timeout +    if chunked? +      chunker = Chunker.new(sock) +      IO.copy_stream(f, chunker) +      chunker.finish +    else +      # copy_stream can sendfile() to sock.io unless we use SSL. +      # If sock.io is an SSLSocket, copy_stream will hit SSL_write() +      IO.copy_stream(f, sock.io) +    end +  end + +  def send_request_with_body_data(sock, ver, path, params) +    if /\Amultipart\/form-data\z/i !~ self.content_type +      self.content_type = 'application/x-www-form-urlencoded' +      return send_request_with_body(sock, ver, path, URI.encode_www_form(params)) +    end + +    opt = @form_option.dup +    require 'securerandom' unless defined?(SecureRandom) +    opt[:boundary] ||= SecureRandom.urlsafe_base64(40) +    self.set_content_type(self.content_type, boundary: opt[:boundary]) +    if chunked? +      write_header sock, ver, path +      encode_multipart_form_data(sock, params, opt) +    else +      require 'tempfile' +      file = Tempfile.new('multipart') +      file.binmode +      encode_multipart_form_data(file, params, opt) +      file.rewind +      self.content_length = file.size +      write_header sock, ver, path +      IO.copy_stream(file, sock) +      file.close(true) +    end +  end + +  def encode_multipart_form_data(out, params, opt) +    charset = opt[:charset] +    boundary = opt[:boundary] +    require 'securerandom' unless defined?(SecureRandom) +    boundary ||= SecureRandom.urlsafe_base64(40) +    chunked_p = chunked? + +    buf = '' +    params.each do |key, value, h={}| +      key = quote_string(key, charset) +      filename = +        h.key?(:filename) ? h[:filename] : +        value.respond_to?(:to_path) ? File.basename(value.to_path) : +        nil + +      buf << "--#{boundary}\r\n" +      if filename +        filename = quote_string(filename, charset) +        type = h[:content_type] || 'application/octet-stream' +        buf << "Content-Disposition: form-data; " \ +          "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \ +          "Content-Type: #{type}\r\n\r\n" +        if !out.respond_to?(:write) || !value.respond_to?(:read) +          # if +out+ is not an IO or +value+ is not an IO +          buf << (value.respond_to?(:read) ? value.read : value) +        elsif value.respond_to?(:size) && chunked_p +          # if +out+ is an IO and +value+ is a File, use IO.copy_stream +          flush_buffer(out, buf, chunked_p) +          out << "%x\r\n" % value.size if chunked_p +          IO.copy_stream(value, out) +          out << "\r\n" if chunked_p +        else +          # +out+ is an IO, and +value+ is not a File but an IO +          flush_buffer(out, buf, chunked_p) +          1 while flush_buffer(out, value.read(4096), chunked_p) +        end +      else +        # non-file field: +        #   HTML5 says, "The parts of the generated multipart/form-data +        #   resource that correspond to non-file fields must not have a +        #   Content-Type header specified." +        buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" +        buf << (value.respond_to?(:read) ? value.read : value) +      end +      buf << "\r\n" +    end +    buf << "--#{boundary}--\r\n" +    flush_buffer(out, buf, chunked_p) +    out << "0\r\n\r\n" if chunked_p +  end + +  def quote_string(str, charset) +    str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset +    str.gsub(/[\\"]/, '\\\\\&') +  end + +  def flush_buffer(out, buf, chunked_p) +    return unless buf +    out << "%x\r\n"%buf.bytesize if chunked_p +    out << buf +    out << "\r\n" if chunked_p +    buf.clear +  end + +  def supply_default_content_type +    return if content_type() +    warn 'net/http: warning: Content-Type did not set; using application/x-www-form-urlencoded' if $VERBOSE +    set_content_type 'application/x-www-form-urlencoded' +  end + +  ## +  # Waits up to the continue timeout for a response from the server provided +  # we're speaking HTTP 1.1 and are expecting a 100-continue response. + +  def wait_for_continue(sock, ver) +    if ver >= '1.1' and @header['expect'] and +        @header['expect'].include?('100-continue') +      if IO.select([sock.io], nil, nil, sock.continue_timeout) +        res = Net::HTTPResponse.read_new(sock) +        unless res.kind_of?(Net::HTTPContinue) +          res.decode_content = @decode_content +          throw :response, res +        end +      end +    end +  end + +  def write_header(sock, ver, path) +    buf = "#{@method} #{path} HTTP/#{ver}\r\n" +    each_capitalized do |k,v| +      buf << "#{k}: #{v}\r\n" +    end +    buf << "\r\n" +    sock.write buf +  end + +end + diff --git a/jni/ruby/lib/net/http/header.rb b/jni/ruby/lib/net/http/header.rb new file mode 100644 index 0000000..912419d --- /dev/null +++ b/jni/ruby/lib/net/http/header.rb @@ -0,0 +1,452 @@ +# The HTTPHeader module defines methods for reading and writing +# HTTP headers. +# +# It is used as a mixin by other classes, to provide hash-like +# access to HTTP header values. Unlike raw hash access, HTTPHeader +# provides access via case-insensitive keys. It also provides +# methods for accessing commonly-used HTTP header values in more +# convenient formats. +# +module Net::HTTPHeader + +  def initialize_http_header(initheader) +    @header = {} +    return unless initheader +    initheader.each do |key, value| +      warn "net/http: warning: duplicated HTTP header: #{key}" if key?(key) and $VERBOSE +      @header[key.downcase] = [value.strip] +    end +  end + +  def size   #:nodoc: obsolete +    @header.size +  end + +  alias length size   #:nodoc: obsolete + +  # Returns the header field corresponding to the case-insensitive key. +  # For example, a key of "Content-Type" might return "text/html" +  def [](key) +    a = @header[key.downcase] or return nil +    a.join(', ') +  end + +  # Sets the header field corresponding to the case-insensitive key. +  def []=(key, val) +    unless val +      @header.delete key.downcase +      return val +    end +    @header[key.downcase] = [val] +  end + +  # [Ruby 1.8.3] +  # Adds a value to a named header field, instead of replacing its value. +  # Second argument +val+ must be a String. +  # See also #[]=, #[] and #get_fields. +  # +  #   request.add_field 'X-My-Header', 'a' +  #   p request['X-My-Header']              #=> "a" +  #   p request.get_fields('X-My-Header')   #=> ["a"] +  #   request.add_field 'X-My-Header', 'b' +  #   p request['X-My-Header']              #=> "a, b" +  #   p request.get_fields('X-My-Header')   #=> ["a", "b"] +  #   request.add_field 'X-My-Header', 'c' +  #   p request['X-My-Header']              #=> "a, b, c" +  #   p request.get_fields('X-My-Header')   #=> ["a", "b", "c"] +  # +  def add_field(key, val) +    if @header.key?(key.downcase) +      @header[key.downcase].push val +    else +      @header[key.downcase] = [val] +    end +  end + +  # [Ruby 1.8.3] +  # Returns an array of header field strings corresponding to the +  # case-insensitive +key+.  This method allows you to get duplicated +  # header fields without any processing.  See also #[]. +  # +  #   p response.get_fields('Set-Cookie') +  #     #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23", +  #          "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"] +  #   p response['Set-Cookie'] +  #     #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23" +  # +  def get_fields(key) +    return nil unless @header[key.downcase] +    @header[key.downcase].dup +  end + +  # Returns the header field corresponding to the case-insensitive key. +  # Returns the default value +args+, or the result of the block, or +  # raises an IndexError if there's no header field named +key+ +  # See Hash#fetch +  def fetch(key, *args, &block)   #:yield: +key+ +    a = @header.fetch(key.downcase, *args, &block) +    a.kind_of?(Array) ? a.join(', ') : a +  end + +  # Iterates through the header names and values, passing in the name +  # and value to the code block supplied. +  # +  # Example: +  # +  #     response.header.each_header {|key,value| puts "#{key} = #{value}" } +  # +  def each_header   #:yield: +key+, +value+ +    block_given? or return enum_for(__method__) +    @header.each do |k,va| +      yield k, va.join(', ') +    end +  end + +  alias each each_header + +  # Iterates through the header names in the header, passing +  # each header name to the code block. +  def each_name(&block)   #:yield: +key+ +    block_given? or return enum_for(__method__) +    @header.each_key(&block) +  end + +  alias each_key each_name + +  # Iterates through the header names in the header, passing +  # capitalized header names to the code block. +  # +  # Note that header names are capitalized systematically; +  # capitalization may not match that used by the remote HTTP +  # server in its response. +  def each_capitalized_name  #:yield: +key+ +    block_given? or return enum_for(__method__) +    @header.each_key do |k| +      yield capitalize(k) +    end +  end + +  # Iterates through header values, passing each value to the +  # code block. +  def each_value   #:yield: +value+ +    block_given? or return enum_for(__method__) +    @header.each_value do |va| +      yield va.join(', ') +    end +  end + +  # Removes a header field, specified by case-insensitive key. +  def delete(key) +    @header.delete(key.downcase) +  end + +  # true if +key+ header exists. +  def key?(key) +    @header.key?(key.downcase) +  end + +  # Returns a Hash consisting of header names and array of values. +  # e.g. +  # {"cache-control" => ["private"], +  #  "content-type" => ["text/html"], +  #  "date" => ["Wed, 22 Jun 2005 22:11:50 GMT"]} +  def to_hash +    @header.dup +  end + +  # As for #each_header, except the keys are provided in capitalized form. +  # +  # Note that header names are capitalized systematically; +  # capitalization may not match that used by the remote HTTP +  # server in its response. +  def each_capitalized +    block_given? or return enum_for(__method__) +    @header.each do |k,v| +      yield capitalize(k), v.join(', ') +    end +  end + +  alias canonical_each each_capitalized + +  def capitalize(name) +    name.split(/-/).map {|s| s.capitalize }.join('-') +  end +  private :capitalize + +  # Returns an Array of Range objects which represent the Range: +  # HTTP header field, or +nil+ if there is no such header. +  def range +    return nil unless @header['range'] + +    value = self['Range'] +    # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec ) +    #   *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] ) +    # corrected collected ABNF +    # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1 +    # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C +    # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5 +    unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value +      raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'" +    end + +    byte_range_set = $1 +    result = byte_range_set.split(/,/).map {|spec| +      m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or +              raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'" +      d1 = m[1].to_i +      d2 = m[2].to_i +      if m[1] and m[2] +        if d1 > d2 +          raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'" +        end +        d1..d2 +      elsif m[1] +        d1..-1 +      elsif m[2] +        -d2..-1 +      else +        raise Net::HTTPHeaderSyntaxError, 'range is not specified' +      end +    } +    # if result.empty? +    # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec +    # but above regexp already denies it. +    if result.size == 1 && result[0].begin == 0 && result[0].end == -1 +      raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length' +    end +    result +  end + +  # Sets the HTTP Range: header. +  # Accepts either a Range object as a single argument, +  # or a beginning index and a length from that index. +  # Example: +  # +  #   req.range = (0..1023) +  #   req.set_range 0, 1023 +  # +  def set_range(r, e = nil) +    unless r +      @header.delete 'range' +      return r +    end +    r = (r...r+e) if e +    case r +    when Numeric +      n = r.to_i +      rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}") +    when Range +      first = r.first +      last = r.end +      last -= 1 if r.exclude_end? +      if last == -1 +        rangestr = (first > 0 ? "#{first}-" : "-#{-first}") +      else +        raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 +        raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 +        raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last +        rangestr = "#{first}-#{last}" +      end +    else +      raise TypeError, 'Range/Integer is required' +    end +    @header['range'] = ["bytes=#{rangestr}"] +    r +  end + +  alias range= set_range + +  # Returns an Integer object which represents the HTTP Content-Length: +  # header field, or +nil+ if that field was not provided. +  def content_length +    return nil unless key?('Content-Length') +    len = self['Content-Length'].slice(/\d+/) or +        raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format' +    len.to_i +  end + +  def content_length=(len) +    unless len +      @header.delete 'content-length' +      return nil +    end +    @header['content-length'] = [len.to_i.to_s] +  end + +  # Returns "true" if the "transfer-encoding" header is present and +  # set to "chunked".  This is an HTTP/1.1 feature, allowing the +  # the content to be sent in "chunks" without at the outset +  # stating the entire content length. +  def chunked? +    return false unless @header['transfer-encoding'] +    field = self['Transfer-Encoding'] +    (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false +  end + +  # Returns a Range object which represents the value of the Content-Range: +  # header field. +  # For a partial entity body, this indicates where this fragment +  # fits inside the full entity body, as range of byte offsets. +  def content_range +    return nil unless @header['content-range'] +    m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or +        raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format' +    m[1].to_i .. m[2].to_i +  end + +  # The length of the range represented in Content-Range: header. +  def range_length +    r = content_range() or return nil +    r.end - r.begin + 1 +  end + +  # Returns a content type string such as "text/html". +  # This method returns nil if Content-Type: header field does not exist. +  def content_type +    return nil unless main_type() +    if sub_type() +    then "#{main_type()}/#{sub_type()}" +    else main_type() +    end +  end + +  # Returns a content type string such as "text". +  # This method returns nil if Content-Type: header field does not exist. +  def main_type +    return nil unless @header['content-type'] +    self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip +  end + +  # Returns a content type string such as "html". +  # This method returns nil if Content-Type: header field does not exist +  # or sub-type is not given (e.g. "Content-Type: text"). +  def sub_type +    return nil unless @header['content-type'] +    _, sub = *self['Content-Type'].split(';').first.to_s.split('/') +    return nil unless sub +    sub.strip +  end + +  # Any parameters specified for the content type, returned as a Hash. +  # For example, a header of Content-Type: text/html; charset=EUC-JP +  # would result in type_params returning {'charset' => 'EUC-JP'} +  def type_params +    result = {} +    list = self['Content-Type'].to_s.split(';') +    list.shift +    list.each do |param| +      k, v = *param.split('=', 2) +      result[k.strip] = v.strip +    end +    result +  end + +  # Sets the content type in an HTTP header. +  # The +type+ should be a full HTTP content type, e.g. "text/html". +  # The +params+ are an optional Hash of parameters to add after the +  # content type, e.g. {'charset' => 'iso-8859-1'} +  def set_content_type(type, params = {}) +    @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] +  end + +  alias content_type= set_content_type + +  # Set header fields and a body from HTML form data. +  # +params+ should be an Array of Arrays or +  # a Hash containing HTML form data. +  # Optional argument +sep+ means data record separator. +  # +  # Values are URL encoded as necessary and the content-type is set to +  # application/x-www-form-urlencoded +  # +  # Example: +  #    http.form_data = {"q" => "ruby", "lang" => "en"} +  #    http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"} +  #    http.set_form_data({"q" => "ruby", "lang" => "en"}, ';') +  # +  def set_form_data(params, sep = '&') +    query = URI.encode_www_form(params) +    query.gsub!(/&/, sep) if sep != '&' +    self.body = query +    self.content_type = 'application/x-www-form-urlencoded' +  end + +  alias form_data= set_form_data + +  # Set a HTML form data set. +  # +params+ is the form data set; it is an Array of Arrays or a Hash +  # +enctype is the type to encode the form data set. +  # It is application/x-www-form-urlencoded or multipart/form-data. +  # +formpot+ is an optional hash to specify the detail. +  # +  # boundary:: the boundary of the multipart message +  # charset::  the charset of the message. All names and the values of +  #            non-file fields are encoded as the charset. +  # +  # Each item of params is an array and contains following items: +  # +name+::  the name of the field +  # +value+:: the value of the field, it should be a String or a File +  # +opt+::   an optional hash to specify additional information +  # +  # Each item is a file field or a normal field. +  # If +value+ is a File object or the +opt+ have a filename key, +  # the item is treated as a file field. +  # +  # If Transfer-Encoding is set as chunked, this send the request in +  # chunked encoding. Because chunked encoding is HTTP/1.1 feature, +  # you must confirm the server to support HTTP/1.1 before sending it. +  # +  # Example: +  #    http.set_form([["q", "ruby"], ["lang", "en"]]) +  # +  # See also RFC 2388, RFC 2616, HTML 4.01, and HTML5 +  # +  def set_form(params, enctype='application/x-www-form-urlencoded', formopt={}) +    @body_data = params +    @body = nil +    @body_stream = nil +    @form_option = formopt +    case enctype +    when /\Aapplication\/x-www-form-urlencoded\z/i, +      /\Amultipart\/form-data\z/i +      self.content_type = enctype +    else +      raise ArgumentError, "invalid enctype: #{enctype}" +    end +  end + +  # Set the Authorization: header for "Basic" authorization. +  def basic_auth(account, password) +    @header['authorization'] = [basic_encode(account, password)] +  end + +  # Set Proxy-Authorization: header for "Basic" authorization. +  def proxy_basic_auth(account, password) +    @header['proxy-authorization'] = [basic_encode(account, password)] +  end + +  def basic_encode(account, password) +    'Basic ' + ["#{account}:#{password}"].pack('m').delete("\r\n") +  end +  private :basic_encode + +  def connection_close? +    tokens(@header['connection']).include?('close') or +    tokens(@header['proxy-connection']).include?('close') +  end + +  def connection_keep_alive? +    tokens(@header['connection']).include?('keep-alive') or +    tokens(@header['proxy-connection']).include?('keep-alive') +  end + +  def tokens(vals) +    return [] unless vals +    vals.map {|v| v.split(',') }.flatten\ +        .reject {|str| str.strip.empty? }\ +        .map {|tok| tok.strip.downcase } +  end +  private :tokens + +end + diff --git a/jni/ruby/lib/net/http/proxy_delta.rb b/jni/ruby/lib/net/http/proxy_delta.rb new file mode 100644 index 0000000..b16c9f1 --- /dev/null +++ b/jni/ruby/lib/net/http/proxy_delta.rb @@ -0,0 +1,16 @@ +module Net::HTTP::ProxyDelta   #:nodoc: internal use only +  private + +  def conn_address +    proxy_address() +  end + +  def conn_port +    proxy_port() +  end + +  def edit_path(path) +    use_ssl? ? path : "http://#{addr_port()}#{path}" +  end +end + diff --git a/jni/ruby/lib/net/http/request.rb b/jni/ruby/lib/net/http/request.rb new file mode 100644 index 0000000..e8b0f48 --- /dev/null +++ b/jni/ruby/lib/net/http/request.rb @@ -0,0 +1,20 @@ +# HTTP request class. +# This class wraps together the request header and the request path. +# You cannot use this class directly. Instead, you should use one of its +# subclasses: Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Head. +# +class Net::HTTPRequest < Net::HTTPGenericRequest +  # Creates an HTTP request object for +path+. +  # +  # +initheader+ are the default headers to use.  Net::HTTP adds +  # Accept-Encoding to enable compression of the response body unless +  # Accept-Encoding or Range are supplied in +initheader+. + +  def initialize(path, initheader = nil) +    super self.class::METHOD, +          self.class::REQUEST_HAS_BODY, +          self.class::RESPONSE_HAS_BODY, +          path, initheader +  end +end + diff --git a/jni/ruby/lib/net/http/requests.rb b/jni/ruby/lib/net/http/requests.rb new file mode 100644 index 0000000..c1f8360 --- /dev/null +++ b/jni/ruby/lib/net/http/requests.rb @@ -0,0 +1,122 @@ +# +# HTTP/1.1 methods --- RFC2616 +# + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Get < Net::HTTPRequest +  METHOD = 'GET' +  REQUEST_HAS_BODY  = false +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Head < Net::HTTPRequest +  METHOD = 'HEAD' +  REQUEST_HAS_BODY = false +  RESPONSE_HAS_BODY = false +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Post < Net::HTTPRequest +  METHOD = 'POST' +  REQUEST_HAS_BODY = true +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Put < Net::HTTPRequest +  METHOD = 'PUT' +  REQUEST_HAS_BODY = true +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +# See Net::HTTP for usage examples. +class Net::HTTP::Delete < Net::HTTPRequest +  METHOD = 'DELETE' +  REQUEST_HAS_BODY = false +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Options < Net::HTTPRequest +  METHOD = 'OPTIONS' +  REQUEST_HAS_BODY = false +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Trace < Net::HTTPRequest +  METHOD = 'TRACE' +  REQUEST_HAS_BODY = false +  RESPONSE_HAS_BODY = true +end + +# +# PATCH method --- RFC5789 +# + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Patch < Net::HTTPRequest +  METHOD = 'PATCH' +  REQUEST_HAS_BODY = true +  RESPONSE_HAS_BODY = true +end + +# +# WebDAV methods --- RFC2518 +# + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Propfind < Net::HTTPRequest +  METHOD = 'PROPFIND' +  REQUEST_HAS_BODY = true +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Proppatch < Net::HTTPRequest +  METHOD = 'PROPPATCH' +  REQUEST_HAS_BODY = true +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Mkcol < Net::HTTPRequest +  METHOD = 'MKCOL' +  REQUEST_HAS_BODY = true +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Copy < Net::HTTPRequest +  METHOD = 'COPY' +  REQUEST_HAS_BODY = false +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Move < Net::HTTPRequest +  METHOD = 'MOVE' +  REQUEST_HAS_BODY = false +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Lock < Net::HTTPRequest +  METHOD = 'LOCK' +  REQUEST_HAS_BODY = true +  RESPONSE_HAS_BODY = true +end + +# See Net::HTTPGenericRequest for attributes and methods. +class Net::HTTP::Unlock < Net::HTTPRequest +  METHOD = 'UNLOCK' +  REQUEST_HAS_BODY = true +  RESPONSE_HAS_BODY = true +end + diff --git a/jni/ruby/lib/net/http/response.rb b/jni/ruby/lib/net/http/response.rb new file mode 100644 index 0000000..126c221 --- /dev/null +++ b/jni/ruby/lib/net/http/response.rb @@ -0,0 +1,416 @@ +# HTTP response class. +# +# This class wraps together the response header and the response body (the +# entity requested). +# +# It mixes in the HTTPHeader module, which provides access to response +# header values both via hash-like methods and via individual readers. +# +# Note that each possible HTTP response code defines its own +# HTTPResponse subclass.  These are listed below. +# +# All classes are defined under the Net module. Indentation indicates +# inheritance.  For a list of the classes see Net::HTTP. +# +# +class Net::HTTPResponse +  class << self +    # true if the response has a body. +    def body_permitted? +      self::HAS_BODY +    end + +    def exception_type   # :nodoc: internal use only +      self::EXCEPTION_TYPE +    end + +    def read_new(sock)   #:nodoc: internal use only +      httpv, code, msg = read_status_line(sock) +      res = response_class(code).new(httpv, code, msg) +      each_response_header(sock) do |k,v| +        res.add_field k, v +      end +      res +    end + +    private + +    def read_status_line(sock) +      str = sock.readline +      m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or +        raise Net::HTTPBadResponse, "wrong status line: #{str.dump}" +      m.captures +    end + +    def response_class(code) +      CODE_TO_OBJ[code] or +      CODE_CLASS_TO_OBJ[code[0,1]] or +      Net::HTTPUnknownResponse +    end + +    def each_response_header(sock) +      key = value = nil +      while true +        line = sock.readuntil("\n", true).sub(/\s+\z/, '') +        break if line.empty? +        if line[0] == ?\s or line[0] == ?\t and value +          value << ' ' unless value.empty? +          value << line.strip +        else +          yield key, value if key +          key, value = line.strip.split(/\s*:\s*/, 2) +          raise Net::HTTPBadResponse, 'wrong header line format' if value.nil? +        end +      end +      yield key, value if key +    end +  end + +  # next is to fix bug in RDoc, where the private inside class << self +  # spills out. +  public + +  include Net::HTTPHeader + +  def initialize(httpv, code, msg)   #:nodoc: internal use only +    @http_version = httpv +    @code         = code +    @message      = msg +    initialize_http_header nil +    @body = nil +    @read = false +    @uri  = nil +    @decode_content = false +  end + +  # The HTTP version supported by the server. +  attr_reader :http_version + +  # The HTTP result code string. For example, '302'.  You can also +  # determine the response type by examining which response subclass +  # the response object is an instance of. +  attr_reader :code + +  # The HTTP result message sent by the server. For example, 'Not Found'. +  attr_reader :message +  alias msg message   # :nodoc: obsolete + +  # The URI used to fetch this response.  The response URI is only available +  # if a URI was used to create the request. +  attr_reader :uri + +  # Set to true automatically when the request did not contain an +  # Accept-Encoding header from the user. +  attr_accessor :decode_content + +  def inspect +    "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" +  end + +  # +  # response <-> exception relationship +  # + +  def code_type   #:nodoc: +    self.class +  end + +  def error!   #:nodoc: +    raise error_type().new(@code + ' ' + @message.dump, self) +  end + +  def error_type   #:nodoc: +    self.class::EXCEPTION_TYPE +  end + +  # Raises an HTTP error if the response is not 2xx (success). +  def value +    error! unless self.kind_of?(Net::HTTPSuccess) +  end + +  def uri= uri # :nodoc: +    @uri = uri.dup if uri +  end + +  # +  # header (for backward compatibility only; DO NOT USE) +  # + +  def response   #:nodoc: +    warn "#{caller(1)[0]}: warning: Net::HTTPResponse#response is obsolete" if $VERBOSE +    self +  end + +  def header   #:nodoc: +    warn "#{caller(1)[0]}: warning: Net::HTTPResponse#header is obsolete" if $VERBOSE +    self +  end + +  def read_header   #:nodoc: +    warn "#{caller(1)[0]}: warning: Net::HTTPResponse#read_header is obsolete" if $VERBOSE +    self +  end + +  # +  # body +  # + +  def reading_body(sock, reqmethodallowbody)  #:nodoc: internal use only +    @socket = sock +    @body_exist = reqmethodallowbody && self.class.body_permitted? +    begin +      yield +      self.body   # ensure to read body +    ensure +      @socket = nil +    end +  end + +  # Gets the entity body returned by the remote HTTP server. +  # +  # If a block is given, the body is passed to the block, and +  # the body is provided in fragments, as it is read in from the socket. +  # +  # Calling this method a second or subsequent time for the same +  # HTTPResponse object will return the value already read. +  # +  #   http.request_get('/index.html') {|res| +  #     puts res.read_body +  #   } +  # +  #   http.request_get('/index.html') {|res| +  #     p res.read_body.object_id   # 538149362 +  #     p res.read_body.object_id   # 538149362 +  #   } +  # +  #   # using iterator +  #   http.request_get('/index.html') {|res| +  #     res.read_body do |segment| +  #       print segment +  #     end +  #   } +  # +  def read_body(dest = nil, &block) +    if @read +      raise IOError, "#{self.class}\#read_body called twice" if dest or block +      return @body +    end +    to = procdest(dest, block) +    stream_check +    if @body_exist +      read_body_0 to +      @body = to +    else +      @body = nil +    end +    @read = true + +    @body +  end + +  # Returns the full entity body. +  # +  # Calling this method a second or subsequent time will return the +  # string already read. +  # +  #   http.request_get('/index.html') {|res| +  #     puts res.body +  #   } +  # +  #   http.request_get('/index.html') {|res| +  #     p res.body.object_id   # 538149362 +  #     p res.body.object_id   # 538149362 +  #   } +  # +  def body +    read_body() +  end + +  # Because it may be necessary to modify the body, Eg, decompression +  # this method facilitates that. +  def body=(value) +    @body = value +  end + +  alias entity body   #:nodoc: obsolete + +  private + +  ## +  # Checks for a supported Content-Encoding header and yields an Inflate +  # wrapper for this response's socket when zlib is present.  If the +  # Content-Encoding is unsupported or zlib is missing the plain socket is +  # yielded. +  # +  # If a Content-Range header is present a plain socket is yielded as the +  # bytes in the range may not be a complete deflate block. + +  def inflater # :nodoc: +    return yield @socket unless Net::HTTP::HAVE_ZLIB +    return yield @socket unless @decode_content +    return yield @socket if self['content-range'] + +    v = self['content-encoding'] +    case v && v.downcase +    when 'deflate', 'gzip', 'x-gzip' then +      self.delete 'content-encoding' + +      inflate_body_io = Inflater.new(@socket) + +      begin +        yield inflate_body_io +      ensure +        orig_err = $! +        begin +          inflate_body_io.finish +        rescue => err +          raise orig_err || err +        end +      end +    when 'none', 'identity' then +      self.delete 'content-encoding' + +      yield @socket +    else +      yield @socket +    end +  end + +  def read_body_0(dest) +    inflater do |inflate_body_io| +      if chunked? +        read_chunked dest, inflate_body_io +        return +      end + +      @socket = inflate_body_io + +      clen = content_length() +      if clen +        @socket.read clen, dest, true   # ignore EOF +        return +      end +      clen = range_length() +      if clen +        @socket.read clen, dest +        return +      end +      @socket.read_all dest +    end +  end + +  ## +  # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF, +  # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip +  # encoded. +  # +  # See RFC 2616 section 3.6.1 for definitions + +  def read_chunked(dest, chunk_data_io) # :nodoc: +    total = 0 +    while true +      line = @socket.readline +      hexlen = line.slice(/[0-9a-fA-F]+/) or +          raise Net::HTTPBadResponse, "wrong chunk size line: #{line}" +      len = hexlen.hex +      break if len == 0 +      begin +        chunk_data_io.read len, dest +      ensure +        total += len +        @socket.read 2   # \r\n +      end +    end +    until @socket.readline.empty? +      # none +    end +  end + +  def stream_check +    raise IOError, 'attempt to read body out of block' if @socket.closed? +  end + +  def procdest(dest, block) +    raise ArgumentError, 'both arg and block given for HTTP method' if +      dest and block +    if block +      Net::ReadAdapter.new(block) +    else +      dest || '' +    end +  end + +  ## +  # Inflater is a wrapper around Net::BufferedIO that transparently inflates +  # zlib and gzip streams. + +  class Inflater # :nodoc: + +    ## +    # Creates a new Inflater wrapping +socket+ + +    def initialize socket +      @socket = socket +      # zlib with automatic gzip detection +      @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS) +    end + +    ## +    # Finishes the inflate stream. + +    def finish +      return if @inflate.total_in == 0 +      @inflate.finish +    end + +    ## +    # Returns a Net::ReadAdapter that inflates each read chunk into +dest+. +    # +    # This allows a large response body to be inflated without storing the +    # entire body in memory. + +    def inflate_adapter(dest) +      if dest.respond_to?(:set_encoding) +        dest.set_encoding(Encoding::ASCII_8BIT) +      elsif dest.respond_to?(:force_encoding) +        dest.force_encoding(Encoding::ASCII_8BIT) +      end +      block = proc do |compressed_chunk| +        @inflate.inflate(compressed_chunk) do |chunk| +          dest << chunk +        end +      end + +      Net::ReadAdapter.new(block) +    end + +    ## +    # Reads +clen+ bytes from the socket, inflates them, then writes them to +    # +dest+.  +ignore_eof+ is passed down to Net::BufferedIO#read +    # +    # Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes. +    # At this time there is no way for a user of Net::HTTPResponse to read a +    # specific number of bytes from the HTTP response body, so this internal +    # API does not return the same number of bytes as were requested. +    # +    # See https://bugs.ruby-lang.org/issues/6492 for further discussion. + +    def read clen, dest, ignore_eof = false +      temp_dest = inflate_adapter(dest) + +      @socket.read clen, temp_dest, ignore_eof +    end + +    ## +    # Reads the rest of the socket, inflates it, then writes it to +dest+. + +    def read_all dest +      temp_dest = inflate_adapter(dest) + +      @socket.read_all temp_dest +    end + +  end + +end + diff --git a/jni/ruby/lib/net/http/responses.rb b/jni/ruby/lib/net/http/responses.rb new file mode 100644 index 0000000..1454a27 --- /dev/null +++ b/jni/ruby/lib/net/http/responses.rb @@ -0,0 +1,273 @@ +# :stopdoc: +class Net::HTTPUnknownResponse < Net::HTTPResponse +  HAS_BODY = true +  EXCEPTION_TYPE = Net::HTTPError +end +class Net::HTTPInformation < Net::HTTPResponse           # 1xx +  HAS_BODY = false +  EXCEPTION_TYPE = Net::HTTPError +end +class Net::HTTPSuccess < Net::HTTPResponse               # 2xx +  HAS_BODY = true +  EXCEPTION_TYPE = Net::HTTPError +end +class Net::HTTPRedirection < Net::HTTPResponse           # 3xx +  HAS_BODY = true +  EXCEPTION_TYPE = Net::HTTPRetriableError +end +class Net::HTTPClientError < Net::HTTPResponse           # 4xx +  HAS_BODY = true +  EXCEPTION_TYPE = Net::HTTPServerException   # for backward compatibility +end +class Net::HTTPServerError < Net::HTTPResponse           # 5xx +  HAS_BODY = true +  EXCEPTION_TYPE = Net::HTTPFatalError    # for backward compatibility +end + +class Net::HTTPContinue < Net::HTTPInformation           # 100 +  HAS_BODY = false +end +class Net::HTTPSwitchProtocol < Net::HTTPInformation     # 101 +  HAS_BODY = false +end +# 102 - RFC 2518; removed in RFC 4918 + +class Net::HTTPOK < Net::HTTPSuccess                            # 200 +  HAS_BODY = true +end +class Net::HTTPCreated < Net::HTTPSuccess                       # 201 +  HAS_BODY = true +end +class Net::HTTPAccepted < Net::HTTPSuccess                      # 202 +  HAS_BODY = true +end +class Net::HTTPNonAuthoritativeInformation < Net::HTTPSuccess   # 203 +  HAS_BODY = true +end +class Net::HTTPNoContent < Net::HTTPSuccess                     # 204 +  HAS_BODY = false +end +class Net::HTTPResetContent < Net::HTTPSuccess                  # 205 +  HAS_BODY = false +end +class Net::HTTPPartialContent < Net::HTTPSuccess                # 206 +  HAS_BODY = true +end +class Net::HTTPMultiStatus < Net::HTTPSuccess                   # 207 - RFC 4918 +  HAS_BODY = true +end +# 208 Already Reported - RFC 5842; experimental +class Net::HTTPIMUsed < Net::HTTPSuccess                        # 226 - RFC 3229 +  HAS_BODY = true +end + +class Net::HTTPMultipleChoices < Net::HTTPRedirection    # 300 +  HAS_BODY = true +end +Net::HTTPMultipleChoice = Net::HTTPMultipleChoices +class Net::HTTPMovedPermanently < Net::HTTPRedirection   # 301 +  HAS_BODY = true +end +class Net::HTTPFound < Net::HTTPRedirection              # 302 +  HAS_BODY = true +end +Net::HTTPMovedTemporarily = Net::HTTPFound +class Net::HTTPSeeOther < Net::HTTPRedirection           # 303 +  HAS_BODY = true +end +class Net::HTTPNotModified < Net::HTTPRedirection        # 304 +  HAS_BODY = false +end +class Net::HTTPUseProxy < Net::HTTPRedirection           # 305 +  HAS_BODY = false +end +# 306 Switch Proxy - no longer unused +class Net::HTTPTemporaryRedirect < Net::HTTPRedirection  # 307 +  HAS_BODY = true +end +class Net::HTTPPermanentRedirect < Net::HTTPRedirection  # 308 +  HAS_BODY = true +end + +class Net::HTTPBadRequest < Net::HTTPClientError                    # 400 +  HAS_BODY = true +end +class Net::HTTPUnauthorized < Net::HTTPClientError                  # 401 +  HAS_BODY = true +end +class Net::HTTPPaymentRequired < Net::HTTPClientError               # 402 +  HAS_BODY = true +end +class Net::HTTPForbidden < Net::HTTPClientError                     # 403 +  HAS_BODY = true +end +class Net::HTTPNotFound < Net::HTTPClientError                      # 404 +  HAS_BODY = true +end +class Net::HTTPMethodNotAllowed < Net::HTTPClientError              # 405 +  HAS_BODY = true +end +class Net::HTTPNotAcceptable < Net::HTTPClientError                 # 406 +  HAS_BODY = true +end +class Net::HTTPProxyAuthenticationRequired < Net::HTTPClientError   # 407 +  HAS_BODY = true +end +class Net::HTTPRequestTimeOut < Net::HTTPClientError                # 408 +  HAS_BODY = true +end +class Net::HTTPConflict < Net::HTTPClientError                      # 409 +  HAS_BODY = true +end +class Net::HTTPGone < Net::HTTPClientError                          # 410 +  HAS_BODY = true +end +class Net::HTTPLengthRequired < Net::HTTPClientError                # 411 +  HAS_BODY = true +end +class Net::HTTPPreconditionFailed < Net::HTTPClientError            # 412 +  HAS_BODY = true +end +class Net::HTTPRequestEntityTooLarge < Net::HTTPClientError         # 413 +  HAS_BODY = true +end +class Net::HTTPRequestURITooLong < Net::HTTPClientError             # 414 +  HAS_BODY = true +end +Net::HTTPRequestURITooLarge = Net::HTTPRequestURITooLong +class Net::HTTPUnsupportedMediaType < Net::HTTPClientError          # 415 +  HAS_BODY = true +end +class Net::HTTPRequestedRangeNotSatisfiable < Net::HTTPClientError  # 416 +  HAS_BODY = true +end +class Net::HTTPExpectationFailed < Net::HTTPClientError             # 417 +  HAS_BODY = true +end +# 418 I'm a teapot - RFC 2324; a joke RFC +# 420 Enhance Your Calm - Twitter +class Net::HTTPUnprocessableEntity < Net::HTTPClientError           # 422 - RFC 4918 +  HAS_BODY = true +end +class Net::HTTPLocked < Net::HTTPClientError                        # 423 - RFC 4918 +  HAS_BODY = true +end +class Net::HTTPFailedDependency < Net::HTTPClientError              # 424 - RFC 4918 +  HAS_BODY = true +end +# 425 Unordered Collection - existed only in draft +class Net::HTTPUpgradeRequired < Net::HTTPClientError               # 426 - RFC 2817 +  HAS_BODY = true +end +class Net::HTTPPreconditionRequired < Net::HTTPClientError          # 428 - RFC 6585 +  HAS_BODY = true +end +class Net::HTTPTooManyRequests < Net::HTTPClientError               # 429 - RFC 6585 +  HAS_BODY = true +end +class Net::HTTPRequestHeaderFieldsTooLarge < Net::HTTPClientError   # 431 - RFC 6585 +  HAS_BODY = true +end +# 444 No Response - Nginx +# 449 Retry With - Microsoft +# 450 Blocked by Windows Parental Controls - Microsoft +# 499 Client Closed Request - Nginx + +class Net::HTTPInternalServerError < Net::HTTPServerError           # 500 +  HAS_BODY = true +end +class Net::HTTPNotImplemented < Net::HTTPServerError                # 501 +  HAS_BODY = true +end +class Net::HTTPBadGateway < Net::HTTPServerError                    # 502 +  HAS_BODY = true +end +class Net::HTTPServiceUnavailable < Net::HTTPServerError            # 503 +  HAS_BODY = true +end +class Net::HTTPGatewayTimeOut < Net::HTTPServerError                # 504 +  HAS_BODY = true +end +class Net::HTTPVersionNotSupported < Net::HTTPServerError           # 505 +  HAS_BODY = true +end +# 506 Variant Also Negotiates - RFC 2295; experimental +class Net::HTTPInsufficientStorage < Net::HTTPServerError           # 507 - RFC 4918 +  HAS_BODY = true +end +# 508 Loop Detected - RFC 5842; experimental +# 509 Bandwidth Limit Exceeded - Apache bw/limited extension +# 510 Not Extended - RFC 2774; experimental +class Net::HTTPNetworkAuthenticationRequired < Net::HTTPServerError # 511 - RFC 6585 +  HAS_BODY = true +end + +class Net::HTTPResponse +  CODE_CLASS_TO_OBJ = { +    '1' => Net::HTTPInformation, +    '2' => Net::HTTPSuccess, +    '3' => Net::HTTPRedirection, +    '4' => Net::HTTPClientError, +    '5' => Net::HTTPServerError +  } +  CODE_TO_OBJ = { +    '100' => Net::HTTPContinue, +    '101' => Net::HTTPSwitchProtocol, + +    '200' => Net::HTTPOK, +    '201' => Net::HTTPCreated, +    '202' => Net::HTTPAccepted, +    '203' => Net::HTTPNonAuthoritativeInformation, +    '204' => Net::HTTPNoContent, +    '205' => Net::HTTPResetContent, +    '206' => Net::HTTPPartialContent, +    '207' => Net::HTTPMultiStatus, +    '226' => Net::HTTPIMUsed, + +    '300' => Net::HTTPMultipleChoices, +    '301' => Net::HTTPMovedPermanently, +    '302' => Net::HTTPFound, +    '303' => Net::HTTPSeeOther, +    '304' => Net::HTTPNotModified, +    '305' => Net::HTTPUseProxy, +    '307' => Net::HTTPTemporaryRedirect, + +    '400' => Net::HTTPBadRequest, +    '401' => Net::HTTPUnauthorized, +    '402' => Net::HTTPPaymentRequired, +    '403' => Net::HTTPForbidden, +    '404' => Net::HTTPNotFound, +    '405' => Net::HTTPMethodNotAllowed, +    '406' => Net::HTTPNotAcceptable, +    '407' => Net::HTTPProxyAuthenticationRequired, +    '408' => Net::HTTPRequestTimeOut, +    '409' => Net::HTTPConflict, +    '410' => Net::HTTPGone, +    '411' => Net::HTTPLengthRequired, +    '412' => Net::HTTPPreconditionFailed, +    '413' => Net::HTTPRequestEntityTooLarge, +    '414' => Net::HTTPRequestURITooLong, +    '415' => Net::HTTPUnsupportedMediaType, +    '416' => Net::HTTPRequestedRangeNotSatisfiable, +    '417' => Net::HTTPExpectationFailed, +    '422' => Net::HTTPUnprocessableEntity, +    '423' => Net::HTTPLocked, +    '424' => Net::HTTPFailedDependency, +    '426' => Net::HTTPUpgradeRequired, +    '428' => Net::HTTPPreconditionRequired, +    '429' => Net::HTTPTooManyRequests, +    '431' => Net::HTTPRequestHeaderFieldsTooLarge, + +    '500' => Net::HTTPInternalServerError, +    '501' => Net::HTTPNotImplemented, +    '502' => Net::HTTPBadGateway, +    '503' => Net::HTTPServiceUnavailable, +    '504' => Net::HTTPGatewayTimeOut, +    '505' => Net::HTTPVersionNotSupported, +    '507' => Net::HTTPInsufficientStorage, +    '511' => Net::HTTPNetworkAuthenticationRequired, +  } +end + +# :startdoc: + diff --git a/jni/ruby/lib/net/https.rb b/jni/ruby/lib/net/https.rb new file mode 100644 index 0000000..d36f820 --- /dev/null +++ b/jni/ruby/lib/net/https.rb @@ -0,0 +1,22 @@ +=begin + += net/https -- SSL/TLS enhancement for Net::HTTP. + +  This file has been merged with net/http.  There is no longer any need to +  require 'net/https' to use HTTPS. + +  See Net::HTTP for details on how to make HTTPS connections. + +== Info +  'OpenSSL for Ruby 2' project +  Copyright (C) 2001 GOTOU Yuuzou <gotoyuzo@notwork.org> +  All rights reserved. + +== Licence +  This program is licenced under the same licence as Ruby. +  (See the file 'LICENCE'.) + +=end + +require 'net/http' +require 'openssl' diff --git a/jni/ruby/lib/net/imap.rb b/jni/ruby/lib/net/imap.rb new file mode 100644 index 0000000..0517ca1 --- /dev/null +++ b/jni/ruby/lib/net/imap.rb @@ -0,0 +1,3622 @@ +# +# = net/imap.rb +# +# Copyright (C) 2000  Shugo Maeda <shugo@ruby-lang.org> +# +# This library is distributed under the terms of the Ruby license. +# You can freely distribute/modify this library. +# +# Documentation: Shugo Maeda, with RDoc conversion and overview by William +# Webber. +# +# See Net::IMAP for documentation. +# + + +require "socket" +require "monitor" +require "digest/md5" +require "strscan" +begin +  require "openssl" +rescue LoadError +end + +module Net + +  # +  # Net::IMAP implements Internet Message Access Protocol (IMAP) client +  # functionality.  The protocol is described in [IMAP]. +  # +  # == IMAP Overview +  # +  # An IMAP client connects to a server, and then authenticates +  # itself using either #authenticate() or #login().  Having +  # authenticated itself, there is a range of commands +  # available to it.  Most work with mailboxes, which may be +  # arranged in an hierarchical namespace, and each of which +  # contains zero or more messages.  How this is implemented on +  # the server is implementation-dependent; on a UNIX server, it +  # will frequently be implemented as files in mailbox format +  # within a hierarchy of directories. +  # +  # To work on the messages within a mailbox, the client must +  # first select that mailbox, using either #select() or (for +  # read-only access) #examine().  Once the client has successfully +  # selected a mailbox, they enter _selected_ state, and that +  # mailbox becomes the _current_ mailbox, on which mail-item +  # related commands implicitly operate. +  # +  # Messages have two sorts of identifiers: message sequence +  # numbers and UIDs. +  # +  # Message sequence numbers number messages within a mailbox +  # from 1 up to the number of items in the mailbox.  If a new +  # message arrives during a session, it receives a sequence +  # number equal to the new size of the mailbox.  If messages +  # are expunged from the mailbox, remaining messages have their +  # sequence numbers "shuffled down" to fill the gaps. +  # +  # UIDs, on the other hand, are permanently guaranteed not to +  # identify another message within the same mailbox, even if +  # the existing message is deleted.  UIDs are required to +  # be assigned in ascending (but not necessarily sequential) +  # order within a mailbox; this means that if a non-IMAP client +  # rearranges the order of mailitems within a mailbox, the +  # UIDs have to be reassigned.  An IMAP client thus cannot +  # rearrange message orders. +  # +  # == Examples of Usage +  # +  # === List sender and subject of all recent messages in the default mailbox +  # +  #   imap = Net::IMAP.new('mail.example.com') +  #   imap.authenticate('LOGIN', 'joe_user', 'joes_password') +  #   imap.examine('INBOX') +  #   imap.search(["RECENT"]).each do |message_id| +  #     envelope = imap.fetch(message_id, "ENVELOPE")[0].attr["ENVELOPE"] +  #     puts "#{envelope.from[0].name}: \t#{envelope.subject}" +  #   end +  # +  # === Move all messages from April 2003 from "Mail/sent-mail" to "Mail/sent-apr03" +  # +  #   imap = Net::IMAP.new('mail.example.com') +  #   imap.authenticate('LOGIN', 'joe_user', 'joes_password') +  #   imap.select('Mail/sent-mail') +  #   if not imap.list('Mail/', 'sent-apr03') +  #     imap.create('Mail/sent-apr03') +  #   end +  #   imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id| +  #     imap.copy(message_id, "Mail/sent-apr03") +  #     imap.store(message_id, "+FLAGS", [:Deleted]) +  #   end +  #   imap.expunge +  # +  # == Thread Safety +  # +  # Net::IMAP supports concurrent threads. For example, +  # +  #   imap = Net::IMAP.new("imap.foo.net", "imap2") +  #   imap.authenticate("cram-md5", "bar", "password") +  #   imap.select("inbox") +  #   fetch_thread = Thread.start { imap.fetch(1..-1, "UID") } +  #   search_result = imap.search(["BODY", "hello"]) +  #   fetch_result = fetch_thread.value +  #   imap.disconnect +  # +  # This script invokes the FETCH command and the SEARCH command concurrently. +  # +  # == Errors +  # +  # An IMAP server can send three different types of responses to indicate +  # failure: +  # +  # NO:: the attempted command could not be successfully completed.  For +  #      instance, the username/password used for logging in are incorrect; +  #      the selected mailbox does not exist; etc. +  # +  # BAD:: the request from the client does not follow the server's +  #       understanding of the IMAP protocol.  This includes attempting +  #       commands from the wrong client state; for instance, attempting +  #       to perform a SEARCH command without having SELECTed a current +  #       mailbox.  It can also signal an internal server +  #       failure (such as a disk crash) has occurred. +  # +  # BYE:: the server is saying goodbye.  This can be part of a normal +  #       logout sequence, and can be used as part of a login sequence +  #       to indicate that the server is (for some reason) unwilling +  #       to accept your connection.  As a response to any other command, +  #       it indicates either that the server is shutting down, or that +  #       the server is timing out the client connection due to inactivity. +  # +  # These three error response are represented by the errors +  # Net::IMAP::NoResponseError, Net::IMAP::BadResponseError, and +  # Net::IMAP::ByeResponseError, all of which are subclasses of +  # Net::IMAP::ResponseError.  Essentially, all methods that involve +  # sending a request to the server can generate one of these errors. +  # Only the most pertinent instances have been documented below. +  # +  # Because the IMAP class uses Sockets for communication, its methods +  # are also susceptible to the various errors that can occur when +  # working with sockets.  These are generally represented as +  # Errno errors.  For instance, any method that involves sending a +  # request to the server and/or receiving a response from it could +  # raise an Errno::EPIPE error if the network connection unexpectedly +  # goes down.  See the socket(7), ip(7), tcp(7), socket(2), connect(2), +  # and associated man pages. +  # +  # Finally, a Net::IMAP::DataFormatError is thrown if low-level data +  # is found to be in an incorrect format (for instance, when converting +  # between UTF-8 and UTF-16), and Net::IMAP::ResponseParseError is +  # thrown if a server response is non-parseable. +  # +  # +  # == References +  # +  # [[IMAP]] +  #    M. Crispin, "INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1", +  #    RFC 2060, December 1996.  (Note: since obsoleted by RFC 3501) +  # +  # [[LANGUAGE-TAGS]] +  #    Alvestrand, H., "Tags for the Identification of +  #    Languages", RFC 1766, March 1995. +  # +  # [[MD5]] +  #    Myers, J., and M. Rose, "The Content-MD5 Header Field", RFC +  #    1864, October 1995. +  # +  # [[MIME-IMB]] +  #    Freed, N., and N. Borenstein, "MIME (Multipurpose Internet +  #    Mail Extensions) Part One: Format of Internet Message Bodies", RFC +  #    2045, November 1996. +  # +  # [[RFC-822]] +  #    Crocker, D., "Standard for the Format of ARPA Internet Text +  #    Messages", STD 11, RFC 822, University of Delaware, August 1982. +  # +  # [[RFC-2087]] +  #    Myers, J., "IMAP4 QUOTA extension", RFC 2087, January 1997. +  # +  # [[RFC-2086]] +  #    Myers, J., "IMAP4 ACL extension", RFC 2086, January 1997. +  # +  # [[RFC-2195]] +  #    Klensin, J., Catoe, R., and Krumviede, P., "IMAP/POP AUTHorize Extension +  #    for Simple Challenge/Response", RFC 2195, September 1997. +  # +  # [[SORT-THREAD-EXT]] +  #    Crispin, M., "INTERNET MESSAGE ACCESS PROTOCOL - SORT and THREAD +  #    Extensions", draft-ietf-imapext-sort, May 2003. +  # +  # [[OSSL]] +  #    http://www.openssl.org +  # +  # [[RSSL]] +  #    http://savannah.gnu.org/projects/rubypki +  # +  # [[UTF7]] +  #    Goldsmith, D. and Davis, M., "UTF-7: A Mail-Safe Transformation Format of +  #    Unicode", RFC 2152, May 1997. +  # +  class IMAP +    include MonitorMixin +    if defined?(OpenSSL::SSL) +      include OpenSSL +      include SSL +    end + +    #  Returns an initial greeting response from the server. +    attr_reader :greeting + +    # Returns recorded untagged responses.  For example: +    # +    #   imap.select("inbox") +    #   p imap.responses["EXISTS"][-1] +    #   #=> 2 +    #   p imap.responses["UIDVALIDITY"][-1] +    #   #=> 968263756 +    attr_reader :responses + +    # Returns all response handlers. +    attr_reader :response_handlers + +    # The thread to receive exceptions. +    attr_accessor :client_thread + +    # Flag indicating a message has been seen. +    SEEN = :Seen + +    # Flag indicating a message has been answered. +    ANSWERED = :Answered + +    # Flag indicating a message has been flagged for special or urgent +    # attention. +    FLAGGED = :Flagged + +    # Flag indicating a message has been marked for deletion.  This +    # will occur when the mailbox is closed or expunged. +    DELETED = :Deleted + +    # Flag indicating a message is only a draft or work-in-progress version. +    DRAFT = :Draft + +    # Flag indicating that the message is "recent," meaning that this +    # session is the first session in which the client has been notified +    # of this message. +    RECENT = :Recent + +    # Flag indicating that a mailbox context name cannot contain +    # children. +    NOINFERIORS = :Noinferiors + +    # Flag indicating that a mailbox is not selected. +    NOSELECT = :Noselect + +    # Flag indicating that a mailbox has been marked "interesting" by +    # the server; this commonly indicates that the mailbox contains +    # new messages. +    MARKED = :Marked + +    # Flag indicating that the mailbox does not contains new messages. +    UNMARKED = :Unmarked + +    # Returns the debug mode. +    def self.debug +      return @@debug +    end + +    # Sets the debug mode. +    def self.debug=(val) +      return @@debug = val +    end + +    # Returns the max number of flags interned to symbols. +    def self.max_flag_count +      return @@max_flag_count +    end + +    # Sets the max number of flags interned to symbols. +    def self.max_flag_count=(count) +      @@max_flag_count = count +    end + +    # Adds an authenticator for Net::IMAP#authenticate.  +auth_type+ +    # is the type of authentication this authenticator supports +    # (for instance, "LOGIN").  The +authenticator+ is an object +    # which defines a process() method to handle authentication with +    # the server.  See Net::IMAP::LoginAuthenticator, +    # Net::IMAP::CramMD5Authenticator, and Net::IMAP::DigestMD5Authenticator +    # for examples. +    # +    # +    # If +auth_type+ refers to an existing authenticator, it will be +    # replaced by the new one. +    def self.add_authenticator(auth_type, authenticator) +      @@authenticators[auth_type] = authenticator +    end + +    # The default port for IMAP connections, port 143 +    def self.default_port +      return PORT +    end + +    # The default port for IMAPS connections, port 993 +    def self.default_tls_port +      return SSL_PORT +    end + +    class << self +      alias default_imap_port default_port +      alias default_imaps_port default_tls_port +      alias default_ssl_port default_tls_port +    end + +    # Disconnects from the server. +    def disconnect +      begin +        begin +          # try to call SSL::SSLSocket#io. +          @sock.io.shutdown +        rescue NoMethodError +          # @sock is not an SSL::SSLSocket. +          @sock.shutdown +        end +      rescue Errno::ENOTCONN +        # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms. +      rescue Exception => e +        @receiver_thread.raise(e) +      end +      @receiver_thread.join +      synchronize do +        unless @sock.closed? +          @sock.close +        end +      end +      raise e if e +    end + +    # Returns true if disconnected from the server. +    def disconnected? +      return @sock.closed? +    end + +    # Sends a CAPABILITY command, and returns an array of +    # capabilities that the server supports.  Each capability +    # is a string.  See [IMAP] for a list of possible +    # capabilities. +    # +    # Note that the Net::IMAP class does not modify its +    # behaviour according to the capabilities of the server; +    # it is up to the user of the class to ensure that +    # a certain capability is supported by a server before +    # using it. +    def capability +      synchronize do +        send_command("CAPABILITY") +        return @responses.delete("CAPABILITY")[-1] +      end +    end + +    # Sends a NOOP command to the server. It does nothing. +    def noop +      send_command("NOOP") +    end + +    # Sends a LOGOUT command to inform the server that the client is +    # done with the connection. +    def logout +      send_command("LOGOUT") +    end + +    # Sends a STARTTLS command to start TLS session. +    def starttls(options = {}, verify = true) +      send_command("STARTTLS") do |resp| +        if resp.kind_of?(TaggedResponse) && resp.name == "OK" +          begin +            # for backward compatibility +            certs = options.to_str +            options = create_ssl_params(certs, verify) +          rescue NoMethodError +          end +          start_tls_session(options) +        end +      end +    end + +    # Sends an AUTHENTICATE command to authenticate the client. +    # The +auth_type+ parameter is a string that represents +    # the authentication mechanism to be used. Currently Net::IMAP +    # supports the authentication mechanisms: +    # +    #   LOGIN:: login using cleartext user and password. +    #   CRAM-MD5:: login with cleartext user and encrypted password +    #              (see [RFC-2195] for a full description).  This +    #              mechanism requires that the server have the user's +    #              password stored in clear-text password. +    # +    # For both of these mechanisms, there should be two +args+: username +    # and (cleartext) password.  A server may not support one or the other +    # of these mechanisms; check #capability() for a capability of +    # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5". +    # +    # Authentication is done using the appropriate authenticator object: +    # see @@authenticators for more information on plugging in your own +    # authenticator. +    # +    # For example: +    # +    #    imap.authenticate('LOGIN', user, password) +    # +    # A Net::IMAP::NoResponseError is raised if authentication fails. +    def authenticate(auth_type, *args) +      auth_type = auth_type.upcase +      unless @@authenticators.has_key?(auth_type) +        raise ArgumentError, +          format('unknown auth type - "%s"', auth_type) +      end +      authenticator = @@authenticators[auth_type].new(*args) +      send_command("AUTHENTICATE", auth_type) do |resp| +        if resp.instance_of?(ContinuationRequest) +          data = authenticator.process(resp.data.text.unpack("m")[0]) +          s = [data].pack("m").gsub(/\n/, "") +          send_string_data(s) +          put_string(CRLF) +        end +      end +    end + +    # Sends a LOGIN command to identify the client and carries +    # the plaintext +password+ authenticating this +user+.  Note +    # that, unlike calling #authenticate() with an +auth_type+ +    # of "LOGIN", #login() does *not* use the login authenticator. +    # +    # A Net::IMAP::NoResponseError is raised if authentication fails. +    def login(user, password) +      send_command("LOGIN", user, password) +    end + +    # Sends a SELECT command to select a +mailbox+ so that messages +    # in the +mailbox+ can be accessed. +    # +    # After you have selected a mailbox, you may retrieve the +    # number of items in that mailbox from @responses["EXISTS"][-1], +    # and the number of recent messages from @responses["RECENT"][-1]. +    # Note that these values can change if new messages arrive +    # during a session; see #add_response_handler() for a way of +    # detecting this event. +    # +    # A Net::IMAP::NoResponseError is raised if the mailbox does not +    # exist or is for some reason non-selectable. +    def select(mailbox) +      synchronize do +        @responses.clear +        send_command("SELECT", mailbox) +      end +    end + +    # Sends a EXAMINE command to select a +mailbox+ so that messages +    # in the +mailbox+ can be accessed.  Behaves the same as #select(), +    # except that the selected +mailbox+ is identified as read-only. +    # +    # A Net::IMAP::NoResponseError is raised if the mailbox does not +    # exist or is for some reason non-examinable. +    def examine(mailbox) +      synchronize do +        @responses.clear +        send_command("EXAMINE", mailbox) +      end +    end + +    # Sends a CREATE command to create a new +mailbox+. +    # +    # A Net::IMAP::NoResponseError is raised if a mailbox with that name +    # cannot be created. +    def create(mailbox) +      send_command("CREATE", mailbox) +    end + +    # Sends a DELETE command to remove the +mailbox+. +    # +    # A Net::IMAP::NoResponseError is raised if a mailbox with that name +    # cannot be deleted, either because it does not exist or because the +    # client does not have permission to delete it. +    def delete(mailbox) +      send_command("DELETE", mailbox) +    end + +    # Sends a RENAME command to change the name of the +mailbox+ to +    # +newname+. +    # +    # A Net::IMAP::NoResponseError is raised if a mailbox with the +    # name +mailbox+ cannot be renamed to +newname+ for whatever +    # reason; for instance, because +mailbox+ does not exist, or +    # because there is already a mailbox with the name +newname+. +    def rename(mailbox, newname) +      send_command("RENAME", mailbox, newname) +    end + +    # Sends a SUBSCRIBE command to add the specified +mailbox+ name to +    # the server's set of "active" or "subscribed" mailboxes as returned +    # by #lsub(). +    # +    # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be +    # subscribed to; for instance, because it does not exist. +    def subscribe(mailbox) +      send_command("SUBSCRIBE", mailbox) +    end + +    # Sends a UNSUBSCRIBE command to remove the specified +mailbox+ name +    # from the server's set of "active" or "subscribed" mailboxes. +    # +    # A Net::IMAP::NoResponseError is raised if +mailbox+ cannot be +    # unsubscribed from; for instance, because the client is not currently +    # subscribed to it. +    def unsubscribe(mailbox) +      send_command("UNSUBSCRIBE", mailbox) +    end + +    # Sends a LIST command, and returns a subset of names from +    # the complete set of all names available to the client. +    # +refname+ provides a context (for instance, a base directory +    # in a directory-based mailbox hierarchy).  +mailbox+ specifies +    # a mailbox or (via wildcards) mailboxes under that context. +    # Two wildcards may be used in +mailbox+: '*', which matches +    # all characters *including* the hierarchy delimiter (for instance, +    # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%', +    # which matches all characters *except* the hierarchy delimiter. +    # +    # If +refname+ is empty, +mailbox+ is used directly to determine +    # which mailboxes to match.  If +mailbox+ is empty, the root +    # name of +refname+ and the hierarchy delimiter are returned. +    # +    # The return value is an array of +Net::IMAP::MailboxList+. For example: +    # +    #   imap.create("foo/bar") +    #   imap.create("foo/baz") +    #   p imap.list("", "foo/%") +    #   #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\ +    #        #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\ +    #        #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">] +    def list(refname, mailbox) +      synchronize do +        send_command("LIST", refname, mailbox) +        return @responses.delete("LIST") +      end +    end + +    # Sends a XLIST command, and returns a subset of names from +    # the complete set of all names available to the client. +    # +refname+ provides a context (for instance, a base directory +    # in a directory-based mailbox hierarchy).  +mailbox+ specifies +    # a mailbox or (via wildcards) mailboxes under that context. +    # Two wildcards may be used in +mailbox+: '*', which matches +    # all characters *including* the hierarchy delimiter (for instance, +    # '/' on a UNIX-hosted directory-based mailbox hierarchy); and '%', +    # which matches all characters *except* the hierarchy delimiter. +    # +    # If +refname+ is empty, +mailbox+ is used directly to determine +    # which mailboxes to match.  If +mailbox+ is empty, the root +    # name of +refname+ and the hierarchy delimiter are returned. +    # +    # The XLIST command is like the LIST command except that the flags +    # returned refer to the function of the folder/mailbox, e.g. :Sent +    # +    # The return value is an array of +Net::IMAP::MailboxList+. For example: +    # +    #   imap.create("foo/bar") +    #   imap.create("foo/baz") +    #   p imap.xlist("", "foo/%") +    #   #=> [#<Net::IMAP::MailboxList attr=[:Noselect], delim="/", name="foo/">, \\ +    #        #<Net::IMAP::MailboxList attr=[:Noinferiors, :Marked], delim="/", name="foo/bar">, \\ +    #        #<Net::IMAP::MailboxList attr=[:Noinferiors], delim="/", name="foo/baz">] +    def xlist(refname, mailbox) +      synchronize do +        send_command("XLIST", refname, mailbox) +        return @responses.delete("XLIST") +      end +    end + +    # Sends the GETQUOTAROOT command along with the specified +mailbox+. +    # This command is generally available to both admin and user. +    # If this mailbox exists, it returns an array containing objects of type +    # Net::IMAP::MailboxQuotaRoot and Net::IMAP::MailboxQuota. +    def getquotaroot(mailbox) +      synchronize do +        send_command("GETQUOTAROOT", mailbox) +        result = [] +        result.concat(@responses.delete("QUOTAROOT")) +        result.concat(@responses.delete("QUOTA")) +        return result +      end +    end + +    # Sends the GETQUOTA command along with specified +mailbox+. +    # If this mailbox exists, then an array containing a +    # Net::IMAP::MailboxQuota object is returned.  This +    # command is generally only available to server admin. +    def getquota(mailbox) +      synchronize do +        send_command("GETQUOTA", mailbox) +        return @responses.delete("QUOTA") +      end +    end + +    # Sends a SETQUOTA command along with the specified +mailbox+ and +    # +quota+.  If +quota+ is nil, then +quota+ will be unset for that +    # mailbox.  Typically one needs to be logged in as a server admin +    # for this to work.  The IMAP quota commands are described in +    # [RFC-2087]. +    def setquota(mailbox, quota) +      if quota.nil? +        data = '()' +      else +        data = '(STORAGE ' + quota.to_s + ')' +      end +      send_command("SETQUOTA", mailbox, RawData.new(data)) +    end + +    # Sends the SETACL command along with +mailbox+, +user+ and the +    # +rights+ that user is to have on that mailbox.  If +rights+ is nil, +    # then that user will be stripped of any rights to that mailbox. +    # The IMAP ACL commands are described in [RFC-2086]. +    def setacl(mailbox, user, rights) +      if rights.nil? +        send_command("SETACL", mailbox, user, "") +      else +        send_command("SETACL", mailbox, user, rights) +      end +    end + +    # Send the GETACL command along with a specified +mailbox+. +    # If this mailbox exists, an array containing objects of +    # Net::IMAP::MailboxACLItem will be returned. +    def getacl(mailbox) +      synchronize do +        send_command("GETACL", mailbox) +        return @responses.delete("ACL")[-1] +      end +    end + +    # Sends a LSUB command, and returns a subset of names from the set +    # of names that the user has declared as being "active" or +    # "subscribed."  +refname+ and +mailbox+ are interpreted as +    # for #list(). +    # The return value is an array of +Net::IMAP::MailboxList+. +    def lsub(refname, mailbox) +      synchronize do +        send_command("LSUB", refname, mailbox) +        return @responses.delete("LSUB") +      end +    end + +    # Sends a STATUS command, and returns the status of the indicated +    # +mailbox+. +attr+ is a list of one or more attributes whose +    # statuses are to be requested.  Supported attributes include: +    # +    #   MESSAGES:: the number of messages in the mailbox. +    #   RECENT:: the number of recent messages in the mailbox. +    #   UNSEEN:: the number of unseen messages in the mailbox. +    # +    # The return value is a hash of attributes. For example: +    # +    #   p imap.status("inbox", ["MESSAGES", "RECENT"]) +    #   #=> {"RECENT"=>0, "MESSAGES"=>44} +    # +    # A Net::IMAP::NoResponseError is raised if status values +    # for +mailbox+ cannot be returned; for instance, because it +    # does not exist. +    def status(mailbox, attr) +      synchronize do +        send_command("STATUS", mailbox, attr) +        return @responses.delete("STATUS")[-1].attr +      end +    end + +    # Sends a APPEND command to append the +message+ to the end of +    # the +mailbox+. The optional +flags+ argument is an array of +    # flags initially passed to the new message.  The optional +    # +date_time+ argument specifies the creation time to assign to the +    # new message; it defaults to the current time. +    # For example: +    # +    #   imap.append("inbox", <<EOF.gsub(/\n/, "\r\n"), [:Seen], Time.now) +    #   Subject: hello +    #   From: shugo@ruby-lang.org +    #   To: shugo@ruby-lang.org +    # +    #   hello world +    #   EOF +    # +    # A Net::IMAP::NoResponseError is raised if the mailbox does +    # not exist (it is not created automatically), or if the flags, +    # date_time, or message arguments contain errors. +    def append(mailbox, message, flags = nil, date_time = nil) +      args = [] +      if flags +        args.push(flags) +      end +      args.push(date_time) if date_time +      args.push(Literal.new(message)) +      send_command("APPEND", mailbox, *args) +    end + +    # Sends a CHECK command to request a checkpoint of the currently +    # selected mailbox.  This performs implementation-specific +    # housekeeping; for instance, reconciling the mailbox's +    # in-memory and on-disk state. +    def check +      send_command("CHECK") +    end + +    # Sends a CLOSE command to close the currently selected mailbox. +    # The CLOSE command permanently removes from the mailbox all +    # messages that have the \Deleted flag set. +    def close +      send_command("CLOSE") +    end + +    # Sends a EXPUNGE command to permanently remove from the currently +    # selected mailbox all messages that have the \Deleted flag set. +    def expunge +      synchronize do +        send_command("EXPUNGE") +        return @responses.delete("EXPUNGE") +      end +    end + +    # Sends a SEARCH command to search the mailbox for messages that +    # match the given searching criteria, and returns message sequence +    # numbers.  +keys+ can either be a string holding the entire +    # search string, or a single-dimension array of search keywords and +    # arguments.  The following are some common search criteria; +    # see [IMAP] section 6.4.4 for a full list. +    # +    # <message set>:: a set of message sequence numbers.  ',' indicates +    #                 an interval, ':' indicates a range.  For instance, +    #                 '2,10:12,15' means "2,10,11,12,15". +    # +    # BEFORE <date>:: messages with an internal date strictly before +    #                 <date>.  The date argument has a format similar +    #                 to 8-Aug-2002. +    # +    # BODY <string>:: messages that contain <string> within their body. +    # +    # CC <string>:: messages containing <string> in their CC field. +    # +    # FROM <string>:: messages that contain <string> in their FROM field. +    # +    # NEW:: messages with the \Recent, but not the \Seen, flag set. +    # +    # NOT <search-key>:: negate the following search key. +    # +    # OR <search-key> <search-key>:: "or" two search keys together. +    # +    # ON <date>:: messages with an internal date exactly equal to <date>, +    #             which has a format similar to 8-Aug-2002. +    # +    # SINCE <date>:: messages with an internal date on or after <date>. +    # +    # SUBJECT <string>:: messages with <string> in their subject. +    # +    # TO <string>:: messages with <string> in their TO field. +    # +    # For example: +    # +    #   p imap.search(["SUBJECT", "hello", "NOT", "NEW"]) +    #   #=> [1, 6, 7, 8] +    def search(keys, charset = nil) +      return search_internal("SEARCH", keys, charset) +    end + +    # Similar to #search(), but returns unique identifiers. +    def uid_search(keys, charset = nil) +      return search_internal("UID SEARCH", keys, charset) +    end + +    # Sends a FETCH command to retrieve data associated with a message +    # in the mailbox. +    # +    # The +set+ parameter is a number or a range between two numbers, +    # or an array of those.  The number is a message sequence number, +    # where -1 repesents a '*' for use in range notation like 100..-1 +    # being interpreted as '100:*'.  Beware that the +exclude_end?+ +    # property of a Range object is ignored, and the contents of a +    # range are independent of the order of the range endpoints as per +    # the protocol specification, so 1...5, 5..1 and 5...1 are all +    # equivalent to 1..5. +    # +    # +attr+ is a list of attributes to fetch; see the documentation +    # for Net::IMAP::FetchData for a list of valid attributes. +    # +    # The return value is an array of Net::IMAP::FetchData or nil +    # (instead of an empty array) if there is no matching message. +    # +    # For example: +    # +    #   p imap.fetch(6..8, "UID") +    #   #=> [#<Net::IMAP::FetchData seqno=6, attr={"UID"=>98}>, \\ +    #        #<Net::IMAP::FetchData seqno=7, attr={"UID"=>99}>, \\ +    #        #<Net::IMAP::FetchData seqno=8, attr={"UID"=>100}>] +    #   p imap.fetch(6, "BODY[HEADER.FIELDS (SUBJECT)]") +    #   #=> [#<Net::IMAP::FetchData seqno=6, attr={"BODY[HEADER.FIELDS (SUBJECT)]"=>"Subject: test\r\n\r\n"}>] +    #   data = imap.uid_fetch(98, ["RFC822.SIZE", "INTERNALDATE"])[0] +    #   p data.seqno +    #   #=> 6 +    #   p data.attr["RFC822.SIZE"] +    #   #=> 611 +    #   p data.attr["INTERNALDATE"] +    #   #=> "12-Oct-2000 22:40:59 +0900" +    #   p data.attr["UID"] +    #   #=> 98 +    def fetch(set, attr) +      return fetch_internal("FETCH", set, attr) +    end + +    # Similar to #fetch(), but +set+ contains unique identifiers. +    def uid_fetch(set, attr) +      return fetch_internal("UID FETCH", set, attr) +    end + +    # Sends a STORE command to alter data associated with messages +    # in the mailbox, in particular their flags. The +set+ parameter +    # is a number, an array of numbers, or a Range object. Each number +    # is a message sequence number.  +attr+ is the name of a data item +    # to store: 'FLAGS' will replace the message's flag list +    # with the provided one, '+FLAGS' will add the provided flags, +    # and '-FLAGS' will remove them.  +flags+ is a list of flags. +    # +    # The return value is an array of Net::IMAP::FetchData. For example: +    # +    #   p imap.store(6..8, "+FLAGS", [:Deleted]) +    #   #=> [#<Net::IMAP::FetchData seqno=6, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\ +    #        #<Net::IMAP::FetchData seqno=7, attr={"FLAGS"=>[:Seen, :Deleted]}>, \\ +    #        #<Net::IMAP::FetchData seqno=8, attr={"FLAGS"=>[:Seen, :Deleted]}>] +    def store(set, attr, flags) +      return store_internal("STORE", set, attr, flags) +    end + +    # Similar to #store(), but +set+ contains unique identifiers. +    def uid_store(set, attr, flags) +      return store_internal("UID STORE", set, attr, flags) +    end + +    # Sends a COPY command to copy the specified message(s) to the end +    # of the specified destination +mailbox+. The +set+ parameter is +    # a number, an array of numbers, or a Range object. The number is +    # a message sequence number. +    def copy(set, mailbox) +      copy_internal("COPY", set, mailbox) +    end + +    # Similar to #copy(), but +set+ contains unique identifiers. +    def uid_copy(set, mailbox) +      copy_internal("UID COPY", set, mailbox) +    end + +    # Sends a SORT command to sort messages in the mailbox. +    # Returns an array of message sequence numbers. For example: +    # +    #   p imap.sort(["FROM"], ["ALL"], "US-ASCII") +    #   #=> [1, 2, 3, 5, 6, 7, 8, 4, 9] +    #   p imap.sort(["DATE"], ["SUBJECT", "hello"], "US-ASCII") +    #   #=> [6, 7, 8, 1] +    # +    # See [SORT-THREAD-EXT] for more details. +    def sort(sort_keys, search_keys, charset) +      return sort_internal("SORT", sort_keys, search_keys, charset) +    end + +    # Similar to #sort(), but returns an array of unique identifiers. +    def uid_sort(sort_keys, search_keys, charset) +      return sort_internal("UID SORT", sort_keys, search_keys, charset) +    end + +    # Adds a response handler. For example, to detect when +    # the server sends a new EXISTS response (which normally +    # indicates new messages being added to the mailbox), +    # add the following handler after selecting the +    # mailbox: +    # +    #   imap.add_response_handler { |resp| +    #     if resp.kind_of?(Net::IMAP::UntaggedResponse) and resp.name == "EXISTS" +    #       puts "Mailbox now has #{resp.data} messages" +    #     end +    #   } +    # +    def add_response_handler(handler = Proc.new) +      @response_handlers.push(handler) +    end + +    # Removes the response handler. +    def remove_response_handler(handler) +      @response_handlers.delete(handler) +    end + +    # Similar to #search(), but returns message sequence numbers in threaded +    # format, as a Net::IMAP::ThreadMember tree.  The supported algorithms +    # are: +    # +    # ORDEREDSUBJECT:: split into single-level threads according to subject, +    #                  ordered by date. +    # REFERENCES:: split into threads by parent/child relationships determined +    #              by which message is a reply to which. +    # +    # Unlike #search(), +charset+ is a required argument.  US-ASCII +    # and UTF-8 are sample values. +    # +    # See [SORT-THREAD-EXT] for more details. +    def thread(algorithm, search_keys, charset) +      return thread_internal("THREAD", algorithm, search_keys, charset) +    end + +    # Similar to #thread(), but returns unique identifiers instead of +    # message sequence numbers. +    def uid_thread(algorithm, search_keys, charset) +      return thread_internal("UID THREAD", algorithm, search_keys, charset) +    end + +    # Sends an IDLE command that waits for notifications of new or expunged +    # messages.  Yields responses from the server during the IDLE. +    # +    # Use #idle_done() to leave IDLE. +    def idle(&response_handler) +      raise LocalJumpError, "no block given" unless response_handler + +      response = nil + +      synchronize do +        tag = Thread.current[:net_imap_tag] = generate_tag +        put_string("#{tag} IDLE#{CRLF}") + +        begin +          add_response_handler(response_handler) +          @idle_done_cond = new_cond +          @idle_done_cond.wait +          @idle_done_cond = nil +          if @receiver_thread_terminating +            raise Net::IMAP::Error, "connection closed" +          end +        ensure +          unless @receiver_thread_terminating +            remove_response_handler(response_handler) +            put_string("DONE#{CRLF}") +            response = get_tagged_response(tag, "IDLE") +          end +        end +      end + +      return response +    end + +    # Leaves IDLE. +    def idle_done +      synchronize do +        if @idle_done_cond.nil? +          raise Net::IMAP::Error, "not during IDLE" +        end +        @idle_done_cond.signal +      end +    end + +    # Decode a string from modified UTF-7 format to UTF-8. +    # +    # UTF-7 is a 7-bit encoding of Unicode [UTF7].  IMAP uses a +    # slightly modified version of this to encode mailbox names +    # containing non-ASCII characters; see [IMAP] section 5.1.3. +    # +    # Net::IMAP does _not_ automatically encode and decode +    # mailbox names to and from UTF-7. +    def self.decode_utf7(s) +      return s.gsub(/&([^-]+)?-/n) { +        if $1 +          ($1.tr(",", "/") + "===").unpack("m")[0].encode(Encoding::UTF_8, Encoding::UTF_16BE) +        else +          "&" +        end +      } +    end + +    # Encode a string from UTF-8 format to modified UTF-7. +    def self.encode_utf7(s) +      return s.gsub(/(&)|[^\x20-\x7e]+/) { +        if $1 +          "&-" +        else +          base64 = [$&.encode(Encoding::UTF_16BE)].pack("m") +          "&" + base64.delete("=\n").tr("/", ",") + "-" +        end +      }.force_encoding("ASCII-8BIT") +    end + +    # Formats +time+ as an IMAP-style date. +    def self.format_date(time) +      return time.strftime('%d-%b-%Y') +    end + +    # Formats +time+ as an IMAP-style date-time. +    def self.format_datetime(time) +      return time.strftime('%d-%b-%Y %H:%M %z') +    end + +    private + +    CRLF = "\r\n"      # :nodoc: +    PORT = 143         # :nodoc: +    SSL_PORT = 993   # :nodoc: + +    @@debug = false +    @@authenticators = {} +    @@max_flag_count = 10000 + +    # :call-seq: +    #    Net::IMAP.new(host, options = {}) +    # +    # Creates a new Net::IMAP object and connects it to the specified +    # +host+. +    # +    # +options+ is an option hash, each key of which is a symbol. +    # +    # The available options are: +    # +    # port::  Port number (default value is 143 for imap, or 993 for imaps) +    # ssl::   If options[:ssl] is true, then an attempt will be made +    #         to use SSL (now TLS) to connect to the server.  For this to work +    #         OpenSSL [OSSL] and the Ruby OpenSSL [RSSL] extensions need to +    #         be installed. +    #         If options[:ssl] is a hash, it's passed to +    #         OpenSSL::SSL::SSLContext#set_params as parameters. +    # +    # The most common errors are: +    # +    # Errno::ECONNREFUSED:: Connection refused by +host+ or an intervening +    #                       firewall. +    # Errno::ETIMEDOUT:: Connection timed out (possibly due to packets +    #                    being dropped by an intervening firewall). +    # Errno::ENETUNREACH:: There is no route to that network. +    # SocketError:: Hostname not known or other socket error. +    # Net::IMAP::ByeResponseError:: The connected to the host was successful, but +    #                               it immediately said goodbye. +    def initialize(host, port_or_options = {}, +                   usessl = false, certs = nil, verify = true) +      super() +      @host = host +      begin +        options = port_or_options.to_hash +      rescue NoMethodError +        # for backward compatibility +        options = {} +        options[:port] = port_or_options +        if usessl +          options[:ssl] = create_ssl_params(certs, verify) +        end +      end +      @port = options[:port] || (options[:ssl] ? SSL_PORT : PORT) +      @tag_prefix = "RUBY" +      @tagno = 0 +      @parser = ResponseParser.new +      @sock = TCPSocket.open(@host, @port) +      begin +        if options[:ssl] +          start_tls_session(options[:ssl]) +          @usessl = true +        else +          @usessl = false +        end +        @responses = Hash.new([].freeze) +        @tagged_responses = {} +        @response_handlers = [] +        @tagged_response_arrival = new_cond +        @continuation_request_arrival = new_cond +        @idle_done_cond = nil +        @logout_command_tag = nil +        @debug_output_bol = true +        @exception = nil + +        @greeting = get_response +        if @greeting.nil? +          raise Error, "connection closed" +        end +        if @greeting.name == "BYE" +          raise ByeResponseError, @greeting +        end + +        @client_thread = Thread.current +        @receiver_thread = Thread.start { +          begin +            receive_responses +          rescue Exception +          end +        } +        @receiver_thread_terminating = false +      rescue Exception +        @sock.close +        raise +      end +    end + +    def receive_responses +      connection_closed = false +      until connection_closed +        synchronize do +          @exception = nil +        end +        begin +          resp = get_response +        rescue Exception => e +          synchronize do +            @sock.close +            @exception = e +          end +          break +        end +        unless resp +          synchronize do +            @exception = EOFError.new("end of file reached") +          end +          break +        end +        begin +          synchronize do +            case resp +            when TaggedResponse +              @tagged_responses[resp.tag] = resp +              @tagged_response_arrival.broadcast +              if resp.tag == @logout_command_tag +                return +              end +            when UntaggedResponse +              record_response(resp.name, resp.data) +              if resp.data.instance_of?(ResponseText) && +                  (code = resp.data.code) +                record_response(code.name, code.data) +              end +              if resp.name == "BYE" && @logout_command_tag.nil? +                @sock.close +                @exception = ByeResponseError.new(resp) +                connection_closed = true +              end +            when ContinuationRequest +              @continuation_request_arrival.signal +            end +            @response_handlers.each do |handler| +              handler.call(resp) +            end +          end +        rescue Exception => e +          @exception = e +          synchronize do +            @tagged_response_arrival.broadcast +            @continuation_request_arrival.broadcast +          end +        end +      end +      synchronize do +        @receiver_thread_terminating = true +        @tagged_response_arrival.broadcast +        @continuation_request_arrival.broadcast +        if @idle_done_cond +          @idle_done_cond.signal +        end +      end +    end + +    def get_tagged_response(tag, cmd) +      until @tagged_responses.key?(tag) +        raise @exception if @exception +        @tagged_response_arrival.wait +      end +      resp = @tagged_responses.delete(tag) +      case resp.name +      when /\A(?:NO)\z/ni +        raise NoResponseError, resp +      when /\A(?:BAD)\z/ni +        raise BadResponseError, resp +      else +        return resp +      end +    end + +    def get_response +      buff = "" +      while true +        s = @sock.gets(CRLF) +        break unless s +        buff.concat(s) +        if /\{(\d+)\}\r\n/n =~ s +          s = @sock.read($1.to_i) +          buff.concat(s) +        else +          break +        end +      end +      return nil if buff.length == 0 +      if @@debug +        $stderr.print(buff.gsub(/^/n, "S: ")) +      end +      return @parser.parse(buff) +    end + +    def record_response(name, data) +      unless @responses.has_key?(name) +        @responses[name] = [] +      end +      @responses[name].push(data) +    end + +    def send_command(cmd, *args, &block) +      synchronize do +        args.each do |i| +          validate_data(i) +        end +        tag = generate_tag +        put_string(tag + " " + cmd) +        args.each do |i| +          put_string(" ") +          send_data(i) +        end +        put_string(CRLF) +        if cmd == "LOGOUT" +          @logout_command_tag = tag +        end +        if block +          add_response_handler(block) +        end +        begin +          return get_tagged_response(tag, cmd) +        ensure +          if block +            remove_response_handler(block) +          end +        end +      end +    end + +    def generate_tag +      @tagno += 1 +      return format("%s%04d", @tag_prefix, @tagno) +    end + +    def put_string(str) +      @sock.print(str) +      if @@debug +        if @debug_output_bol +          $stderr.print("C: ") +        end +        $stderr.print(str.gsub(/\n(?!\z)/n, "\nC: ")) +        if /\r\n\z/n.match(str) +          @debug_output_bol = true +        else +          @debug_output_bol = false +        end +      end +    end + +    def validate_data(data) +      case data +      when nil +      when String +      when Integer +        NumValidator.ensure_number(data) +      when Array +        data.each do |i| +          validate_data(i) +        end +      when Time +      when Symbol +      else +        data.validate +      end +    end + +    def send_data(data) +      case data +      when nil +        put_string("NIL") +      when String +        send_string_data(data) +      when Integer +        send_number_data(data) +      when Array +        send_list_data(data) +      when Time +        send_time_data(data) +      when Symbol +        send_symbol_data(data) +      else +        data.send_data(self) +      end +    end + +    def send_string_data(str) +      case str +      when "" +        put_string('""') +      when /[\x80-\xff\r\n]/n +        # literal +        send_literal(str) +      when /[(){ \x00-\x1f\x7f%*"\\]/n +        # quoted string +        send_quoted_string(str) +      else +        put_string(str) +      end +    end + +    def send_quoted_string(str) +      put_string('"' + str.gsub(/["\\]/n, "\\\\\\&") + '"') +    end + +    def send_literal(str) +      put_string("{" + str.bytesize.to_s + "}" + CRLF) +      @continuation_request_arrival.wait +      raise @exception if @exception +      put_string(str) +    end + +    def send_number_data(num) +      put_string(num.to_s) +    end + +    def send_list_data(list) +      put_string("(") +      first = true +      list.each do |i| +        if first +          first = false +        else +          put_string(" ") +        end +        send_data(i) +      end +      put_string(")") +    end + +    DATE_MONTH = %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) + +    def send_time_data(time) +      t = time.dup.gmtime +      s = format('"%2d-%3s-%4d %02d:%02d:%02d +0000"', +                 t.day, DATE_MONTH[t.month - 1], t.year, +                 t.hour, t.min, t.sec) +      put_string(s) +    end + +    def send_symbol_data(symbol) +      put_string("\\" + symbol.to_s) +    end + +    def search_internal(cmd, keys, charset) +      if keys.instance_of?(String) +        keys = [RawData.new(keys)] +      else +        normalize_searching_criteria(keys) +      end +      synchronize do +        if charset +          send_command(cmd, "CHARSET", charset, *keys) +        else +          send_command(cmd, *keys) +        end +        return @responses.delete("SEARCH")[-1] +      end +    end + +    def fetch_internal(cmd, set, attr) +      case attr +      when String then +        attr = RawData.new(attr) +      when Array then +        attr = attr.map { |arg| +          arg.is_a?(String) ? RawData.new(arg) : arg +        } +      end + +      synchronize do +        @responses.delete("FETCH") +        send_command(cmd, MessageSet.new(set), attr) +        return @responses.delete("FETCH") +      end +    end + +    def store_internal(cmd, set, attr, flags) +      if attr.instance_of?(String) +        attr = RawData.new(attr) +      end +      synchronize do +        @responses.delete("FETCH") +        send_command(cmd, MessageSet.new(set), attr, flags) +        return @responses.delete("FETCH") +      end +    end + +    def copy_internal(cmd, set, mailbox) +      send_command(cmd, MessageSet.new(set), mailbox) +    end + +    def sort_internal(cmd, sort_keys, search_keys, charset) +      if search_keys.instance_of?(String) +        search_keys = [RawData.new(search_keys)] +      else +        normalize_searching_criteria(search_keys) +      end +      normalize_searching_criteria(search_keys) +      synchronize do +        send_command(cmd, sort_keys, charset, *search_keys) +        return @responses.delete("SORT")[-1] +      end +    end + +    def thread_internal(cmd, algorithm, search_keys, charset) +      if search_keys.instance_of?(String) +        search_keys = [RawData.new(search_keys)] +      else +        normalize_searching_criteria(search_keys) +      end +      normalize_searching_criteria(search_keys) +      send_command(cmd, algorithm, charset, *search_keys) +      return @responses.delete("THREAD")[-1] +    end + +    def normalize_searching_criteria(keys) +      keys.collect! do |i| +        case i +        when -1, Range, Array +          MessageSet.new(i) +        else +          i +        end +      end +    end + +    def create_ssl_params(certs = nil, verify = true) +      params = {} +      if certs +        if File.file?(certs) +          params[:ca_file] = certs +        elsif File.directory?(certs) +          params[:ca_path] = certs +        end +      end +      if verify +        params[:verify_mode] = VERIFY_PEER +      else +        params[:verify_mode] = VERIFY_NONE +      end +      return params +    end + +    def start_tls_session(params = {}) +      unless defined?(OpenSSL::SSL) +        raise "SSL extension not installed" +      end +      if @sock.kind_of?(OpenSSL::SSL::SSLSocket) +        raise RuntimeError, "already using SSL" +      end +      begin +        params = params.to_hash +      rescue NoMethodError +        params = {} +      end +      context = SSLContext.new +      context.set_params(params) +      if defined?(VerifyCallbackProc) +        context.verify_callback = VerifyCallbackProc +      end +      @sock = SSLSocket.new(@sock, context) +      @sock.sync_close = true +      @sock.connect +      if context.verify_mode != VERIFY_NONE +        @sock.post_connection_check(@host) +      end +    end + +    class RawData # :nodoc: +      def send_data(imap) +        imap.send(:put_string, @data) +      end + +      def validate +      end + +      private + +      def initialize(data) +        @data = data +      end +    end + +    class Atom # :nodoc: +      def send_data(imap) +        imap.send(:put_string, @data) +      end + +      def validate +      end + +      private + +      def initialize(data) +        @data = data +      end +    end + +    class QuotedString # :nodoc: +      def send_data(imap) +        imap.send(:send_quoted_string, @data) +      end + +      def validate +      end + +      private + +      def initialize(data) +        @data = data +      end +    end + +    class Literal # :nodoc: +      def send_data(imap) +        imap.send(:send_literal, @data) +      end + +      def validate +      end + +      private + +      def initialize(data) +        @data = data +      end +    end + +    class MessageSet # :nodoc: +      def send_data(imap) +        imap.send(:put_string, format_internal(@data)) +      end + +      def validate +        validate_internal(@data) +      end + +      private + +      def initialize(data) +        @data = data +      end + +      def format_internal(data) +        case data +        when "*" +          return data +        when Integer +          if data == -1 +            return "*" +          else +            return data.to_s +          end +        when Range +          return format_internal(data.first) + +            ":" + format_internal(data.last) +        when Array +          return data.collect {|i| format_internal(i)}.join(",") +        when ThreadMember +          return data.seqno.to_s + +            ":" + data.children.collect {|i| format_internal(i).join(",")} +        end +      end + +      def validate_internal(data) +        case data +        when "*" +        when Integer +          NumValidator.ensure_nz_number(data) +        when Range +        when Array +          data.each do |i| +            validate_internal(i) +          end +        when ThreadMember +          data.children.each do |i| +            validate_internal(i) +          end +        else +          raise DataFormatError, data.inspect +        end +      end +    end + +    # Common validators of number and nz_number types +    module NumValidator # :nodoc +      class << self +        # Check is passed argument valid 'number' in RFC 3501 terminology +        def valid_number?(num) +          # [RFC 3501] +          # number          = 1*DIGIT +          #                    ; Unsigned 32-bit integer +          #                    ; (0 <= n < 4,294,967,296) +          num >= 0 && num < 4294967296 +        end + +        # Check is passed argument valid 'nz_number' in RFC 3501 terminology +        def valid_nz_number?(num) +          # [RFC 3501] +          # nz-number       = digit-nz *DIGIT +          #                    ; Non-zero unsigned 32-bit integer +          #                    ; (0 < n < 4,294,967,296) +          num != 0 && valid_number?(num) +        end + +        # Ensure argument is 'number' or raise DataFormatError +        def ensure_number(num) +          return if valid_number?(num) + +          msg = "number must be unsigned 32-bit integer: #{num}" +          raise DataFormatError, msg +        end + +        # Ensure argument is 'nz_number' or raise DataFormatError +        def ensure_nz_number(num) +          return if valid_nz_number?(num) + +          msg = "nz_number must be non-zero unsigned 32-bit integer: #{num}" +          raise DataFormatError, msg +        end +      end +    end + +    # Net::IMAP::ContinuationRequest represents command continuation requests. +    # +    # The command continuation request response is indicated by a "+" token +    # instead of a tag.  This form of response indicates that the server is +    # ready to accept the continuation of a command from the client.  The +    # remainder of this response is a line of text. +    # +    #   continue_req    ::= "+" SPACE (resp_text / base64) +    # +    # ==== Fields: +    # +    # data:: Returns the data (Net::IMAP::ResponseText). +    # +    # raw_data:: Returns the raw data string. +    ContinuationRequest = Struct.new(:data, :raw_data) + +    # Net::IMAP::UntaggedResponse represents untagged responses. +    # +    # Data transmitted by the server to the client and status responses +    # that do not indicate command completion are prefixed with the token +    # "*", and are called untagged responses. +    # +    #   response_data   ::= "*" SPACE (resp_cond_state / resp_cond_bye / +    #                       mailbox_data / message_data / capability_data) +    # +    # ==== Fields: +    # +    # name:: Returns the name, such as "FLAGS", "LIST", or "FETCH". +    # +    # data:: Returns the data such as an array of flag symbols, +    #        a ((<Net::IMAP::MailboxList>)) object. +    # +    # raw_data:: Returns the raw data string. +    UntaggedResponse = Struct.new(:name, :data, :raw_data) + +    # Net::IMAP::TaggedResponse represents tagged responses. +    # +    # The server completion result response indicates the success or +    # failure of the operation.  It is tagged with the same tag as the +    # client command which began the operation. +    # +    #   response_tagged ::= tag SPACE resp_cond_state CRLF +    # +    #   tag             ::= 1*<any ATOM_CHAR except "+"> +    # +    #   resp_cond_state ::= ("OK" / "NO" / "BAD") SPACE resp_text +    # +    # ==== Fields: +    # +    # tag:: Returns the tag. +    # +    # name:: Returns the name, one of "OK", "NO", or "BAD". +    # +    # data:: Returns the data. See ((<Net::IMAP::ResponseText>)). +    # +    # raw_data:: Returns the raw data string. +    # +    TaggedResponse = Struct.new(:tag, :name, :data, :raw_data) + +    # Net::IMAP::ResponseText represents texts of responses. +    # The text may be prefixed by the response code. +    # +    #   resp_text       ::= ["[" resp_text_code "]" SPACE] (text_mime2 / text) +    #                       ;; text SHOULD NOT begin with "[" or "=" +    # +    # ==== Fields: +    # +    # code:: Returns the response code. See ((<Net::IMAP::ResponseCode>)). +    # +    # text:: Returns the text. +    # +    ResponseText = Struct.new(:code, :text) + +    # Net::IMAP::ResponseCode represents response codes. +    # +    #   resp_text_code  ::= "ALERT" / "PARSE" / +    #                       "PERMANENTFLAGS" SPACE "(" #(flag / "\*") ")" / +    #                       "READ-ONLY" / "READ-WRITE" / "TRYCREATE" / +    #                       "UIDVALIDITY" SPACE nz_number / +    #                       "UNSEEN" SPACE nz_number / +    #                       atom [SPACE 1*<any TEXT_CHAR except "]">] +    # +    # ==== Fields: +    # +    # name:: Returns the name, such as "ALERT", "PERMANENTFLAGS", or "UIDVALIDITY". +    # +    # data:: Returns the data, if it exists. +    # +    ResponseCode = Struct.new(:name, :data) + +    # Net::IMAP::MailboxList represents contents of the LIST response. +    # +    #   mailbox_list    ::= "(" #("\Marked" / "\Noinferiors" / +    #                       "\Noselect" / "\Unmarked" / flag_extension) ")" +    #                       SPACE (<"> QUOTED_CHAR <"> / nil) SPACE mailbox +    # +    # ==== Fields: +    # +    # attr:: Returns the name attributes. Each name attribute is a symbol +    #        capitalized by String#capitalize, such as :Noselect (not :NoSelect). +    # +    # delim:: Returns the hierarchy delimiter. +    # +    # name:: Returns the mailbox name. +    # +    MailboxList = Struct.new(:attr, :delim, :name) + +    # Net::IMAP::MailboxQuota represents contents of GETQUOTA response. +    # This object can also be a response to GETQUOTAROOT.  In the syntax +    # specification below, the delimiter used with the "#" construct is a +    # single space (SPACE). +    # +    #    quota_list      ::= "(" #quota_resource ")" +    # +    #    quota_resource  ::= atom SPACE number SPACE number +    # +    #    quota_response  ::= "QUOTA" SPACE astring SPACE quota_list +    # +    # ==== Fields: +    # +    # mailbox:: The mailbox with the associated quota. +    # +    # usage:: Current storage usage of the mailbox. +    # +    # quota:: Quota limit imposed on the mailbox. +    # +    MailboxQuota = Struct.new(:mailbox, :usage, :quota) + +    # Net::IMAP::MailboxQuotaRoot represents part of the GETQUOTAROOT +    # response. (GETQUOTAROOT can also return Net::IMAP::MailboxQuota.) +    # +    #    quotaroot_response ::= "QUOTAROOT" SPACE astring *(SPACE astring) +    # +    # ==== Fields: +    # +    # mailbox:: The mailbox with the associated quota. +    # +    # quotaroots:: Zero or more quotaroots that affect the quota on the +    #              specified mailbox. +    # +    MailboxQuotaRoot = Struct.new(:mailbox, :quotaroots) + +    # Net::IMAP::MailboxACLItem represents the response from GETACL. +    # +    #    acl_data        ::= "ACL" SPACE mailbox *(SPACE identifier SPACE rights) +    # +    #    identifier      ::= astring +    # +    #    rights          ::= astring +    # +    # ==== Fields: +    # +    # user:: Login name that has certain rights to the mailbox +    #        that was specified with the getacl command. +    # +    # rights:: The access rights the indicated user has to the +    #          mailbox. +    # +    MailboxACLItem = Struct.new(:user, :rights, :mailbox) + +    # Net::IMAP::StatusData represents the contents of the STATUS response. +    # +    # ==== Fields: +    # +    # mailbox:: Returns the mailbox name. +    # +    # attr:: Returns a hash. Each key is one of "MESSAGES", "RECENT", "UIDNEXT", +    #        "UIDVALIDITY", "UNSEEN". Each value is a number. +    # +    StatusData = Struct.new(:mailbox, :attr) + +    # Net::IMAP::FetchData represents the contents of the FETCH response. +    # +    # ==== Fields: +    # +    # seqno:: Returns the message sequence number. +    #         (Note: not the unique identifier, even for the UID command response.) +    # +    # attr:: Returns a hash. Each key is a data item name, and each value is +    #        its value. +    # +    #        The current data items are: +    # +    #        [BODY] +    #           A form of BODYSTRUCTURE without extension data. +    #        [BODY[<section>]<<origin_octet>>] +    #           A string expressing the body contents of the specified section. +    #        [BODYSTRUCTURE] +    #           An object that describes the [MIME-IMB] body structure of a message. +    #           See Net::IMAP::BodyTypeBasic, Net::IMAP::BodyTypeText, +    #           Net::IMAP::BodyTypeMessage, Net::IMAP::BodyTypeMultipart. +    #        [ENVELOPE] +    #           A Net::IMAP::Envelope object that describes the envelope +    #           structure of a message. +    #        [FLAGS] +    #           A array of flag symbols that are set for this message. Flag symbols +    #           are capitalized by String#capitalize. +    #        [INTERNALDATE] +    #           A string representing the internal date of the message. +    #        [RFC822] +    #           Equivalent to BODY[]. +    #        [RFC822.HEADER] +    #           Equivalent to BODY.PEEK[HEADER]. +    #        [RFC822.SIZE] +    #           A number expressing the [RFC-822] size of the message. +    #        [RFC822.TEXT] +    #           Equivalent to BODY[TEXT]. +    #        [UID] +    #           A number expressing the unique identifier of the message. +    # +    FetchData = Struct.new(:seqno, :attr) + +    # Net::IMAP::Envelope represents envelope structures of messages. +    # +    # ==== Fields: +    # +    # date:: Returns a string that represents the date. +    # +    # subject:: Returns a string that represents the subject. +    # +    # from:: Returns an array of Net::IMAP::Address that represents the from. +    # +    # sender:: Returns an array of Net::IMAP::Address that represents the sender. +    # +    # reply_to:: Returns an array of Net::IMAP::Address that represents the reply-to. +    # +    # to:: Returns an array of Net::IMAP::Address that represents the to. +    # +    # cc:: Returns an array of Net::IMAP::Address that represents the cc. +    # +    # bcc:: Returns an array of Net::IMAP::Address that represents the bcc. +    # +    # in_reply_to:: Returns a string that represents the in-reply-to. +    # +    # message_id:: Returns a string that represents the message-id. +    # +    Envelope = Struct.new(:date, :subject, :from, :sender, :reply_to, +                          :to, :cc, :bcc, :in_reply_to, :message_id) + +    # +    # Net::IMAP::Address represents electronic mail addresses. +    # +    # ==== Fields: +    # +    # name:: Returns the phrase from [RFC-822] mailbox. +    # +    # route:: Returns the route from [RFC-822] route-addr. +    # +    # mailbox:: nil indicates end of [RFC-822] group. +    #           If non-nil and host is nil, returns [RFC-822] group name. +    #           Otherwise, returns [RFC-822] local-part. +    # +    # host:: nil indicates [RFC-822] group syntax. +    #        Otherwise, returns [RFC-822] domain name. +    # +    Address = Struct.new(:name, :route, :mailbox, :host) + +    # +    # Net::IMAP::ContentDisposition represents Content-Disposition fields. +    # +    # ==== Fields: +    # +    # dsp_type:: Returns the disposition type. +    # +    # param:: Returns a hash that represents parameters of the Content-Disposition +    #         field. +    # +    ContentDisposition = Struct.new(:dsp_type, :param) + +    # Net::IMAP::ThreadMember represents a thread-node returned +    # by Net::IMAP#thread. +    # +    # ==== Fields: +    # +    # seqno:: The sequence number of this message. +    # +    # children:: An array of Net::IMAP::ThreadMember objects for mail +    #            items that are children of this in the thread. +    # +    ThreadMember = Struct.new(:seqno, :children) + +    # Net::IMAP::BodyTypeBasic represents basic body structures of messages. +    # +    # ==== Fields: +    # +    # media_type:: Returns the content media type name as defined in [MIME-IMB]. +    # +    # subtype:: Returns the content subtype name as defined in [MIME-IMB]. +    # +    # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. +    # +    # content_id:: Returns a string giving the content id as defined in [MIME-IMB]. +    # +    # description:: Returns a string giving the content description as defined in +    #               [MIME-IMB]. +    # +    # encoding:: Returns a string giving the content transfer encoding as defined in +    #            [MIME-IMB]. +    # +    # size:: Returns a number giving the size of the body in octets. +    # +    # md5:: Returns a string giving the body MD5 value as defined in [MD5]. +    # +    # disposition:: Returns a Net::IMAP::ContentDisposition object giving +    #               the content disposition. +    # +    # language:: Returns a string or an array of strings giving the body +    #            language value as defined in [LANGUAGE-TAGS]. +    # +    # extension:: Returns extension data. +    # +    # multipart?:: Returns false. +    # +    class BodyTypeBasic < Struct.new(:media_type, :subtype, +                                     :param, :content_id, +                                     :description, :encoding, :size, +                                     :md5, :disposition, :language, +                                     :extension) +      def multipart? +        return false +      end + +      # Obsolete: use +subtype+ instead.  Calling this will +      # generate a warning message to +stderr+, then return +      # the value of +subtype+. +      def media_subtype +        $stderr.printf("warning: media_subtype is obsolete.\n") +        $stderr.printf("         use subtype instead.\n") +        return subtype +      end +    end + +    # Net::IMAP::BodyTypeText represents TEXT body structures of messages. +    # +    # ==== Fields: +    # +    # lines:: Returns the size of the body in text lines. +    # +    # And Net::IMAP::BodyTypeText has all fields of Net::IMAP::BodyTypeBasic. +    # +    class BodyTypeText < Struct.new(:media_type, :subtype, +                                    :param, :content_id, +                                    :description, :encoding, :size, +                                    :lines, +                                    :md5, :disposition, :language, +                                    :extension) +      def multipart? +        return false +      end + +      # Obsolete: use +subtype+ instead.  Calling this will +      # generate a warning message to +stderr+, then return +      # the value of +subtype+. +      def media_subtype +        $stderr.printf("warning: media_subtype is obsolete.\n") +        $stderr.printf("         use subtype instead.\n") +        return subtype +      end +    end + +    # Net::IMAP::BodyTypeMessage represents MESSAGE/RFC822 body structures of messages. +    # +    # ==== Fields: +    # +    # envelope:: Returns a Net::IMAP::Envelope giving the envelope structure. +    # +    # body:: Returns an object giving the body structure. +    # +    # And Net::IMAP::BodyTypeMessage has all methods of Net::IMAP::BodyTypeText. +    # +    class BodyTypeMessage < Struct.new(:media_type, :subtype, +                                       :param, :content_id, +                                       :description, :encoding, :size, +                                       :envelope, :body, :lines, +                                       :md5, :disposition, :language, +                                       :extension) +      def multipart? +        return false +      end + +      # Obsolete: use +subtype+ instead.  Calling this will +      # generate a warning message to +stderr+, then return +      # the value of +subtype+. +      def media_subtype +        $stderr.printf("warning: media_subtype is obsolete.\n") +        $stderr.printf("         use subtype instead.\n") +        return subtype +      end +    end + +    # Net::IMAP::BodyTypeAttachment represents attachment body structures +    # of messages. +    # +    # ==== Fields: +    # +    # media_type:: Returns the content media type name. +    # +    # subtype:: Returns +nil+. +    # +    # param:: Returns a hash that represents parameters. +    # +    # multipart?:: Returns false. +    # +    class BodyTypeAttachment < Struct.new(:media_type, :subtype, +                                          :param) +      def multipart? +        return false +      end +    end + +    # Net::IMAP::BodyTypeMultipart represents multipart body structures +    # of messages. +    # +    # ==== Fields: +    # +    # media_type:: Returns the content media type name as defined in [MIME-IMB]. +    # +    # subtype:: Returns the content subtype name as defined in [MIME-IMB]. +    # +    # parts:: Returns multiple parts. +    # +    # param:: Returns a hash that represents parameters as defined in [MIME-IMB]. +    # +    # disposition:: Returns a Net::IMAP::ContentDisposition object giving +    #               the content disposition. +    # +    # language:: Returns a string or an array of strings giving the body +    #            language value as defined in [LANGUAGE-TAGS]. +    # +    # extension:: Returns extension data. +    # +    # multipart?:: Returns true. +    # +    class BodyTypeMultipart < Struct.new(:media_type, :subtype, +                                         :parts, +                                         :param, :disposition, :language, +                                         :extension) +      def multipart? +        return true +      end + +      # Obsolete: use +subtype+ instead.  Calling this will +      # generate a warning message to +stderr+, then return +      # the value of +subtype+. +      def media_subtype +        $stderr.printf("warning: media_subtype is obsolete.\n") +        $stderr.printf("         use subtype instead.\n") +        return subtype +      end +    end + +    class BodyTypeExtension < Struct.new(:media_type, :subtype, +                                         :params, :content_id, +                                         :description, :encoding, :size) +      def multipart? +        return false +      end +    end + +    class ResponseParser # :nodoc: +      def initialize +        @str = nil +        @pos = nil +        @lex_state = nil +        @token = nil +        @flag_symbols = {} +      end + +      def parse(str) +        @str = str +        @pos = 0 +        @lex_state = EXPR_BEG +        @token = nil +        return response +      end + +      private + +      EXPR_BEG          = :EXPR_BEG +      EXPR_DATA         = :EXPR_DATA +      EXPR_TEXT         = :EXPR_TEXT +      EXPR_RTEXT        = :EXPR_RTEXT +      EXPR_CTEXT        = :EXPR_CTEXT + +      T_SPACE   = :SPACE +      T_NIL     = :NIL +      T_NUMBER  = :NUMBER +      T_ATOM    = :ATOM +      T_QUOTED  = :QUOTED +      T_LPAR    = :LPAR +      T_RPAR    = :RPAR +      T_BSLASH  = :BSLASH +      T_STAR    = :STAR +      T_LBRA    = :LBRA +      T_RBRA    = :RBRA +      T_LITERAL = :LITERAL +      T_PLUS    = :PLUS +      T_PERCENT = :PERCENT +      T_CRLF    = :CRLF +      T_EOF     = :EOF +      T_TEXT    = :TEXT + +      BEG_REGEXP = /\G(?:\ +(?# 1:  SPACE   )( +)|\ +(?# 2:  NIL     )(NIL)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\ +(?# 3:  NUMBER  )(\d+)(?=[\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+])|\ +(?# 4:  ATOM    )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\\[\]+]+)|\ +(?# 5:  QUOTED  )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\ +(?# 6:  LPAR    )(\()|\ +(?# 7:  RPAR    )(\))|\ +(?# 8:  BSLASH  )(\\)|\ +(?# 9:  STAR    )(\*)|\ +(?# 10: LBRA    )(\[)|\ +(?# 11: RBRA    )(\])|\ +(?# 12: LITERAL )\{(\d+)\}\r\n|\ +(?# 13: PLUS    )(\+)|\ +(?# 14: PERCENT )(%)|\ +(?# 15: CRLF    )(\r\n)|\ +(?# 16: EOF     )(\z))/ni + +      DATA_REGEXP = /\G(?:\ +(?# 1:  SPACE   )( )|\ +(?# 2:  NIL     )(NIL)|\ +(?# 3:  NUMBER  )(\d+)|\ +(?# 4:  QUOTED  )"((?:[^\x00\r\n"\\]|\\["\\])*)"|\ +(?# 5:  LITERAL )\{(\d+)\}\r\n|\ +(?# 6:  LPAR    )(\()|\ +(?# 7:  RPAR    )(\)))/ni + +      TEXT_REGEXP = /\G(?:\ +(?# 1:  TEXT    )([^\x00\r\n]*))/ni + +      RTEXT_REGEXP = /\G(?:\ +(?# 1:  LBRA    )(\[)|\ +(?# 2:  TEXT    )([^\x00\r\n]*))/ni + +      CTEXT_REGEXP = /\G(?:\ +(?# 1:  TEXT    )([^\x00\r\n\]]*))/ni + +      Token = Struct.new(:symbol, :value) + +      def response +        token = lookahead +        case token.symbol +        when T_PLUS +          result = continue_req +        when T_STAR +          result = response_untagged +        else +          result = response_tagged +        end +        match(T_CRLF) +        match(T_EOF) +        return result +      end + +      def continue_req +        match(T_PLUS) +        match(T_SPACE) +        return ContinuationRequest.new(resp_text, @str) +      end + +      def response_untagged +        match(T_STAR) +        match(T_SPACE) +        token = lookahead +        if token.symbol == T_NUMBER +          return numeric_response +        elsif token.symbol == T_ATOM +          case token.value +          when /\A(?:OK|NO|BAD|BYE|PREAUTH)\z/ni +            return response_cond +          when /\A(?:FLAGS)\z/ni +            return flags_response +          when /\A(?:LIST|LSUB|XLIST)\z/ni +            return list_response +          when /\A(?:QUOTA)\z/ni +            return getquota_response +          when /\A(?:QUOTAROOT)\z/ni +            return getquotaroot_response +          when /\A(?:ACL)\z/ni +            return getacl_response +          when /\A(?:SEARCH|SORT)\z/ni +            return search_response +          when /\A(?:THREAD)\z/ni +            return thread_response +          when /\A(?:STATUS)\z/ni +            return status_response +          when /\A(?:CAPABILITY)\z/ni +            return capability_response +          else +            return text_response +          end +        else +          parse_error("unexpected token %s", token.symbol) +        end +      end + +      def response_tagged +        tag = atom +        match(T_SPACE) +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        return TaggedResponse.new(tag, name, resp_text, @str) +      end + +      def response_cond +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        return UntaggedResponse.new(name, resp_text, @str) +      end + +      def numeric_response +        n = number +        match(T_SPACE) +        token = match(T_ATOM) +        name = token.value.upcase +        case name +        when "EXISTS", "RECENT", "EXPUNGE" +          return UntaggedResponse.new(name, n, @str) +        when "FETCH" +          shift_token +          match(T_SPACE) +          data = FetchData.new(n, msg_att(n)) +          return UntaggedResponse.new(name, data, @str) +        end +      end + +      def msg_att(n) +        match(T_LPAR) +        attr = {} +        while true +          token = lookahead +          case token.symbol +          when T_RPAR +            shift_token +            break +          when T_SPACE +            shift_token +            next +          end +          case token.value +          when /\A(?:ENVELOPE)\z/ni +            name, val = envelope_data +          when /\A(?:FLAGS)\z/ni +            name, val = flags_data +          when /\A(?:INTERNALDATE)\z/ni +            name, val = internaldate_data +          when /\A(?:RFC822(?:\.HEADER|\.TEXT)?)\z/ni +            name, val = rfc822_text +          when /\A(?:RFC822\.SIZE)\z/ni +            name, val = rfc822_size +          when /\A(?:BODY(?:STRUCTURE)?)\z/ni +            name, val = body_data +          when /\A(?:UID)\z/ni +            name, val = uid_data +          else +            parse_error("unknown attribute `%s' for {%d}", token.value, n) +          end +          attr[name] = val +        end +        return attr +      end + +      def envelope_data +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        return name, envelope +      end + +      def envelope +        @lex_state = EXPR_DATA +        token = lookahead +        if token.symbol == T_NIL +          shift_token +          result = nil +        else +          match(T_LPAR) +          date = nstring +          match(T_SPACE) +          subject = nstring +          match(T_SPACE) +          from = address_list +          match(T_SPACE) +          sender = address_list +          match(T_SPACE) +          reply_to = address_list +          match(T_SPACE) +          to = address_list +          match(T_SPACE) +          cc = address_list +          match(T_SPACE) +          bcc = address_list +          match(T_SPACE) +          in_reply_to = nstring +          match(T_SPACE) +          message_id = nstring +          match(T_RPAR) +          result = Envelope.new(date, subject, from, sender, reply_to, +                                to, cc, bcc, in_reply_to, message_id) +        end +        @lex_state = EXPR_BEG +        return result +      end + +      def flags_data +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        return name, flag_list +      end + +      def internaldate_data +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        token = match(T_QUOTED) +        return name, token.value +      end + +      def rfc822_text +        token = match(T_ATOM) +        name = token.value.upcase +        token = lookahead +        if token.symbol == T_LBRA +          shift_token +          match(T_RBRA) +        end +        match(T_SPACE) +        return name, nstring +      end + +      def rfc822_size +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        return name, number +      end + +      def body_data +        token = match(T_ATOM) +        name = token.value.upcase +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +          return name, body +        end +        name.concat(section) +        token = lookahead +        if token.symbol == T_ATOM +          name.concat(token.value) +          shift_token +        end +        match(T_SPACE) +        data = nstring +        return name, data +      end + +      def body +        @lex_state = EXPR_DATA +        token = lookahead +        if token.symbol == T_NIL +          shift_token +          result = nil +        else +          match(T_LPAR) +          token = lookahead +          if token.symbol == T_LPAR +            result = body_type_mpart +          else +            result = body_type_1part +          end +          match(T_RPAR) +        end +        @lex_state = EXPR_BEG +        return result +      end + +      def body_type_1part +        token = lookahead +        case token.value +        when /\A(?:TEXT)\z/ni +          return body_type_text +        when /\A(?:MESSAGE)\z/ni +          return body_type_msg +        when /\A(?:ATTACHMENT)\z/ni +          return body_type_attachment +        when /\A(?:MIXED)\z/ni +          return body_type_mixed +        else +          return body_type_basic +        end +      end + +      def body_type_basic +        mtype, msubtype = media_type +        token = lookahead +        if token.symbol == T_RPAR +          return BodyTypeBasic.new(mtype, msubtype) +        end +        match(T_SPACE) +        param, content_id, desc, enc, size = body_fields +        md5, disposition, language, extension = body_ext_1part +        return BodyTypeBasic.new(mtype, msubtype, +                                 param, content_id, +                                 desc, enc, size, +                                 md5, disposition, language, extension) +      end + +      def body_type_text +        mtype, msubtype = media_type +        match(T_SPACE) +        param, content_id, desc, enc, size = body_fields +        match(T_SPACE) +        lines = number +        md5, disposition, language, extension = body_ext_1part +        return BodyTypeText.new(mtype, msubtype, +                                param, content_id, +                                desc, enc, size, +                                lines, +                                md5, disposition, language, extension) +      end + +      def body_type_msg +        mtype, msubtype = media_type +        match(T_SPACE) +        param, content_id, desc, enc, size = body_fields + +        token = lookahead +        if token.symbol == T_RPAR +          # If this is not message/rfc822, we shouldn't apply the RFC822 +          # spec to it.  We should handle anything other than +          # message/rfc822 using multipart extension data [rfc3501] (i.e. +          # the data itself won't be returned, we would have to retrieve it +          # with BODYSTRUCTURE instead of with BODY + +          # Also, sometimes a message/rfc822 is included as a large +          # attachment instead of having all of the other details +          # (e.g. attaching a .eml file to an email) +          if msubtype == "RFC822" +            return BodyTypeMessage.new(mtype, msubtype, param, content_id, +                                       desc, enc, size, nil, nil, nil, nil, +                                       nil, nil, nil) +          else +            return BodyTypeExtension.new(mtype, msubtype, +                                         param, content_id, +                                         desc, enc, size) +          end +        end + +        match(T_SPACE) +        env = envelope +        match(T_SPACE) +        b = body +        match(T_SPACE) +        lines = number +        md5, disposition, language, extension = body_ext_1part +        return BodyTypeMessage.new(mtype, msubtype, +                                   param, content_id, +                                   desc, enc, size, +                                   env, b, lines, +                                   md5, disposition, language, extension) +      end + +      def body_type_attachment +        mtype = case_insensitive_string +        match(T_SPACE) +        param = body_fld_param +        return BodyTypeAttachment.new(mtype, nil, param) +      end + +      def body_type_mixed +        mtype = "MULTIPART" +        msubtype = case_insensitive_string +        param, disposition, language, extension = body_ext_mpart +        return BodyTypeBasic.new(mtype, msubtype, param, nil, nil, nil, nil, nil, disposition, language, extension) +      end + +      def body_type_mpart +        parts = [] +        while true +          token = lookahead +          if token.symbol == T_SPACE +            shift_token +            break +          end +          parts.push(body) +        end +        mtype = "MULTIPART" +        msubtype = case_insensitive_string +        param, disposition, language, extension = body_ext_mpart +        return BodyTypeMultipart.new(mtype, msubtype, parts, +                                     param, disposition, language, +                                     extension) +      end + +      def media_type +        mtype = case_insensitive_string +        token = lookahead +        if token.symbol != T_SPACE +          return mtype, nil +        end +        match(T_SPACE) +        msubtype = case_insensitive_string +        return mtype, msubtype +      end + +      def body_fields +        param = body_fld_param +        match(T_SPACE) +        content_id = nstring +        match(T_SPACE) +        desc = nstring +        match(T_SPACE) +        enc = case_insensitive_string +        match(T_SPACE) +        size = number +        return param, content_id, desc, enc, size +      end + +      def body_fld_param +        token = lookahead +        if token.symbol == T_NIL +          shift_token +          return nil +        end +        match(T_LPAR) +        param = {} +        while true +          token = lookahead +          case token.symbol +          when T_RPAR +            shift_token +            break +          when T_SPACE +            shift_token +          end +          name = case_insensitive_string +          match(T_SPACE) +          val = string +          param[name] = val +        end +        return param +      end + +      def body_ext_1part +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +        else +          return nil +        end +        md5 = nstring + +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +        else +          return md5 +        end +        disposition = body_fld_dsp + +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +        else +          return md5, disposition +        end +        language = body_fld_lang + +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +        else +          return md5, disposition, language +        end + +        extension = body_extensions +        return md5, disposition, language, extension +      end + +      def body_ext_mpart +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +        else +          return nil +        end +        param = body_fld_param + +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +        else +          return param +        end +        disposition = body_fld_dsp + +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +        else +          return param, disposition +        end +        language = body_fld_lang + +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +        else +          return param, disposition, language +        end + +        extension = body_extensions +        return param, disposition, language, extension +      end + +      def body_fld_dsp +        token = lookahead +        if token.symbol == T_NIL +          shift_token +          return nil +        end +        match(T_LPAR) +        dsp_type = case_insensitive_string +        match(T_SPACE) +        param = body_fld_param +        match(T_RPAR) +        return ContentDisposition.new(dsp_type, param) +      end + +      def body_fld_lang +        token = lookahead +        if token.symbol == T_LPAR +          shift_token +          result = [] +          while true +            token = lookahead +            case token.symbol +            when T_RPAR +              shift_token +              return result +            when T_SPACE +              shift_token +            end +            result.push(case_insensitive_string) +          end +        else +          lang = nstring +          if lang +            return lang.upcase +          else +            return lang +          end +        end +      end + +      def body_extensions +        result = [] +        while true +          token = lookahead +          case token.symbol +          when T_RPAR +            return result +          when T_SPACE +            shift_token +          end +          result.push(body_extension) +        end +      end + +      def body_extension +        token = lookahead +        case token.symbol +        when T_LPAR +          shift_token +          result = body_extensions +          match(T_RPAR) +          return result +        when T_NUMBER +          return number +        else +          return nstring +        end +      end + +      def section +        str = "" +        token = match(T_LBRA) +        str.concat(token.value) +        token = match(T_ATOM, T_NUMBER, T_RBRA) +        if token.symbol == T_RBRA +          str.concat(token.value) +          return str +        end +        str.concat(token.value) +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +          str.concat(token.value) +          token = match(T_LPAR) +          str.concat(token.value) +          while true +            token = lookahead +            case token.symbol +            when T_RPAR +              str.concat(token.value) +              shift_token +              break +            when T_SPACE +              shift_token +              str.concat(token.value) +            end +            str.concat(format_string(astring)) +          end +        end +        token = match(T_RBRA) +        str.concat(token.value) +        return str +      end + +      def format_string(str) +        case str +        when "" +          return '""' +        when /[\x80-\xff\r\n]/n +          # literal +          return "{" + str.bytesize.to_s + "}" + CRLF + str +        when /[(){ \x00-\x1f\x7f%*"\\]/n +          # quoted string +          return '"' + str.gsub(/["\\]/n, "\\\\\\&") + '"' +        else +          # atom +          return str +        end +      end + +      def uid_data +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        return name, number +      end + +      def text_response +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        @lex_state = EXPR_TEXT +        token = match(T_TEXT) +        @lex_state = EXPR_BEG +        return UntaggedResponse.new(name, token.value) +      end + +      def flags_response +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        return UntaggedResponse.new(name, flag_list, @str) +      end + +      def list_response +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        return UntaggedResponse.new(name, mailbox_list, @str) +      end + +      def mailbox_list +        attr = flag_list +        match(T_SPACE) +        token = match(T_QUOTED, T_NIL) +        if token.symbol == T_NIL +          delim = nil +        else +          delim = token.value +        end +        match(T_SPACE) +        name = astring +        return MailboxList.new(attr, delim, name) +      end + +      def getquota_response +        # If quota never established, get back +        # `NO Quota root does not exist'. +        # If quota removed, get `()' after the +        # folder spec with no mention of `STORAGE'. +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        mailbox = astring +        match(T_SPACE) +        match(T_LPAR) +        token = lookahead +        case token.symbol +        when T_RPAR +          shift_token +          data = MailboxQuota.new(mailbox, nil, nil) +          return UntaggedResponse.new(name, data, @str) +        when T_ATOM +          shift_token +          match(T_SPACE) +          token = match(T_NUMBER) +          usage = token.value +          match(T_SPACE) +          token = match(T_NUMBER) +          quota = token.value +          match(T_RPAR) +          data = MailboxQuota.new(mailbox, usage, quota) +          return UntaggedResponse.new(name, data, @str) +        else +          parse_error("unexpected token %s", token.symbol) +        end +      end + +      def getquotaroot_response +        # Similar to getquota, but only admin can use getquota. +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        mailbox = astring +        quotaroots = [] +        while true +          token = lookahead +          break unless token.symbol == T_SPACE +          shift_token +          quotaroots.push(astring) +        end +        data = MailboxQuotaRoot.new(mailbox, quotaroots) +        return UntaggedResponse.new(name, data, @str) +      end + +      def getacl_response +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        mailbox = astring +        data = [] +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +          while true +            token = lookahead +            case token.symbol +            when T_CRLF +              break +            when T_SPACE +              shift_token +            end +            user = astring +            match(T_SPACE) +            rights = astring +            data.push(MailboxACLItem.new(user, rights, mailbox)) +          end +        end +        return UntaggedResponse.new(name, data, @str) +      end + +      def search_response +        token = match(T_ATOM) +        name = token.value.upcase +        token = lookahead +        if token.symbol == T_SPACE +          shift_token +          data = [] +          while true +            token = lookahead +            case token.symbol +            when T_CRLF +              break +            when T_SPACE +              shift_token +            when T_NUMBER +              data.push(number) +            when T_LPAR +              # TODO: include the MODSEQ value in a response +              shift_token +              match(T_ATOM) +              match(T_SPACE) +              match(T_NUMBER) +              match(T_RPAR) +            end +          end +        else +          data = [] +        end +        return UntaggedResponse.new(name, data, @str) +      end + +      def thread_response +        token = match(T_ATOM) +        name = token.value.upcase +        token = lookahead + +        if token.symbol == T_SPACE +          threads = [] + +          while true +            shift_token +            token = lookahead + +            case token.symbol +            when T_LPAR +              threads << thread_branch(token) +            when T_CRLF +              break +            end +          end +        else +          # no member +          threads = [] +        end + +        return UntaggedResponse.new(name, threads, @str) +      end + +      def thread_branch(token) +        rootmember = nil +        lastmember = nil + +        while true +          shift_token    # ignore first T_LPAR +          token = lookahead + +          case token.symbol +          when T_NUMBER +            # new member +            newmember = ThreadMember.new(number, []) +            if rootmember.nil? +              rootmember = newmember +            else +              lastmember.children << newmember +            end +            lastmember = newmember +          when T_SPACE +            # do nothing +          when T_LPAR +            if rootmember.nil? +              # dummy member +              lastmember = rootmember = ThreadMember.new(nil, []) +            end + +            lastmember.children << thread_branch(token) +          when T_RPAR +            break +          end +        end + +        return rootmember +      end + +      def status_response +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        mailbox = astring +        match(T_SPACE) +        match(T_LPAR) +        attr = {} +        while true +          token = lookahead +          case token.symbol +          when T_RPAR +            shift_token +            break +          when T_SPACE +            shift_token +          end +          token = match(T_ATOM) +          key = token.value.upcase +          match(T_SPACE) +          val = number +          attr[key] = val +        end +        data = StatusData.new(mailbox, attr) +        return UntaggedResponse.new(name, data, @str) +      end + +      def capability_response +        token = match(T_ATOM) +        name = token.value.upcase +        match(T_SPACE) +        data = [] +        while true +          token = lookahead +          case token.symbol +          when T_CRLF +            break +          when T_SPACE +            shift_token +            next +          end +          data.push(atom.upcase) +        end +        return UntaggedResponse.new(name, data, @str) +      end + +      def resp_text +        @lex_state = EXPR_RTEXT +        token = lookahead +        if token.symbol == T_LBRA +          code = resp_text_code +        else +          code = nil +        end +        token = match(T_TEXT) +        @lex_state = EXPR_BEG +        return ResponseText.new(code, token.value) +      end + +      def resp_text_code +        @lex_state = EXPR_BEG +        match(T_LBRA) +        token = match(T_ATOM) +        name = token.value.upcase +        case name +        when /\A(?:ALERT|PARSE|READ-ONLY|READ-WRITE|TRYCREATE|NOMODSEQ)\z/n +          result = ResponseCode.new(name, nil) +        when /\A(?:PERMANENTFLAGS)\z/n +          match(T_SPACE) +          result = ResponseCode.new(name, flag_list) +        when /\A(?:UIDVALIDITY|UIDNEXT|UNSEEN)\z/n +          match(T_SPACE) +          result = ResponseCode.new(name, number) +        else +          token = lookahead +          if token.symbol == T_SPACE +            shift_token +            @lex_state = EXPR_CTEXT +            token = match(T_TEXT) +            @lex_state = EXPR_BEG +            result = ResponseCode.new(name, token.value) +          else +            result = ResponseCode.new(name, nil) +          end +        end +        match(T_RBRA) +        @lex_state = EXPR_RTEXT +        return result +      end + +      def address_list +        token = lookahead +        if token.symbol == T_NIL +          shift_token +          return nil +        else +          result = [] +          match(T_LPAR) +          while true +            token = lookahead +            case token.symbol +            when T_RPAR +              shift_token +              break +            when T_SPACE +              shift_token +            end +            result.push(address) +          end +          return result +        end +      end + +      ADDRESS_REGEXP = /\G\ +(?# 1: NAME     )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ +(?# 2: ROUTE    )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ +(?# 3: MAILBOX  )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)") \ +(?# 4: HOST     )(?:NIL|"((?:[^\x80-\xff\x00\r\n"\\]|\\["\\])*)")\ +\)/ni + +      def address +        match(T_LPAR) +        if @str.index(ADDRESS_REGEXP, @pos) +          # address does not include literal. +          @pos = $~.end(0) +          name = $1 +          route = $2 +          mailbox = $3 +          host = $4 +          for s in [name, route, mailbox, host] +            if s +              s.gsub!(/\\(["\\])/n, "\\1") +            end +          end +        else +          name = nstring +          match(T_SPACE) +          route = nstring +          match(T_SPACE) +          mailbox = nstring +          match(T_SPACE) +          host = nstring +          match(T_RPAR) +        end +        return Address.new(name, route, mailbox, host) +      end + +      FLAG_REGEXP = /\ +(?# FLAG        )\\([^\x80-\xff(){ \x00-\x1f\x7f%"\\]+)|\ +(?# ATOM        )([^\x80-\xff(){ \x00-\x1f\x7f%*"\\]+)/n + +      def flag_list +        if @str.index(/\(([^)]*)\)/ni, @pos) +          @pos = $~.end(0) +          return $1.scan(FLAG_REGEXP).collect { |flag, atom| +            if atom +              atom +            else +              symbol = flag.capitalize.untaint.intern +              @flag_symbols[symbol] = true +              if @flag_symbols.length > IMAP.max_flag_count +                raise FlagCountError, "number of flag symbols exceeded" +              end +              symbol +            end +          } +        else +          parse_error("invalid flag list") +        end +      end + +      def nstring +        token = lookahead +        if token.symbol == T_NIL +          shift_token +          return nil +        else +          return string +        end +      end + +      def astring +        token = lookahead +        if string_token?(token) +          return string +        else +          return atom +        end +      end + +      def string +        token = lookahead +        if token.symbol == T_NIL +          shift_token +          return nil +        end +        token = match(T_QUOTED, T_LITERAL) +        return token.value +      end + +      STRING_TOKENS = [T_QUOTED, T_LITERAL, T_NIL] + +      def string_token?(token) +        return STRING_TOKENS.include?(token.symbol) +      end + +      def case_insensitive_string +        token = lookahead +        if token.symbol == T_NIL +          shift_token +          return nil +        end +        token = match(T_QUOTED, T_LITERAL) +        return token.value.upcase +      end + +      def atom +        result = "" +        while true +          token = lookahead +          if atom_token?(token) +            result.concat(token.value) +            shift_token +          else +            if result.empty? +              parse_error("unexpected token %s", token.symbol) +            else +              return result +            end +          end +        end +      end + +      ATOM_TOKENS = [ +        T_ATOM, +        T_NUMBER, +        T_NIL, +        T_LBRA, +        T_RBRA, +        T_PLUS +      ] + +      def atom_token?(token) +        return ATOM_TOKENS.include?(token.symbol) +      end + +      def number +        token = lookahead +        if token.symbol == T_NIL +          shift_token +          return nil +        end +        token = match(T_NUMBER) +        return token.value.to_i +      end + +      def nil_atom +        match(T_NIL) +        return nil +      end + +      def match(*args) +        token = lookahead +        unless args.include?(token.symbol) +          parse_error('unexpected token %s (expected %s)', +                      token.symbol.id2name, +                      args.collect {|i| i.id2name}.join(" or ")) +        end +        shift_token +        return token +      end + +      def lookahead +        unless @token +          @token = next_token +        end +        return @token +      end + +      def shift_token +        @token = nil +      end + +      def next_token +        case @lex_state +        when EXPR_BEG +          if @str.index(BEG_REGEXP, @pos) +            @pos = $~.end(0) +            if $1 +              return Token.new(T_SPACE, $+) +            elsif $2 +              return Token.new(T_NIL, $+) +            elsif $3 +              return Token.new(T_NUMBER, $+) +            elsif $4 +              return Token.new(T_ATOM, $+) +            elsif $5 +              return Token.new(T_QUOTED, +                               $+.gsub(/\\(["\\])/n, "\\1")) +            elsif $6 +              return Token.new(T_LPAR, $+) +            elsif $7 +              return Token.new(T_RPAR, $+) +            elsif $8 +              return Token.new(T_BSLASH, $+) +            elsif $9 +              return Token.new(T_STAR, $+) +            elsif $10 +              return Token.new(T_LBRA, $+) +            elsif $11 +              return Token.new(T_RBRA, $+) +            elsif $12 +              len = $+.to_i +              val = @str[@pos, len] +              @pos += len +              return Token.new(T_LITERAL, val) +            elsif $13 +              return Token.new(T_PLUS, $+) +            elsif $14 +              return Token.new(T_PERCENT, $+) +            elsif $15 +              return Token.new(T_CRLF, $+) +            elsif $16 +              return Token.new(T_EOF, $+) +            else +              parse_error("[Net::IMAP BUG] BEG_REGEXP is invalid") +            end +          else +            @str.index(/\S*/n, @pos) +            parse_error("unknown token - %s", $&.dump) +          end +        when EXPR_DATA +          if @str.index(DATA_REGEXP, @pos) +            @pos = $~.end(0) +            if $1 +              return Token.new(T_SPACE, $+) +            elsif $2 +              return Token.new(T_NIL, $+) +            elsif $3 +              return Token.new(T_NUMBER, $+) +            elsif $4 +              return Token.new(T_QUOTED, +                               $+.gsub(/\\(["\\])/n, "\\1")) +            elsif $5 +              len = $+.to_i +              val = @str[@pos, len] +              @pos += len +              return Token.new(T_LITERAL, val) +            elsif $6 +              return Token.new(T_LPAR, $+) +            elsif $7 +              return Token.new(T_RPAR, $+) +            else +              parse_error("[Net::IMAP BUG] DATA_REGEXP is invalid") +            end +          else +            @str.index(/\S*/n, @pos) +            parse_error("unknown token - %s", $&.dump) +          end +        when EXPR_TEXT +          if @str.index(TEXT_REGEXP, @pos) +            @pos = $~.end(0) +            if $1 +              return Token.new(T_TEXT, $+) +            else +              parse_error("[Net::IMAP BUG] TEXT_REGEXP is invalid") +            end +          else +            @str.index(/\S*/n, @pos) +            parse_error("unknown token - %s", $&.dump) +          end +        when EXPR_RTEXT +          if @str.index(RTEXT_REGEXP, @pos) +            @pos = $~.end(0) +            if $1 +              return Token.new(T_LBRA, $+) +            elsif $2 +              return Token.new(T_TEXT, $+) +            else +              parse_error("[Net::IMAP BUG] RTEXT_REGEXP is invalid") +            end +          else +            @str.index(/\S*/n, @pos) +            parse_error("unknown token - %s", $&.dump) +          end +        when EXPR_CTEXT +          if @str.index(CTEXT_REGEXP, @pos) +            @pos = $~.end(0) +            if $1 +              return Token.new(T_TEXT, $+) +            else +              parse_error("[Net::IMAP BUG] CTEXT_REGEXP is invalid") +            end +          else +            @str.index(/\S*/n, @pos) #/ +            parse_error("unknown token - %s", $&.dump) +          end +        else +          parse_error("invalid @lex_state - %s", @lex_state.inspect) +        end +      end + +      def parse_error(fmt, *args) +        if IMAP.debug +          $stderr.printf("@str: %s\n", @str.dump) +          $stderr.printf("@pos: %d\n", @pos) +          $stderr.printf("@lex_state: %s\n", @lex_state) +          if @token +            $stderr.printf("@token.symbol: %s\n", @token.symbol) +            $stderr.printf("@token.value: %s\n", @token.value.inspect) +          end +        end +        raise ResponseParseError, format(fmt, *args) +      end +    end + +    # Authenticator for the "LOGIN" authentication type.  See +    # #authenticate(). +    class LoginAuthenticator +      def process(data) +        case @state +        when STATE_USER +          @state = STATE_PASSWORD +          return @user +        when STATE_PASSWORD +          return @password +        end +      end + +      private + +      STATE_USER = :USER +      STATE_PASSWORD = :PASSWORD + +      def initialize(user, password) +        @user = user +        @password = password +        @state = STATE_USER +      end +    end +    add_authenticator "LOGIN", LoginAuthenticator + +    # Authenticator for the "PLAIN" authentication type.  See +    # #authenticate(). +    class PlainAuthenticator +      def process(data) +        return "\0#{@user}\0#{@password}" +      end + +      private + +      def initialize(user, password) +        @user = user +        @password = password +      end +    end +    add_authenticator "PLAIN", PlainAuthenticator + +    # Authenticator for the "CRAM-MD5" authentication type.  See +    # #authenticate(). +    class CramMD5Authenticator +      def process(challenge) +        digest = hmac_md5(challenge, @password) +        return @user + " " + digest +      end + +      private + +      def initialize(user, password) +        @user = user +        @password = password +      end + +      def hmac_md5(text, key) +        if key.length > 64 +          key = Digest::MD5.digest(key) +        end + +        k_ipad = key + "\0" * (64 - key.length) +        k_opad = key + "\0" * (64 - key.length) +        for i in 0..63 +          k_ipad[i] = (k_ipad[i].ord ^ 0x36).chr +          k_opad[i] = (k_opad[i].ord ^ 0x5c).chr +        end + +        digest = Digest::MD5.digest(k_ipad + text) + +        return Digest::MD5.hexdigest(k_opad + digest) +      end +    end +    add_authenticator "CRAM-MD5", CramMD5Authenticator + +    # Authenticator for the "DIGEST-MD5" authentication type.  See +    # #authenticate(). +    class DigestMD5Authenticator +      def process(challenge) +        case @stage +        when STAGE_ONE +          @stage = STAGE_TWO +          sparams = {} +          c = StringScanner.new(challenge) +          while c.scan(/(?:\s*,)?\s*(\w+)=("(?:[^\\"]+|\\.)*"|[^,]+)\s*/) +            k, v = c[1], c[2] +            if v =~ /^"(.*)"$/ +              v = $1 +              if v =~ /,/ +                v = v.split(',') +              end +            end +            sparams[k] = v +          end + +          raise DataFormatError, "Bad Challenge: '#{challenge}'" unless c.rest.size == 0 +          raise Error, "Server does not support auth (qop = #{sparams['qop'].join(',')})" unless sparams['qop'].include?("auth") + +          response = { +            :nonce => sparams['nonce'], +            :username => @user, +            :realm => sparams['realm'], +            :cnonce => Digest::MD5.hexdigest("%.15f:%.15f:%d" % [Time.now.to_f, rand, Process.pid.to_s]), +            :'digest-uri' => 'imap/' + sparams['realm'], +            :qop => 'auth', +            :maxbuf => 65535, +            :nc => "%08d" % nc(sparams['nonce']), +            :charset => sparams['charset'], +          } + +          response[:authzid] = @authname unless @authname.nil? + +          # now, the real thing +          a0 = Digest::MD5.digest( [ response.values_at(:username, :realm), @password ].join(':') ) + +          a1 = [ a0, response.values_at(:nonce,:cnonce) ].join(':') +          a1 << ':' + response[:authzid] unless response[:authzid].nil? + +          a2 = "AUTHENTICATE:" + response[:'digest-uri'] +          a2 << ":00000000000000000000000000000000" if response[:qop] and response[:qop] =~ /^auth-(?:conf|int)$/ + +          response[:response] = Digest::MD5.hexdigest( +            [ +             Digest::MD5.hexdigest(a1), +             response.values_at(:nonce, :nc, :cnonce, :qop), +             Digest::MD5.hexdigest(a2) +            ].join(':') +          ) + +          return response.keys.map {|key| qdval(key.to_s, response[key]) }.join(',') +        when STAGE_TWO +          @stage = nil +          # if at the second stage, return an empty string +          if challenge =~ /rspauth=/ +            return '' +          else +            raise ResponseParseError, challenge +          end +        else +          raise ResponseParseError, challenge +        end +      end + +      def initialize(user, password, authname = nil) +        @user, @password, @authname = user, password, authname +        @nc, @stage = {}, STAGE_ONE +      end + +      private + +      STAGE_ONE = :stage_one +      STAGE_TWO = :stage_two + +      def nc(nonce) +        if @nc.has_key? nonce +          @nc[nonce] = @nc[nonce] + 1 +        else +          @nc[nonce] = 1 +        end +        return @nc[nonce] +      end + +      # some responses need quoting +      def qdval(k, v) +        return if k.nil? or v.nil? +        if %w"username authzid realm nonce cnonce digest-uri qop".include? k +          v.gsub!(/([\\"])/, "\\\1") +          return '%s="%s"' % [k, v] +        else +          return '%s=%s' % [k, v] +        end +      end +    end +    add_authenticator "DIGEST-MD5", DigestMD5Authenticator + +    # Superclass of IMAP errors. +    class Error < StandardError +    end + +    # Error raised when data is in the incorrect format. +    class DataFormatError < Error +    end + +    # Error raised when a response from the server is non-parseable. +    class ResponseParseError < Error +    end + +    # Superclass of all errors used to encapsulate "fail" responses +    # from the server. +    class ResponseError < Error + +      # The response that caused this error +      attr_accessor :response + +      def initialize(response) +        @response = response + +        super @response.data.text +      end + +    end + +    # Error raised upon a "NO" response from the server, indicating +    # that the client command could not be completed successfully. +    class NoResponseError < ResponseError +    end + +    # Error raised upon a "BAD" response from the server, indicating +    # that the client command violated the IMAP protocol, or an internal +    # server failure has occurred. +    class BadResponseError < ResponseError +    end + +    # Error raised upon a "BYE" response from the server, indicating +    # that the client is not being allowed to login, or has been timed +    # out due to inactivity. +    class ByeResponseError < ResponseError +    end + +    # Error raised when too many flags are interned to symbols. +    class FlagCountError < Error +    end +  end +end diff --git a/jni/ruby/lib/net/pop.rb b/jni/ruby/lib/net/pop.rb new file mode 100644 index 0000000..cec7aa9 --- /dev/null +++ b/jni/ruby/lib/net/pop.rb @@ -0,0 +1,1021 @@ +# = net/pop.rb +# +# Copyright (c) 1999-2007 Yukihiro Matsumoto. +# +# Copyright (c) 1999-2007 Minero Aoki. +# +# Written & maintained by Minero Aoki <aamine@loveruby.net>. +# +# Documented by William Webber and Minero Aoki. +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms as Ruby itself, +# Ruby Distribute License. +# +# NOTE: You can find Japanese version of this document at: +# http://www.ruby-lang.org/ja/man/html/net_pop.html +# +#   $Id: pop.rb 44164 2013-12-13 02:38:55Z a_matsuda $ +# +# See Net::POP3 for documentation. +# + +require 'net/protocol' +require 'digest/md5' +require 'timeout' + +begin +  require "openssl" +rescue LoadError +end + +module Net + +  # Non-authentication POP3 protocol error +  # (reply code "-ERR", except authentication). +  class POPError < ProtocolError; end + +  # POP3 authentication error. +  class POPAuthenticationError < ProtoAuthError; end + +  # Unexpected response from the server. +  class POPBadResponse < POPError; end + +  # +  # == What is This Library? +  # +  # This library provides functionality for retrieving +  # email via POP3, the Post Office Protocol version 3. For details +  # of POP3, see [RFC1939] (http://www.ietf.org/rfc/rfc1939.txt). +  # +  # == Examples +  # +  # === Retrieving Messages +  # +  # This example retrieves messages from the server and deletes them +  # on the server. +  # +  # Messages are written to files named 'inbox/1', 'inbox/2', .... +  # Replace 'pop.example.com' with your POP3 server address, and +  # 'YourAccount' and 'YourPassword' with the appropriate account +  # details. +  # +  #     require 'net/pop' +  # +  #     pop = Net::POP3.new('pop.example.com') +  #     pop.start('YourAccount', 'YourPassword')             # (1) +  #     if pop.mails.empty? +  #       puts 'No mail.' +  #     else +  #       i = 0 +  #       pop.each_mail do |m|   # or "pop.mails.each ..."   # (2) +  #         File.open("inbox/#{i}", 'w') do |f| +  #           f.write m.pop +  #         end +  #         m.delete +  #         i += 1 +  #       end +  #       puts "#{pop.mails.size} mails popped." +  #     end +  #     pop.finish                                           # (3) +  # +  # 1. Call Net::POP3#start and start POP session. +  # 2. Access messages by using POP3#each_mail and/or POP3#mails. +  # 3. Close POP session by calling POP3#finish or use the block form of #start. +  # +  # === Shortened Code +  # +  # The example above is very verbose. You can shorten the code by using +  # some utility methods. First, the block form of Net::POP3.start can +  # be used instead of POP3.new, POP3#start and POP3#finish. +  # +  #     require 'net/pop' +  # +  #     Net::POP3.start('pop.example.com', 110, +  #                     'YourAccount', 'YourPassword') do |pop| +  #       if pop.mails.empty? +  #         puts 'No mail.' +  #       else +  #         i = 0 +  #         pop.each_mail do |m|   # or "pop.mails.each ..." +  #           File.open("inbox/#{i}", 'w') do |f| +  #             f.write m.pop +  #           end +  #           m.delete +  #           i += 1 +  #         end +  #         puts "#{pop.mails.size} mails popped." +  #       end +  #     end +  # +  # POP3#delete_all is an alternative for #each_mail and #delete. +  # +  #     require 'net/pop' +  # +  #     Net::POP3.start('pop.example.com', 110, +  #                     'YourAccount', 'YourPassword') do |pop| +  #       if pop.mails.empty? +  #         puts 'No mail.' +  #       else +  #         i = 1 +  #         pop.delete_all do |m| +  #           File.open("inbox/#{i}", 'w') do |f| +  #             f.write m.pop +  #           end +  #           i += 1 +  #         end +  #       end +  #     end +  # +  # And here is an even shorter example. +  # +  #     require 'net/pop' +  # +  #     i = 0 +  #     Net::POP3.delete_all('pop.example.com', 110, +  #                          'YourAccount', 'YourPassword') do |m| +  #       File.open("inbox/#{i}", 'w') do |f| +  #         f.write m.pop +  #       end +  #       i += 1 +  #     end +  # +  # === Memory Space Issues +  # +  # All the examples above get each message as one big string. +  # This example avoids this. +  # +  #     require 'net/pop' +  # +  #     i = 1 +  #     Net::POP3.delete_all('pop.example.com', 110, +  #                          'YourAccount', 'YourPassword') do |m| +  #       File.open("inbox/#{i}", 'w') do |f| +  #         m.pop do |chunk|    # get a message little by little. +  #           f.write chunk +  #         end +  #         i += 1 +  #       end +  #     end +  # +  # === Using APOP +  # +  # The net/pop library supports APOP authentication. +  # To use APOP, use the Net::APOP class instead of the Net::POP3 class. +  # You can use the utility method, Net::POP3.APOP(). For example: +  # +  #     require 'net/pop' +  # +  #     # Use APOP authentication if $isapop == true +  #     pop = Net::POP3.APOP($is_apop).new('apop.example.com', 110) +  #     pop.start(YourAccount', 'YourPassword') do |pop| +  #       # Rest of the code is the same. +  #     end +  # +  # === Fetch Only Selected Mail Using 'UIDL' POP Command +  # +  # If your POP server provides UIDL functionality, +  # you can grab only selected mails from the POP server. +  # e.g. +  # +  #     def need_pop?( id ) +  #       # determine if we need pop this mail... +  #     end +  # +  #     Net::POP3.start('pop.example.com', 110, +  #                     'Your account', 'Your password') do |pop| +  #       pop.mails.select { |m| need_pop?(m.unique_id) }.each do |m| +  #         do_something(m.pop) +  #       end +  #     end +  # +  # The POPMail#unique_id() method returns the unique-id of the message as a +  # String. Normally the unique-id is a hash of the message. +  # +  class POP3 < Protocol + +    # svn revision of this library +    Revision = %q$Revision: 44164 $.split[1] + +    # +    # Class Parameters +    # + +    # returns the port for POP3 +    def POP3.default_port +      default_pop3_port() +    end + +    # The default port for POP3 connections, port 110 +    def POP3.default_pop3_port +      110 +    end + +    # The default port for POP3S connections, port 995 +    def POP3.default_pop3s_port +      995 +    end + +    def POP3.socket_type   #:nodoc: obsolete +      Net::InternetMessageIO +    end + +    # +    # Utilities +    # + +    # Returns the APOP class if +isapop+ is true; otherwise, returns +    # the POP class.  For example: +    # +    #     # Example 1 +    #     pop = Net::POP3::APOP($is_apop).new(addr, port) +    # +    #     # Example 2 +    #     Net::POP3::APOP($is_apop).start(addr, port) do |pop| +    #       .... +    #     end +    # +    def POP3.APOP(isapop) +      isapop ? APOP : POP3 +    end + +    # Starts a POP3 session and iterates over each POPMail object, +    # yielding it to the +block+. +    # This method is equivalent to: +    # +    #     Net::POP3.start(address, port, account, password) do |pop| +    #       pop.each_mail do |m| +    #         yield m +    #       end +    #     end +    # +    # This method raises a POPAuthenticationError if authentication fails. +    # +    # === Example +    # +    #     Net::POP3.foreach('pop.example.com', 110, +    #                       'YourAccount', 'YourPassword') do |m| +    #       file.write m.pop +    #       m.delete if $DELETE +    #     end +    # +    def POP3.foreach(address, port = nil, +                     account = nil, password = nil, +                     isapop = false, &block)  # :yields: message +      start(address, port, account, password, isapop) {|pop| +        pop.each_mail(&block) +      } +    end + +    # Starts a POP3 session and deletes all messages on the server. +    # If a block is given, each POPMail object is yielded to it before +    # being deleted. +    # +    # This method raises a POPAuthenticationError if authentication fails. +    # +    # === Example +    # +    #     Net::POP3.delete_all('pop.example.com', 110, +    #                          'YourAccount', 'YourPassword') do |m| +    #       file.write m.pop +    #     end +    # +    def POP3.delete_all(address, port = nil, +                        account = nil, password = nil, +                        isapop = false, &block) +      start(address, port, account, password, isapop) {|pop| +        pop.delete_all(&block) +      } +    end + +    # Opens a POP3 session, attempts authentication, and quits. +    # +    # This method raises POPAuthenticationError if authentication fails. +    # +    # === Example: normal POP3 +    # +    #     Net::POP3.auth_only('pop.example.com', 110, +    #                         'YourAccount', 'YourPassword') +    # +    # === Example: APOP +    # +    #     Net::POP3.auth_only('pop.example.com', 110, +    #                         'YourAccount', 'YourPassword', true) +    # +    def POP3.auth_only(address, port = nil, +                       account = nil, password = nil, +                       isapop = false) +      new(address, port, isapop).auth_only account, password +    end + +    # Starts a pop3 session, attempts authentication, and quits. +    # This method must not be called while POP3 session is opened. +    # This method raises POPAuthenticationError if authentication fails. +    def auth_only(account, password) +      raise IOError, 'opening previously opened POP session' if started? +      start(account, password) { +        ; +      } +    end + +    # +    # SSL +    # + +    @ssl_params = nil + +    # :call-seq: +    #    Net::POP.enable_ssl(params = {}) +    # +    # Enable SSL for all new instances. +    # +params+ is passed to OpenSSL::SSLContext#set_params. +    def POP3.enable_ssl(*args) +      @ssl_params = create_ssl_params(*args) +    end + +    # Constructs proper parameters from arguments +    def POP3.create_ssl_params(verify_or_params = {}, certs = nil) +      begin +        params = verify_or_params.to_hash +      rescue NoMethodError +        params = {} +        params[:verify_mode] = verify_or_params +        if certs +          if File.file?(certs) +            params[:ca_file] = certs +          elsif File.directory?(certs) +            params[:ca_path] = certs +          end +        end +      end +      return params +    end + +    # Disable SSL for all new instances. +    def POP3.disable_ssl +      @ssl_params = nil +    end + +    # returns the SSL Parameters +    # +    # see also POP3.enable_ssl +    def POP3.ssl_params +      return @ssl_params +    end + +    # returns +true+ if POP3.ssl_params is set +    def POP3.use_ssl? +      return !@ssl_params.nil? +    end + +    # returns whether verify_mode is enable from POP3.ssl_params +    def POP3.verify +      return @ssl_params[:verify_mode] +    end + +    # returns the :ca_file or :ca_path from POP3.ssl_params +    def POP3.certs +      return @ssl_params[:ca_file] || @ssl_params[:ca_path] +    end + +    # +    # Session management +    # + +    # Creates a new POP3 object and open the connection.  Equivalent to +    # +    #   Net::POP3.new(address, port, isapop).start(account, password) +    # +    # If +block+ is provided, yields the newly-opened POP3 object to it, +    # and automatically closes it at the end of the session. +    # +    # === Example +    # +    #    Net::POP3.start(addr, port, account, password) do |pop| +    #      pop.each_mail do |m| +    #        file.write m.pop +    #        m.delete +    #      end +    #    end +    # +    def POP3.start(address, port = nil, +                   account = nil, password = nil, +                   isapop = false, &block)   # :yield: pop +      new(address, port, isapop).start(account, password, &block) +    end + +    # Creates a new POP3 object. +    # +    # +address+ is the hostname or ip address of your POP3 server. +    # +    # The optional +port+ is the port to connect to. +    # +    # The optional +isapop+ specifies whether this connection is going +    # to use APOP authentication; it defaults to +false+. +    # +    # This method does *not* open the TCP connection. +    def initialize(addr, port = nil, isapop = false) +      @address = addr +      @ssl_params = POP3.ssl_params +      @port = port +      @apop = isapop + +      @command = nil +      @socket = nil +      @started = false +      @open_timeout = 30 +      @read_timeout = 60 +      @debug_output = nil + +      @mails = nil +      @n_mails = nil +      @n_bytes = nil +    end + +    # Does this instance use APOP authentication? +    def apop? +      @apop +    end + +    # does this instance use SSL? +    def use_ssl? +      return !@ssl_params.nil? +    end + +    # :call-seq: +    #    Net::POP#enable_ssl(params = {}) +    # +    # Enables SSL for this instance.  Must be called before the connection is +    # established to have any effect. +    # +params[:port]+ is port to establish the SSL connection on; Defaults to 995. +    # +params+ (except :port) is passed to OpenSSL::SSLContext#set_params. +    def enable_ssl(verify_or_params = {}, certs = nil, port = nil) +      begin +        @ssl_params = verify_or_params.to_hash.dup +        @port = @ssl_params.delete(:port) || @port +      rescue NoMethodError +        @ssl_params = POP3.create_ssl_params(verify_or_params, certs) +        @port = port || @port +      end +    end + +    # Disable SSL for all new instances. +    def disable_ssl +      @ssl_params = nil +    end + +    # Provide human-readable stringification of class state. +    def inspect +      "#<#{self.class} #{@address}:#{@port} open=#{@started}>" +    end + +    # *WARNING*: This method causes a serious security hole. +    # Use this method only for debugging. +    # +    # Set an output stream for debugging. +    # +    # === Example +    # +    #   pop = Net::POP.new(addr, port) +    #   pop.set_debug_output $stderr +    #   pop.start(account, passwd) do |pop| +    #     .... +    #   end +    # +    def set_debug_output(arg) +      @debug_output = arg +    end + +    # The address to connect to. +    attr_reader :address + +    # The port number to connect to. +    def port +      return @port || (use_ssl? ? POP3.default_pop3s_port : POP3.default_pop3_port) +    end + +    # Seconds to wait until a connection is opened. +    # If the POP3 object cannot open a connection within this time, +    # it raises a Net::OpenTimeout exception. The default value is 30 seconds. +    attr_accessor :open_timeout + +    # Seconds to wait until reading one block (by one read(1) call). +    # If the POP3 object cannot complete a read() within this time, +    # it raises a Net::ReadTimeout exception. The default value is 60 seconds. +    attr_reader :read_timeout + +    # Set the read timeout. +    def read_timeout=(sec) +      @command.socket.read_timeout = sec if @command +      @read_timeout = sec +    end + +    # +true+ if the POP3 session has started. +    def started? +      @started +    end + +    alias active? started?   #:nodoc: obsolete + +    # Starts a POP3 session. +    # +    # When called with block, gives a POP3 object to the block and +    # closes the session after block call finishes. +    # +    # This method raises a POPAuthenticationError if authentication fails. +    def start(account, password) # :yield: pop +      raise IOError, 'POP session already started' if @started +      if block_given? +        begin +          do_start account, password +          return yield(self) +        ensure +          do_finish +        end +      else +        do_start account, password +        return self +      end +    end + +    # internal method for Net::POP3.start +    def do_start(account, password) # :nodoc: +      s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do +        TCPSocket.open(@address, port) +      end +      if use_ssl? +        raise 'openssl library not installed' unless defined?(OpenSSL) +        context = OpenSSL::SSL::SSLContext.new +        context.set_params(@ssl_params) +        s = OpenSSL::SSL::SSLSocket.new(s, context) +        s.sync_close = true +        s.connect +        if context.verify_mode != OpenSSL::SSL::VERIFY_NONE +          s.post_connection_check(@address) +        end +      end +      @socket = InternetMessageIO.new(s) +      logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})" +      @socket.read_timeout = @read_timeout +      @socket.debug_output = @debug_output +      on_connect +      @command = POP3Command.new(@socket) +      if apop? +        @command.apop account, password +      else +        @command.auth account, password +      end +      @started = true +    ensure +      # Authentication failed, clean up connection. +      unless @started +        s.close if s and not s.closed? +        @socket = nil +        @command = nil +      end +    end +    private :do_start + +    # Does nothing +    def on_connect # :nodoc: +    end +    private :on_connect + +    # Finishes a POP3 session and closes TCP connection. +    def finish +      raise IOError, 'POP session not yet started' unless started? +      do_finish +    end + +    # nil's out the: +    # - mails +    # - number counter for mails +    # - number counter for bytes +    # - quits the current command, if any +    def do_finish # :nodoc: +      @mails = nil +      @n_mails = nil +      @n_bytes = nil +      @command.quit if @command +    ensure +      @started = false +      @command = nil +      @socket.close if @socket and not @socket.closed? +      @socket = nil +    end +    private :do_finish + +    # Returns the current command. +    # +    # Raises IOError if there is no active socket +    def command # :nodoc: +      raise IOError, 'POP session not opened yet' \ +                                      if not @socket or @socket.closed? +      @command +    end +    private :command + +    # +    # POP protocol wrapper +    # + +    # Returns the number of messages on the POP server. +    def n_mails +      return @n_mails if @n_mails +      @n_mails, @n_bytes = command().stat +      @n_mails +    end + +    # Returns the total size in bytes of all the messages on the POP server. +    def n_bytes +      return @n_bytes if @n_bytes +      @n_mails, @n_bytes = command().stat +      @n_bytes +    end + +    # Returns an array of Net::POPMail objects, representing all the +    # messages on the server.  This array is renewed when the session +    # restarts; otherwise, it is fetched from the server the first time +    # this method is called (directly or indirectly) and cached. +    # +    # This method raises a POPError if an error occurs. +    def mails +      return @mails.dup if @mails +      if n_mails() == 0 +        # some popd raises error for LIST on the empty mailbox. +        @mails = [] +        return [] +      end + +      @mails = command().list.map {|num, size| +        POPMail.new(num, size, self, command()) +      } +      @mails.dup +    end + +    # Yields each message to the passed-in block in turn. +    # Equivalent to: +    # +    #   pop3.mails.each do |popmail| +    #     .... +    #   end +    # +    # This method raises a POPError if an error occurs. +    def each_mail(&block)  # :yield: message +      mails().each(&block) +    end + +    alias each each_mail + +    # Deletes all messages on the server. +    # +    # If called with a block, yields each message in turn before deleting it. +    # +    # === Example +    # +    #     n = 1 +    #     pop.delete_all do |m| +    #       File.open("inbox/#{n}") do |f| +    #         f.write m.pop +    #       end +    #       n += 1 +    #     end +    # +    # This method raises a POPError if an error occurs. +    # +    def delete_all # :yield: message +      mails().each do |m| +        yield m if block_given? +        m.delete unless m.deleted? +      end +    end + +    # Resets the session.  This clears all "deleted" marks from messages. +    # +    # This method raises a POPError if an error occurs. +    def reset +      command().rset +      mails().each do |m| +        m.instance_eval { +          @deleted = false +        } +      end +    end + +    def set_all_uids   #:nodoc: internal use only (called from POPMail#uidl) +      uidl = command().uidl +      @mails.each {|m| m.uid = uidl[m.number] } +    end + +    # debugging output for +msg+ +    def logging(msg) +      @debug_output << msg + "\n" if @debug_output +    end + +  end   # class POP3 + +  # class aliases +  POP = POP3 # :nodoc: +  POPSession  = POP3 # :nodoc: +  POP3Session = POP3 # :nodoc: + +  # +  # This class is equivalent to POP3, except that it uses APOP authentication. +  # +  class APOP < POP3 +    # Always returns true. +    def apop? +      true +    end +  end + +  # class aliases +  APOPSession = APOP + +  # +  # This class represents a message which exists on the POP server. +  # Instances of this class are created by the POP3 class; they should +  # not be directly created by the user. +  # +  class POPMail + +    def initialize(num, len, pop, cmd)   #:nodoc: +      @number = num +      @length = len +      @pop = pop +      @command = cmd +      @deleted = false +      @uid = nil +    end + +    # The sequence number of the message on the server. +    attr_reader :number + +    # The length of the message in octets. +    attr_reader :length +    alias size length + +    # Provide human-readable stringification of class state. +    def inspect +      "#<#{self.class} #{@number}#{@deleted ? ' deleted' : ''}>" +    end + +    # +    # This method fetches the message.  If called with a block, the +    # message is yielded to the block one chunk at a time.  If called +    # without a block, the message is returned as a String.  The optional +    # +dest+ argument will be prepended to the returned String; this +    # argument is essentially obsolete. +    # +    # === Example without block +    # +    #     POP3.start('pop.example.com', 110, +    #                'YourAccount, 'YourPassword') do |pop| +    #       n = 1 +    #       pop.mails.each do |popmail| +    #         File.open("inbox/#{n}", 'w') do |f| +    #           f.write popmail.pop +    #         end +    #         popmail.delete +    #         n += 1 +    #       end +    #     end +    # +    # === Example with block +    # +    #     POP3.start('pop.example.com', 110, +    #                'YourAccount, 'YourPassword') do |pop| +    #       n = 1 +    #       pop.mails.each do |popmail| +    #         File.open("inbox/#{n}", 'w') do |f| +    #           popmail.pop do |chunk|            #### +    #             f.write chunk +    #           end +    #         end +    #         n += 1 +    #       end +    #     end +    # +    # This method raises a POPError if an error occurs. +    # +    def pop( dest = '', &block ) # :yield: message_chunk +      if block_given? +        @command.retr(@number, &block) +        nil +      else +        @command.retr(@number) do |chunk| +          dest << chunk +        end +        dest +      end +    end + +    alias all pop    #:nodoc: obsolete +    alias mail pop   #:nodoc: obsolete + +    # Fetches the message header and +lines+ lines of body. +    # +    # The optional +dest+ argument is obsolete. +    # +    # This method raises a POPError if an error occurs. +    def top(lines, dest = '') +      @command.top(@number, lines) do |chunk| +        dest << chunk +      end +      dest +    end + +    # Fetches the message header. +    # +    # The optional +dest+ argument is obsolete. +    # +    # This method raises a POPError if an error occurs. +    def header(dest = '') +      top(0, dest) +    end + +    # Marks a message for deletion on the server.  Deletion does not +    # actually occur until the end of the session; deletion may be +    # cancelled for _all_ marked messages by calling POP3#reset(). +    # +    # This method raises a POPError if an error occurs. +    # +    # === Example +    # +    #     POP3.start('pop.example.com', 110, +    #                'YourAccount, 'YourPassword') do |pop| +    #       n = 1 +    #       pop.mails.each do |popmail| +    #         File.open("inbox/#{n}", 'w') do |f| +    #           f.write popmail.pop +    #         end +    #         popmail.delete         #### +    #         n += 1 +    #       end +    #     end +    # +    def delete +      @command.dele @number +      @deleted = true +    end + +    alias delete! delete    #:nodoc: obsolete + +    # True if the mail has been deleted. +    def deleted? +      @deleted +    end + +    # Returns the unique-id of the message. +    # Normally the unique-id is a hash string of the message. +    # +    # This method raises a POPError if an error occurs. +    def unique_id +      return @uid if @uid +      @pop.set_all_uids +      @uid +    end + +    alias uidl unique_id + +    def uid=(uid)   #:nodoc: internal use only +      @uid = uid +    end + +  end   # class POPMail + + +  class POP3Command   #:nodoc: internal use only + +    def initialize(sock) +      @socket = sock +      @error_occurred = false +      res = check_response(critical { recv_response() }) +      @apop_stamp = res.slice(/<[!-~]+@[!-~]+>/) +    end + +    attr_reader :socket + +    def inspect +      "#<#{self.class} socket=#{@socket}>" +    end + +    def auth(account, password) +      check_response_auth(critical { +        check_response_auth(get_response('USER %s', account)) +        get_response('PASS %s', 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)) +      }) +    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 +        return list +      } +    end + +    def stat +      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 +      check_response(critical { get_response('RSET') }) +    end + +    def top(num, lines = 0, &block) +      critical { +        getok('TOP %d %d', num, lines) +        @socket.each_message_chunk(&block) +      } +    end + +    def retr(num, &block) +      critical { +        getok('RETR %d', num) +        @socket.each_message_chunk(&block) +      } +    end + +    def dele(num) +      check_response(critical { get_response('DELE %d', num) }) +    end + +    def uidl(num = nil) +      if num +        res = check_response(critical { get_response('UIDL %d', num) }) +        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 +          return table +        } +      end +    end + +    def quit +      check_response(critical { get_response('QUIT') }) +    end + +    private + +    def getok(fmt, *fargs) +      @socket.writeline sprintf(fmt, *fargs) +      check_response(recv_response()) +    end + +    def get_response(fmt, *fargs) +      @socket.writeline sprintf(fmt, *fargs) +      recv_response() +    end + +    def recv_response +      @socket.readline +    end + +    def check_response(res) +      raise POPError, res unless /\A\+OK/i =~ res +      res +    end + +    def check_response_auth(res) +      raise POPAuthenticationError, res unless /\A\+OK/i =~ res +      res +    end + +    def critical +      return '+OK dummy ok response' if @error_occurred +      begin +        return yield() +      rescue Exception +        @error_occurred = true +        raise +      end +    end + +  end   # class POP3Command + +end   # module Net diff --git a/jni/ruby/lib/net/protocol.rb b/jni/ruby/lib/net/protocol.rb new file mode 100644 index 0000000..ae3620d --- /dev/null +++ b/jni/ruby/lib/net/protocol.rb @@ -0,0 +1,420 @@ +# +# = net/protocol.rb +# +#-- +# Copyright (c) 1999-2004 Yukihiro Matsumoto +# Copyright (c) 1999-2004 Minero Aoki +# +# written and maintained by Minero Aoki <aamine@loveruby.net> +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms as Ruby itself, +# Ruby Distribute License or GNU General Public License. +# +# $Id: protocol.rb 46060 2014-05-23 12:36:30Z nobu $ +#++ +# +# WARNING: This file is going to remove. +# Do not rely on the implementation written in this file. +# + +require 'socket' +require 'timeout' + +module Net # :nodoc: + +  class Protocol   #:nodoc: internal use only +    private +    def Protocol.protocol_param(name, val) +      module_eval(<<-End, __FILE__, __LINE__ + 1) +        def #{name} +          #{val} +        end +      End +    end +  end + + +  class ProtocolError          < StandardError; end +  class ProtoSyntaxError       < ProtocolError; end +  class ProtoFatalError        < ProtocolError; end +  class ProtoUnknownError      < ProtocolError; end +  class ProtoServerError       < ProtocolError; end +  class ProtoAuthError         < ProtocolError; end +  class ProtoCommandError      < ProtocolError; end +  class ProtoRetriableError    < ProtocolError; end +  ProtocRetryError = ProtoRetriableError + +  ## +  # OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot +  # be created within the open_timeout. + +  class OpenTimeout            < Timeout::Error; end + +  ## +  # ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the +  # response cannot be read within the read_timeout. + +  class ReadTimeout            < Timeout::Error; end + + +  class BufferedIO   #:nodoc: internal use only +    def initialize(io) +      @io = io +      @read_timeout = 60 +      @continue_timeout = nil +      @debug_output = nil +      @rbuf = '' +    end + +    attr_reader :io +    attr_accessor :read_timeout +    attr_accessor :continue_timeout +    attr_accessor :debug_output + +    def inspect +      "#<#{self.class} io=#{@io}>" +    end + +    def eof? +      @io.eof? +    end + +    def closed? +      @io.closed? +    end + +    def close +      @io.close +    end + +    # +    # Read +    # + +    public + +    def read(len, dest = '', ignore_eof = false) +      LOG "reading #{len} bytes..." +      read_bytes = 0 +      begin +        while read_bytes + @rbuf.size < len +          dest << (s = rbuf_consume(@rbuf.size)) +          read_bytes += s.size +          rbuf_fill +        end +        dest << (s = rbuf_consume(len - read_bytes)) +        read_bytes += s.size +      rescue EOFError +        raise unless ignore_eof +      end +      LOG "read #{read_bytes} bytes" +      dest +    end + +    def read_all(dest = '') +      LOG 'reading all...' +      read_bytes = 0 +      begin +        while true +          dest << (s = rbuf_consume(@rbuf.size)) +          read_bytes += s.size +          rbuf_fill +        end +      rescue EOFError +        ; +      end +      LOG "read #{read_bytes} bytes" +      dest +    end + +    def readuntil(terminator, ignore_eof = false) +      begin +        until idx = @rbuf.index(terminator) +          rbuf_fill +        end +        return rbuf_consume(idx + terminator.size) +      rescue EOFError +        raise unless ignore_eof +        return rbuf_consume(@rbuf.size) +      end +    end + +    def readline +      readuntil("\n").chop +    end + +    private + +    BUFSIZE = 1024 * 16 + +    def rbuf_fill +      begin +        @rbuf << @io.read_nonblock(BUFSIZE) +      rescue IO::WaitReadable +        if IO.select([@io], nil, nil, @read_timeout) +          retry +        else +          raise Net::ReadTimeout +        end +      rescue IO::WaitWritable +        # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable. +        # http://www.openssl.org/support/faq.html#PROG10 +        if IO.select(nil, [@io], nil, @read_timeout) +          retry +        else +          raise Net::ReadTimeout +        end +      end +    end + +    def rbuf_consume(len) +      s = @rbuf.slice!(0, len) +      @debug_output << %Q[-> #{s.dump}\n] if @debug_output +      s +    end + +    # +    # Write +    # + +    public + +    def write(str) +      writing { +        write0 str +      } +    end + +    alias << write + +    def writeline(str) +      writing { +        write0 str + "\r\n" +      } +    end + +    private + +    def writing +      @written_bytes = 0 +      @debug_output << '<- ' if @debug_output +      yield +      @debug_output << "\n" if @debug_output +      bytes = @written_bytes +      @written_bytes = nil +      bytes +    end + +    def write0(str) +      @debug_output << str.dump if @debug_output +      len = @io.write(str) +      @written_bytes += len +      len +    end + +    # +    # Logging +    # + +    private + +    def LOG_off +      @save_debug_out = @debug_output +      @debug_output = nil +    end + +    def LOG_on +      @debug_output = @save_debug_out +    end + +    def LOG(msg) +      return unless @debug_output +      @debug_output << msg + "\n" +    end +  end + + +  class InternetMessageIO < BufferedIO   #:nodoc: internal use only +    def initialize(io) +      super +      @wbuf = nil +    end + +    # +    # Read +    # + +    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 + +    def write_message_0(src) +      prev = @written_bytes +      each_crlf_line(src) do |line| +        write0 dot_stuff(line) +      end +      @written_bytes - prev +    end + +    # +    # Write +    # + +    def write_message(src) +      LOG "writing message from #{src.class}" +      LOG_off() +      len = writing { +        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 = writing { +        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 dot_stuff(s) +      s.sub(/\A\./, '..') +    end + +    def using_each_crlf_line +      @wbuf = '' +      yield +      if not @wbuf.empty?   # unterminated last line +        write0 dot_stuff(@wbuf.chomp) + "\r\n" +      elsif @written_bytes == 0   # empty src +        write0 "\r\n" +      end +      write0 ".\r\n" +      @wbuf = nil +    end + +    def each_crlf_line(src) +      buffer_filling(@wbuf, src) do +        while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/) +          yield line.chomp("\n") + "\r\n" +        end +      end +    end + +    def buffer_filling(buf, src) +      case src +      when String    # for speeding up. +        0.step(src.size - 1, 1024) do |i| +          buf << src[i, 1024] +          yield +        end +      when File    # for speeding up. +        while s = src.read(1024) +          buf << s +          yield +        end +      else    # generic reader +        src.each do |str| +          buf << str +          yield if buf.size > 1024 +        end +        yield unless buf.empty? +      end +    end +  end + + +  # +  # The writer adapter class +  # +  class WriteAdapter +    def initialize(socket, method) +      @socket = socket +      @method_id = method +    end + +    def inspect +      "#<#{self.class} socket=#{@socket.inspect}>" +    end + +    def write(str) +      @socket.__send__(@method_id, str) +    end + +    alias print write + +    def <<(str) +      write str +      self +    end + +    def puts(str = '') +      write str.chomp("\n") + "\n" +    end + +    def printf(*args) +      write sprintf(*args) +    end +  end + + +  class ReadAdapter   #:nodoc: internal use only +    def initialize(block) +      @block = block +    end + +    def inspect +      "#<#{self.class}>" +    end + +    def <<(str) +      call_block(str, &@block) if @block +    end + +    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 +  end + + +  module NetPrivate   #:nodoc: obsolete +    Socket = ::Net::InternetMessageIO +  end + +end   # module Net diff --git a/jni/ruby/lib/net/smtp.rb b/jni/ruby/lib/net/smtp.rb new file mode 100644 index 0000000..04640e3 --- /dev/null +++ b/jni/ruby/lib/net/smtp.rb @@ -0,0 +1,1073 @@ +# = net/smtp.rb +# +# Copyright (c) 1999-2007 Yukihiro Matsumoto. +# +# Copyright (c) 1999-2007 Minero Aoki. +# +# Written & maintained by Minero Aoki <aamine@loveruby.net>. +# +# Documented by William Webber and Minero Aoki. +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms as Ruby itself. +# +# NOTE: You can find Japanese version of this document at: +# http://www.ruby-lang.org/ja/man/html/net_smtp.html +# +# $Id: smtp.rb 46793 2014-07-11 19:22:19Z kosaki $ +# +# See Net::SMTP for documentation. +# + +require 'net/protocol' +require 'digest/md5' +require 'timeout' +begin +  require 'openssl' +rescue LoadError +end + +module Net + +  # Module mixed in to all SMTP error classes +  module SMTPError +    # This *class* is a module for backward compatibility. +    # In later release, this module becomes a class. +  end + +  # Represents an SMTP authentication error. +  class SMTPAuthenticationError < ProtoAuthError +    include SMTPError +  end + +  # Represents SMTP error code 420 or 450, a temporary error. +  class SMTPServerBusy < ProtoServerError +    include SMTPError +  end + +  # Represents an SMTP command syntax error (error code 500) +  class SMTPSyntaxError < ProtoSyntaxError +    include SMTPError +  end + +  # Represents a fatal SMTP error (error code 5xx, except for 500) +  class SMTPFatalError < ProtoFatalError +    include SMTPError +  end + +  # Unexpected reply code returned from server. +  class SMTPUnknownError < ProtoUnknownError +    include SMTPError +  end + +  # Command is not supported on server. +  class SMTPUnsupportedCommand < ProtocolError +    include SMTPError +  end + +  # +  # == What is This Library? +  # +  # This library provides functionality to send internet +  # mail via SMTP, the Simple Mail Transfer Protocol. For details of +  # SMTP itself, see [RFC2821] (http://www.ietf.org/rfc/rfc2821.txt). +  # +  # == What is This Library NOT? +  # +  # This library does NOT provide functions to compose internet mails. +  # You must create them by yourself. If you want better mail support, +  # try RubyMail or TMail or search for alternatives in +  # {RubyGems.org}[https://rubygems.org/] or {The Ruby +  # Toolbox}[https://www.ruby-toolbox.com/]. +  # +  # FYI: the official documentation on internet mail is: [RFC2822] (http://www.ietf.org/rfc/rfc2822.txt). +  # +  # == Examples +  # +  # === Sending Messages +  # +  # You must open a connection to an SMTP server before sending messages. +  # The first argument is the address of your SMTP server, and the second +  # argument is the port number. Using SMTP.start with a block is the simplest +  # way to do this. This way, the SMTP connection is closed automatically +  # after the block is executed. +  # +  #     require 'net/smtp' +  #     Net::SMTP.start('your.smtp.server', 25) do |smtp| +  #       # Use the SMTP object smtp only in this block. +  #     end +  # +  # Replace 'your.smtp.server' with your SMTP server. Normally +  # your system manager or internet provider supplies a server +  # for you. +  # +  # Then you can send messages. +  # +  #     msgstr = <<END_OF_MESSAGE +  #     From: Your Name <your@mail.address> +  #     To: Destination Address <someone@example.com> +  #     Subject: test message +  #     Date: Sat, 23 Jun 2001 16:26:43 +0900 +  #     Message-Id: <unique.message.id.string@example.com> +  # +  #     This is a test message. +  #     END_OF_MESSAGE +  # +  #     require 'net/smtp' +  #     Net::SMTP.start('your.smtp.server', 25) do |smtp| +  #       smtp.send_message msgstr, +  #                         'your@mail.address', +  #                         'his_address@example.com' +  #     end +  # +  # === Closing the Session +  # +  # You MUST close the SMTP session after sending messages, by calling +  # the #finish method: +  # +  #     # using SMTP#finish +  #     smtp = Net::SMTP.start('your.smtp.server', 25) +  #     smtp.send_message msgstr, 'from@address', 'to@address' +  #     smtp.finish +  # +  # You can also use the block form of SMTP.start/SMTP#start.  This closes +  # the SMTP session automatically: +  # +  #     # using block form of SMTP.start +  #     Net::SMTP.start('your.smtp.server', 25) do |smtp| +  #       smtp.send_message msgstr, 'from@address', 'to@address' +  #     end +  # +  # I strongly recommend this scheme.  This form is simpler and more robust. +  # +  # === HELO domain +  # +  # In almost all situations, you must provide a third argument +  # to SMTP.start/SMTP#start. This is the domain name which you are on +  # (the host to send mail from). It is called the "HELO domain". +  # The SMTP server will judge whether it should send or reject +  # the SMTP session by inspecting the HELO domain. +  # +  #     Net::SMTP.start('your.smtp.server', 25, +  #                     'mail.from.domain') { |smtp| ... } +  # +  # === SMTP Authentication +  # +  # The Net::SMTP class supports three authentication schemes; +  # PLAIN, LOGIN and CRAM MD5.  (SMTP Authentication: [RFC2554]) +  # To use SMTP authentication, pass extra arguments to +  # SMTP.start/SMTP#start. +  # +  #     # PLAIN +  #     Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', +  #                     'Your Account', 'Your Password', :plain) +  #     # LOGIN +  #     Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', +  #                     'Your Account', 'Your Password', :login) +  # +  #     # CRAM MD5 +  #     Net::SMTP.start('your.smtp.server', 25, 'mail.from.domain', +  #                     'Your Account', 'Your Password', :cram_md5) +  # +  class SMTP + +    Revision = %q$Revision: 46793 $.split[1] + +    # The default SMTP port number, 25. +    def SMTP.default_port +      25 +    end + +    # The default mail submission port number, 587. +    def SMTP.default_submission_port +      587 +    end + +    # The default SMTPS port number, 465. +    def SMTP.default_tls_port +      465 +    end + +    class << self +      alias default_ssl_port default_tls_port +    end + +    def SMTP.default_ssl_context +      OpenSSL::SSL::SSLContext.new +    end + +    # +    # Creates a new Net::SMTP object. +    # +    # +address+ is the hostname or ip address of your SMTP +    # server.  +port+ is the port to connect to; it defaults to +    # port 25. +    # +    # This method does not open the TCP connection.  You can use +    # SMTP.start instead of SMTP.new if you want to do everything +    # at once.  Otherwise, follow SMTP.new with SMTP#start. +    # +    def initialize(address, port = nil) +      @address = address +      @port = (port || SMTP.default_port) +      @esmtp = true +      @capabilities = nil +      @socket = nil +      @started = false +      @open_timeout = 30 +      @read_timeout = 60 +      @error_occurred = false +      @debug_output = nil +      @tls = false +      @starttls = false +      @ssl_context = nil +    end + +    # Provide human-readable stringification of class state. +    def inspect +      "#<#{self.class} #{@address}:#{@port} started=#{@started}>" +    end + +    # +    # Set whether to use ESMTP or not.  This should be done before +    # calling #start.  Note that if #start is called in ESMTP mode, +    # and the connection fails due to a ProtocolError, the SMTP +    # object will automatically switch to plain SMTP mode and +    # retry (but not vice versa). +    # +    attr_accessor :esmtp + +    # +true+ if the SMTP object uses ESMTP (which it does by default). +    alias :esmtp? :esmtp + +    # true if server advertises STARTTLS. +    # You cannot get valid value before opening SMTP session. +    def capable_starttls? +      capable?('STARTTLS') +    end + +    def capable?(key) +      return nil unless @capabilities +      @capabilities[key] ? true : false +    end +    private :capable? + +    # true if server advertises AUTH PLAIN. +    # You cannot get valid value before opening SMTP session. +    def capable_plain_auth? +      auth_capable?('PLAIN') +    end + +    # true if server advertises AUTH LOGIN. +    # You cannot get valid value before opening SMTP session. +    def capable_login_auth? +      auth_capable?('LOGIN') +    end + +    # true if server advertises AUTH CRAM-MD5. +    # You cannot get valid value before opening SMTP session. +    def capable_cram_md5_auth? +      auth_capable?('CRAM-MD5') +    end + +    def auth_capable?(type) +      return nil unless @capabilities +      return false unless @capabilities['AUTH'] +      @capabilities['AUTH'].include?(type) +    end +    private :auth_capable? + +    # Returns supported authentication methods on this server. +    # You cannot get valid value before opening SMTP session. +    def capable_auth_types +      return [] unless @capabilities +      return [] unless @capabilities['AUTH'] +      @capabilities['AUTH'] +    end + +    # true if this object uses SMTP/TLS (SMTPS). +    def tls? +      @tls +    end + +    alias ssl? tls? + +    # Enables SMTP/TLS (SMTPS: SMTP over direct TLS connection) for +    # this object.  Must be called before the connection is established +    # to have any effect.  +context+ is a OpenSSL::SSL::SSLContext object. +    def enable_tls(context = SMTP.default_ssl_context) +      raise 'openssl library not installed' unless defined?(OpenSSL) +      raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls +      @tls = true +      @ssl_context = context +    end + +    alias enable_ssl enable_tls + +    # Disables SMTP/TLS for this object.  Must be called before the +    # connection is established to have any effect. +    def disable_tls +      @tls = false +      @ssl_context = nil +    end + +    alias disable_ssl disable_tls + +    # Returns truth value if this object uses STARTTLS. +    # If this object always uses STARTTLS, returns :always. +    # If this object uses STARTTLS when the server support TLS, returns :auto. +    def starttls? +      @starttls +    end + +    # true if this object uses STARTTLS. +    def starttls_always? +      @starttls == :always +    end + +    # true if this object uses STARTTLS when server advertises STARTTLS. +    def starttls_auto? +      @starttls == :auto +    end + +    # Enables SMTP/TLS (STARTTLS) for this object. +    # +context+ is a OpenSSL::SSL::SSLContext object. +    def enable_starttls(context = SMTP.default_ssl_context) +      raise 'openssl library not installed' unless defined?(OpenSSL) +      raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls +      @starttls = :always +      @ssl_context = context +    end + +    # Enables SMTP/TLS (STARTTLS) for this object if server accepts. +    # +context+ is a OpenSSL::SSL::SSLContext object. +    def enable_starttls_auto(context = SMTP.default_ssl_context) +      raise 'openssl library not installed' unless defined?(OpenSSL) +      raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls +      @starttls = :auto +      @ssl_context = context +    end + +    # Disables SMTP/TLS (STARTTLS) for this object.  Must be called +    # before the connection is established to have any effect. +    def disable_starttls +      @starttls = false +      @ssl_context = nil +    end + +    # The address of the SMTP server to connect to. +    attr_reader :address + +    # The port number of the SMTP server to connect to. +    attr_reader :port + +    # Seconds to wait while attempting to open a connection. +    # If the connection cannot be opened within this time, a +    # Net::OpenTimeout is raised. The default value is 30 seconds. +    attr_accessor :open_timeout + +    # Seconds to wait while reading one block (by one read(2) call). +    # If the read(2) call does not complete within this time, a +    # Net::ReadTimeout is raised. The default value is 60 seconds. +    attr_reader :read_timeout + +    # Set the number of seconds to wait until timing-out a read(2) +    # call. +    def read_timeout=(sec) +      @socket.read_timeout = sec if @socket +      @read_timeout = sec +    end + +    # +    # WARNING: This method causes serious security holes. +    # Use this method for only debugging. +    # +    # Set an output stream for debug logging. +    # You must call this before #start. +    # +    #   # example +    #   smtp = Net::SMTP.new(addr, port) +    #   smtp.set_debug_output $stderr +    #   smtp.start do |smtp| +    #     .... +    #   end +    # +    def debug_output=(arg) +      @debug_output = arg +    end + +    alias set_debug_output debug_output= + +    # +    # SMTP session control +    # + +    # +    # Creates a new Net::SMTP object and connects to the server. +    # +    # This method is equivalent to: +    # +    #   Net::SMTP.new(address, port).start(helo_domain, account, password, authtype) +    # +    # === Example +    # +    #     Net::SMTP.start('your.smtp.server') do |smtp| +    #       smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] +    #     end +    # +    # === Block Usage +    # +    # If called with a block, the newly-opened Net::SMTP object is yielded +    # to the block, and automatically closed when the block finishes.  If called +    # without a block, the newly-opened Net::SMTP object is returned to +    # the caller, and it is the caller's responsibility to close it when +    # finished. +    # +    # === Parameters +    # +    # +address+ is the hostname or ip address of your smtp server. +    # +    # +port+ is the port to connect to; it defaults to port 25. +    # +    # +helo+ is the _HELO_ _domain_ provided by the client to the +    # server (see overview comments); it defaults to 'localhost'. +    # +    # The remaining arguments are used for SMTP authentication, if required +    # or desired.  +user+ is the account name; +secret+ is your password +    # or other authentication token; and +authtype+ is the authentication +    # type, one of :plain, :login, or :cram_md5.  See the discussion of +    # SMTP Authentication in the overview notes. +    # +    # === Errors +    # +    # This method may raise: +    # +    # * Net::SMTPAuthenticationError +    # * Net::SMTPServerBusy +    # * Net::SMTPSyntaxError +    # * Net::SMTPFatalError +    # * Net::SMTPUnknownError +    # * Net::OpenTimeout +    # * Net::ReadTimeout +    # * IOError +    # +    def SMTP.start(address, port = nil, helo = 'localhost', +                   user = nil, secret = nil, authtype = nil, +                   &block)   # :yield: smtp +      new(address, port).start(helo, user, secret, authtype, &block) +    end + +    # +true+ if the SMTP session has been started. +    def started? +      @started +    end + +    # +    # Opens a TCP connection and starts the SMTP session. +    # +    # === Parameters +    # +    # +helo+ is the _HELO_ _domain_ that you'll dispatch mails from; see +    # the discussion in the overview notes. +    # +    # If both of +user+ and +secret+ are given, SMTP authentication +    # will be attempted using the AUTH command.  +authtype+ specifies +    # the type of authentication to attempt; it must be one of +    # :login, :plain, and :cram_md5.  See the notes on SMTP Authentication +    # in the overview. +    # +    # === Block Usage +    # +    # When this methods is called with a block, the newly-started SMTP +    # object is yielded to the block, and automatically closed after +    # the block call finishes.  Otherwise, it is the caller's +    # responsibility to close the session when finished. +    # +    # === Example +    # +    # This is very similar to the class method SMTP.start. +    # +    #     require 'net/smtp' +    #     smtp = Net::SMTP.new('smtp.mail.server', 25) +    #     smtp.start(helo_domain, account, password, authtype) do |smtp| +    #       smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] +    #     end +    # +    # The primary use of this method (as opposed to SMTP.start) +    # is probably to set debugging (#set_debug_output) or ESMTP +    # (#esmtp=), which must be done before the session is +    # started. +    # +    # === Errors +    # +    # If session has already been started, an IOError will be raised. +    # +    # This method may raise: +    # +    # * Net::SMTPAuthenticationError +    # * Net::SMTPServerBusy +    # * Net::SMTPSyntaxError +    # * Net::SMTPFatalError +    # * Net::SMTPUnknownError +    # * Net::OpenTimeout +    # * Net::ReadTimeout +    # * IOError +    # +    def start(helo = 'localhost', +              user = nil, secret = nil, authtype = nil)   # :yield: smtp +      if block_given? +        begin +          do_start helo, user, secret, authtype +          return yield(self) +        ensure +          do_finish +        end +      else +        do_start helo, user, secret, authtype +        return self +      end +    end + +    # Finishes the SMTP session and closes TCP connection. +    # Raises IOError if not started. +    def finish +      raise IOError, 'not yet started' unless started? +      do_finish +    end + +    private + +    def tcp_socket(address, port) +      TCPSocket.open address, port +    end + +    def do_start(helo_domain, user, secret, authtype) +      raise IOError, 'SMTP session already started' if @started +      if user or secret +        check_auth_method(authtype || DEFAULT_AUTH_TYPE) +        check_auth_args user, secret +      end +      s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do +        tcp_socket(@address, @port) +      end +      logging "Connection opened: #{@address}:#{@port}" +      @socket = new_internet_message_io(tls? ? tlsconnect(s) : s) +      check_response critical { recv_response() } +      do_helo helo_domain +      if starttls_always? or (capable_starttls? and starttls_auto?) +        unless capable_starttls? +          raise SMTPUnsupportedCommand, +              "STARTTLS is not supported on this server" +        end +        starttls +        @socket = new_internet_message_io(tlsconnect(s)) +        # helo response may be different after STARTTLS +        do_helo helo_domain +      end +      authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user +      @started = true +    ensure +      unless @started +        # authentication failed, cancel connection. +        s.close if s and not s.closed? +        @socket = nil +      end +    end + +    def ssl_socket(socket, context) +      OpenSSL::SSL::SSLSocket.new socket, context +    end + +    def tlsconnect(s) +      verified = false +      s = ssl_socket(s, @ssl_context) +      logging "TLS connection started" +      s.sync_close = true +      s.connect +      if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE +        s.post_connection_check(@address) +      end +      verified = true +      s +    ensure +      s.close unless verified +    end + +    def new_internet_message_io(s) +      io = InternetMessageIO.new(s) +      io.read_timeout = @read_timeout +      io.debug_output = @debug_output +      io +    end + +    def do_helo(helo_domain) +      res = @esmtp ? ehlo(helo_domain) : helo(helo_domain) +      @capabilities = res.capabilities +    rescue SMTPError +      if @esmtp +        @esmtp = false +        @error_occurred = false +        retry +      end +      raise +    end + +    def do_finish +      quit if @socket and not @socket.closed? and not @error_occurred +    ensure +      @started = false +      @error_occurred = false +      @socket.close if @socket and not @socket.closed? +      @socket = nil +    end + +    # +    # Message Sending +    # + +    public + +    # +    # Sends +msgstr+ as a message.  Single CR ("\r") and LF ("\n") found +    # in the +msgstr+, are converted into the CR LF pair.  You cannot send a +    # binary message with this method. +msgstr+ should include both +    # the message headers and body. +    # +    # +from_addr+ is a String representing the source mail address. +    # +    # +to_addr+ is a String or Strings or Array of Strings, representing +    # the destination mail address or addresses. +    # +    # === Example +    # +    #     Net::SMTP.start('smtp.example.com') do |smtp| +    #       smtp.send_message msgstr, +    #                         'from@example.com', +    #                         ['dest@example.com', 'dest2@example.com'] +    #     end +    # +    # === Errors +    # +    # This method may raise: +    # +    # * Net::SMTPServerBusy +    # * Net::SMTPSyntaxError +    # * Net::SMTPFatalError +    # * Net::SMTPUnknownError +    # * Net::ReadTimeout +    # * IOError +    # +    def send_message(msgstr, from_addr, *to_addrs) +      raise IOError, 'closed session' unless @socket +      mailfrom from_addr +      rcptto_list(to_addrs) {data msgstr} +    end + +    alias send_mail send_message +    alias sendmail send_message   # obsolete + +    # +    # Opens a message writer stream and gives it to the block. +    # The stream is valid only in the block, and has these methods: +    # +    # puts(str = '')::       outputs STR and CR LF. +    # print(str)::           outputs STR. +    # printf(fmt, *args)::   outputs sprintf(fmt,*args). +    # write(str)::           outputs STR and returns the length of written bytes. +    # <<(str)::              outputs STR and returns self. +    # +    # If a single CR ("\r") or LF ("\n") is found in the message, +    # it is converted to the CR LF pair.  You cannot send a binary +    # message with this method. +    # +    # === Parameters +    # +    # +from_addr+ is a String representing the source mail address. +    # +    # +to_addr+ is a String or Strings or Array of Strings, representing +    # the destination mail address or addresses. +    # +    # === Example +    # +    #     Net::SMTP.start('smtp.example.com', 25) do |smtp| +    #       smtp.open_message_stream('from@example.com', ['dest@example.com']) do |f| +    #         f.puts 'From: from@example.com' +    #         f.puts 'To: dest@example.com' +    #         f.puts 'Subject: test message' +    #         f.puts +    #         f.puts 'This is a test message.' +    #       end +    #     end +    # +    # === Errors +    # +    # This method may raise: +    # +    # * Net::SMTPServerBusy +    # * Net::SMTPSyntaxError +    # * Net::SMTPFatalError +    # * Net::SMTPUnknownError +    # * Net::ReadTimeout +    # * IOError +    # +    def open_message_stream(from_addr, *to_addrs, &block)   # :yield: stream +      raise IOError, 'closed session' unless @socket +      mailfrom from_addr +      rcptto_list(to_addrs) {data(&block)} +    end + +    alias ready open_message_stream   # obsolete + +    # +    # Authentication +    # + +    public + +    DEFAULT_AUTH_TYPE = :plain + +    def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE) +      check_auth_method authtype +      check_auth_args user, secret +      send auth_method(authtype), user, secret +    end + +    def auth_plain(user, secret) +      check_auth_args user, secret +      res = critical { +        get_response('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}")) +      } +      check_auth_response res +      res +    end + +    def auth_login(user, secret) +      check_auth_args user, secret +      res = critical { +        check_auth_continue get_response('AUTH LOGIN') +        check_auth_continue get_response(base64_encode(user)) +        get_response(base64_encode(secret)) +      } +      check_auth_response res +      res +    end + +    def auth_cram_md5(user, secret) +      check_auth_args user, secret +      res = critical { +        res0 = get_response('AUTH CRAM-MD5') +        check_auth_continue res0 +        crammed = cram_md5_response(secret, res0.cram_md5_challenge) +        get_response(base64_encode("#{user} #{crammed}")) +      } +      check_auth_response res +      res +    end + +    private + +    def check_auth_method(type) +      unless respond_to?(auth_method(type), true) +        raise ArgumentError, "wrong authentication type #{type}" +      end +    end + +    def auth_method(type) +      "auth_#{type.to_s.downcase}".intern +    end + +    def check_auth_args(user, secret, authtype = DEFAULT_AUTH_TYPE) +      unless user +        raise ArgumentError, 'SMTP-AUTH requested but missing user name' +      end +      unless secret +        raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase' +      end +    end + +    def base64_encode(str) +      # expects "str" may not become too long +      [str].pack('m').gsub(/\s+/, '') +    end + +    IMASK = 0x36 +    OMASK = 0x5c + +    # CRAM-MD5: [RFC2195] +    def cram_md5_response(secret, challenge) +      tmp = Digest::MD5.digest(cram_secret(secret, IMASK) + challenge) +      Digest::MD5.hexdigest(cram_secret(secret, OMASK) + tmp) +    end + +    CRAM_BUFSIZE = 64 + +    def cram_secret(secret, mask) +      secret = Digest::MD5.digest(secret) if secret.size > CRAM_BUFSIZE +      buf = secret.ljust(CRAM_BUFSIZE, "\0") +      0.upto(buf.size - 1) do |i| +        buf[i] = (buf[i].ord ^ mask).chr +      end +      buf +    end + +    # +    # SMTP command dispatcher +    # + +    public + +    # Aborts the current mail transaction + +    def rset +      getok('RSET') +    end + +    def starttls +      getok('STARTTLS') +    end + +    def helo(domain) +      getok("HELO #{domain}") +    end + +    def ehlo(domain) +      getok("EHLO #{domain}") +    end + +    def mailfrom(from_addr) +      if $SAFE > 0 +        raise SecurityError, 'tainted from_addr' if from_addr.tainted? +      end +      getok("MAIL FROM:<#{from_addr}>") +    end + +    def rcptto_list(to_addrs) +      raise ArgumentError, 'mail destination not given' if to_addrs.empty? +      ok_users = [] +      unknown_users = [] +      to_addrs.flatten.each do |addr| +        begin +          rcptto addr +        rescue SMTPAuthenticationError +          unknown_users << addr.dump +        else +          ok_users << addr +        end +      end +      raise ArgumentError, 'mail destination not given' if ok_users.empty? +      ret = yield +      unless unknown_users.empty? +        raise SMTPAuthenticationError, "failed to deliver for #{unknown_users.join(', ')}" +      end +      ret +    end + +    def rcptto(to_addr) +      if $SAFE > 0 +        raise SecurityError, 'tainted to_addr' if to_addr.tainted? +      end +      getok("RCPT TO:<#{to_addr}>") +    end + +    # This method sends a message. +    # If +msgstr+ is given, sends it as a message. +    # If block is given, yield a message writer stream. +    # You must write message before the block is closed. +    # +    #   # Example 1 (by string) +    #   smtp.data(<<EndMessage) +    #   From: john@example.com +    #   To: betty@example.com +    #   Subject: I found a bug +    # +    #   Check vm.c:58879. +    #   EndMessage +    # +    #   # Example 2 (by block) +    #   smtp.data {|f| +    #     f.puts "From: john@example.com" +    #     f.puts "To: betty@example.com" +    #     f.puts "Subject: I found a bug" +    #     f.puts "" +    #     f.puts "Check vm.c:58879." +    #   } +    # +    def data(msgstr = nil, &block)   #:yield: stream +      if msgstr and block +        raise ArgumentError, "message and block are exclusive" +      end +      unless msgstr or block +        raise ArgumentError, "message or block is required" +      end +      res = critical { +        check_continue get_response('DATA') +        socket_sync_bak = @socket.io.sync +        begin +          @socket.io.sync = false +          if msgstr +            @socket.write_message msgstr +          else +            @socket.write_message_by_block(&block) +          end +        ensure +          @socket.io.flush +          @socket.io.sync = socket_sync_bak +        end +        recv_response() +      } +      check_response res +      res +    end + +    def quit +      getok('QUIT') +    end + +    private + +    def getok(reqline) +      res = critical { +        @socket.writeline reqline +        recv_response() +      } +      check_response res +      res +    end + +    def get_response(reqline) +      @socket.writeline reqline +      recv_response() +    end + +    def recv_response +      buf = '' +      while true +        line = @socket.readline +        buf << line << "\n" +        break unless line[3,1] == '-'   # "210-PIPELINING" +      end +      Response.parse(buf) +    end + +    def critical +      return Response.parse('200 dummy reply code') if @error_occurred +      begin +        return yield() +      rescue Exception +        @error_occurred = true +        raise +      end +    end + +    def check_response(res) +      unless res.success? +        raise res.exception_class, res.message +      end +    end + +    def check_continue(res) +      unless res.continue? +        raise SMTPUnknownError, "could not get 3xx (#{res.status}: #{res.string})" +      end +    end + +    def check_auth_response(res) +      unless res.success? +        raise SMTPAuthenticationError, res.message +      end +    end + +    def check_auth_continue(res) +      unless res.continue? +        raise res.exception_class, res.message +      end +    end + +    # This class represents a response received by the SMTP server. Instances +    # of this class are created by the SMTP class; they should not be directly +    # created by the user. For more information on SMTP responses, view +    # {Section 4.2 of RFC 5321}[http://tools.ietf.org/html/rfc5321#section-4.2] +    class Response +      # Parses the received response and separates the reply code and the human +      # readable reply text +      def self.parse(str) +        new(str[0,3], str) +      end + +      # Creates a new instance of the Response class and sets the status and +      # string attributes +      def initialize(status, string) +        @status = status +        @string = string +      end + +      # The three digit reply code of the SMTP response +      attr_reader :status + +      # The human readable reply text of the SMTP response +      attr_reader :string + +      # Takes the first digit of the reply code to determine the status type +      def status_type_char +        @status[0, 1] +      end + +      # Determines whether the response received was a Positive Completion +      # reply (2xx reply code) +      def success? +        status_type_char() == '2' +      end + +      # Determines whether the response received was a Positive Intermediate +      # reply (3xx reply code) +      def continue? +        status_type_char() == '3' +      end + +      # The first line of the human readable reply text +      def message +        @string.lines.first +      end + +      # Creates a CRAM-MD5 challenge. You can view more information on CRAM-MD5 +      # on Wikipedia: http://en.wikipedia.org/wiki/CRAM-MD5 +      def cram_md5_challenge +        @string.split(/ /)[1].unpack('m')[0] +      end + +      # Returns a hash of the human readable reply text in the response if it +      # is multiple lines. It does not return the first line. The key of the +      # hash is the first word the value of the hash is an array with each word +      # thereafter being a value in the array +      def capabilities +        return {} unless @string[3, 1] == '-' +        h = {} +        @string.lines.drop(1).each do |line| +          k, *v = line[4..-1].chomp.split +          h[k] = v +        end +        h +      end + +      # Determines whether there was an error and raises the appropriate error +      # based on the reply code of the response +      def exception_class +        case @status +        when /\A4/  then SMTPServerBusy +        when /\A50/ then SMTPSyntaxError +        when /\A53/ then SMTPAuthenticationError +        when /\A5/  then SMTPFatalError +        else             SMTPUnknownError +        end +      end +    end + +    def logging(msg) +      @debug_output << msg + "\n" if @debug_output +    end + +  end   # class SMTP + +  SMTPSession = SMTP # :nodoc: + +end diff --git a/jni/ruby/lib/net/telnet.rb b/jni/ruby/lib/net/telnet.rb new file mode 100644 index 0000000..2b403cd --- /dev/null +++ b/jni/ruby/lib/net/telnet.rb @@ -0,0 +1,763 @@ +# = net/telnet.rb - Simple Telnet Client Library +# +# Author:: Wakou Aoyama <wakou@ruby-lang.org> +# Documentation:: William Webber and Wakou Aoyama +# +# This file holds the class Net::Telnet, which provides client-side +# telnet functionality. +# +# For documentation, see Net::Telnet. +# + +require "net/protocol" +require "English" + +module Net + +  # +  # == Net::Telnet +  # +  # Provides telnet client functionality. +  # +  # This class also has, through delegation, all the methods of a +  # socket object (by default, a +TCPSocket+, but can be set by the +  # +Proxy+ option to <tt>new()</tt>).  This provides methods such as +  # <tt>close()</tt> to end the session and <tt>sysread()</tt> to read +  # data directly from the host, instead of via the <tt>waitfor()</tt> +  # mechanism.  Note that if you do use <tt>sysread()</tt> directly +  # when in telnet mode, you should probably pass the output through +  # <tt>preprocess()</tt> to extract telnet command sequences. +  # +  # == Overview +  # +  # The telnet protocol allows a client to login remotely to a user +  # account on a server and execute commands via a shell.  The equivalent +  # is done by creating a Net::Telnet class with the +Host+ option +  # set to your host, calling #login() with your user and password, +  # issuing one or more #cmd() calls, and then calling #close() +  # to end the session.  The #waitfor(), #print(), #puts(), and +  # #write() methods, which #cmd() is implemented on top of, are +  # only needed if you are doing something more complicated. +  # +  # A Net::Telnet object can also be used to connect to non-telnet +  # services, such as SMTP or HTTP.  In this case, you normally +  # want to provide the +Port+ option to specify the port to +  # connect to, and set the +Telnetmode+ option to false to prevent +  # the client from attempting to interpret telnet command sequences. +  # Generally, #login() will not work with other protocols, and you +  # have to handle authentication yourself. +  # +  # For some protocols, it will be possible to specify the +Prompt+ +  # option once when you create the Telnet object and use #cmd() calls; +  # for others, you will have to specify the response sequence to +  # look for as the Match option to every #cmd() call, or call +  # #puts() and #waitfor() directly; for yet others, you will have +  # to use #sysread() instead of #waitfor() and parse server +  # responses yourself. +  # +  # It is worth noting that when you create a new Net::Telnet object, +  # you can supply a proxy IO channel via the Proxy option.  This +  # can be used to attach the Telnet object to other Telnet objects, +  # to already open sockets, or to any read-write IO object.  This +  # can be useful, for instance, for setting up a test fixture for +  # unit testing. +  # +  # == Examples +  # +  # === Log in and send a command, echoing all output to stdout +  # +  #   localhost = Net::Telnet::new("Host" => "localhost", +  #                                "Timeout" => 10, +  #                                "Prompt" => /[$%#>] \z/n) +  #   localhost.login("username", "password") { |c| print c } +  #   localhost.cmd("command") { |c| print c } +  #   localhost.close +  # +  # +  # === Check a POP server to see if you have mail +  # +  #   pop = Net::Telnet::new("Host" => "your_destination_host_here", +  #                          "Port" => 110, +  #                          "Telnetmode" => false, +  #                          "Prompt" => /^\+OK/n) +  #   pop.cmd("user " + "your_username_here") { |c| print c } +  #   pop.cmd("pass " + "your_password_here") { |c| print c } +  #   pop.cmd("list") { |c| print c } +  # +  # == References +  # +  # There are a large number of RFCs relevant to the Telnet protocol. +  # RFCs 854-861 define the base protocol.  For a complete listing +  # of relevant RFCs, see +  # http://www.omnifarious.org/~hopper/technical/telnet-rfc.html +  # +  class Telnet + +    # :stopdoc: +    IAC   = 255.chr # "\377" # "\xff" # interpret as command +    DONT  = 254.chr # "\376" # "\xfe" # you are not to use option +    DO    = 253.chr # "\375" # "\xfd" # please, you use option +    WONT  = 252.chr # "\374" # "\xfc" # I won't use option +    WILL  = 251.chr # "\373" # "\xfb" # I will use option +    SB    = 250.chr # "\372" # "\xfa" # interpret as subnegotiation +    GA    = 249.chr # "\371" # "\xf9" # you may reverse the line +    EL    = 248.chr # "\370" # "\xf8" # erase the current line +    EC    = 247.chr # "\367" # "\xf7" # erase the current character +    AYT   = 246.chr # "\366" # "\xf6" # are you there +    AO    = 245.chr # "\365" # "\xf5" # abort output--but let prog finish +    IP    = 244.chr # "\364" # "\xf4" # interrupt process--permanently +    BREAK = 243.chr # "\363" # "\xf3" # break +    DM    = 242.chr # "\362" # "\xf2" # data mark--for connect. cleaning +    NOP   = 241.chr # "\361" # "\xf1" # nop +    SE    = 240.chr # "\360" # "\xf0" # end sub negotiation +    EOR   = 239.chr # "\357" # "\xef" # end of record (transparent mode) +    ABORT = 238.chr # "\356" # "\xee" # Abort process +    SUSP  = 237.chr # "\355" # "\xed" # Suspend process +    EOF   = 236.chr # "\354" # "\xec" # End of file +    SYNCH = 242.chr # "\362" # "\xf2" # for telfunc calls + +    OPT_BINARY         =   0.chr # "\000" # "\x00" # Binary Transmission +    OPT_ECHO           =   1.chr # "\001" # "\x01" # Echo +    OPT_RCP            =   2.chr # "\002" # "\x02" # Reconnection +    OPT_SGA            =   3.chr # "\003" # "\x03" # Suppress Go Ahead +    OPT_NAMS           =   4.chr # "\004" # "\x04" # Approx Message Size Negotiation +    OPT_STATUS         =   5.chr # "\005" # "\x05" # Status +    OPT_TM             =   6.chr # "\006" # "\x06" # Timing Mark +    OPT_RCTE           =   7.chr # "\a"   # "\x07" # Remote Controlled Trans and Echo +    OPT_NAOL           =   8.chr # "\010" # "\x08" # Output Line Width +    OPT_NAOP           =   9.chr # "\t"   # "\x09" # Output Page Size +    OPT_NAOCRD         =  10.chr # "\n"   # "\x0a" # Output Carriage-Return Disposition +    OPT_NAOHTS         =  11.chr # "\v"   # "\x0b" # Output Horizontal Tab Stops +    OPT_NAOHTD         =  12.chr # "\f"   # "\x0c" # Output Horizontal Tab Disposition +    OPT_NAOFFD         =  13.chr # "\r"   # "\x0d" # Output Formfeed Disposition +    OPT_NAOVTS         =  14.chr # "\016" # "\x0e" # Output Vertical Tabstops +    OPT_NAOVTD         =  15.chr # "\017" # "\x0f" # Output Vertical Tab Disposition +    OPT_NAOLFD         =  16.chr # "\020" # "\x10" # Output Linefeed Disposition +    OPT_XASCII         =  17.chr # "\021" # "\x11" # Extended ASCII +    OPT_LOGOUT         =  18.chr # "\022" # "\x12" # Logout +    OPT_BM             =  19.chr # "\023" # "\x13" # Byte Macro +    OPT_DET            =  20.chr # "\024" # "\x14" # Data Entry Terminal +    OPT_SUPDUP         =  21.chr # "\025" # "\x15" # SUPDUP +    OPT_SUPDUPOUTPUT   =  22.chr # "\026" # "\x16" # SUPDUP Output +    OPT_SNDLOC         =  23.chr # "\027" # "\x17" # Send Location +    OPT_TTYPE          =  24.chr # "\030" # "\x18" # Terminal Type +    OPT_EOR            =  25.chr # "\031" # "\x19" # End of Record +    OPT_TUID           =  26.chr # "\032" # "\x1a" # TACACS User Identification +    OPT_OUTMRK         =  27.chr # "\e"   # "\x1b" # Output Marking +    OPT_TTYLOC         =  28.chr # "\034" # "\x1c" # Terminal Location Number +    OPT_3270REGIME     =  29.chr # "\035" # "\x1d" # Telnet 3270 Regime +    OPT_X3PAD          =  30.chr # "\036" # "\x1e" # X.3 PAD +    OPT_NAWS           =  31.chr # "\037" # "\x1f" # Negotiate About Window Size +    OPT_TSPEED         =  32.chr # " "    # "\x20" # Terminal Speed +    OPT_LFLOW          =  33.chr # "!"    # "\x21" # Remote Flow Control +    OPT_LINEMODE       =  34.chr # "\""   # "\x22" # Linemode +    OPT_XDISPLOC       =  35.chr # "#"    # "\x23" # X Display Location +    OPT_OLD_ENVIRON    =  36.chr # "$"    # "\x24" # Environment Option +    OPT_AUTHENTICATION =  37.chr # "%"    # "\x25" # Authentication Option +    OPT_ENCRYPT        =  38.chr # "&"    # "\x26" # Encryption Option +    OPT_NEW_ENVIRON    =  39.chr # "'"    # "\x27" # New Environment Option +    OPT_EXOPL          = 255.chr # "\377" # "\xff" # Extended-Options-List + +    NULL = "\000" +    CR   = "\015" +    LF   = "\012" +    EOL  = CR + LF +    REVISION = '$Id: telnet.rb 47298 2014-08-27 12:10:21Z hsbt $' +    # :startdoc: + +    # +    # Creates a new Net::Telnet object. +    # +    # Attempts to connect to the host (unless the Proxy option is +    # provided: see below).  If a block is provided, it is yielded +    # status messages on the attempt to connect to the server, of +    # the form: +    # +    #   Trying localhost... +    #   Connected to localhost. +    # +    # +options+ is a hash of options.  The following example lists +    # all options and their default values. +    # +    #   host = Net::Telnet::new( +    #            "Host"       => "localhost",  # default: "localhost" +    #            "Port"       => 23,           # default: 23 +    #            "Binmode"    => false,        # default: false +    #            "Output_log" => "output_log", # default: nil (no output) +    #            "Dump_log"   => "dump_log",   # default: nil (no output) +    #            "Prompt"     => /[$%#>] \z/n, # default: /[$%#>] \z/n +    #            "Telnetmode" => true,         # default: true +    #            "Timeout"    => 10,           # default: 10 +    #              # if ignore timeout then set "Timeout" to false. +    #            "Waittime"   => 0,            # default: 0 +    #            "Proxy"      => proxy         # default: nil +    #                            # proxy is Net::Telnet or IO object +    #          ) +    # +    # The options have the following meanings: +    # +    # Host:: the hostname or IP address of the host to connect to, as a String. +    #        Defaults to "localhost". +    # +    # Port:: the port to connect to.  Defaults to 23. +    # +    # Binmode:: if false (the default), newline substitution is performed. +    #           Outgoing LF is +    #           converted to CRLF, and incoming CRLF is converted to LF.  If +    #           true, this substitution is not performed.  This value can +    #           also be set with the #binmode() method.  The +    #           outgoing conversion only applies to the #puts() and #print() +    #           methods, not the #write() method.  The precise nature of +    #           the newline conversion is also affected by the telnet options +    #           SGA and BIN. +    # +    # Output_log:: the name of the file to write connection status messages +    #              and all received traffic to.  In the case of a proper +    #              Telnet session, this will include the client input as +    #              echoed by the host; otherwise, it only includes server +    #              responses.  Output is appended verbatim to this file. +    #              By default, no output log is kept. +    # +    # Dump_log:: as for Output_log, except that output is written in hexdump +    #            format (16 bytes per line as hex pairs, followed by their +    #            printable equivalent), with connection status messages +    #            preceded by '#', sent traffic preceded by '>', and +    #            received traffic preceded by '<'.  By default, not dump log +    #            is kept. +    # +    # Prompt:: a regular expression matching the host's command-line prompt +    #          sequence.  This is needed by the Telnet class to determine +    #          when the output from a command has finished and the host is +    #          ready to receive a new command.  By default, this regular +    #          expression is /[$%#>] \z/n. +    # +    # Telnetmode:: a boolean value, true by default.  In telnet mode, +    #              traffic received from the host is parsed for special +    #              command sequences, and these sequences are escaped +    #              in outgoing traffic sent using #puts() or #print() +    #              (but not #write()).  If you are using the Net::Telnet +    #              object to connect to a non-telnet service (such as +    #              SMTP or POP), this should be set to "false" to prevent +    #              undesired data corruption.  This value can also be set +    #              by the #telnetmode() method. +    # +    # Timeout:: the number of seconds to wait before timing out both the +    #           initial attempt to connect to host (in this constructor), +    #           which raises a Net::OpenTimeout, and all attempts to read data +    #           from the host, which raises a Net::ReadTimeout (in #waitfor(), +    #           #cmd(), and #login()).  The default value is 10 seconds. +    #           You can disable the timeout by setting this value to false. +    #           In this case, the connect attempt will eventually timeout +    #           on the underlying connect(2) socket call with an +    #           Errno::ETIMEDOUT error (but generally only after a few +    #           minutes), but other attempts to read data from the host +    #           will hang indefinitely if no data is forthcoming. +    # +    # Waittime:: the amount of time to wait after seeing what looks like a +    #            prompt (that is, received data that matches the Prompt +    #            option regular expression) to see if more data arrives. +    #            If more data does arrive in this time, Net::Telnet assumes +    #            that what it saw was not really a prompt.  This is to try to +    #            avoid false matches, but it can also lead to missing real +    #            prompts (if, for instance, a background process writes to +    #            the terminal soon after the prompt is displayed).  By +    #            default, set to 0, meaning not to wait for more data. +    # +    # Proxy:: a proxy object to used instead of opening a direct connection +    #         to the host.  Must be either another Net::Telnet object or +    #         an IO object.  If it is another Net::Telnet object, this +    #         instance will use that one's socket for communication.  If an +    #         IO object, it is used directly for communication.  Any other +    #         kind of object will cause an error to be raised. +    # +    def initialize(options) # :yield: mesg +      @options = options +      @options["Host"]       = "localhost"   unless @options.has_key?("Host") +      @options["Port"]       = 23            unless @options.has_key?("Port") +      @options["Prompt"]     = /[$%#>] \z/n  unless @options.has_key?("Prompt") +      @options["Timeout"]    = 10            unless @options.has_key?("Timeout") +      @options["Waittime"]   = 0             unless @options.has_key?("Waittime") +      unless @options.has_key?("Binmode") +        @options["Binmode"]    = false +      else +        unless (true == @options["Binmode"] or false == @options["Binmode"]) +          raise ArgumentError, "Binmode option must be true or false" +        end +      end + +      unless @options.has_key?("Telnetmode") +        @options["Telnetmode"] = true +      else +        unless (true == @options["Telnetmode"] or false == @options["Telnetmode"]) +          raise ArgumentError, "Telnetmode option must be true or false" +        end +      end + +      @telnet_option = { "SGA" => false, "BINARY" => false } + +      if @options.has_key?("Output_log") +        @log = File.open(@options["Output_log"], 'a+') +        @log.sync = true +        @log.binmode +      end + +      if @options.has_key?("Dump_log") +        @dumplog = File.open(@options["Dump_log"], 'a+') +        @dumplog.sync = true +        @dumplog.binmode +        def @dumplog.log_dump(dir, x)  # :nodoc: +          len = x.length +          addr = 0 +          offset = 0 +          while 0 < len +            if len < 16 +              line = x[offset, len] +            else +              line = x[offset, 16] +            end +            hexvals = line.unpack('H*')[0] +            hexvals += ' ' * (32 - hexvals.length) +            hexvals = format("%s %s %s %s  " * 4, *hexvals.unpack('a2' * 16)) +            line = line.gsub(/[\000-\037\177-\377]/n, '.') +            printf "%s 0x%5.5x: %s%s\n", dir, addr, hexvals, line +            addr += 16 +            offset += 16 +            len -= 16 +          end +          print "\n" +        end +      end + +      if @options.has_key?("Proxy") +        if @options["Proxy"].kind_of?(Net::Telnet) +          @sock = @options["Proxy"].sock +        elsif @options["Proxy"].kind_of?(IO) +          @sock = @options["Proxy"] +        else +          raise "Error: Proxy must be an instance of Net::Telnet or IO." +        end +      else +        message = "Trying " + @options["Host"] + "...\n" +        yield(message) if block_given? +        @log.write(message) if @options.has_key?("Output_log") +        @dumplog.log_dump('#', message) if @options.has_key?("Dump_log") + +        begin +          if @options["Timeout"] == false +            @sock = TCPSocket.open(@options["Host"], @options["Port"]) +          else +            Timeout.timeout(@options["Timeout"], Net::OpenTimeout) do +              @sock = TCPSocket.open(@options["Host"], @options["Port"]) +            end +          end +        rescue Net::OpenTimeout +          raise Net::OpenTimeout, "timed out while opening a connection to the host" +        rescue +          @log.write($ERROR_INFO.to_s + "\n") if @options.has_key?("Output_log") +          @dumplog.log_dump('#', $ERROR_INFO.to_s + "\n") if @options.has_key?("Dump_log") +          raise +        end +        @sock.sync = true +        @sock.binmode + +        message = "Connected to " + @options["Host"] + ".\n" +        yield(message) if block_given? +        @log.write(message) if @options.has_key?("Output_log") +        @dumplog.log_dump('#', message) if @options.has_key?("Dump_log") +      end + +    end # initialize + +    # The socket the Telnet object is using.  Note that this object becomes +    # a delegate of the Telnet object, so normally you invoke its methods +    # directly on the Telnet object. +    attr_reader :sock + +    # Set telnet command interpretation on (+mode+ == true) or off +    # (+mode+ == false), or return the current value (+mode+ not +    # provided).  It should be on for true telnet sessions, off if +    # using Net::Telnet to connect to a non-telnet service such +    # as SMTP. +    def telnetmode(mode = nil) +      case mode +      when nil +        @options["Telnetmode"] +      when true, false +        @options["Telnetmode"] = mode +      else +        raise ArgumentError, "argument must be true or false, or missing" +      end +    end + +    # Turn telnet command interpretation on (true) or off (false).  It +    # should be on for true telnet sessions, off if using Net::Telnet +    # to connect to a non-telnet service such as SMTP. +    def telnetmode=(mode) +      if (true == mode or false == mode) +        @options["Telnetmode"] = mode +      else +        raise ArgumentError, "argument must be true or false" +      end +    end + +    # Turn newline conversion on (+mode+ == false) or off (+mode+ == true), +    # or return the current value (+mode+ is not specified). +    def binmode(mode = nil) +      case mode +      when nil +        @options["Binmode"] +      when true, false +        @options["Binmode"] = mode +      else +        raise ArgumentError, "argument must be true or false" +      end +    end + +    # Turn newline conversion on (false) or off (true). +    def binmode=(mode) +      if (true == mode or false == mode) +        @options["Binmode"] = mode +      else +        raise ArgumentError, "argument must be true or false" +      end +    end + +    # Preprocess received data from the host. +    # +    # Performs newline conversion and detects telnet command sequences. +    # Called automatically by #waitfor().  You should only use this +    # method yourself if you have read input directly using sysread() +    # or similar, and even then only if in telnet mode. +    def preprocess(string) +      # combine CR+NULL into CR +      string = string.gsub(/#{CR}#{NULL}/no, CR) if @options["Telnetmode"] + +      # combine EOL into "\n" +      string = string.gsub(/#{EOL}/no, "\n") unless @options["Binmode"] + +      # remove NULL +      string = string.gsub(/#{NULL}/no, '') unless @options["Binmode"] + +      string.gsub(/#{IAC}( +                   [#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]| +                   [#{DO}#{DONT}#{WILL}#{WONT}] +                     [#{OPT_BINARY}-#{OPT_NEW_ENVIRON}#{OPT_EXOPL}]| +                   #{SB}[^#{IAC}]*#{IAC}#{SE} +                 )/xno) do +        if    IAC == $1  # handle escaped IAC characters +          IAC +        elsif AYT == $1  # respond to "IAC AYT" (are you there) +          self.write("nobody here but us pigeons" + EOL) +          '' +        elsif DO[0] == $1[0]  # respond to "IAC DO x" +          if OPT_BINARY[0] == $1[1] +            @telnet_option["BINARY"] = true +            self.write(IAC + WILL + OPT_BINARY) +          else +            self.write(IAC + WONT + $1[1..1]) +          end +          '' +        elsif DONT[0] == $1[0]  # respond to "IAC DON'T x" with "IAC WON'T x" +          self.write(IAC + WONT + $1[1..1]) +          '' +        elsif WILL[0] == $1[0]  # respond to "IAC WILL x" +          if    OPT_BINARY[0] == $1[1] +            self.write(IAC + DO + OPT_BINARY) +          elsif OPT_ECHO[0] == $1[1] +            self.write(IAC + DO + OPT_ECHO) +          elsif OPT_SGA[0]  == $1[1] +            @telnet_option["SGA"] = true +            self.write(IAC + DO + OPT_SGA) +          else +            self.write(IAC + DONT + $1[1..1]) +          end +          '' +        elsif WONT[0] == $1[0]  # respond to "IAC WON'T x" +          if    OPT_ECHO[0] == $1[1] +            self.write(IAC + DONT + OPT_ECHO) +          elsif OPT_SGA[0]  == $1[1] +            @telnet_option["SGA"] = false +            self.write(IAC + DONT + OPT_SGA) +          else +            self.write(IAC + DONT + $1[1..1]) +          end +          '' +        else +          '' +        end +      end +    end # preprocess + +    # Read data from the host until a certain sequence is matched. +    # +    # If a block is given, the received data will be yielded as it +    # is read in (not necessarily all in one go), or nil if EOF +    # occurs before any data is received.  Whether a block is given +    # or not, all data read will be returned in a single string, or again +    # nil if EOF occurs before any data is received.  Note that +    # received data includes the matched sequence we were looking for. +    # +    # +options+ can be either a regular expression or a hash of options. +    # If a regular expression, this specifies the data to wait for. +    # If a hash, this can specify the following options: +    # +    # Match:: a regular expression, specifying the data to wait for. +    # Prompt:: as for Match; used only if Match is not specified. +    # String:: as for Match, except a string that will be converted +    #          into a regular expression.  Used only if Match and +    #          Prompt are not specified. +    # Timeout:: the number of seconds to wait for data from the host +    #           before raising a Timeout::Error.  If set to false, +    #           no timeout will occur.  If not specified, the +    #           Timeout option value specified when this instance +    #           was created will be used, or, failing that, the +    #           default value of 10 seconds. +    # Waittime:: the number of seconds to wait after matching against +    #            the input data to see if more data arrives.  If more +    #            data arrives within this time, we will judge ourselves +    #            not to have matched successfully, and will continue +    #            trying to match.  If not specified, the Waittime option +    #            value specified when this instance was created will be +    #            used, or, failing that, the default value of 0 seconds, +    #            which means not to wait for more input. +    # FailEOF:: if true, when the remote end closes the connection then an +    #           EOFError will be raised. Otherwise, defaults to the old +    #           behaviour that the function will return whatever data +    #           has been received already, or nil if nothing was received. +    # +    def waitfor(options) # :yield: recvdata +      time_out = @options["Timeout"] +      waittime = @options["Waittime"] +      fail_eof = @options["FailEOF"] + +      if options.kind_of?(Hash) +        prompt   = if options.has_key?("Match") +                     options["Match"] +                   elsif options.has_key?("Prompt") +                     options["Prompt"] +                   elsif options.has_key?("String") +                     Regexp.new( Regexp.quote(options["String"]) ) +                   end +        time_out = options["Timeout"]  if options.has_key?("Timeout") +        waittime = options["Waittime"] if options.has_key?("Waittime") +        fail_eof = options["FailEOF"]  if options.has_key?("FailEOF") +      else +        prompt = options +      end + +      if time_out == false +        time_out = nil +      end + +      line = '' +      buf = '' +      rest = '' +      until(prompt === line and not IO::select([@sock], nil, nil, waittime)) +        unless IO::select([@sock], nil, nil, time_out) +          raise Net::ReadTimeout, "timed out while waiting for more data" +        end +        begin +          c = @sock.readpartial(1024 * 1024) +          @dumplog.log_dump('<', c) if @options.has_key?("Dump_log") +          if @options["Telnetmode"] +            c = rest + c +            if Integer(c.rindex(/#{IAC}#{SE}/no) || 0) < +               Integer(c.rindex(/#{IAC}#{SB}/no) || 0) +              buf = preprocess(c[0 ... c.rindex(/#{IAC}#{SB}/no)]) +              rest = c[c.rindex(/#{IAC}#{SB}/no) .. -1] +            elsif pt = c.rindex(/#{IAC}[^#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]?\z/no) || +                       c.rindex(/\r\z/no) +              buf = preprocess(c[0 ... pt]) +              rest = c[pt .. -1] +            else +              buf = preprocess(c) +              rest = '' +            end +         else +           # Not Telnetmode. +           # +           # We cannot use preprocess() on this data, because that +           # method makes some Telnetmode-specific assumptions. +           buf = rest + c +           rest = '' +           unless @options["Binmode"] +             if pt = buf.rindex(/\r\z/no) +               buf = buf[0 ... pt] +               rest = buf[pt .. -1] +             end +             buf.gsub!(/#{EOL}/no, "\n") +           end +          end +          @log.print(buf) if @options.has_key?("Output_log") +          line += buf +          yield buf if block_given? +        rescue EOFError # End of file reached +          raise if fail_eof +          if line == '' +            line = nil +            yield nil if block_given? +          end +          break +        end +      end +      line +    end + +    # Write +string+ to the host. +    # +    # Does not perform any conversions on +string+.  Will log +string+ to the +    # dumplog, if the Dump_log option is set. +    def write(string) +      length = string.length +      while 0 < length +        IO::select(nil, [@sock]) +        @dumplog.log_dump('>', string[-length..-1]) if @options.has_key?("Dump_log") +        length -= @sock.syswrite(string[-length..-1]) +      end +    end + +    # Sends a string to the host. +    # +    # This does _not_ automatically append a newline to the string.  Embedded +    # newlines may be converted and telnet command sequences escaped +    # depending upon the values of telnetmode, binmode, and telnet options +    # set by the host. +    def print(string) +      string = string.gsub(/#{IAC}/no, IAC + IAC) if @options["Telnetmode"] + +      if @options["Binmode"] +        self.write(string) +      else +        if @telnet_option["BINARY"] and @telnet_option["SGA"] +          # IAC WILL SGA IAC DO BIN send EOL --> CR +          self.write(string.gsub(/\n/n, CR)) +        elsif @telnet_option["SGA"] +          # IAC WILL SGA send EOL --> CR+NULL +          self.write(string.gsub(/\n/n, CR + NULL)) +        else +          # NONE send EOL --> CR+LF +          self.write(string.gsub(/\n/n, EOL)) +        end +      end +    end + +    # Sends a string to the host. +    # +    # Same as #print(), but appends a newline to the string. +    def puts(string) +      self.print(string + "\n") +    end + +    # Send a command to the host. +    # +    # More exactly, sends a string to the host, and reads in all received +    # data until is sees the prompt or other matched sequence. +    # +    # If a block is given, the received data will be yielded to it as +    # it is read in.  Whether a block is given or not, the received data +    # will be return as a string.  Note that the received data includes +    # the prompt and in most cases the host's echo of our command. +    # +    # +options+ is either a String, specified the string or command to +    # send to the host; or it is a hash of options.  If a hash, the +    # following options can be specified: +    # +    # String:: the command or other string to send to the host. +    # Match:: a regular expression, the sequence to look for in +    #         the received data before returning.  If not specified, +    #         the Prompt option value specified when this instance +    #         was created will be used, or, failing that, the default +    #         prompt of /[$%#>] \z/n. +    # Timeout:: the seconds to wait for data from the host before raising +    #           a Timeout error.  If not specified, the Timeout option +    #           value specified when this instance was created will be +    #           used, or, failing that, the default value of 10 seconds. +    # +    # The command or other string will have the newline sequence appended +    # to it. +    def cmd(options) # :yield: recvdata +      match    = @options["Prompt"] +      time_out = @options["Timeout"] +      fail_eof = @options["FailEOF"] + +      if options.kind_of?(Hash) +        string   = options["String"] +        match    = options["Match"]   if options.has_key?("Match") +        time_out = options["Timeout"] if options.has_key?("Timeout") +        fail_eof = options["FailEOF"] if options.has_key?("FailEOF") +      else +        string = options +      end + +      self.puts(string) +      if block_given? +        waitfor({"Prompt" => match, "Timeout" => time_out, "FailEOF" => fail_eof}){|c| yield c } +      else +        waitfor({"Prompt" => match, "Timeout" => time_out, "FailEOF" => fail_eof}) +      end +    end + +    # Login to the host with a given username and password. +    # +    # The username and password can either be provided as two string +    # arguments in that order, or as a hash with keys "Name" and +    # "Password". +    # +    # This method looks for the strings "login" and "Password" from the +    # host to determine when to send the username and password.  If the +    # login sequence does not follow this pattern (for instance, you +    # are connecting to a service other than telnet), you will need +    # to handle login yourself. +    # +    # The password can be omitted, either by only +    # provided one String argument, which will be used as the username, +    # or by providing a has that has no "Password" key.  In this case, +    # the method will not look for the "Password:" prompt; if it is +    # sent, it will have to be dealt with by later calls. +    # +    # The method returns all data received during the login process from +    # the host, including the echoed username but not the password (which +    # the host should not echo).  If a block is passed in, this received +    # data is also yielded to the block as it is received. +    def login(options, password = nil) # :yield: recvdata +      login_prompt = /[Ll]ogin[: ]*\z/n +      password_prompt = /[Pp]ass(?:word|phrase)[: ]*\z/n +      if options.kind_of?(Hash) +        username = options["Name"] +        password = options["Password"] +        login_prompt = options["LoginPrompt"] if options["LoginPrompt"] +        password_prompt = options["PasswordPrompt"] if options["PasswordPrompt"] +      else +        username = options +      end + +      if block_given? +        line = waitfor(login_prompt){|c| yield c } +        if password +          line += cmd({"String" => username, +                       "Match" => password_prompt}){|c| yield c } +          line += cmd(password){|c| yield c } +        else +          line += cmd(username){|c| yield c } +        end +      else +        line = waitfor(login_prompt) +        if password +          line += cmd({"String" => username, +                       "Match" => password_prompt}) +          line += cmd(password) +        else +          line += cmd(username) +        end +      end +      line +    end + +    # Closes the connection +    def close +      @sock.close +    end + +  end  # class Telnet +end  # module Net + | 
