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 + |