Warning: file_get_contents(https://raw.githubusercontent.com/Den1xxx/Filemanager/master/languages/ru.json): failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found
in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 88
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 215
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 216
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 217
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 218
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 219
Warning: Cannot modify header information - headers already sent by (output started at /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php:88) in /home/afelisqd/cppseducation.sc.tz/admin/images/photos/17587263121019776732_admin-dbb.php on line 220
PK ! C~ ~ telnet.rbnu [ # = net/telnet.rb - Simple Telnet Client Library
#
# Author:: Wakou Aoyama
# 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 "socket"
require "delegate"
require "timeout"
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 new()). This provides methods such as
# close() to end the session and sysread() to read
# data directly from the host, instead of via the waitfor()
# mechanism. Note that if you do use sysread() directly
# when in telnet mode, you should probably pass the output through
# preprocess() 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 < SimpleDelegator
# :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 16458 2008-05-18 15:02:36Z knu $'
# :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),
# and all attempts to read data from the host (in #waitfor(),
# #cmd(), and #login()). Exceeding this timeout causes a
# TimeoutError to be raised. 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 hand 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(@options["Timeout"]) do
@sock = TCPSocket.open(@options["Host"], @options["Port"])
end
end
rescue TimeoutError
raise TimeoutError, "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
super(@sock)
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 :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"]
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 TimeoutError. 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 TimeoutError, "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)) <
Integer(c.rindex(/#{IAC}#{SB}/no))
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"]
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")
else
string = options
end
self.puts(string)
if block_given?
waitfor({"Prompt" => match, "Timeout" => time_out}){|c| yield c }
else
waitfor({"Prompt" => match, "Timeout" => time_out})
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
end # class Telnet
end # module Net
PK ! D=* * protocol.rbnu [ # frozen_string_literal: true
#
# = net/protocol.rb
#
#--
# Copyright (c) 1999-2004 Yukihiro Matsumoto
# Copyright (c) 1999-2004 Minero Aoki
#
# written and maintained by 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 or GNU General Public License.
#
# $Id$
#++
#
# WARNING: This file is going to remove.
# Do not rely on the implementation written in this file.
#
require 'socket'
require 'timeout'
require 'io/wait'
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
def ssl_socket_connect(s, timeout)
if timeout
while true
raise Net::OpenTimeout if timeout <= 0
start = Process.clock_gettime Process::CLOCK_MONOTONIC
# to_io is required because SSLSocket doesn't have wait_readable yet
case s.connect_nonblock(exception: false)
when :wait_readable; s.to_io.wait_readable(timeout)
when :wait_writable; s.to_io.wait_writable(timeout)
else; break
end
timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
end
else
s.connect
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
def initialize(io = nil)
@io = io
end
attr_reader :io
def message
msg = super
if @io
msg = "#{msg} with #{@io.inspect}"
end
msg
end
end
##
# WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the
# response cannot be written within the write_timeout. Not raised on Windows.
class WriteTimeout < Timeout::Error
def initialize(io = nil)
@io = io
end
attr_reader :io
def message
msg = super
if @io
msg = "#{msg} with #{@io.inspect}"
end
msg
end
end
class BufferedIO #:nodoc: internal use only
def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil)
@io = io
@read_timeout = read_timeout
@write_timeout = write_timeout
@continue_timeout = continue_timeout
@debug_output = debug_output
@rbuf = ''.b
end
attr_reader :io
attr_accessor :read_timeout
attr_accessor :write_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 = ''.b, ignore_eof = false)
LOG "reading #{len} bytes..."
read_bytes = 0
begin
while read_bytes + @rbuf.size < len
s = rbuf_consume(@rbuf.size)
read_bytes += s.size
dest << s
rbuf_fill
end
s = rbuf_consume(len - read_bytes)
read_bytes += s.size
dest << s
rescue EOFError
raise unless ignore_eof
end
LOG "read #{read_bytes} bytes"
dest
end
def read_all(dest = ''.b)
LOG 'reading all...'
read_bytes = 0
begin
while true
s = rbuf_consume(@rbuf.size)
read_bytes += s.size
dest << s
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
tmp = @rbuf.empty? ? @rbuf : nil
case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false)
when String
return if rv.equal?(tmp)
@rbuf << rv
rv.clear
return
when :wait_readable
(io = @io.to_io).wait_readable(@read_timeout) or raise Net::ReadTimeout.new(io)
# continue looping
when :wait_writable
# OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
# http://www.openssl.org/support/faq.html#PROG10
(io = @io.to_io).wait_writable(@read_timeout) or raise Net::ReadTimeout.new(io)
# continue looping
when nil
raise EOFError, 'end of file reached'
end while true
end
def rbuf_consume(len)
if len == @rbuf.size
s = @rbuf
@rbuf = ''.b
else
s = @rbuf.slice!(0, len)
end
@debug_output << %Q[-> #{s.dump}\n] if @debug_output
s
end
#
# Write
#
public
def write(*strs)
writing {
write0(*strs)
}
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(*strs)
@debug_output << strs.map(&:dump).join if @debug_output
orig_written_bytes = @written_bytes
strs.each_with_index do |str, i|
need_retry = true
case len = @io.write_nonblock(str, exception: false)
when Integer
@written_bytes += len
len -= str.bytesize
if len == 0
if strs.size == i+1
return @written_bytes - orig_written_bytes
else
need_retry = false
# next string
end
elsif len < 0
str = str.byteslice(len, -len)
else # len > 0
need_retry = false
# next string
end
# continue looping
when :wait_writable
(io = @io.to_io).wait_writable(@write_timeout) or raise Net::WriteTimeout.new(io)
# continue looping
end while need_retry
end
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(*, **)
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.delete_prefix('.')
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 = ''.b
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
PK ! dܱ https.rbnu [ # frozen_string_literal: false
=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
All rights reserved.
== Licence
This program is licensed under the same licence as Ruby.
(See the file 'LICENCE'.)
=end
require_relative 'http'
require 'openssl'
PK ! RKvij ij pop.rbnu [ # frozen_string_literal: true
# = net/pop.rb
#
# Copyright (c) 1999-2007 Yukihiro Matsumoto.
#
# Copyright (c) 1999-2007 Minero Aoki.
#
# Written & maintained by Minero Aoki .
#
# 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://docs.ruby-lang.org/ja/latest/library/net=2fpop.html
#
# $Id$
#
# 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($isapop).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$.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.hostname = @address
s.sync_close = true
ssl_socket_connect(s, @open_timeout)
if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
s.post_connection_check(@address)
end
end
@socket = InternetMessageIO.new(s,
read_timeout: @read_timeout,
debug_output: @debug_output)
logging "POP session started: #{@address}:#{@port} (#{@apop ? 'APOP' : 'POP'})"
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
@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
@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
PK ! l http.rbnu [ # frozen_string_literal: false
#
# = 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 .
# HTTPS support added by GOTOU Yuuzou .
#
# 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_relative 'protocol'
require 'uri'
autoload :OpenSSL, 'openssl'
module Net #:nodoc:
# :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 an 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.
#
# If you wish to re-use a connection across multiple HTTP requests without
# automatically closing it you can use ::new and then call #start and
# #finish manually.
#
# The request types Net::HTTP supports are listed below in the section "HTTP
# Request Classes".
#
# 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 URL encoded 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
#
# To send multipart/form-data use Net::HTTPHeader#set_form:
#
# req = Net::HTTP::Post.new(uri)
# req.set_form([['upload', File.open('foo.bar')]], '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 => true) do |http|
# request = Net::HTTP::Get.new uri
# response = http.request request # Net::HTTPResponse object
# end
#
# Or if you simply want to make a GET request, you may pass in an URI
# object that has an HTTPS URL. Net::HTTP automatically turns on TLS
# verification if the URI object has a 'https' URI scheme.
#
# uri = URI('https://example.com/')
# Net::HTTP.get(uri) # => String
#
# 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
# HTTPUnavailableForLegalReasons:: 451
# 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$.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 data to the specified URI object.
#
# Example:
#
# require 'net/http'
# require 'uri'
#
# Net::HTTP.post URI('http://www.example.com/api/search'),
# { "q" => "ruby", "max" => "50" }.to_json,
# "Content-Type" => "application/json"
#
def HTTP.post(url, data, header = nil)
start(url.hostname, url.port,
:use_ssl => url.scheme == 'https' ) {|http|
http.post(url, data, header)
}
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=:ENV, 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 ipaddr, ca_file, ca_path, cert, cert_store, ciphers,
# close_on_empty_response, key, open_timeout, read_timeout, write_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
p_addr = :ENV if arg.size < 2
port = https_default_port if !port && opt && opt[:use_ssl]
http = new(address, port, p_addr, p_port, p_user, p_pass)
http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr]
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+ specifies the DNS name
# or IP address of the proxy host, +p_port+ the port to use to access the
# proxy, +p_user+ and +p_pass+ the username and password if authorization
# is required to use the proxy, and p_no_proxy hosts which do not
# use the proxy.
#
def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = 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
if p_addr && p_no_proxy && !URI::Generic.use_proxy?(p_addr, p_addr, p_port, p_no_proxy)
p_addr = nil
p_port = nil
end
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)
@ipaddr = nil
@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 = 60
@read_timeout = 60
@write_timeout = 60
@continue_timeout = nil
@max_retries = 1
@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
@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', uplevel: 1 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
# The IP address to connect to/used to connect to
def ipaddr
started? ? @socket.io.peeraddr[3] : @ipaddr
end
# Set the IP address to connect to
def ipaddr=(addr)
raise IOError, "ipaddr value changed, but session already started" if started?
@ipaddr = addr
end
# 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 60 seconds.
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
# Number of seconds to wait for one block to be written (via one write(2)
# call). Any number may be used, including Floats for fractional
# seconds. If the HTTP object cannot write data in this many seconds,
# it raises a Net::WriteTimeout exception. The default value is 60 seconds.
# Net::WriteTimeout is not raised on Windows.
attr_reader :write_timeout
# Maximum number of times to retry an idempotent request in case of
# Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
# Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError,
# Timeout::Error.
# Should be a non-negative integer number. Zero means no retries.
# The default value is 1.
def max_retries=(retries)
retries = retries.to_int
if retries < 0
raise ArgumentError, 'max_retries should be non-negative integer number'
end
@max_retries = retries
end
attr_reader :max_retries
# Setter for the read_timeout attribute.
def read_timeout=(sec)
@socket.read_timeout = sec if @socket
@read_timeout = sec
end
# Setter for the write_timeout attribute.
def write_timeout=(sec)
@socket.write_timeout = sec if @socket
@write_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,
:@min_version,
:@max_version,
:@verify_callback,
:@verify_depth,
:@verify_mode,
]
SSL_ATTRIBUTES = [
:ca_file,
:ca_path,
:cert,
:cert_store,
:ciphers,
:key,
:ssl_timeout,
:ssl_version,
:min_version,
:max_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 minimum SSL version. See OpenSSL::SSL::SSLContext#min_version=
attr_accessor :min_version
# Sets the maximum SSL version. See OpenSSL::SSL::SSLContext#max_version=
attr_accessor :max_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_addr = proxy_address
conn_port = proxy_port
else
conn_addr = conn_address
conn_port = port
end
D "opening connection to #{conn_addr}:#{conn_port}..."
s = Timeout.timeout(@open_timeout, Net::OpenTimeout) {
begin
TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
rescue => e
raise e, "Failed to open TCP connection to " +
"#{conn_addr}:#{conn_port} (#{e.message})"
end
}
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
D "opened"
if use_ssl?
if proxy?
plain_sock = BufferedIO.new(s, read_timeout: @read_timeout,
write_timeout: @write_timeout,
continue_timeout: @continue_timeout,
debug_output: @debug_output)
buf = "CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n"
buf << "Host: #{@address}:#{@port}\r\n"
if proxy_user
credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
buf << "Proxy-Authorization: Basic #{credential}\r\n"
end
buf << "\r\n"
plain_sock.write(buf)
HTTPResponse.read_new(plain_sock).value
# assuming nothing left in buffers after successful CONNECT response
end
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)
@ssl_context.session_cache_mode =
OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
@ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
D "starting SSL for #{conn_addr}:#{conn_port}..."
s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
s.sync_close = true
# Server Name Indication (SNI) RFC 3546
s.hostname = @address if s.respond_to? :hostname=
if @ssl_session and
Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
s.session = @ssl_session
end
ssl_socket_connect(s, @open_timeout)
if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
s.post_connection_check(@address)
end
D "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
end
@socket = BufferedIO.new(s, read_timeout: @read_timeout,
write_timeout: @write_timeout,
continue_timeout: @continue_timeout,
debug_output: @debug_output)
on_connect
rescue => exception
if s
D "Conn close because of connect error #{exception}"
s.close
end
raise
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
@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?
!!(@proxy_from_env ? proxy_uri : @proxy_address)
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:
return if @proxy_uri == false
@proxy_uri ||= URI::HTTP.new(
"http".freeze, nil, address, port, nil, nil, nil, nil, nil
).find_proxy || false
@proxy_uri || nil
end
# The address of the proxy server, if one is configured.
def proxy_address
if @proxy_from_env then
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&.port
else
@proxy_port
end
end
# [Bug #12921]
if /linux|freebsd|darwin/ =~ RUBY_PLATFORM
ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = true
else
ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = false
end
# The username of the proxy server, if one is configured.
def proxy_user
if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
proxy_uri&.user
else
@proxy_user
end
end
# The password of the proxy server, if one is configured.
def proxy_pass
if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
proxy_uri&.password
else
@proxy_pass
end
end
alias proxyaddr proxy_address #:nodoc: obsolete
alias proxyport proxy_port #:nodoc: obsolete
private
# without proxy, obsolete
def conn_address # :nodoc:
@ipaddr || address()
end
def conn_port # :nodoc:
port()
end
def edit_path(path)
if proxy?
if path.start_with?("ftp://") || use_ssl?
path
else
"http://#{addr_port}#{path}"
end
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) {
begin
req.exec @socket, @curr_http_version, edit_path(req.path)
rescue Errno::EPIPE
# Failure when writing full request, but we can probably
# still read the received response.
end
begin
res = HTTPResponse.read_new(@socket)
res.decode_content = req.decode_content
end while res.kind_of?(HTTPInformation)
res.uri = req.uri
res
}
res.reading_body(@socket, req.response_body_permitted?) {
yield res if block_given?
}
rescue Net::OpenTimeout
raise
rescue Net::ReadTimeout, IOError, EOFError,
Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT,
# avoid a dependency on OpenSSL
defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
Timeout::Error => exception
if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method)
count += 1
@socket.close if @socket
D "Conn close because of error #{exception}, and retry"
retry
end
D "Conn close because of error #{exception}"
@socket.close if @socket
raise
end
end_transport req, res
res
rescue => exception
D "Conn close because of error #{exception}"
@socket.close if @socket
raise exception
end
def begin_transport(req)
if @socket.closed?
connect
elsif @last_communicated
if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
D 'Conn close because of keep_alive_timeout'
@socket.close
connect
elsif @socket.io.to_io.wait_readable(0) && @socket.eof?
D "Conn close because of EOF"
@socket.close
connect
end
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 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
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
addr = address
addr = "[#{addr}]" if addr.include?(":")
default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port
default_port == port ? addr : "#{addr}:#{port}"
end
def D(msg)
return unless @debug_output
@debug_output << msg
@debug_output << "\n"
end
end
end
require_relative 'http/exceptions'
require_relative 'http/header'
require_relative 'http/generic_request'
require_relative 'http/request'
require_relative 'http/requests'
require_relative 'http/response'
require_relative 'http/responses'
require_relative 'http/proxy_delta'
require_relative 'http/backward'
PK ! R_ _ imap.rbnu [ # frozen_string_literal: true
#
# = net/imap.rb
#
# Copyright (C) 2000 Shugo Maeda
#
# 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"
require_relative 'protocol'
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 < Protocol
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
# Seconds to wait until a connection is opened.
# If the IMAP object cannot open a connection within this time,
# it raises a Net::OpenTimeout exception. The default value is 30 seconds.
attr_reader :open_timeout
# 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
return if disconnected?
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
@sock.close
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("m0")
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/%")
# #=> [#, \\
# #, \\
# #]
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/%")
# #=> [#, \\
# #, \\
# #]
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", <:: 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 :: messages with an internal date strictly before
# . The date argument has a format similar
# to 8-Aug-2002.
#
# BODY :: messages that contain within their body.
#
# CC :: messages containing in their CC field.
#
# FROM :: messages that contain in their FROM field.
#
# NEW:: messages with the \Recent, but not the \Seen, flag set.
#
# NOT :: negate the following search key.
#
# OR :: "or" two search keys together.
#
# ON :: messages with an internal date exactly equal to ,
# which has a format similar to 8-Aug-2002.
#
# SINCE :: messages with an internal date on or after .
#
# SUBJECT :: messages with in their subject.
#
# TO :: messages with 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 represents 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")
# #=> [#98}>, \\
# #99}>, \\
# #100}>]
# p imap.fetch(6, "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, mod = nil)
return fetch_internal("FETCH", set, attr, mod)
end
# Similar to #fetch(), but +set+ contains unique identifiers.
def uid_fetch(set, attr, mod = nil)
return fetch_internal("UID FETCH", set, attr, mod)
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])
# #=> [#[:Seen, :Deleted]}>, \\
# #[:Seen, :Deleted]}>, \\
# #[: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 MOVE command to move 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.
# The IMAP MOVE extension is described in [RFC-6851].
def move(set, mailbox)
copy_internal("MOVE", set, mailbox)
end
# Similar to #move(), but +set+ contains unique identifiers.
def uid_move(set, mailbox)
copy_internal("UID MOVE", 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 = nil, &block)
raise ArgumentError, "two Procs are passed" if handler && block
@response_handlers.push(block || 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.
#
# If +timeout+ is given, this method returns after +timeout+ seconds passed.
# +timeout+ can be used for keep-alive. For example, the following code
# checks the connection for each 60 seconds.
#
# loop do
# imap.idle(60) do |res|
# ...
# end
# end
def idle(timeout = nil, &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(timeout)
@idle_done_cond = nil
if @receiver_thread_terminating
raise @exception || Net::IMAP::Error.new("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(",", "/") + "===").unpack1("m").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("m0")
"&" + base64.delete("=").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.
# open_timeout:: Seconds to wait until a connection is opened
#
# 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
@open_timeout = options[:open_timeout] || 30
@parser = ResponseParser.new
@sock = tcp_socket(@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
@continued_command_tag = nil
@continuation_request_arrival = new_cond
@continuation_request_exception = nil
@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 tcp_socket(host, port)
s = Socket.tcp(host, port, :connect_timeout => @open_timeout)
s.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, true)
s
rescue Errno::ETIMEDOUT
raise Net::OpenTimeout, "Timeout to open TCP connection to " +
"#{host}:#{port} (exceeds #{@open_timeout} seconds)"
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
case resp.tag
when @logout_command_tag
return
when @continued_command_tag
@continuation_request_exception =
RESPONSE_ERRORS[resp.name].new(resp)
@continuation_request_arrival.signal
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(?:OK)\z/ni
return resp
when /\A(?:NO)\z/ni
raise NoResponseError, resp
when /\A(?:BAD)\z/ni
raise BadResponseError, resp
else
raise UnknownResponseError, resp
end
end
def get_response
buff = String.new
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, tag)
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
if data[0] == 'CHANGEDSINCE'
NumValidator.ensure_mod_sequence_value(data[1])
else
data.each do |i|
validate_data(i)
end
end
when Time
when Symbol
else
data.validate
end
end
def send_data(data, tag = nil)
case data
when nil
put_string("NIL")
when String
send_string_data(data, tag)
when Integer
send_number_data(data)
when Array
send_list_data(data, tag)
when Time
send_time_data(data)
when Symbol
send_symbol_data(data)
else
data.send_data(self, tag)
end
end
def send_string_data(str, tag = nil)
case str
when ""
put_string('""')
when /[\x80-\xff\r\n]/n
# literal
send_literal(str, tag)
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, tag = nil)
synchronize do
put_string("{" + str.bytesize.to_s + "}" + CRLF)
@continued_command_tag = tag
@continuation_request_exception = nil
begin
@continuation_request_arrival.wait
e = @continuation_request_exception || @exception
raise e if e
put_string(str)
ensure
@continued_command_tag = nil
@continuation_request_exception = nil
end
end
end
def send_number_data(num)
put_string(num.to_s)
end
def send_list_data(list, tag = nil)
put_string("(")
first = true
list.each do |i|
if first
first = false
else
put_string(" ")
end
send_data(i, tag)
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, mod = nil)
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")
if mod
send_command(cmd, MessageSet.new(set), attr, mod)
else
send_command(cmd, MessageSet.new(set), attr)
end
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.hostname = @host if @sock.respond_to? :hostname=
ssl_socket_connect(@sock, @open_timeout)
if context.verify_mode != VERIFY_NONE
@sock.post_connection_check(@host)
end
end
class RawData # :nodoc:
def send_data(imap, tag)
imap.send(:put_string, @data)
end
def validate
end
private
def initialize(data)
@data = data
end
end
class Atom # :nodoc:
def send_data(imap, tag)
imap.send(:put_string, @data)
end
def validate
end
private
def initialize(data)
@data = data
end
end
class QuotedString # :nodoc:
def send_data(imap, tag)
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, tag)
imap.send(:send_literal, @data, tag)
end
def validate
end
private
def initialize(data)
@data = data
end
end
class MessageSet # :nodoc:
def send_data(imap, tag)
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
# Check is passed argument valid 'mod_sequence_value' in RFC 4551 terminology
def valid_mod_sequence_value?(num)
# mod-sequence-value = 1*DIGIT
# ; Positive unsigned 64-bit integer
# ; (mod-sequence)
# ; (1 <= n < 18,446,744,073,709,551,615)
num >= 1 && num < 18446744073709551615
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
# Ensure argument is 'mod_sequence_value' or raise DataFormatError
def ensure_mod_sequence_value(num)
return if valid_mod_sequence_value?(num)
msg = "mod_sequence_value must be unsigned 64-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 (()) 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*
#
# 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 (()).
#
# 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 (()).
#
# 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*]
#
# ==== 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[]<>]
# 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
warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
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
warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
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
warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
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
warn("media_subtype is obsolete, use subtype instead.\n", uplevel: 1)
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
while lookahead.symbol == T_SPACE
# Ignore trailing space for Microsoft Exchange Server
shift_token
end
match(T_CRLF)
match(T_EOF)
return result
end
def continue_req
match(T_PLUS)
token = lookahead
if token.symbol == T_SPACE
shift_token
return ContinuationRequest.new(resp_text, @str)
else
return ContinuationRequest.new(ResponseText.new(nil, ""), @str)
end
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
when /\A(?:MODSEQ)\z/ni
name, val = modseq_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 = String.new
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 modseq_data
token = match(T_ATOM)
name = token.value.upcase
match(T_SPACE)
match(T_LPAR)
modseq = number
match(T_RPAR)
return name, modseq
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.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 = String.new
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 upon an unknown response from the server.
class UnknownResponseError < ResponseError
end
RESPONSE_ERRORS = Hash.new(ResponseError)
RESPONSE_ERRORS["NO"] = NoResponseError
RESPONSE_ERRORS["BAD"] = BadResponseError
# Error raised when too many flags are interned to symbols.
class FlagCountError < Error
end
end
end
PK ! tBK K ftp.rbnu [ # frozen_string_literal: true
#
# = net/ftp.rb - FTP Client Library
#
# Written by Shugo Maeda .
#
# 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_relative "protocol"
require "time"
begin
require "openssl"
rescue LoadError
end
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 < Protocol
include MonitorMixin
if defined?(OpenSSL::SSL)
include OpenSSL
include SSL
end
# :stopdoc:
FTP_PORT = 21
CRLF = "\r\n"
DEFAULT_BLOCKSIZE = BufferedIO::BUFSIZE
@@default_passive = true
# :startdoc:
# When +true+, transfers are performed in binary mode. Default: +true+.
attr_reader :binary
# When +true+, the connection is in passive mode. Default: +true+.
attr_accessor :passive
# When +true+, use the IP address in PASV responses. Otherwise, it uses
# the same IP address for the control connection. Default: +false+.
attr_accessor :use_pasv_ip
# 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 the TLS handshake. Any number
# may be used, including Floats for fractional seconds. If the FTP
# object cannot complete the TLS handshake in this many seconds, it
# raises a Net::OpenTimeout exception. The default value is +nil+.
# If +ssl_handshake_timeout+ is +nil+, +open_timeout+ is used instead.
attr_accessor :ssl_handshake_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
# When +true+, connections are in passive mode per default.
# Default: +true+.
def self.default_passive=(value)
@@default_passive = value
end
# When +true+, connections are in passive mode per default.
# Default: +true+.
def self.default_passive
@@default_passive
end
#
# A synonym for FTP.new, 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, *args)
if block_given?
ftp = new(host, *args)
begin
yield ftp
ensure
ftp.close
end
else
new(host, *args)
end
end
# :call-seq:
# Net::FTP.new(host = nil, options = {})
#
# Creates and returns a new +FTP+ object. If a +host+ is given, a connection
# is made.
#
# +options+ is an option hash, each key of which is a symbol.
#
# The available options are:
#
# port:: Port number (default value is 21)
# 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.
# private_data_connection:: If true, TLS is used for data connections.
# Default: +true+ when options[:ssl] is true.
# username:: Username for login. If options[:username] is the string
# "anonymous" and the options[:password] is +nil+,
# "anonymous@" is used as a password.
# password:: Password for login.
# account:: Account information for ACCT.
# passive:: When +true+, the connection is in passive mode. Default:
# +true+.
# open_timeout:: Number of seconds to wait for the connection to open.
# See Net::FTP#open_timeout for details. Default: +nil+.
# read_timeout:: Number of seconds to wait for one block to be read.
# See Net::FTP#read_timeout for details. Default: +60+.
# ssl_handshake_timeout:: Number of seconds to wait for the TLS
# handshake.
# See Net::FTP#ssl_handshake_timeout for
# details. Default: +nil+.
# use_pasv_ip:: When +true+, use the IP address in PASV responses.
# Otherwise, it uses the same IP address for the control
# connection. Default: +false+.
# debug_mode:: When +true+, all traffic to and from the server is
# written to +$stdout+. Default: +false+.
#
def initialize(host = nil, user_or_options = {}, passwd = nil, acct = nil)
super()
begin
options = user_or_options.to_hash
rescue NoMethodError
# for backward compatibility
options = {}
options[:username] = user_or_options
options[:password] = passwd
options[:account] = acct
end
@host = nil
if options[:ssl]
unless defined?(OpenSSL::SSL)
raise "SSL extension not installed"
end
ssl_params = options[:ssl] == true ? {} : options[:ssl]
@ssl_context = SSLContext.new
@ssl_context.set_params(ssl_params)
if defined?(VerifyCallbackProc)
@ssl_context.verify_callback = VerifyCallbackProc
end
@ssl_context.session_cache_mode =
OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
@ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
@ssl_session = nil
if options[:private_data_connection].nil?
@private_data_connection = true
else
@private_data_connection = options[:private_data_connection]
end
else
@ssl_context = nil
if options[:private_data_connection]
raise ArgumentError,
"private_data_connection can be set to true only when ssl is enabled"
end
@private_data_connection = false
end
@binary = true
if options[:passive].nil?
@passive = @@default_passive
else
@passive = options[:passive]
end
if options[:debug_mode].nil?
@debug_mode = false
else
@debug_mode = options[:debug_mode]
end
@resume = false
@bare_sock = @sock = NullSocket.new
@logged_in = false
@open_timeout = options[:open_timeout]
@ssl_handshake_timeout = options[:ssl_handshake_timeout]
@read_timeout = options[:read_timeout] || 60
@use_pasv_ip = options[:use_pasv_ip] || false
if host
connect(host, options[:port] || FTP_PORT)
if options[:username]
login(options[:username], options[:password], options[:account])
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:
warn("Net::FTP#return_code is obsolete and do nothing", uplevel: 1)
return "\n"
end
# Obsolete
def return_code=(s) # :nodoc:
warn("Net::FTP#return_code= is obsolete and do nothing", uplevel: 1)
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 Socket is
# returned.
def open_socket(host, port) # :nodoc:
return Timeout.timeout(@open_timeout, OpenTimeout) {
if defined? SOCKSSocket and ENV["SOCKS_SERVER"]
@passive = true
SOCKSSocket.open(host, port)
else
Socket.tcp(host, port)
end
}
end
private :open_socket
def start_tls_session(sock)
ssl_sock = SSLSocket.new(sock, @ssl_context)
ssl_sock.sync_close = true
ssl_sock.hostname = @host if ssl_sock.respond_to? :hostname=
if @ssl_session &&
Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
# ProFTPD returns 425 for data connections if session is not reused.
ssl_sock.session = @ssl_session
end
ssl_socket_connect(ssl_sock, @ssl_handshake_timeout || @open_timeout)
if @ssl_context.verify_mode != VERIFY_NONE
ssl_sock.post_connection_check(@host)
end
return ssl_sock
end
private :start_tls_session
#
# 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
# Errno::ECONNREFUSED) if the connection cannot be established.
#
def connect(host, port = FTP_PORT)
if @debug_mode
print "connect: ", host, ", ", port, "\n"
end
synchronize do
@host = host
@bare_sock = open_socket(host, port)
@sock = BufferedSocket.new(@bare_sock, read_timeout: @read_timeout)
voidresp
if @ssl_context
begin
voidcmd("AUTH TLS")
ssl_sock = start_tls_session(@bare_sock)
@sock = BufferedSSLSocket.new(ssl_sock, read_timeout: @read_timeout)
if @private_data_connection
voidcmd("PBSZ 0")
voidcmd("PROT P")
end
rescue OpenSSL::SSL::SSLError, OpenTimeout
@sock.close
raise
end
end
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
if /[\r\n]/ =~ line
raise ArgumentError, "A line must not contain CR or LF"
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:
lines = []
lines << getline
code = lines.last.slice(/\A([0-9a-zA-Z]{3})-/, 1)
if code
delimiter = code + " "
begin
lines << getline
end until lines.last.start_with?(delimiter)
end
return lines.join("\n") + "\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.start_with?("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:
remote_address = @bare_sock.remote_address
if remote_address.ipv4?
cmd = "PORT " + (host.split(".") + port.divmod(256)).join(",")
elsif remote_address.ipv6?
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:
Addrinfo.tcp(@bare_sock.local_address.ip_address, 0).listen
end
private :makeport
# sends the appropriate command to enable a passive connection
def makepasv # :nodoc:
if @bare_sock.remote_address.ipv4?
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.start_with?("3")
raise FTPReplyError, resp
end
end
resp = sendcmd(cmd)
# skip 2XX for some ftp servers
resp = getresp if resp.start_with?("2")
if !resp.start_with?("1")
raise FTPReplyError, resp
end
else
sock = makeport
begin
addr = sock.local_address
sendport(addr.ip_address, addr.ip_port)
if @resume and rest_offset
resp = sendcmd("REST " + rest_offset.to_s)
if !resp.start_with?("3")
raise FTPReplyError, resp
end
end
resp = sendcmd(cmd)
# skip 2XX for some ftp servers
resp = getresp if resp.start_with?("2")
if !resp.start_with?("1")
raise FTPReplyError, resp
end
conn, = sock.accept
sock.shutdown(Socket::SHUT_WR) rescue nil
sock.read rescue nil
ensure
sock.close
end
end
if @private_data_connection
return BufferedSSLSocket.new(start_tls_session(conn),
read_timeout: @read_timeout)
else
return BufferedSocket.new(conn, read_timeout: @read_timeout)
end
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 Net::FTPPermError).
#
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.start_with?("3")
raise FTPReplyError, resp if passwd.nil?
resp = sendcmd('PASS ' + passwd)
end
if resp.start_with?("3")
raise FTPReplyError, resp if acct.nil?
resp = sendcmd('ACCT ' + acct)
end
end
if !resp.start_with?("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)
while data = conn.read(blocksize)
yield(data)
end
conn.shutdown(Socket::SHUT_WR) rescue nil
conn.read_timeout = 1
conn.read rescue nil
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)
while line = conn.gets
yield(line.sub(/\r?\n\z/, ""), !line.match(/\n\z/).nil?)
end
conn.shutdown(Socket::SHUT_WR) rescue nil
conn.read_timeout = 1
conn.read rescue nil
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
begin
conn = transfercmd(cmd)
while buf = file.read(blocksize)
conn.write(buf)
yield(buf) if block_given?
end
conn.shutdown(Socket::SHUT_WR) rescue nil
conn.read_timeout = 1
conn.read rescue nil
ensure
conn.close if conn
end
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
begin
conn = transfercmd(cmd)
while buf = file.gets
if buf[-2, 2] != CRLF
buf = buf.chomp + CRLF
end
conn.write(buf)
yield(buf) if block_given?
end
conn.shutdown(Socket::SHUT_WR) rescue nil
conn.read_timeout = 1
conn.read rescue nil
ensure
conn.close if conn
end
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, &block) # :yield: data
f = nil
result = nil
if localfile
if @resume
rest_offset = File.size?(localfile)
f = File.open(localfile, "a")
else
rest_offset = nil
f = File.open(localfile, "w")
end
elsif !block_given?
result = String.new
end
begin
f&.binmode
retrbinary("RETR #{remotefile}", blocksize, rest_offset) do |data|
f&.write(data)
block&.(data)
result&.concat(data)
end
return result
ensure
f&.close
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),
&block) # :yield: line
f = nil
result = nil
if localfile
f = File.open(localfile, "w")
elsif !block_given?
result = String.new
end
begin
retrlines("RETR #{remotefile}") do |line, newline|
l = newline ? line + "\n" : line
f&.print(l)
block&.(line, newline)
result&.concat(l)
end
return result
ensure
f&.close
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 = File.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 = File.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}"
end
lines = []
retrlines(cmd) do |line|
lines << line
end
if block
lines.each(&block)
end
return lines
end
alias ls list
alias dir list
#
# MLSxEntry represents an entry in responses of MLST/MLSD.
# Each entry has the facts (e.g., size, last modification time, etc.)
# and the pathname.
#
class MLSxEntry
attr_reader :facts, :pathname
def initialize(facts, pathname)
@facts = facts
@pathname = pathname
end
standard_facts = %w(size modify create type unique perm
lang media-type charset)
standard_facts.each do |factname|
define_method factname.gsub(/-/, "_") do
facts[factname]
end
end
#
# Returns +true+ if the entry is a file (i.e., the value of the type
# fact is file).
#
def file?
return facts["type"] == "file"
end
#
# Returns +true+ if the entry is a directory (i.e., the value of the
# type fact is dir, cdir, or pdir).
#
def directory?
if /\A[cp]?dir\z/.match(facts["type"])
return true
else
return false
end
end
#
# Returns +true+ if the APPE command may be applied to the file.
#
def appendable?
return facts["perm"].include?(?a)
end
#
# Returns +true+ if files may be created in the directory by STOU,
# STOR, APPE, and RNTO.
#
def creatable?
return facts["perm"].include?(?c)
end
#
# Returns +true+ if the file or directory may be deleted by DELE/RMD.
#
def deletable?
return facts["perm"].include?(?d)
end
#
# Returns +true+ if the directory may be entered by CWD/CDUP.
#
def enterable?
return facts["perm"].include?(?e)
end
#
# Returns +true+ if the file or directory may be renamed by RNFR.
#
def renamable?
return facts["perm"].include?(?f)
end
#
# Returns +true+ if the listing commands, LIST, NLST, and MLSD are
# applied to the directory.
#
def listable?
return facts["perm"].include?(?l)
end
#
# Returns +true+ if the MKD command may be used to create a new
# directory within the directory.
#
def directory_makable?
return facts["perm"].include?(?m)
end
#
# Returns +true+ if the objects in the directory may be deleted, or
# the directory may be purged.
#
def purgeable?
return facts["perm"].include?(?p)
end
#
# Returns +true+ if the RETR command may be applied to the file.
#
def readable?
return facts["perm"].include?(?r)
end
#
# Returns +true+ if the STOR command may be applied to the file.
#
def writable?
return facts["perm"].include?(?w)
end
end
CASE_DEPENDENT_PARSER = ->(value) { value }
CASE_INDEPENDENT_PARSER = ->(value) { value.downcase }
DECIMAL_PARSER = ->(value) { value.to_i }
OCTAL_PARSER = ->(value) { value.to_i(8) }
TIME_PARSER = ->(value, local = false) {
unless /\A(?\d{4})(?\d{2})(?\d{2})
(?\d{2})(?\d{2})(?\d{2})
(\.(?\d+))?/x =~ value
raise FTPProtoError, "invalid time-val: #{value}"
end
usec = fractions.to_i * 10 ** (6 - fractions.to_s.size)
Time.send(local ? :local : :utc, year, month, day, hour, min, sec, usec)
}
FACT_PARSERS = Hash.new(CASE_DEPENDENT_PARSER)
FACT_PARSERS["size"] = DECIMAL_PARSER
FACT_PARSERS["modify"] = TIME_PARSER
FACT_PARSERS["create"] = TIME_PARSER
FACT_PARSERS["type"] = CASE_INDEPENDENT_PARSER
FACT_PARSERS["unique"] = CASE_DEPENDENT_PARSER
FACT_PARSERS["perm"] = CASE_INDEPENDENT_PARSER
FACT_PARSERS["lang"] = CASE_INDEPENDENT_PARSER
FACT_PARSERS["media-type"] = CASE_INDEPENDENT_PARSER
FACT_PARSERS["charset"] = CASE_INDEPENDENT_PARSER
FACT_PARSERS["unix.mode"] = OCTAL_PARSER
FACT_PARSERS["unix.owner"] = DECIMAL_PARSER
FACT_PARSERS["unix.group"] = DECIMAL_PARSER
FACT_PARSERS["unix.ctime"] = TIME_PARSER
FACT_PARSERS["unix.atime"] = TIME_PARSER
def parse_mlsx_entry(entry)
facts, pathname = entry.chomp.split(/ /, 2)
unless pathname
raise FTPProtoError, entry
end
return MLSxEntry.new(
facts.scan(/(.*?)=(.*?);/).each_with_object({}) {
|(factname, value), h|
name = factname.downcase
h[name] = FACT_PARSERS[name].(value)
},
pathname)
end
private :parse_mlsx_entry
#
# Returns data (e.g., size, last modification time, entry type, etc.)
# about the file or directory specified by +pathname+.
# If +pathname+ is omitted, the current directory is assumed.
#
def mlst(pathname = nil)
cmd = pathname ? "MLST #{pathname}" : "MLST"
resp = sendcmd(cmd)
if !resp.start_with?("250")
raise FTPReplyError, resp
end
line = resp.lines[1]
unless line
raise FTPProtoError, resp
end
entry = line.sub(/\A(250-| *)/, "")
return parse_mlsx_entry(entry)
end
#
# Returns an array of the entries of the directory specified by
# +pathname+.
# Each entry has the facts (e.g., size, last modification time, etc.)
# and the pathname.
# If a block is given, it iterates through the listing.
# If +pathname+ is omitted, the current directory is assumed.
#
def mlsd(pathname = nil, &block) # :yield: entry
cmd = pathname ? "MLSD #{pathname}" : "MLSD"
entries = []
retrlines(cmd) do |line|
entries << parse_mlsx_entry(line)
end
if block
entries.each(&block)
end
return entries
end
#
# Renames a file on the server.
#
def rename(fromname, toname)
resp = sendcmd("RNFR #{fromname}")
if !resp.start_with?("3")
raise FTPReplyError, resp
end
voidcmd("RNTO #{toname}")
end
#
# Deletes a file on the server.
#
def delete(filename)
resp = sendcmd("DELE #{filename}")
if resp.start_with?("250")
return
elsif resp.start_with?("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
def get_body(resp) # :nodoc:
resp.slice(/\A[0-9a-zA-Z]{3} (.*)$/, 1)
end
private :get_body
#
# Returns the size of the given (remote) filename.
#
def size(filename)
with_binary(true) do
resp = sendcmd("SIZE #{filename}")
if !resp.start_with?("213")
raise FTPReplyError, resp
end
return get_body(resp).to_i
end
end
#
# 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)
return TIME_PARSER.(mdtm(filename), local)
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.start_with?("215")
raise FTPReplyError, resp
end
return get_body(resp)
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).
#
# pathname:: when stat is invoked with pathname as a parameter it acts like
# list but a lot faster and over the same tcp session.
#
def status(pathname = nil)
line = pathname ? "STAT #{pathname}" : "STAT"
if /[\r\n]/ =~ line
raise ArgumentError, "A line must not contain CR or LF"
end
print "put: #{line}\n" if @debug_mode
@sock.send(line + CRLF, 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.start_with?("213")
return get_body(resp)
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
#
# Issues a FEAT command
#
# Returns an array of supported optional features
#
def features
resp = sendcmd("FEAT")
if !resp.start_with?("211")
raise FTPReplyError, resp
end
feats = []
resp.split("\n").each do |line|
next if !line.start_with?(' ') # skip status lines
feats << line.strip
end
return feats
end
#
# Issues an OPTS command
# - name Should be the name of the option to set
# - params is any optional parameters to supply with the option
#
# example: option('UTF8', 'ON') => 'OPTS UTF8 ON'
#
def option(name, params = nil)
cmd = "OPTS #{name}"
cmd += " #{params}" if params
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.start_with?("227")
raise FTPReplyError, resp
end
if m = /\((?\d+(,\d+){3}),(?\d+,\d+)\)/.match(resp)
if @use_pasv_ip
host = parse_pasv_ipv4_host(m["host"])
else
host = @bare_sock.remote_address.ip_address
end
return 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.start_with?("228")
raise FTPReplyError, resp
end
if m = /\(4,4,(?\d+(,\d+){3}),2,(?\d+,\d+)\)/.match(resp)
return parse_pasv_ipv4_host(m["host"]), parse_pasv_port(m["port"])
elsif m = /\(6,16,(?\d+(,(\d+)){15}),2,(?\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.start_with?("229")
raise FTPReplyError, resp
end
if m = /\((?[!-~])\k\k(?\d+)\k\)/.match(resp)
return @bare_sock.remote_address.ip_address, 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.start_with?("257")
raise FTPReplyError, resp
end
return resp.slice(/"(([^"]|"")*)"/, 1).to_s.gsub(/""/, '"')
end
private :parse257
# :stopdoc:
class NullSocket
def read_timeout=(sec)
end
def closed?
true
end
def close
end
def method_missing(mid, *args)
raise FTPConnectionError, "not connected"
end
end
class BufferedSocket < BufferedIO
[:local_address, :remote_address, :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, String.new, true)
return s.empty? ? nil : s
else
result = String.new
while s = super(DEFAULT_BLOCKSIZE, String.new, 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
if defined?(OpenSSL::SSL::SSLSocket)
class BufferedSSLSocket < BufferedSocket
def initialize(*args, **options)
super
@is_shutdown = false
end
def shutdown(*args)
# SSL_shutdown() will be called from SSLSocket#close, and
# SSL_shutdown() will send the "close notify" alert to the peer,
# so shutdown(2) should not be called.
@is_shutdown = true
end
def send(mesg, flags, dest = nil)
# Ignore flags and dest.
@io.write(mesg)
end
private
def rbuf_fill
if @is_shutdown
raise EOFError, "shutdown has been called"
else
super
end
end
end
end
# :startdoc:
end
end
# Documentation comments:
# - sourced from pickaxe and nutshell, with improvements (hopefully)
PK ! 2iw w smtp.rbnu [ # frozen_string_literal: true
# = net/smtp.rb
#
# Copyright (c) 1999-2007 Yukihiro Matsumoto.
#
# Copyright (c) 1999-2007 Minero Aoki.
#
# Written & maintained by Minero Aoki .
#
# 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.
#
# $Id$
#
# 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 4xx, 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 = <
# To: Destination Address
# Subject: test message
# Date: Sat, 23 Jun 2001 16:26:43 +0900
# Message-Id:
#
# 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 < Protocol
Revision = %q$Revision$.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
@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
ssl_socket_connect(s, @open_timeout)
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)
InternetMessageIO.new(s, read_timeout: @read_timeout,
debug_output: @debug_output)
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
@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('m0')
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)
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)
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(<